From 9a89bfef092390e385ea7436866120eb23cc8cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Wed, 17 Oct 2018 12:07:50 +0200 Subject: [PATCH] [Infra UI] Merge InfraOps feature branch (#24068) --- package.json | 2 + src/dev/ci_setup/git_setup.sh | 13 +- src/type_definitions/react_virtualized.d.ts | 22 + .../public/autocomplete_providers/index.d.ts | 55 + .../public/index_patterns/_index_pattern.d.ts | 16 + src/ui/public/index_patterns/index.d.ts | 2 +- src/ui/public/kuery/ast/ast.d.ts | 48 + src/ui/public/kuery/ast/index.d.ts | 20 + src/ui/public/kuery/index.d.ts | 20 + src/ui/public/registry/feature_catalogue.d.ts | 40 + src/ui/public/routes/index.d.ts | 23 + src/ui/public/routes/route_manager.d.ts | 38 + src/ui/public/routes/routes.d.ts | 27 + x-pack/.gitignore | 1 + x-pack/index.js | 4 +- x-pack/package.json | 43 +- .../infra/common/graphql/introspection.json | 2666 +++++++++++++++++ .../infra/common/graphql/root/index.ts | 7 + .../infra/common/graphql/root/schema.gql.ts | 18 + .../graphql/shared/fragments.gql_query.ts | 16 + .../infra/common/graphql/shared/index.ts | 8 + .../infra/common/graphql/shared/schema.gql.ts | 34 + .../infra/common/graphql/typed_resolvers.ts | 82 + x-pack/plugins/infra/common/graphql/types.ts | 858 ++++++ x-pack/plugins/infra/common/http_api/index.ts | 8 + .../common/http_api/search_results_api.ts | 37 + .../common/http_api/search_summary_api.ts | 26 + .../infra/common/http_api/timed_api.ts | 13 + .../plugins/infra/common/log_entry/index.ts | 8 + .../infra/common/log_entry/log_entry.ts | 63 + .../infra/common/log_entry/log_entry_list.ts | 43 + .../infra/common/log_search_result/index.ts | 12 + .../log_search_result/log_search_result.ts | 30 + .../infra/common/log_search_summary/index.ts | 7 + .../log_search_summary/log_search_summary.ts | 14 + .../plugins/infra/common/log_summary/index.ts | 7 + .../infra/common/log_summary/log_summary.ts | 13 + .../infra/common/log_text_scale/index.ts | 7 + .../common/log_text_scale/log_text_scale.ts | 15 + x-pack/plugins/infra/common/time/index.ts | 10 + x-pack/plugins/infra/common/time/time.ts | 13 + x-pack/plugins/infra/common/time/time_key.ts | 79 + .../plugins/infra/common/time/time_scale.ts | 37 + x-pack/plugins/infra/common/time/time_unit.ts | 41 + x-pack/plugins/infra/common/typed_json.ts | 13 + x-pack/plugins/infra/docs/arch.md | 106 + x-pack/plugins/infra/docs/arch_client.md | 132 + x-pack/plugins/infra/docs/assets/arch.png | Bin 0 -> 69550 bytes x-pack/plugins/infra/docs/graphql.md | 53 + x-pack/plugins/infra/index.ts | 56 + x-pack/plugins/infra/package.json | 17 + x-pack/plugins/infra/public/app.ts | 7 + .../plugins/infra/public/apps/kibana_app.ts | 11 + .../plugins/infra/public/apps/start_app.tsx | 43 + .../plugins/infra/public/apps/testing_app.ts | 9 + .../infra/public/components/auto_sizer.tsx | 166 + .../autocomplete_field/autocomplete_field.tsx | 305 ++ .../components/autocomplete_field/index.ts | 7 + .../autocomplete_field/suggestion_item.tsx | 124 + .../infra/public/components/empty_page.tsx | 32 + .../infra/public/components/eui/index.ts | 7 + .../public/components/eui/toolbar/index.ts | 7 + .../public/components/eui/toolbar/toolbar.tsx | 21 + .../infra/public/components/header.tsx | 43 + .../infra/public/components/loading/index.tsx | 47 + .../infra/public/components/loading_page.tsx | 35 + .../logging/log_customization_menu.tsx | 65 + .../logging/log_minimap/density_chart.tsx | 64 + .../log_minimap/highlighted_interval.tsx | 43 + .../components/logging/log_minimap/index.ts | 7 + .../logging/log_minimap/log_minimap.tsx | 166 + .../logging/log_minimap/search_marker.tsx | 114 + .../log_minimap/search_marker_tooltip.tsx | 53 + .../logging/log_minimap/search_markers.tsx | 53 + .../logging/log_minimap/time_ruler.tsx | 57 + .../components/logging/log_minimap/types.ts | 11 + .../logging/log_minimap_scale_controls.tsx | 59 + .../logging/log_search_controls/index.ts | 7 + .../log_search_buttons.tsx | 71 + .../log_search_controls.tsx | 63 + .../log_search_controls/log_search_input.tsx | 75 + .../components/logging/log_statusbar.tsx | 23 + .../logging/log_text_scale_controls.tsx | 41 + .../logging/log_text_stream/empty_view.tsx | 31 + .../logging/log_text_stream/index.ts | 7 + .../logging/log_text_stream/item.ts | 47 + .../log_text_stream/item_date_field.tsx | 64 + .../logging/log_text_stream/item_field.tsx | 23 + .../log_text_stream/item_message_field.tsx | 109 + .../logging/log_text_stream/item_view.tsx | 34 + .../log_text_stream/loading_item_view.tsx | 114 + .../log_text_stream/log_entry_item_view.tsx | 93 + .../log_entry_stream_item_view_.tsx | 31 + .../log_text_stream/measurable_item_view.tsx | 58 + .../logging/log_text_stream/relative_time.tsx | 77 + .../scrollable_log_text_stream_view.tsx | 182 ++ .../log_text_stream/vertical_scroll_panel.tsx | 274 ++ .../logging/log_text_wrap_controls.tsx | 29 + .../components/logging/log_time_controls.tsx | 84 + .../infra/public/components/metrics/index.tsx | 84 + .../public/components/metrics/section.tsx | 38 + .../metrics/sections/chart_section.tsx | 228 ++ .../metrics/sections/gauges_section.tsx | 103 + .../components/metrics/sections/index.ts | 14 + .../components/metrics/time_controls.tsx | 209 ++ .../plugins/infra/public/components/page.tsx | 26 + .../components/range_date_picker/index.tsx | 416 +++ .../components/waffle/gradient_legend.tsx | 100 + .../public/components/waffle/group_name.tsx | 87 + .../components/waffle/group_of_groups.tsx | 62 + .../components/waffle/group_of_nodes.tsx | 72 + .../infra/public/components/waffle/index.tsx | 209 ++ .../infra/public/components/waffle/legend.tsx | 34 + .../waffle/lib/apply_wafflemap_layout.ts | 106 + .../components/waffle/lib/color_from_value.ts | 88 + .../components/waffle/lib/size_of_squares.ts | 38 + .../components/waffle/lib/type_guards.ts | 19 + .../infra/public/components/waffle/node.tsx | 145 + .../components/waffle/node_context_menu.tsx | 127 + .../public/components/waffle/steps_legend.tsx | 81 + .../waffle/waffle_group_by_controls.tsx | 132 + .../waffle/waffle_metric_controls.tsx | 106 + .../waffle/waffle_node_type_switcher.tsx | 45 + .../waffle/waffle_time_controls.tsx | 82 + .../capabilities/capabilities.gql_query.ts | 19 + .../capabilities/with_capabilites.tsx | 88 + .../infra/public/containers/host/index.ts | 12 + .../public/containers/host/with_all_hosts.ts | 30 + .../containers/logs/with_log_filter.tsx | 79 + .../containers/logs/with_log_minimap.tsx | 101 + .../containers/logs/with_log_position.tsx | 125 + .../logs/with_log_search_controls_props.ts | 35 + .../containers/logs/with_log_textview.tsx | 91 + .../containers/logs/with_stream_items.ts | 65 + .../public/containers/logs/with_summary.ts | 22 + .../containers/metrics/metrics.gql_query.ts | 31 + .../containers/metrics/with_metrics.tsx | 79 + .../containers/metrics/with_metrics_time.tsx | 96 + .../infra/public/containers/waffle/index.ts | 8 + .../containers/waffle/nodes_to_wafflemap.ts | 117 + .../public/containers/waffle/type_guards.ts | 15 + .../waffle/waffle_nodes.gql_query.ts | 32 + .../containers/waffle/with_waffle_filters.tsx | 74 + .../containers/waffle/with_waffle_nodes.tsx | 76 + .../containers/waffle/with_waffle_options.tsx | 112 + .../containers/waffle/with_waffle_time.tsx | 96 + .../public/containers/with_kibana_chrome.tsx | 28 + .../containers/with_kuery_autocompletion.tsx | 108 + .../infra/public/containers/with_options.tsx | 95 + .../infra/public/containers/with_source.ts | 18 + .../containers/with_state_from_location.tsx | 126 + x-pack/plugins/infra/public/images/docker.svg | 12 + x-pack/plugins/infra/public/images/hosts.svg | 12 + .../infra/public/images/infra_mono_white.svg | 15 + x-pack/plugins/infra/public/images/k8.svg | 13 + .../public/images/logging_mono_white.svg | 18 + .../plugins/infra/public/images/services.svg | 9 + .../framework/kibana_framework_adapter.ts | 188 ++ .../framework/testing_framework_adapter.ts | 29 + .../observable_api/kibana_observable_api.ts | 45 + .../public/lib/compose/kibana_compose.ts | 69 + .../public/lib/compose/testing_compose.ts | 59 + x-pack/plugins/infra/public/lib/lib.ts | 203 ++ x-pack/plugins/infra/public/pages/404.tsx | 13 + x-pack/plugins/infra/public/pages/error.tsx | 59 + .../plugins/infra/public/pages/home/index.tsx | 54 + .../infra/public/pages/home/page_content.tsx | 60 + .../infra/public/pages/home/toolbar.tsx | 114 + .../infra/public/pages/link_to/index.ts | 13 + .../infra/public/pages/link_to/link_to.tsx | 40 + .../public/pages/link_to/query_params.ts | 14 + .../link_to/redirect_to_container_detail.tsx | 15 + .../link_to/redirect_to_container_logs.tsx | 43 + .../pages/link_to/redirect_to_host_detail.tsx | 14 + .../pages/link_to/redirect_to_host_logs.tsx | 38 + .../pages/link_to/redirect_to_pod_detail.tsx | 14 + .../pages/link_to/redirect_to_pod_logs.tsx | 35 + .../plugins/infra/public/pages/logs/index.ts | 7 + .../plugins/infra/public/pages/logs/logs.tsx | 56 + .../infra/public/pages/logs/page_content.tsx | 120 + .../infra/public/pages/logs/toolbar.tsx | 98 + .../infra/public/pages/metrics/index.tsx | 230 ++ .../public/pages/metrics/layouts/container.ts | 131 + .../public/pages/metrics/layouts/host.ts | 219 ++ .../public/pages/metrics/layouts/index.ts | 20 + .../public/pages/metrics/layouts/nginx.ts | 83 + .../infra/public/pages/metrics/layouts/pod.ts | 100 + .../public/pages/metrics/layouts/types.ts | 57 + .../plugins/infra/public/register_feature.ts | 34 + x-pack/plugins/infra/public/routes.tsx | 34 + x-pack/plugins/infra/public/store/actions.ts | 17 + x-pack/plugins/infra/public/store/epics.ts | 13 + x-pack/plugins/infra/public/store/index.ts | 11 + .../infra/public/store/local/actions.ts | 14 + .../plugins/infra/public/store/local/epic.ts | 18 + .../plugins/infra/public/store/local/index.ts | 10 + .../public/store/local/log_filter/actions.ts | 15 + .../public/store/local/log_filter/index.ts | 11 + .../public/store/local/log_filter/reducer.ts | 38 + .../store/local/log_filter/selectors.ts | 30 + .../public/store/local/log_minimap/actions.ts | 11 + .../public/store/local/log_minimap/index.ts | 11 + .../public/store/local/log_minimap/reducer.ts | 23 + .../store/local/log_minimap/selectors.ts | 9 + .../store/local/log_position/actions.ts | 47 + .../public/store/local/log_position/epic.ts | 23 + .../public/store/local/log_position/index.ts | 12 + .../store/local/log_position/reducer.ts | 99 + .../store/local/log_position/selectors.ts | 56 + .../store/local/log_textview/actions.ts | 15 + .../public/store/local/log_textview/index.ts | 11 + .../store/local/log_textview/reducer.ts | 36 + .../store/local/log_textview/selectors.ts | 11 + .../public/store/local/metric_time/actions.ts | 21 + .../public/store/local/metric_time/epic.ts | 59 + .../public/store/local/metric_time/index.ts | 12 + .../public/store/local/metric_time/reducer.ts | 63 + .../store/local/metric_time/selectors.ts | 19 + .../infra/public/store/local/reducer.ts | 53 + .../infra/public/store/local/selectors.ts | 56 + .../store/local/waffle_filter/actions.ts | 17 + .../public/store/local/waffle_filter/index.ts | 11 + .../store/local/waffle_filter/reducer.ts | 38 + .../store/local/waffle_filter/selectors.ts | 30 + .../store/local/waffle_options/actions.ts | 15 + .../store/local/waffle_options/index.ts | 11 + .../store/local/waffle_options/reducer.ts | 49 + .../store/local/waffle_options/selector.ts | 11 + .../public/store/local/waffle_time/actions.ts | 15 + .../public/store/local/waffle_time/epic.ts | 41 + .../public/store/local/waffle_time/index.ts | 12 + .../public/store/local/waffle_time/reducer.ts | 52 + .../store/local/waffle_time/selectors.ts | 23 + x-pack/plugins/infra/public/store/reducer.ts | 25 + .../infra/public/store/remote/actions.ts | 9 + .../plugins/infra/public/store/remote/epic.ts | 18 + .../infra/public/store/remote/index.ts | 10 + .../store/remote/log_entries/actions.ts | 11 + .../public/store/remote/log_entries/epic.ts | 166 + .../public/store/remote/log_entries/index.ts | 13 + .../remote/log_entries/operations/load.ts | 30 + .../log_entries/operations/load_more.ts | 72 + .../operations/log_entries.gql_query.ts | 56 + .../store/remote/log_entries/reducer.ts | 17 + .../store/remote/log_entries/selectors.ts | 77 + .../public/store/remote/log_entries/state.ts | 16 + .../store/remote/log_summary/actions.ts | 9 + .../public/store/remote/log_summary/epic.ts | 99 + .../public/store/remote/log_summary/index.ts | 13 + .../remote/log_summary/operations/load.ts | 30 + .../operations/log_summary.gql_query.ts | 35 + .../store/remote/log_summary/reducer.ts | 15 + .../store/remote/log_summary/selectors.ts | 17 + .../public/store/remote/log_summary/state.ts | 18 + .../infra/public/store/remote/reducer.ts | 28 + .../infra/public/store/remote/selectors.ts | 26 + .../public/store/remote/source/actions.ts | 9 + .../infra/public/store/remote/source/epic.ts | 22 + .../infra/public/store/remote/source/index.ts | 13 + .../store/remote/source/operations/load.ts | 30 + .../operations/query_source.gql_query.ts | 33 + .../public/store/remote/source/reducer.ts | 13 + .../public/store/remote/source/selectors.ts | 64 + .../infra/public/store/remote/source/state.ts | 16 + .../plugins/infra/public/store/selectors.ts | 108 + x-pack/plugins/infra/public/store/store.ts | 72 + .../infra/public/utils/formatters/data.ts | 73 + .../infra/public/utils/formatters/index.ts | 32 + .../infra/public/utils/formatters/number.ts | 11 + .../infra/public/utils/formatters/percent.ts | 10 + x-pack/plugins/infra/public/utils/handlers.ts | 24 + .../infra/public/utils/loading_state/index.ts | 30 + .../utils/loading_state/loading_policy.ts | 24 + .../utils/loading_state/loading_progress.ts | 40 + .../utils/loading_state/loading_result.ts | 88 + .../utils/loading_state/loading_state.ts | 27 + .../infra/public/utils/log_entry/index.ts | 7 + .../infra/public/utils/log_entry/log_entry.ts | 28 + .../infra/public/utils/memoize_last.ts | 48 + .../remote_state/remote_graphql_state.ts | 212 ++ x-pack/plugins/infra/public/utils/styles.ts | 44 + .../infra/public/utils/typed_react.tsx | 65 + .../plugins/infra/public/utils/typed_redux.ts | 68 + .../plugins/infra/public/utils/url_state.tsx | 164 + .../plugins/infra/scripts/combined_schema.ts | 16 + .../scripts/generate_types_from_graphql.js | 49 + x-pack/plugins/infra/scripts/gql_gen.json | 11 + .../server/graphql/capabilities/index.ts | 8 + .../server/graphql/capabilities/resolvers.ts | 37 + .../server/graphql/capabilities/schema.gql.ts | 20 + x-pack/plugins/infra/server/graphql/index.ts | 25 + .../infra/server/graphql/log_entries/index.ts | 7 + .../server/graphql/log_entries/resolvers.ts | 143 + .../server/graphql/log_entries/schema.gql.ts | 118 + .../infra/server/graphql/metrics/index.ts | 8 + .../infra/server/graphql/metrics/resolvers.ts | 44 + .../server/graphql/metrics/schema.gql.ts | 63 + .../infra/server/graphql/nodes/index.ts | 8 + .../infra/server/graphql/nodes/resolvers.ts | 78 + .../infra/server/graphql/nodes/schema.gql.ts | 91 + .../server/graphql/source_status/index.ts | 7 + .../server/graphql/source_status/resolvers.ts | 98 + .../graphql/source_status/schema.gql.ts | 38 + .../infra/server/graphql/sources/index.ts | 8 + .../infra/server/graphql/sources/resolvers.ts | 73 + .../server/graphql/sources/schema.gql.ts | 55 + x-pack/plugins/infra/server/infra_server.ts | 34 + x-pack/plugins/infra/server/kibana.index.ts | 61 + .../adapters/capabilities/adapter_types.ts | 23 + .../elasticsearch_capabilities_adapter.ts | 126 + .../server/lib/adapters/capabilities/index.ts | 7 + .../adapters/configuration/adapter_types.ts | 9 + .../lib/adapters/configuration/index.ts | 7 + .../inmemory_configuration_adapter.ts | 16 + .../kibana_configuration_adapter.test.ts | 40 + .../kibana_configuration_adapter.ts | 75 + .../lib/adapters/fields/adapter_types.ts | 18 + .../fields/framework_fields_adapter.ts | 27 + .../infra/server/lib/adapters/fields/index.ts | 7 + .../lib/adapters/framework/adapter_types.ts | 205 ++ .../adapters/framework/apollo_server_hapi.ts | 129 + .../server/lib/adapters/framework/index.ts | 7 + .../framework/kibana_framework_adapter.ts | 158 + .../lib/adapters/log_entries/adapter_types.ts | 5 + .../server/lib/adapters/log_entries/index.ts | 5 + .../log_entries/kibana_log_entries_adapter.ts | 278 ++ .../lib/adapters/metrics/adapter_types.ts | 113 + .../server/lib/adapters/metrics/index.ts | 7 + .../metrics/kibana_metrics_adapter.ts | 79 + .../adapters/metrics/lib/check_valid_node.ts | 28 + .../models/container/container_cpu_kernel.ts | 29 + .../models/container/container_cpu_usage.ts | 29 + .../container/container_disk_io_bytes.ts | 44 + .../models/container/container_diskio_ops.ts | 40 + .../models/container/container_memory.ts | 29 + .../container/container_network_traffic.ts | 56 + .../models/container/container_overview.ts | 62 + .../metrics/models/host/host_cpu_usage.ts | 249 ++ .../metrics/models/host/host_filesystem.ts | 33 + .../metrics/models/host/host_k8s_cpu_cap.ts | 53 + .../metrics/models/host/host_k8s_disk_cap.ts | 41 + .../models/host/host_k8s_memory_cap.ts | 41 + .../metrics/models/host/host_k8s_overview.ts | 150 + .../metrics/models/host/host_k8s_pod_cap.ts | 42 + .../adapters/metrics/models/host/host_load.ts | 51 + .../metrics/models/host/host_memory_usage.ts | 73 + .../models/host/host_network_traffic.ts | 89 + .../models/host/host_system_overview.ts | 141 + .../lib/adapters/metrics/models/index.ts | 74 + .../models/nginx/nginx_active_connections.ts | 33 + .../metrics/models/nginx/nginx_hits.ts | 62 + .../models/nginx/nginx_request_rate.ts | 41 + .../nginx/nginx_requests_per_connection.ts | 48 + .../metrics/models/pod/pod_cpu_usage.ts | 29 + .../metrics/models/pod/pod_log_usage.ts | 51 + .../metrics/models/pod/pod_memory_usage.ts | 29 + .../metrics/models/pod/pod_network_traffic.ts | 76 + .../metrics/models/pod/pod_overview.ts | 86 + .../lib/adapters/nodes/adapter_types.ts | 236 ++ .../server/lib/adapters/nodes/constants.ts | 9 + .../nodes/elasticsearch_nodes_adapter.ts | 64 + .../extract_group_by_and_node_from_path.ts | 56 + .../infra/server/lib/adapters/nodes/index.ts | 7 + .../nodes/lib/calculate_cardinality.ts | 49 + .../lib/convert_nodes_response_to_groups.ts | 20 + .../adapters/nodes/lib/create_base_path.ts | 13 + .../adapters/nodes/lib/create_node_item.ts | 63 + .../nodes/lib/create_node_request_body.ts | 15 + .../nodes/lib/create_partition_bodies.ts | 49 + .../lib/adapters/nodes/lib/create_query.ts | 77 + .../adapters/nodes/lib/extract_group_paths.ts | 52 + .../nodes/lib/get_bucket_size_in_seconds.ts | 31 + .../lib/adapters/nodes/lib/process_nodes.ts | 26 + .../lib/adapters/nodes/lib/type_guards.ts | 16 + .../metric_aggregation_creators/count.ts | 22 + .../nodes/metric_aggregation_creators/cpu.ts | 18 + .../metric_aggregation_creators/index.ts | 24 + .../nodes/metric_aggregation_creators/load.ts | 20 + .../metric_aggregation_creators/log_rate.ts | 34 + .../metric_aggregation_creators/memory.ts | 17 + .../nodes/metric_aggregation_creators/rate.ts | 41 + .../nodes/metric_aggregation_creators/rx.ts | 15 + .../nodes/metric_aggregation_creators/tx.ts | 16 + .../common/field_filter_processor.ts | 24 + .../processors/common/group_by_processor.ts | 58 + .../processors/common/nodes_processor.ts | 40 + .../nodes/processors/common/query_procssor.ts | 19 + .../last/date_histogram_processor.ts | 49 + .../adapters/nodes/processors/last/index.ts | 31 + .../last/metric_buckets_processor.ts | 22 + .../elasticsearch_source_status_adapter.ts | 57 + .../lib/adapters/source_status/index.ts | 7 + .../lib/adapters/sources/adapter_types.ts | 21 + .../configuration_sources_adapter.test.ts | 111 + .../sources/configuration_sources_adapter.ts | 64 + .../server/lib/adapters/sources/index.ts | 7 + .../infra/server/lib/compose/kibana.ts | 58 + .../capabilities_domain.ts | 69 + .../lib/domains/capabilities_domain/index.ts | 7 + .../infra/server/lib/domains/fields_domain.ts | 34 + .../builtin_rules/filebeat_apache2.ts | 60 + .../builtin_rules/filebeat_nginx.ts | 60 + .../builtin_rules/filebeat_redis.ts | 24 + .../builtin_rules/filebeat_system.ts | 72 + .../builtin_rules/generic.ts | 28 + .../log_entries_domain/builtin_rules/index.ts | 42 + .../lib/domains/log_entries_domain/index.ts | 7 + .../log_entries_domain/log_entries_domain.ts | 186 ++ .../lib/domains/log_entries_domain/message.ts | 201 ++ .../server/lib/domains/metrics_domain.ts | 24 + .../infra/server/lib/domains/nodes_domain.ts | 24 + .../plugins/infra/server/lib/infra_types.ts | 43 + .../plugins/infra/server/lib/source_status.ts | 61 + x-pack/plugins/infra/server/lib/sources.ts | 45 + .../logging_legacy/adjacent_search_results.ts | 189 ++ .../contained_search_results.ts | 135 + .../infra/server/logging_legacy/converters.ts | 70 + .../server/logging_legacy/elasticsearch.ts | 79 + .../infra/server/logging_legacy/index.ts | 16 + .../logging_legacy/latest_log_entries.ts | 42 + .../infra/server/logging_legacy/schemas.ts | 34 + .../server/logging_legacy/search_summary.ts | 156 + .../infra/server/usage/usage_collector.ts | 114 + x-pack/plugins/infra/server/utils/README.md | 1 + .../infra/server/utils/serialized_query.ts | 34 + x-pack/plugins/infra/types/eui.d.ts | 190 ++ .../plugins/infra/types/eui_experimental.d.ts | 67 + .../plugins/infra/types/graphql_fields.d.ts | 10 + .../plugins/infra/types/redux_observable.d.ts | 85 + x-pack/plugins/infra/types/rison_node.d.ts | 24 + x-pack/plugins/infra/yarn.lock | 56 + .../reporting/public/lib/reporting_client.ts | 6 +- .../privileges/kibana/privilege_selector.tsx | 5 +- .../api/__fixtures__/create_test_handler.ts | 7 +- x-pack/yarn.lock | 845 +++++- yarn.lock | 309 +- 436 files changed, 27376 insertions(+), 59 deletions(-) create mode 100644 src/type_definitions/react_virtualized.d.ts create mode 100644 src/ui/public/autocomplete_providers/index.d.ts create mode 100644 src/ui/public/kuery/ast/ast.d.ts create mode 100644 src/ui/public/kuery/ast/index.d.ts create mode 100644 src/ui/public/kuery/index.d.ts create mode 100644 src/ui/public/registry/feature_catalogue.d.ts create mode 100644 src/ui/public/routes/index.d.ts create mode 100644 src/ui/public/routes/route_manager.d.ts create mode 100644 src/ui/public/routes/routes.d.ts create mode 100644 x-pack/plugins/infra/common/graphql/introspection.json create mode 100644 x-pack/plugins/infra/common/graphql/root/index.ts create mode 100644 x-pack/plugins/infra/common/graphql/root/schema.gql.ts create mode 100644 x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts create mode 100644 x-pack/plugins/infra/common/graphql/shared/index.ts create mode 100644 x-pack/plugins/infra/common/graphql/shared/schema.gql.ts create mode 100644 x-pack/plugins/infra/common/graphql/typed_resolvers.ts create mode 100644 x-pack/plugins/infra/common/graphql/types.ts create mode 100644 x-pack/plugins/infra/common/http_api/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/search_results_api.ts create mode 100644 x-pack/plugins/infra/common/http_api/search_summary_api.ts create mode 100644 x-pack/plugins/infra/common/http_api/timed_api.ts create mode 100644 x-pack/plugins/infra/common/log_entry/index.ts create mode 100644 x-pack/plugins/infra/common/log_entry/log_entry.ts create mode 100644 x-pack/plugins/infra/common/log_entry/log_entry_list.ts create mode 100644 x-pack/plugins/infra/common/log_search_result/index.ts create mode 100644 x-pack/plugins/infra/common/log_search_result/log_search_result.ts create mode 100644 x-pack/plugins/infra/common/log_search_summary/index.ts create mode 100644 x-pack/plugins/infra/common/log_search_summary/log_search_summary.ts create mode 100644 x-pack/plugins/infra/common/log_summary/index.ts create mode 100644 x-pack/plugins/infra/common/log_summary/log_summary.ts create mode 100644 x-pack/plugins/infra/common/log_text_scale/index.ts create mode 100644 x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts create mode 100644 x-pack/plugins/infra/common/time/index.ts create mode 100644 x-pack/plugins/infra/common/time/time.ts create mode 100644 x-pack/plugins/infra/common/time/time_key.ts create mode 100644 x-pack/plugins/infra/common/time/time_scale.ts create mode 100644 x-pack/plugins/infra/common/time/time_unit.ts create mode 100644 x-pack/plugins/infra/common/typed_json.ts create mode 100644 x-pack/plugins/infra/docs/arch.md create mode 100644 x-pack/plugins/infra/docs/arch_client.md create mode 100644 x-pack/plugins/infra/docs/assets/arch.png create mode 100644 x-pack/plugins/infra/docs/graphql.md create mode 100644 x-pack/plugins/infra/index.ts create mode 100644 x-pack/plugins/infra/package.json create mode 100644 x-pack/plugins/infra/public/app.ts create mode 100644 x-pack/plugins/infra/public/apps/kibana_app.ts create mode 100644 x-pack/plugins/infra/public/apps/start_app.tsx create mode 100644 x-pack/plugins/infra/public/apps/testing_app.ts create mode 100644 x-pack/plugins/infra/public/components/auto_sizer.tsx create mode 100644 x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx create mode 100644 x-pack/plugins/infra/public/components/autocomplete_field/index.ts create mode 100644 x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx create mode 100644 x-pack/plugins/infra/public/components/empty_page.tsx create mode 100644 x-pack/plugins/infra/public/components/eui/index.ts create mode 100644 x-pack/plugins/infra/public/components/eui/toolbar/index.ts create mode 100644 x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx create mode 100644 x-pack/plugins/infra/public/components/header.tsx create mode 100644 x-pack/plugins/infra/public/components/loading/index.tsx create mode 100644 x-pack/plugins/infra/public/components/loading_page.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/index.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/search_markers.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap/types.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_minimap_scale_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_search_controls/index.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_statusbar.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_scale_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/empty_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_stream_item_view_.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/measurable_item_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/relative_time.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_wrap_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_time_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/metrics/index.tsx create mode 100644 x-pack/plugins/infra/public/components/metrics/section.tsx create mode 100644 x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx create mode 100644 x-pack/plugins/infra/public/components/metrics/sections/gauges_section.tsx create mode 100644 x-pack/plugins/infra/public/components/metrics/sections/index.ts create mode 100644 x-pack/plugins/infra/public/components/metrics/time_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/page.tsx create mode 100644 x-pack/plugins/infra/public/components/range_date_picker/index.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/group_name.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/index.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/legend.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts create mode 100644 x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts create mode 100644 x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts create mode 100644 x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts create mode 100644 x-pack/plugins/infra/public/components/waffle/node.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/steps_legend.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/waffle_metric_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/waffle_node_type_switcher.tsx create mode 100644 x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx create mode 100644 x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts create mode 100644 x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx create mode 100644 x-pack/plugins/infra/public/containers/host/index.ts create mode 100644 x-pack/plugins/infra/public/containers/host/with_all_hosts.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_position.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_search_controls_props.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/with_stream_items.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/with_summary.ts create mode 100644 x-pack/plugins/infra/public/containers/metrics/metrics.gql_query.ts create mode 100644 x-pack/plugins/infra/public/containers/metrics/with_metrics.tsx create mode 100644 x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx create mode 100644 x-pack/plugins/infra/public/containers/waffle/index.ts create mode 100644 x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts create mode 100644 x-pack/plugins/infra/public/containers/waffle/type_guards.ts create mode 100644 x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts create mode 100644 x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx create mode 100644 x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx create mode 100644 x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx create mode 100644 x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx create mode 100644 x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx create mode 100644 x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx create mode 100644 x-pack/plugins/infra/public/containers/with_options.tsx create mode 100644 x-pack/plugins/infra/public/containers/with_source.ts create mode 100644 x-pack/plugins/infra/public/containers/with_state_from_location.tsx create mode 100644 x-pack/plugins/infra/public/images/docker.svg create mode 100644 x-pack/plugins/infra/public/images/hosts.svg create mode 100644 x-pack/plugins/infra/public/images/infra_mono_white.svg create mode 100644 x-pack/plugins/infra/public/images/k8.svg create mode 100644 x-pack/plugins/infra/public/images/logging_mono_white.svg create mode 100644 x-pack/plugins/infra/public/images/services.svg create mode 100644 x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts create mode 100644 x-pack/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts create mode 100644 x-pack/plugins/infra/public/lib/compose/kibana_compose.ts create mode 100644 x-pack/plugins/infra/public/lib/compose/testing_compose.ts create mode 100644 x-pack/plugins/infra/public/lib/lib.ts create mode 100644 x-pack/plugins/infra/public/pages/404.tsx create mode 100644 x-pack/plugins/infra/public/pages/error.tsx create mode 100644 x-pack/plugins/infra/public/pages/home/index.tsx create mode 100644 x-pack/plugins/infra/public/pages/home/page_content.tsx create mode 100644 x-pack/plugins/infra/public/pages/home/toolbar.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/index.ts create mode 100644 x-pack/plugins/infra/public/pages/link_to/link_to.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/query_params.ts create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_container_detail.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_container_logs.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_host_logs.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_detail.tsx create mode 100644 x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_logs.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/index.ts create mode 100644 x-pack/plugins/infra/public/pages/logs/logs.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/page_content.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/toolbar.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/index.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/container.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/host.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/index.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/layouts/types.ts create mode 100644 x-pack/plugins/infra/public/register_feature.ts create mode 100644 x-pack/plugins/infra/public/routes.tsx create mode 100644 x-pack/plugins/infra/public/store/actions.ts create mode 100644 x-pack/plugins/infra/public/store/epics.ts create mode 100644 x-pack/plugins/infra/public/store/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/epic.ts create mode 100644 x-pack/plugins/infra/public/store/local/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_filter/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_filter/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_filter/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_filter/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_minimap/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_minimap/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_minimap/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_minimap/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_position/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_position/epic.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_position/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_position/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_position/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_textview/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_textview/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_textview/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_textview/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/metric_time/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/metric_time/epic.ts create mode 100644 x-pack/plugins/infra/public/store/local/metric_time/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/metric_time/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/metric_time/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_filter/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_options/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_options/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_options/selector.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_time/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_time/epic.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_time/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/remote/actions.ts create mode 100644 x-pack/plugins/infra/public/store/remote/epic.ts create mode 100644 x-pack/plugins/infra/public/store/remote/index.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/actions.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/epic.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/index.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/operations/load.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/operations/load_more.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_entries/state.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/actions.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/epic.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/index.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/operations/load.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/operations/log_summary.gql_query.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/remote/log_summary/state.ts create mode 100644 x-pack/plugins/infra/public/store/remote/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/remote/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/actions.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/epic.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/index.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/operations/load.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/operations/query_source.gql_query.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/remote/source/state.ts create mode 100644 x-pack/plugins/infra/public/store/selectors.ts create mode 100644 x-pack/plugins/infra/public/store/store.ts create mode 100644 x-pack/plugins/infra/public/utils/formatters/data.ts create mode 100644 x-pack/plugins/infra/public/utils/formatters/index.ts create mode 100644 x-pack/plugins/infra/public/utils/formatters/number.ts create mode 100644 x-pack/plugins/infra/public/utils/formatters/percent.ts create mode 100644 x-pack/plugins/infra/public/utils/handlers.ts create mode 100644 x-pack/plugins/infra/public/utils/loading_state/index.ts create mode 100644 x-pack/plugins/infra/public/utils/loading_state/loading_policy.ts create mode 100644 x-pack/plugins/infra/public/utils/loading_state/loading_progress.ts create mode 100644 x-pack/plugins/infra/public/utils/loading_state/loading_result.ts create mode 100644 x-pack/plugins/infra/public/utils/loading_state/loading_state.ts create mode 100644 x-pack/plugins/infra/public/utils/log_entry/index.ts create mode 100644 x-pack/plugins/infra/public/utils/log_entry/log_entry.ts create mode 100644 x-pack/plugins/infra/public/utils/memoize_last.ts create mode 100644 x-pack/plugins/infra/public/utils/remote_state/remote_graphql_state.ts create mode 100644 x-pack/plugins/infra/public/utils/styles.ts create mode 100644 x-pack/plugins/infra/public/utils/typed_react.tsx create mode 100644 x-pack/plugins/infra/public/utils/typed_redux.ts create mode 100644 x-pack/plugins/infra/public/utils/url_state.tsx create mode 100644 x-pack/plugins/infra/scripts/combined_schema.ts create mode 100644 x-pack/plugins/infra/scripts/generate_types_from_graphql.js create mode 100644 x-pack/plugins/infra/scripts/gql_gen.json create mode 100644 x-pack/plugins/infra/server/graphql/capabilities/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/graphql/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/log_entries/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/graphql/metrics/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/metrics/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/metrics/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/graphql/nodes/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/nodes/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/graphql/source_status/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/source_status/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/source_status/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/graphql/sources/index.ts create mode 100644 x-pack/plugins/infra/server/graphql/sources/resolvers.ts create mode 100644 x-pack/plugins/infra/server/graphql/sources/schema.gql.ts create mode 100644 x-pack/plugins/infra/server/infra_server.ts create mode 100644 x-pack/plugins/infra/server/kibana.index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/configuration/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/configuration/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/fields/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/fields/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/framework/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/log_entries/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/log_entries/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/lib/check_valid_node.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_kernel.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_disk_io_bytes.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_diskio_ops.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_memory.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_network_traffic.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_overview.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_cpu_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_filesystem.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_cpu_cap.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_disk_cap.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_memory_cap.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_overview.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_pod_cap.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_load.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_memory_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_network_traffic.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_system_overview.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_active_connections.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_hits.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_request_rate.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_requests_per_connection.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_cpu_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_log_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_memory_usage.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_network_traffic.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_overview.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/constants.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/elasticsearch_nodes_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/extract_group_by_and_node_from_path.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/calculate_cardinality.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/convert_nodes_response_to_groups.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_base_path.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_request_body.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_partition_bodies.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_query.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/extract_group_paths.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/get_bucket_size_in_seconds.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/process_nodes.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/lib/type_guards.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/count.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/cpu.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/load.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/log_rate.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/memory.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rate.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rx.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/tx.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/field_filter_processor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/group_by_processor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/nodes_processor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/query_procssor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/metric_buckets_processor.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/source_status/index.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/sources/adapter_types.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.test.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.ts create mode 100644 x-pack/plugins/infra/server/lib/adapters/sources/index.ts create mode 100644 x-pack/plugins/infra/server/lib/compose/kibana.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/fields_domain.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_redis.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_system.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/index.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/index.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/metrics_domain.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/nodes_domain.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_types.ts create mode 100644 x-pack/plugins/infra/server/lib/source_status.ts create mode 100644 x-pack/plugins/infra/server/lib/sources.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/adjacent_search_results.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/contained_search_results.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/converters.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/elasticsearch.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/index.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/latest_log_entries.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/schemas.ts create mode 100644 x-pack/plugins/infra/server/logging_legacy/search_summary.ts create mode 100644 x-pack/plugins/infra/server/usage/usage_collector.ts create mode 100644 x-pack/plugins/infra/server/utils/README.md create mode 100644 x-pack/plugins/infra/server/utils/serialized_query.ts create mode 100644 x-pack/plugins/infra/types/eui.d.ts create mode 100644 x-pack/plugins/infra/types/eui_experimental.d.ts create mode 100644 x-pack/plugins/infra/types/graphql_fields.d.ts create mode 100644 x-pack/plugins/infra/types/redux_observable.d.ts create mode 100644 x-pack/plugins/infra/types/rison_node.d.ts create mode 100644 x-pack/plugins/infra/yarn.lock diff --git a/package.json b/package.json index f46c7d0827663..f49807d42ca8f 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,8 @@ "@kbn/pm": "link:packages/kbn-pm", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", "@kbn/ui-framework": "link:packages/kbn-ui-framework", + "@types/mustache": "^0.8.31", + "JSONStream": "1.1.1", "abortcontroller-polyfill": "^1.1.9", "angular": "1.6.9", "angular-aria": "1.6.6", diff --git a/src/dev/ci_setup/git_setup.sh b/src/dev/ci_setup/git_setup.sh index ed9304a0cba90..2829d9cf9393a 100755 --- a/src/dev/ci_setup/git_setup.sh +++ b/src/dev/ci_setup/git_setup.sh @@ -63,19 +63,22 @@ function checkout_sibling { cloneAuthor="elastic" cloneBranch="${PR_SOURCE_BRANCH:-${GIT_BRANCH#*/}}" # GIT_BRANCH starts with the repo, i.e., origin/master - cloneBranch="${cloneBranch:-master}" # fall back to CI branch if not testing a PR if clone_target_is_valid ; then return 0 fi - cloneBranch="$PR_TARGET_BRANCH" + cloneBranch="${PR_TARGET_BRANCH:-master}" if clone_target_is_valid ; then return 0 fi - # remove xpack_ prefix from target branch if all other options fail - cloneBranch="${PR_TARGET_BRANCH#xpack_}" - return 0 + cloneBranch="master" + if clone_target_is_valid; then + return 0 + fi + + echo "failed to find a valid branch to clone" + return 1 } function checkout_clone_target { diff --git a/src/type_definitions/react_virtualized.d.ts b/src/type_definitions/react_virtualized.d.ts new file mode 100644 index 0000000000000..8a8ae4aff4bf2 --- /dev/null +++ b/src/type_definitions/react_virtualized.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module 'react-virtualized' { + export type ListProps = any; +} diff --git a/src/ui/public/autocomplete_providers/index.d.ts b/src/ui/public/autocomplete_providers/index.d.ts new file mode 100644 index 0000000000000..4f18ca62d29de --- /dev/null +++ b/src/ui/public/autocomplete_providers/index.d.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * WARNING: these typings are incomplete + */ +import { StaticIndexPattern } from 'ui/index_patterns'; + +export type AutocompleteProvider = ( + args: { + config: { + get(configKey: string): any; + }; + indexPatterns: StaticIndexPattern[]; + boolFilter: any; + } +) => GetSuggestions; + +export type GetSuggestions = ( + args: { + query: string; + selectionStart: number; + selectionEnd: number; + } +) => Promise; + +export type AutocompleteSuggestionType = 'field' | 'value' | 'operator' | 'conjunction'; + +export interface AutocompleteSuggestion { + description: string; + end: number; + start: number; + text: string; + type: AutocompleteSuggestionType; +} + +export function addAutocompleteProvider(language: string, provider: AutocompleteProvider): void; + +export function getAutocompleteProvider(language: string): AutocompleteProvider | undefined; diff --git a/src/ui/public/index_patterns/_index_pattern.d.ts b/src/ui/public/index_patterns/_index_pattern.d.ts index d31b546a6a97a..0ff899839c42c 100644 --- a/src/ui/public/index_patterns/_index_pattern.d.ts +++ b/src/ui/public/index_patterns/_index_pattern.d.ts @@ -17,4 +17,20 @@ * under the License. */ +/** + * WARNING: these types are incomplete + */ + export type IndexPattern = any; + +export interface StaticIndexPatternField { + name: string; + type: string; + aggregatable: boolean; + searchable: boolean; +} + +export interface StaticIndexPattern { + fields: StaticIndexPatternField[]; + title: string; +} diff --git a/src/ui/public/index_patterns/index.d.ts b/src/ui/public/index_patterns/index.d.ts index 44bcd163d1747..92f04543c237e 100644 --- a/src/ui/public/index_patterns/index.d.ts +++ b/src/ui/public/index_patterns/index.d.ts @@ -17,4 +17,4 @@ * under the License. */ -export { IndexPattern } from './_index_pattern'; +export { IndexPattern, StaticIndexPattern } from 'ui/index_patterns/_index_pattern'; diff --git a/src/ui/public/kuery/ast/ast.d.ts b/src/ui/public/kuery/ast/ast.d.ts new file mode 100644 index 0000000000000..711e2e9f37762 --- /dev/null +++ b/src/ui/public/kuery/ast/ast.d.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * WARNING: these typings are incomplete + */ + +import { StaticIndexPattern } from 'ui/index_patterns'; + +export type KueryNode = any; + +export interface KueryParseOptions { + helpers: { + [key: string]: any; + }; + startRule: string; +} + +type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +interface JsonObject { + [key: string]: JsonValue; +} + +interface JsonArray extends Array {} + +export function fromKueryExpression( + expression: string, + parseOptions?: KueryParseOptions +): KueryNode; + +export function toElasticsearchQuery(node: KueryNode, indexPattern: StaticIndexPattern): JsonObject; diff --git a/src/ui/public/kuery/ast/index.d.ts b/src/ui/public/kuery/ast/index.d.ts new file mode 100644 index 0000000000000..369ad1239d258 --- /dev/null +++ b/src/ui/public/kuery/ast/index.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from 'ui/kuery/ast/ast'; diff --git a/src/ui/public/kuery/index.d.ts b/src/ui/public/kuery/index.d.ts new file mode 100644 index 0000000000000..982b64e7c9c51 --- /dev/null +++ b/src/ui/public/kuery/index.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from 'ui/kuery/ast'; diff --git a/src/ui/public/registry/feature_catalogue.d.ts b/src/ui/public/registry/feature_catalogue.d.ts new file mode 100644 index 0000000000000..f45e30f3778d6 --- /dev/null +++ b/src/ui/public/registry/feature_catalogue.d.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum FeatureCatalogueCategory { + ADMIN = 'admin', + DATA = 'data', + OTHER = 'other', +} + +interface FeatureCatalogueObject { + id: string; + title: string; + description: string; + icon: string; + path: string; + showOnHomePage: boolean; + category: FeatureCatalogueCategory; +} + +type FeatureCatalogueRegistryFunction = () => FeatureCatalogueObject; + +export const FeatureCatalogueRegistryProvider: { + register: (fn: FeatureCatalogueRegistryFunction) => void; +}; diff --git a/src/ui/public/routes/index.d.ts b/src/ui/public/routes/index.d.ts new file mode 100644 index 0000000000000..9e1a3bbd534ee --- /dev/null +++ b/src/ui/public/routes/index.d.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { uiRoutes, UIRoutes } from 'ui/routes/routes'; + +export default uiRoutes; +export { UIRoutes }; diff --git a/src/ui/public/routes/route_manager.d.ts b/src/ui/public/routes/route_manager.d.ts new file mode 100644 index 0000000000000..e872027cd1a69 --- /dev/null +++ b/src/ui/public/routes/route_manager.d.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * WARNING: these types are incomplete + */ + +interface RouteConfiguration { + controller?: string | ((...args: any[]) => void); + redirectTo?: string; + reloadOnSearch?: boolean; + resolve?: object; + template?: string; +} + +interface RouteManager { + when(path: string, routeConfiguration: RouteConfiguration): RouteManager; + otherwise(routeConfiguration: RouteConfiguration): RouteManager; + defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; +} + +export default RouteManager; diff --git a/src/ui/public/routes/routes.d.ts b/src/ui/public/routes/routes.d.ts new file mode 100644 index 0000000000000..1a0a89612bf1d --- /dev/null +++ b/src/ui/public/routes/routes.d.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import RouteManager from 'ui/routes/route_manager'; + +interface DefaultRouteManager extends RouteManager { + enable(): void; +} + +export const uiRoutes: DefaultRouteManager; +export type UIRoutes = DefaultRouteManager; diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 7e582d057eac4..eb1aec466bbfa 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -9,3 +9,4 @@ /.aws-config.json /.env /.kibana-plugin-helpers.dev.* +!/plugins/infra/**/target diff --git a/x-pack/index.js b/x-pack/index.js index 5988b79799037..f047000d8c606 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -25,6 +25,7 @@ import { spaces } from './plugins/spaces'; import { notifications } from './plugins/notifications'; import { kueryAutocomplete } from './plugins/kuery_autocomplete'; import { canvas } from './plugins/canvas'; +import { infra } from './plugins/infra'; module.exports = function (kibana) { return [ @@ -48,6 +49,7 @@ module.exports = function (kibana) { indexManagement(kibana), consoleExtensions(kibana), notifications(kibana), - kueryAutocomplete(kibana) + kueryAutocomplete(kibana), + infra(kibana), ]; }; diff --git a/x-pack/package.json b/x-pack/package.json index 22bed29c90525..d15f142ed3d67 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -25,12 +25,32 @@ "@kbn/es": "link:../packages/kbn-es", "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", + "@types/angular": "^1.6.50", + "@types/d3-array": "^1.2.1", + "@types/d3-scale": "^2.0.0", + "@types/d3-shape": "^1.2.2", + "@types/d3-time": "^1.0.7", + "@types/d3-time-format": "^2.1.0", + "@types/elasticsearch": "^5.0.22", "@types/expect.js": "^0.3.29", + "@types/graphql": "^0.13.1", + "@types/hapi": "15.0.1", + "@types/history": "^4.6.2", "@types/jest": "^23.3.1", "@types/joi": "^10.4.4", + "@types/lodash": "^3.10.1", "@types/mocha": "^5.2.5", "@types/pngjs": "^3.3.1", + "@types/prop-types": "^15.5.3", + "@types/react": "^16.3.14", + "@types/react-datepicker": "^1.1.5", + "@types/react-dom": "^16.0.5", + "@types/react-redux": "^6.0.6", + "@types/react-router-dom": "^4.2.6", + "@types/reduce-reducers": "^0.1.3", + "@types/sinon": "^5.0.1", "@types/supertest": "^2.0.5", + "@types/uuid": "^3.4.4", "abab": "^1.0.4", "ansi-colors": "^3.0.5", "ansicolors": "0.3.2", @@ -55,6 +75,9 @@ "expect.js": "0.3.1", "fancy-log": "^1.3.2", "fetch-mock": "^5.13.1", + "graphql-code-generator": "^0.10.1", + "graphql-codegen-introspection-template": "^0.10.5", + "graphql-codegen-typescript-template": "^0.10.1", "gulp": "3.9.1", "gulp-mocha": "2.2.0", "gulp-multi-process": "^1.3.1", @@ -105,6 +128,14 @@ "angular-resource": "1.4.9", "angular-sanitize": "1.4.9", "angular-ui-ace": "0.2.3", + "apollo-cache-inmemory": "^1.2.7", + "apollo-client": "^2.3.8", + "apollo-link": "^1.2.2", + "apollo-link-http": "^1.5.4", + "apollo-link-schema": "^1.1.0", + "apollo-link-state": "^0.4.1", + "apollo-server-errors": "^2.0.2", + "apollo-server-hapi": "^1.3.6", "axios": "^0.18.0", "babel-core": "^6.26.0", "babel-preset-es2015": "^6.24.1", @@ -119,6 +150,7 @@ "copy-to-clipboard": "^3.0.8", "d3": "3.5.6", "d3-scale": "1.0.6", + "dataloader": "^1.4.0", "dedent": "^0.7.0", "dragselect": "1.8.1", "elasticsearch": "^15.1.1", @@ -128,6 +160,10 @@ "get-port": "2.1.0", "getos": "^3.1.0", "glob": "6.0.4", + "graphql": "^0.13.2", + "graphql-fields": "^1.0.2", + "graphql-tag": "^2.9.2", + "graphql-tools": "^3.0.2", "handlebars": "^4.0.10", "hapi-auth-cookie": "6.1.1", "history": "4.7.2", @@ -169,6 +205,7 @@ "puppeteer-core": "^1.7.0", "raw-loader": "0.5.1", "react": "^16.3.0", + "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", "react-clipboard.js": "^1.1.2", "react-datetime": "^2.14.0", @@ -186,13 +223,15 @@ "react-syntax-highlighter": "^5.7.0", "react-vis": "^1.8.1", "recompose": "^0.26.0", - "reduce-reducers": "^0.1.2", + "reduce-reducers": "^0.4.3", "redux": "4.0.0", "redux-actions": "2.2.1", + "redux-observable": "^1.0.0", "redux-thunk": "2.3.0", "redux-thunks": "^1.0.0", "request": "^2.85.0", "reselect": "3.0.1", + "resize-observer-polyfill": "^1.5.0", "rimraf": "^2.6.2", "rison-node": "0.3.1", "rxjs": "^6.2.1", @@ -208,6 +247,8 @@ "tinycolor2": "1.3.0", "tinymath": "^0.5.0", "tslib": "^1.9.3", + "typescript-fsa": "^2.5.0", + "typescript-fsa-reducers": "^0.4.5", "ui-select": "0.19.4", "unbzip2-stream": "1.0.9", "uuid": "3.0.1", diff --git a/x-pack/plugins/infra/common/graphql/introspection.json b/x-pack/plugins/infra/common/graphql/introspection.json new file mode 100644 index 0000000000000..cd3e36a84c306 --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/introspection.json @@ -0,0 +1,2666 @@ +{ + "__schema": { + "queryType": { "name": "Query" }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": "", + "fields": [ + { + "name": "source", + "description": "Get an infrastructure data source by id", + "args": [ + { + "name": "id", + "description": "The id of the source", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraSource", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "allSources", + "description": "Get a list of all infrastructure data sources", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraSource", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": + "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraSource", + "description": "A source of infrastructure data", + "fields": [ + { + "name": "id", + "description": "The id of the source", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "configuration", + "description": "The raw configuration of the source", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraSourceConfiguration", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": "The status of the source", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraSourceStatus", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "capabilitiesByNode", + "description": "A hierarchy of capabilities available on nodes", + "args": [ + { + "name": "nodeName", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "nodeType", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraNodeType", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraNodeCapability", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logEntriesAround", + "description": "A consecutive span of log entries surrounding a point in time", + "args": [ + { + "name": "key", + "description": "The sort key that corresponds to the point in time", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "countBefore", + "description": "The maximum number of preceding to return", + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "defaultValue": "0" + }, + { + "name": "countAfter", + "description": "The maximum number of following to return", + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "defaultValue": "0" + }, + { + "name": "filterQuery", + "description": "The query to filter the log entries by", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "highlightQuery", + "description": "The query to highlight the log entries with", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraLogEntryInterval", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logEntriesBetween", + "description": "A consecutive span of log entries within an interval", + "args": [ + { + "name": "startKey", + "description": "The sort key that corresponds to the start of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "endKey", + "description": "The sort key that corresponds to the end of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "The query to filter the log entries by", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "highlightQuery", + "description": "The query to highlight the log entries with", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraLogEntryInterval", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logSummaryBetween", + "description": "A consecutive span of summary buckets within an interval", + "args": [ + { + "name": "start", + "description": + "The millisecond timestamp that corresponds to the start of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "end", + "description": + "The millisecond timestamp that corresponds to the end of the interval", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "bucketSize", + "description": "The size of each bucket in milliseconds", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "The query to filter the log entries by", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraLogSummaryInterval", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "map", + "description": "A hierarchy of hosts, pods, containers, services or arbitrary groups", + "args": [ + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InfraTimerangeInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "type": { "kind": "OBJECT", "name": "InfraResponse", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "metrics", + "description": "", + "args": [ + { + "name": "nodeId", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "nodeType", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraNodeType", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InfraTimerangeInput", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "metrics", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraMetric", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraMetricData", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraSourceConfiguration", + "description": "A set of configuration options for an infrastructure data source", + "fields": [ + { + "name": "metricAlias", + "description": "The alias to read metric data from", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logAlias", + "description": "The alias to read log data from", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": "The field mapping to use for this source", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraSourceFields", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": + "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraSourceFields", + "description": "A mapping of semantic fields to their document counterparts", + "fields": [ + { + "name": "container", + "description": "The field to identify a container by", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "host", + "description": "The fields to identify a host by", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": + "The fields that may contain the log event message. The first field found win.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pod", + "description": "The field to identify a pod by", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tiebreaker", + "description": + "The field to use as a tiebreaker for log events that have identical timestamps", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "The field to use as a timestamp for metrics and logs", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraSourceStatus", + "description": "The status of an infrastructure data source", + "fields": [ + { + "name": "metricAliasExists", + "description": "Whether the configured metric alias exists", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logAliasExists", + "description": "Whether the configured log alias exists", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "metricIndicesExist", + "description": + "Whether the configured alias or wildcard pattern resolve to any metric indices", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logIndicesExist", + "description": + "Whether the configured alias or wildcard pattern resolve to any log indices", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "metricIndices", + "description": "The list of indices in the metric alias", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "logIndices", + "description": "The list of indices in the log alias", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "indexFields", + "description": "The list of fields defined in the index mappings", + "args": [ + { + "name": "indexType", + "description": "", + "type": { "kind": "ENUM", "name": "InfraIndexType", "ofType": null }, + "defaultValue": "ANY" + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraIndexField", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraIndexType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "ANY", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "LOGS", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "METRICS", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraIndexField", + "description": "A descriptor of a field in an index", + "fields": [ + { + "name": "name", + "description": "The name of the field", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "The type of the field's values as recognized by Kibana", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchable", + "description": "Whether the field's values can be efficiently searched for", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "aggregatable", + "description": "Whether the field's values can be aggregated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraNodeType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "pod", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "container", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraNodeCapability", + "description": + "One specific capability available on a node. A capability corresponds to a fileset or metricset", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InfraTimeKeyInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "time", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "tiebreaker", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": + "The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": + "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogEntryInterval", + "description": "A consecutive sequence of log entries", + "fields": [ + { + "name": "start", + "description": + "The key corresponding to the start of the interval covered by the entries", + "args": [], + "type": { "kind": "OBJECT", "name": "InfraTimeKey", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "end", + "description": + "The key corresponding to the end of the interval covered by the entries", + "args": [], + "type": { "kind": "OBJECT", "name": "InfraTimeKey", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasMoreBefore", + "description": "Whether there are more log entries available before the start", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasMoreAfter", + "description": "Whether there are more log entries available after the end", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filterQuery", + "description": "The query the log entries were filtered by", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "highlightQuery", + "description": "The query the log entries were highlighted with", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entries", + "description": "A list of the log entries", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraLogEntry", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraTimeKey", + "description": "A representation of the log entry's position in the event stream", + "fields": [ + { + "name": "time", + "description": "The timestamp of the event that the log entry corresponds to", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tiebreaker", + "description": "The tiebreaker that disambiguates events with the same timestamp", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogEntry", + "description": "A log entry", + "fields": [ + { + "name": "key", + "description": + "A unique representation of the log entry's position in the event stream", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraTimeKey", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "gid", + "description": "The log entry's id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "The source id", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "A list of the formatted log entry segments", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "UNION", "name": "InfraLogMessageSegment", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "UNION", + "name": "InfraLogMessageSegment", + "description": "A segment of the log entry message", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { "kind": "OBJECT", "name": "InfraLogMessageFieldSegment", "ofType": null }, + { "kind": "OBJECT", "name": "InfraLogMessageConstantSegment", "ofType": null } + ] + }, + { + "kind": "OBJECT", + "name": "InfraLogMessageFieldSegment", + "description": "A segment of the log entry message that was derived from a field", + "fields": [ + { + "name": "field", + "description": "The field the segment was derived from", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "The segment's message", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "highlights", + "description": "A list of highlighted substrings of the value", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogMessageConstantSegment", + "description": "A segment of the log entry message that was derived from a field", + "fields": [ + { + "name": "constant", + "description": "The segment's message", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogSummaryInterval", + "description": "A consecutive sequence of log summary buckets", + "fields": [ + { + "name": "start", + "description": + "The millisecond timestamp corresponding to the start of the interval covered by the summary", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "end", + "description": + "The millisecond timestamp corresponding to the end of the interval covered by the summary", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filterQuery", + "description": "The query the log entries were filtered by", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "buckets", + "description": "A list of the log entries", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraLogSummaryBucket", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraLogSummaryBucket", + "description": "A log summary bucket", + "fields": [ + { + "name": "start", + "description": "The start timestamp of the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "end", + "description": "The end timestamp of the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "entriesCount", + "description": "The number of entries inside the bucket", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InfraTimerangeInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "interval", + "description": + "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "to", + "description": "The end of the timerange", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "from", + "description": "The beginning of the timerange", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraResponse", + "description": "", + "fields": [ + { + "name": "nodes", + "description": "", + "args": [ + { + "name": "path", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraPathInput", "ofType": null } + } + } + }, + "defaultValue": null + }, + { + "name": "metric", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraMetricInput", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraNode", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InfraPathInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "type", + "description": "The type of path", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraPathType", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "label", + "description": + "The label to use in the results for the group by for the terms group by", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "field", + "description": + "The field to group by from a terms aggregation, this is ignored by the filter type", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "filters", + "description": "The fitlers for the filter group by", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "InfraPathFilterInput", "ofType": null } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraPathType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "terms", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "filters", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "hosts", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "pods", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "containers", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InfraPathFilterInput", + "description": "A group by filter", + "fields": null, + "inputFields": [ + { + "name": "label", + "description": + "The label for the filter, this will be used as the group name in the final results", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "query", + "description": "The query string query", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InfraMetricInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "type", + "description": "The type of metric", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraMetricType", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraMetricType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "count", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "cpu", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "load", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "memory", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "tx", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "rx", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "logRate", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraNode", + "description": "", + "fields": [ + { + "name": "path", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraNodePath", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "metric", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraNodeMetric", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraNodePath", + "description": "", + "fields": [ + { + "name": "value", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraNodeMetric", + "description": "", + "fields": [ + { + "name": "name", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "InfraMetricType", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraMetric", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "hostSystemOverview", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostCpuUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostFilesystem", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostK8sOverview", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostK8sCpuCap", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostK8sDiskCap", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostK8sMemoryCap", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostK8sPodCap", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostLoad", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostMemoryUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hostNetworkTraffic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podOverview", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podCpuUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podMemoryUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podLogUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "podNetworkTraffic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerOverview", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerCpuKernel", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerCpuUsage", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerDiskIOOps", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerDiskIOBytes", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerMemory", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "containerNetworkTraffic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nginxHits", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nginxRequestRate", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nginxActiveConnections", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nginxRequestsPerConnection", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraMetricData", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "InfraMetric", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "series", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraDataSeries", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraDataSeries", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "data", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "InfraDataPoint", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "InfraDataPoint", + "description": "", + "fields": [ + { + "name": "timestamp", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": + "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": + "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": + "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Directive", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": + "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "__TypeKind", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Field", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__EnumValue", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": + "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": + "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": + "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": + "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": + "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": + "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": + "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": + "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "__DirectiveLocation", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": true, + "deprecationReason": "Use `locations`." + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": + "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "InfraOperator", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "gt", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "gte", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "lt", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "lte", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "eq", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "skip", + "description": + "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "defaultValue": null + } + ] + }, + { + "name": "include", + "description": + "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "defaultValue": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], + "args": [ + { + "name": "reason", + "description": + "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": "\"No longer supported\"" + } + ] + } + ] + } +} diff --git a/x-pack/plugins/infra/common/graphql/root/index.ts b/x-pack/plugins/infra/common/graphql/root/index.ts new file mode 100644 index 0000000000000..47417b6376307 --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/root/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { rootSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/common/graphql/root/schema.gql.ts b/x-pack/plugins/infra/common/graphql/root/schema.gql.ts new file mode 100644 index 0000000000000..0819f2e2808b8 --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/root/schema.gql.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const rootSchema = gql` + schema { + query: Query + #mutation: Mutation + } + + type Query + + #type Mutation +`; diff --git a/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts new file mode 100644 index 0000000000000..44a5be6a85638 --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/shared/fragments.gql_query.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const sharedFragments = { + InfraTimeKey: gql` + fragment InfraTimeKeyFields on InfraTimeKey { + time + tiebreaker + } + `, +}; diff --git a/x-pack/plugins/infra/common/graphql/shared/index.ts b/x-pack/plugins/infra/common/graphql/shared/index.ts new file mode 100644 index 0000000000000..56c8675e76caf --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/shared/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { sharedFragments } from './fragments.gql_query'; +export { sharedSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts b/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts new file mode 100644 index 0000000000000..fd86e605b8747 --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/shared/schema.gql.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const sharedSchema = gql` + "A representation of the log entry's position in the event stream" + type InfraTimeKey { + "The timestamp of the event that the log entry corresponds to" + time: Float! + "The tiebreaker that disambiguates events with the same timestamp" + tiebreaker: Float! + } + + input InfraTimeKeyInput { + time: Float! + tiebreaker: Float! + } + + enum InfraIndexType { + ANY + LOGS + METRICS + } + + enum InfraNodeType { + pod + container + host + } +`; diff --git a/x-pack/plugins/infra/common/graphql/typed_resolvers.ts b/x-pack/plugins/infra/common/graphql/typed_resolvers.ts new file mode 100644 index 0000000000000..50b169601894b --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/typed_resolvers.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GraphQLResolveInfo } from 'graphql'; + +type BasicResolver = ( + parent: any, + args: Args, + context: any, + info: GraphQLResolveInfo +) => Promise | Result; + +type InfraResolverResult = + | Promise + | Promise<{ [P in keyof R]: () => Promise }> + | { [P in keyof R]: () => Promise } + | { [P in keyof R]: () => R[P] } + | R; + +export type InfraResolvedResult = Resolver extends InfraResolver< + infer Result, + any, + any, + any +> + ? Result + : never; + +export type SubsetResolverWithFields = R extends BasicResolver< + Array, + infer ArgsInArray +> + ? BasicResolver< + Array>>, + ArgsInArray + > + : R extends BasicResolver + ? BasicResolver>, Args> + : never; + +export type SubsetResolverWithoutFields = R extends BasicResolver< + Array, + infer ArgsInArray +> + ? BasicResolver< + Array>>, + ArgsInArray + > + : R extends BasicResolver + ? BasicResolver>, Args> + : never; + +export type InfraResolver = ( + parent: Parent, + args: Args, + context: Context, + info: GraphQLResolveInfo +) => InfraResolverResult; + +export type InfraResolverOf = Resolver extends BasicResolver< + infer Result, + infer Args +> + ? InfraResolver + : never; + +export type InfraResolverWithFields< + Resolver, + Parent, + Context, + IncludedFields extends string +> = InfraResolverOf, Parent, Context>; + +export type InfraResolverWithoutFields< + Resolver, + Parent, + Context, + ExcludedFields extends string +> = InfraResolverOf, Parent, Context>; diff --git a/x-pack/plugins/infra/common/graphql/types.ts b/x-pack/plugins/infra/common/graphql/types.ts new file mode 100644 index 0000000000000..c79280bd7f43b --- /dev/null +++ b/x-pack/plugins/infra/common/graphql/types.ts @@ -0,0 +1,858 @@ +/* tslint:disable */ +import { GraphQLResolveInfo } from 'graphql'; + +type Resolver = ( + parent: any, + args: Args, + context: any, + info: GraphQLResolveInfo +) => Promise | Result; + +export interface Query { + source: InfraSource /** Get an infrastructure data source by id */; + allSources: InfraSource[] /** Get a list of all infrastructure data sources */; +} +/** A source of infrastructure data */ +export interface InfraSource { + id: string /** The id of the source */; + configuration: InfraSourceConfiguration /** The raw configuration of the source */; + status: InfraSourceStatus /** The status of the source */; + capabilitiesByNode: (InfraNodeCapability | null)[] /** A hierarchy of capabilities available on nodes */; + logEntriesAround: InfraLogEntryInterval /** A consecutive span of log entries surrounding a point in time */; + logEntriesBetween: InfraLogEntryInterval /** A consecutive span of log entries within an interval */; + logSummaryBetween: InfraLogSummaryInterval /** A consecutive span of summary buckets within an interval */; + map?: InfraResponse | null /** A hierarchy of hosts, pods, containers, services or arbitrary groups */; + metrics: InfraMetricData[]; +} +/** A set of configuration options for an infrastructure data source */ +export interface InfraSourceConfiguration { + metricAlias: string /** The alias to read metric data from */; + logAlias: string /** The alias to read log data from */; + fields: InfraSourceFields /** The field mapping to use for this source */; +} +/** A mapping of semantic fields to their document counterparts */ +export interface InfraSourceFields { + container: string /** The field to identify a container by */; + host: string /** The fields to identify a host by */; + message: string[] /** The fields that may contain the log event message. The first field found win. */; + pod: string /** The field to identify a pod by */; + tiebreaker: string /** The field to use as a tiebreaker for log events that have identical timestamps */; + timestamp: string /** The field to use as a timestamp for metrics and logs */; +} +/** The status of an infrastructure data source */ +export interface InfraSourceStatus { + metricAliasExists: boolean /** Whether the configured metric alias exists */; + logAliasExists: boolean /** Whether the configured log alias exists */; + metricIndicesExist: boolean /** Whether the configured alias or wildcard pattern resolve to any metric indices */; + logIndicesExist: boolean /** Whether the configured alias or wildcard pattern resolve to any log indices */; + metricIndices: string[] /** The list of indices in the metric alias */; + logIndices: string[] /** The list of indices in the log alias */; + indexFields: InfraIndexField[] /** The list of fields defined in the index mappings */; +} +/** A descriptor of a field in an index */ +export interface InfraIndexField { + name: string /** The name of the field */; + type: string /** The type of the field's values as recognized by Kibana */; + searchable: boolean /** Whether the field's values can be efficiently searched for */; + aggregatable: boolean /** Whether the field's values can be aggregated */; +} +/** One specific capability available on a node. A capability corresponds to a fileset or metricset */ +export interface InfraNodeCapability { + name: string; + source: string; +} +/** A consecutive sequence of log entries */ +export interface InfraLogEntryInterval { + start?: InfraTimeKey | null /** The key corresponding to the start of the interval covered by the entries */; + end?: InfraTimeKey | null /** The key corresponding to the end of the interval covered by the entries */; + hasMoreBefore: boolean /** Whether there are more log entries available before the start */; + hasMoreAfter: boolean /** Whether there are more log entries available after the end */; + filterQuery?: string | null /** The query the log entries were filtered by */; + highlightQuery?: string | null /** The query the log entries were highlighted with */; + entries: InfraLogEntry[] /** A list of the log entries */; +} +/** A representation of the log entry's position in the event stream */ +export interface InfraTimeKey { + time: number /** The timestamp of the event that the log entry corresponds to */; + tiebreaker: number /** The tiebreaker that disambiguates events with the same timestamp */; +} +/** A log entry */ +export interface InfraLogEntry { + key: InfraTimeKey /** A unique representation of the log entry's position in the event stream */; + gid: string /** The log entry's id */; + source: string /** The source id */; + message: InfraLogMessageSegment[] /** A list of the formatted log entry segments */; +} +/** A segment of the log entry message that was derived from a field */ +export interface InfraLogMessageFieldSegment { + field: string /** The field the segment was derived from */; + value: string /** The segment's message */; + highlights: string[] /** A list of highlighted substrings of the value */; +} +/** A segment of the log entry message that was derived from a field */ +export interface InfraLogMessageConstantSegment { + constant: string /** The segment's message */; +} +/** A consecutive sequence of log summary buckets */ +export interface InfraLogSummaryInterval { + start?: + | number + | null /** The millisecond timestamp corresponding to the start of the interval covered by the summary */; + end?: + | number + | null /** The millisecond timestamp corresponding to the end of the interval covered by the summary */; + filterQuery?: string | null /** The query the log entries were filtered by */; + buckets: InfraLogSummaryBucket[] /** A list of the log entries */; +} +/** A log summary bucket */ +export interface InfraLogSummaryBucket { + start: number /** The start timestamp of the bucket */; + end: number /** The end timestamp of the bucket */; + entriesCount: number /** The number of entries inside the bucket */; +} + +export interface InfraResponse { + nodes: InfraNode[]; +} + +export interface InfraNode { + path: InfraNodePath[]; + metric: InfraNodeMetric; +} + +export interface InfraNodePath { + value: string; +} + +export interface InfraNodeMetric { + name: InfraMetricType; + value: number; +} + +export interface InfraMetricData { + id?: InfraMetric | null; + series: InfraDataSeries[]; +} + +export interface InfraDataSeries { + id: string; + data: InfraDataPoint[]; +} + +export interface InfraDataPoint { + timestamp: number; + value?: number | null; +} + +export namespace QueryResolvers { + export interface Resolvers { + source?: SourceResolver /** Get an infrastructure data source by id */; + allSources?: AllSourcesResolver /** Get a list of all infrastructure data sources */; + } + + export type SourceResolver = Resolver; + export interface SourceArgs { + id: string /** The id of the source */; + } + + export type AllSourcesResolver = Resolver; +} +/** A source of infrastructure data */ +export namespace InfraSourceResolvers { + export interface Resolvers { + id?: IdResolver /** The id of the source */; + configuration?: ConfigurationResolver /** The raw configuration of the source */; + status?: StatusResolver /** The status of the source */; + capabilitiesByNode?: CapabilitiesByNodeResolver /** A hierarchy of capabilities available on nodes */; + logEntriesAround?: LogEntriesAroundResolver /** A consecutive span of log entries surrounding a point in time */; + logEntriesBetween?: LogEntriesBetweenResolver /** A consecutive span of log entries within an interval */; + logSummaryBetween?: LogSummaryBetweenResolver /** A consecutive span of summary buckets within an interval */; + map?: MapResolver /** A hierarchy of hosts, pods, containers, services or arbitrary groups */; + metrics?: MetricsResolver; + } + + export type IdResolver = Resolver; + export type ConfigurationResolver = Resolver; + export type StatusResolver = Resolver; + export type CapabilitiesByNodeResolver = Resolver< + (InfraNodeCapability | null)[], + CapabilitiesByNodeArgs + >; + export interface CapabilitiesByNodeArgs { + nodeName: string; + nodeType: InfraNodeType; + } + + export type LogEntriesAroundResolver = Resolver; + export interface LogEntriesAroundArgs { + key: InfraTimeKeyInput /** The sort key that corresponds to the point in time */; + countBefore?: number | null /** The maximum number of preceding to return */; + countAfter?: number | null /** The maximum number of following to return */; + filterQuery?: string | null /** The query to filter the log entries by */; + highlightQuery?: string | null /** The query to highlight the log entries with */; + } + + export type LogEntriesBetweenResolver = Resolver; + export interface LogEntriesBetweenArgs { + startKey: InfraTimeKeyInput /** The sort key that corresponds to the start of the interval */; + endKey: InfraTimeKeyInput /** The sort key that corresponds to the end of the interval */; + filterQuery?: string | null /** The query to filter the log entries by */; + highlightQuery?: string | null /** The query to highlight the log entries with */; + } + + export type LogSummaryBetweenResolver = Resolver; + export interface LogSummaryBetweenArgs { + start: number /** The millisecond timestamp that corresponds to the start of the interval */; + end: number /** The millisecond timestamp that corresponds to the end of the interval */; + bucketSize: number /** The size of each bucket in milliseconds */; + filterQuery?: string | null /** The query to filter the log entries by */; + } + + export type MapResolver = Resolver; + export interface MapArgs { + timerange: InfraTimerangeInput; + filterQuery?: string | null; + } + + export type MetricsResolver = Resolver; + export interface MetricsArgs { + nodeId: string; + nodeType: InfraNodeType; + timerange: InfraTimerangeInput; + metrics: InfraMetric[]; + } +} +/** A set of configuration options for an infrastructure data source */ +export namespace InfraSourceConfigurationResolvers { + export interface Resolvers { + metricAlias?: MetricAliasResolver /** The alias to read metric data from */; + logAlias?: LogAliasResolver /** The alias to read log data from */; + fields?: FieldsResolver /** The field mapping to use for this source */; + } + + export type MetricAliasResolver = Resolver; + export type LogAliasResolver = Resolver; + export type FieldsResolver = Resolver; +} +/** A mapping of semantic fields to their document counterparts */ +export namespace InfraSourceFieldsResolvers { + export interface Resolvers { + container?: ContainerResolver /** The field to identify a container by */; + host?: HostResolver /** The fields to identify a host by */; + message?: MessageResolver /** The fields that may contain the log event message. The first field found win. */; + pod?: PodResolver /** The field to identify a pod by */; + tiebreaker?: TiebreakerResolver /** The field to use as a tiebreaker for log events that have identical timestamps */; + timestamp?: TimestampResolver /** The field to use as a timestamp for metrics and logs */; + } + + export type ContainerResolver = Resolver; + export type HostResolver = Resolver; + export type MessageResolver = Resolver; + export type PodResolver = Resolver; + export type TiebreakerResolver = Resolver; + export type TimestampResolver = Resolver; +} +/** The status of an infrastructure data source */ +export namespace InfraSourceStatusResolvers { + export interface Resolvers { + metricAliasExists?: MetricAliasExistsResolver /** Whether the configured metric alias exists */; + logAliasExists?: LogAliasExistsResolver /** Whether the configured log alias exists */; + metricIndicesExist?: MetricIndicesExistResolver /** Whether the configured alias or wildcard pattern resolve to any metric indices */; + logIndicesExist?: LogIndicesExistResolver /** Whether the configured alias or wildcard pattern resolve to any log indices */; + metricIndices?: MetricIndicesResolver /** The list of indices in the metric alias */; + logIndices?: LogIndicesResolver /** The list of indices in the log alias */; + indexFields?: IndexFieldsResolver /** The list of fields defined in the index mappings */; + } + + export type MetricAliasExistsResolver = Resolver; + export type LogAliasExistsResolver = Resolver; + export type MetricIndicesExistResolver = Resolver; + export type LogIndicesExistResolver = Resolver; + export type MetricIndicesResolver = Resolver; + export type LogIndicesResolver = Resolver; + export type IndexFieldsResolver = Resolver; + export interface IndexFieldsArgs { + indexType?: InfraIndexType | null; + } +} +/** A descriptor of a field in an index */ +export namespace InfraIndexFieldResolvers { + export interface Resolvers { + name?: NameResolver /** The name of the field */; + type?: TypeResolver /** The type of the field's values as recognized by Kibana */; + searchable?: SearchableResolver /** Whether the field's values can be efficiently searched for */; + aggregatable?: AggregatableResolver /** Whether the field's values can be aggregated */; + } + + export type NameResolver = Resolver; + export type TypeResolver = Resolver; + export type SearchableResolver = Resolver; + export type AggregatableResolver = Resolver; +} +/** One specific capability available on a node. A capability corresponds to a fileset or metricset */ +export namespace InfraNodeCapabilityResolvers { + export interface Resolvers { + name?: NameResolver; + source?: SourceResolver; + } + + export type NameResolver = Resolver; + export type SourceResolver = Resolver; +} +/** A consecutive sequence of log entries */ +export namespace InfraLogEntryIntervalResolvers { + export interface Resolvers { + start?: StartResolver /** The key corresponding to the start of the interval covered by the entries */; + end?: EndResolver /** The key corresponding to the end of the interval covered by the entries */; + hasMoreBefore?: HasMoreBeforeResolver /** Whether there are more log entries available before the start */; + hasMoreAfter?: HasMoreAfterResolver /** Whether there are more log entries available after the end */; + filterQuery?: FilterQueryResolver /** The query the log entries were filtered by */; + highlightQuery?: HighlightQueryResolver /** The query the log entries were highlighted with */; + entries?: EntriesResolver /** A list of the log entries */; + } + + export type StartResolver = Resolver; + export type EndResolver = Resolver; + export type HasMoreBeforeResolver = Resolver; + export type HasMoreAfterResolver = Resolver; + export type FilterQueryResolver = Resolver; + export type HighlightQueryResolver = Resolver; + export type EntriesResolver = Resolver; +} +/** A representation of the log entry's position in the event stream */ +export namespace InfraTimeKeyResolvers { + export interface Resolvers { + time?: TimeResolver /** The timestamp of the event that the log entry corresponds to */; + tiebreaker?: TiebreakerResolver /** The tiebreaker that disambiguates events with the same timestamp */; + } + + export type TimeResolver = Resolver; + export type TiebreakerResolver = Resolver; +} +/** A log entry */ +export namespace InfraLogEntryResolvers { + export interface Resolvers { + key?: KeyResolver /** A unique representation of the log entry's position in the event stream */; + gid?: GidResolver /** The log entry's id */; + source?: SourceResolver /** The source id */; + message?: MessageResolver /** A list of the formatted log entry segments */; + } + + export type KeyResolver = Resolver; + export type GidResolver = Resolver; + export type SourceResolver = Resolver; + export type MessageResolver = Resolver; +} +/** A segment of the log entry message that was derived from a field */ +export namespace InfraLogMessageFieldSegmentResolvers { + export interface Resolvers { + field?: FieldResolver /** The field the segment was derived from */; + value?: ValueResolver /** The segment's message */; + highlights?: HighlightsResolver /** A list of highlighted substrings of the value */; + } + + export type FieldResolver = Resolver; + export type ValueResolver = Resolver; + export type HighlightsResolver = Resolver; +} +/** A segment of the log entry message that was derived from a field */ +export namespace InfraLogMessageConstantSegmentResolvers { + export interface Resolvers { + constant?: ConstantResolver /** The segment's message */; + } + + export type ConstantResolver = Resolver; +} +/** A consecutive sequence of log summary buckets */ +export namespace InfraLogSummaryIntervalResolvers { + export interface Resolvers { + start?: StartResolver /** The millisecond timestamp corresponding to the start of the interval covered by the summary */; + end?: EndResolver /** The millisecond timestamp corresponding to the end of the interval covered by the summary */; + filterQuery?: FilterQueryResolver /** The query the log entries were filtered by */; + buckets?: BucketsResolver /** A list of the log entries */; + } + + export type StartResolver = Resolver; + export type EndResolver = Resolver; + export type FilterQueryResolver = Resolver; + export type BucketsResolver = Resolver; +} +/** A log summary bucket */ +export namespace InfraLogSummaryBucketResolvers { + export interface Resolvers { + start?: StartResolver /** The start timestamp of the bucket */; + end?: EndResolver /** The end timestamp of the bucket */; + entriesCount?: EntriesCountResolver /** The number of entries inside the bucket */; + } + + export type StartResolver = Resolver; + export type EndResolver = Resolver; + export type EntriesCountResolver = Resolver; +} + +export namespace InfraResponseResolvers { + export interface Resolvers { + nodes?: NodesResolver; + } + + export type NodesResolver = Resolver; + export interface NodesArgs { + path: InfraPathInput[]; + metric: InfraMetricInput; + } +} + +export namespace InfraNodeResolvers { + export interface Resolvers { + path?: PathResolver; + metric?: MetricResolver; + } + + export type PathResolver = Resolver; + export type MetricResolver = Resolver; +} + +export namespace InfraNodePathResolvers { + export interface Resolvers { + value?: ValueResolver; + } + + export type ValueResolver = Resolver; +} + +export namespace InfraNodeMetricResolvers { + export interface Resolvers { + name?: NameResolver; + value?: ValueResolver; + } + + export type NameResolver = Resolver; + export type ValueResolver = Resolver; +} + +export namespace InfraMetricDataResolvers { + export interface Resolvers { + id?: IdResolver; + series?: SeriesResolver; + } + + export type IdResolver = Resolver; + export type SeriesResolver = Resolver; +} + +export namespace InfraDataSeriesResolvers { + export interface Resolvers { + id?: IdResolver; + data?: DataResolver; + } + + export type IdResolver = Resolver; + export type DataResolver = Resolver; +} + +export namespace InfraDataPointResolvers { + export interface Resolvers { + timestamp?: TimestampResolver; + value?: ValueResolver; + } + + export type TimestampResolver = Resolver; + export type ValueResolver = Resolver; +} + +export interface InfraTimeKeyInput { + time: number; + tiebreaker: number; +} + +export interface InfraTimerangeInput { + interval: string /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */; + to: number /** The end of the timerange */; + from: number /** The beginning of the timerange */; +} + +export interface InfraPathInput { + type: InfraPathType /** The type of path */; + label?: + | string + | null /** The label to use in the results for the group by for the terms group by */; + field?: + | string + | null /** The field to group by from a terms aggregation, this is ignored by the filter type */; + filters?: InfraPathFilterInput[] | null /** The fitlers for the filter group by */; +} +/** A group by filter */ +export interface InfraPathFilterInput { + label: string /** The label for the filter, this will be used as the group name in the final results */; + query: string /** The query string query */; +} + +export interface InfraMetricInput { + type: InfraMetricType /** The type of metric */; +} +export interface SourceQueryArgs { + id: string /** The id of the source */; +} +export interface CapabilitiesByNodeInfraSourceArgs { + nodeName: string; + nodeType: InfraNodeType; +} +export interface LogEntriesAroundInfraSourceArgs { + key: InfraTimeKeyInput /** The sort key that corresponds to the point in time */; + countBefore?: number | null /** The maximum number of preceding to return */; + countAfter?: number | null /** The maximum number of following to return */; + filterQuery?: string | null /** The query to filter the log entries by */; + highlightQuery?: string | null /** The query to highlight the log entries with */; +} +export interface LogEntriesBetweenInfraSourceArgs { + startKey: InfraTimeKeyInput /** The sort key that corresponds to the start of the interval */; + endKey: InfraTimeKeyInput /** The sort key that corresponds to the end of the interval */; + filterQuery?: string | null /** The query to filter the log entries by */; + highlightQuery?: string | null /** The query to highlight the log entries with */; +} +export interface LogSummaryBetweenInfraSourceArgs { + start: number /** The millisecond timestamp that corresponds to the start of the interval */; + end: number /** The millisecond timestamp that corresponds to the end of the interval */; + bucketSize: number /** The size of each bucket in milliseconds */; + filterQuery?: string | null /** The query to filter the log entries by */; +} +export interface MapInfraSourceArgs { + timerange: InfraTimerangeInput; + filterQuery?: string | null; +} +export interface MetricsInfraSourceArgs { + nodeId: string; + nodeType: InfraNodeType; + timerange: InfraTimerangeInput; + metrics: InfraMetric[]; +} +export interface IndexFieldsInfraSourceStatusArgs { + indexType?: InfraIndexType | null; +} +export interface NodesInfraResponseArgs { + path: InfraPathInput[]; + metric: InfraMetricInput; +} + +export enum InfraIndexType { + ANY = 'ANY', + LOGS = 'LOGS', + METRICS = 'METRICS', +} + +export enum InfraNodeType { + pod = 'pod', + container = 'container', + host = 'host', +} + +export enum InfraPathType { + terms = 'terms', + filters = 'filters', + hosts = 'hosts', + pods = 'pods', + containers = 'containers', +} + +export enum InfraMetricType { + count = 'count', + cpu = 'cpu', + load = 'load', + memory = 'memory', + tx = 'tx', + rx = 'rx', + logRate = 'logRate', +} + +export enum InfraMetric { + hostSystemOverview = 'hostSystemOverview', + hostCpuUsage = 'hostCpuUsage', + hostFilesystem = 'hostFilesystem', + hostK8sOverview = 'hostK8sOverview', + hostK8sCpuCap = 'hostK8sCpuCap', + hostK8sDiskCap = 'hostK8sDiskCap', + hostK8sMemoryCap = 'hostK8sMemoryCap', + hostK8sPodCap = 'hostK8sPodCap', + hostLoad = 'hostLoad', + hostMemoryUsage = 'hostMemoryUsage', + hostNetworkTraffic = 'hostNetworkTraffic', + podOverview = 'podOverview', + podCpuUsage = 'podCpuUsage', + podMemoryUsage = 'podMemoryUsage', + podLogUsage = 'podLogUsage', + podNetworkTraffic = 'podNetworkTraffic', + containerOverview = 'containerOverview', + containerCpuKernel = 'containerCpuKernel', + containerCpuUsage = 'containerCpuUsage', + containerDiskIOOps = 'containerDiskIOOps', + containerDiskIOBytes = 'containerDiskIOBytes', + containerMemory = 'containerMemory', + containerNetworkTraffic = 'containerNetworkTraffic', + nginxHits = 'nginxHits', + nginxRequestRate = 'nginxRequestRate', + nginxActiveConnections = 'nginxActiveConnections', + nginxRequestsPerConnection = 'nginxRequestsPerConnection', +} + +export enum InfraOperator { + gt = 'gt', + gte = 'gte', + lt = 'lt', + lte = 'lte', + eq = 'eq', +} +/** A segment of the log entry message */ +export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment; + +export namespace CapabilitiesQuery { + export type Variables = { + sourceId: string; + nodeId: string; + nodeType: InfraNodeType; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + id: string; + capabilitiesByNode: (CapabilitiesByNode | null)[]; + }; + + export type CapabilitiesByNode = { + __typename?: 'InfraNodeCapability'; + name: string; + source: string; + }; +} +export namespace MetricsQuery { + export type Variables = { + sourceId: string; + timerange: InfraTimerangeInput; + metrics: InfraMetric[]; + nodeId: string; + nodeType: InfraNodeType; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + id: string; + metrics: Metrics[]; + }; + + export type Metrics = { + __typename?: 'InfraMetricData'; + id?: InfraMetric | null; + series: Series[]; + }; + + export type Series = { + __typename?: 'InfraDataSeries'; + id: string; + data: Data[]; + }; + + export type Data = { + __typename?: 'InfraDataPoint'; + timestamp: number; + value?: number | null; + }; +} +export namespace WaffleNodesQuery { + export type Variables = { + sourceId: string; + timerange: InfraTimerangeInput; + filterQuery?: string | null; + metric: InfraMetricInput; + path: InfraPathInput[]; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + id: string; + map?: Map | null; + }; + + export type Map = { + __typename?: 'InfraResponse'; + nodes: Nodes[]; + }; + + export type Nodes = { + __typename?: 'InfraNode'; + path: Path[]; + metric: Metric; + }; + + export type Path = { + __typename?: 'InfraNodePath'; + value: string; + }; + + export type Metric = { + __typename?: 'InfraNodeMetric'; + name: InfraMetricType; + value: number; + }; +} +export namespace LogEntries { + export type Variables = { + sourceId?: string | null; + timeKey: InfraTimeKeyInput; + countBefore?: number | null; + countAfter?: number | null; + filterQuery?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + id: string; + logEntriesAround: LogEntriesAround; + }; + + export type LogEntriesAround = { + __typename?: 'InfraLogEntryInterval'; + start?: Start | null; + end?: End | null; + hasMoreBefore: boolean; + hasMoreAfter: boolean; + entries: Entries[]; + }; + + export type Start = InfraTimeKeyFields.Fragment; + + export type End = InfraTimeKeyFields.Fragment; + + export type Entries = { + __typename?: 'InfraLogEntry'; + gid: string; + key: Key; + message: Message[]; + }; + + export type Key = { + __typename?: 'InfraTimeKey'; + time: number; + tiebreaker: number; + }; + + export type Message = + | InfraLogMessageFieldSegmentInlineFragment + | InfraLogMessageConstantSegmentInlineFragment; + + export type InfraLogMessageFieldSegmentInlineFragment = { + __typename?: 'InfraLogMessageFieldSegment'; + field: string; + value: string; + }; + + export type InfraLogMessageConstantSegmentInlineFragment = { + __typename?: 'InfraLogMessageConstantSegment'; + constant: string; + }; +} +export namespace LogSummary { + export type Variables = { + sourceId?: string | null; + start: number; + end: number; + bucketSize: number; + filterQuery?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + id: string; + logSummaryBetween: LogSummaryBetween; + }; + + export type LogSummaryBetween = { + __typename?: 'InfraLogSummaryInterval'; + start?: number | null; + end?: number | null; + buckets: Buckets[]; + }; + + export type Buckets = { + __typename?: 'InfraLogSummaryBucket'; + start: number; + end: number; + entriesCount: number; + }; +} +export namespace SourceQuery { + export type Variables = { + sourceId?: string | null; + }; + + export type Query = { + __typename?: 'Query'; + source: Source; + }; + + export type Source = { + __typename?: 'InfraSource'; + configuration: Configuration; + status: Status; + }; + + export type Configuration = { + __typename?: 'InfraSourceConfiguration'; + metricAlias: string; + logAlias: string; + fields: Fields; + }; + + export type Fields = { + __typename?: 'InfraSourceFields'; + container: string; + host: string; + pod: string; + }; + + export type Status = { + __typename?: 'InfraSourceStatus'; + indexFields: IndexFields[]; + logIndicesExist: boolean; + metricIndicesExist: boolean; + }; + + export type IndexFields = { + __typename?: 'InfraIndexField'; + name: string; + type: string; + searchable: boolean; + aggregatable: boolean; + }; +} + +export namespace InfraTimeKeyFields { + export type Fragment = { + __typename?: 'InfraTimeKey'; + time: number; + tiebreaker: number; + }; +} diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts new file mode 100644 index 0000000000000..90afdcb43ffb1 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './search_results_api'; +export * from './search_summary_api'; diff --git a/x-pack/plugins/infra/common/http_api/search_results_api.ts b/x-pack/plugins/infra/common/http_api/search_results_api.ts new file mode 100644 index 0000000000000..b866a7e1ab088 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/search_results_api.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogEntryFieldsMapping, LogEntryTime } from '../log_entry'; +import { SearchResult } from '../log_search_result'; +import { TimedApiResponse } from './timed_api'; + +interface CommonSearchResultsPostPayload { + indices: string[]; + fields: LogEntryFieldsMapping; + query: string; +} + +export interface AdjacentSearchResultsApiPostPayload extends CommonSearchResultsPostPayload { + target: LogEntryTime; + before: number; + after: number; +} + +export interface AdjacentSearchResultsApiPostResponse extends TimedApiResponse { + results: { + before: SearchResult[]; + after: SearchResult[]; + }; +} + +export interface ContainedSearchResultsApiPostPayload extends CommonSearchResultsPostPayload { + start: LogEntryTime; + end: LogEntryTime; +} + +export interface ContainedSearchResultsApiPostResponse extends TimedApiResponse { + results: SearchResult[]; +} diff --git a/x-pack/plugins/infra/common/http_api/search_summary_api.ts b/x-pack/plugins/infra/common/http_api/search_summary_api.ts new file mode 100644 index 0000000000000..90e8fa8e78bc0 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/search_summary_api.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogEntryFieldsMapping } from '../log_entry'; +import { SearchSummaryBucket } from '../log_search_summary'; +import { SummaryBucketSize } from '../log_summary'; +import { TimedApiResponse } from './timed_api'; + +export interface SearchSummaryApiPostPayload { + bucketSize: { + unit: SummaryBucketSize; + value: number; + }; + fields: LogEntryFieldsMapping; + indices: string[]; + start: number; + end: number; + query: string; +} + +export interface SearchSummaryApiPostResponse extends TimedApiResponse { + buckets: SearchSummaryBucket[]; +} diff --git a/x-pack/plugins/infra/common/http_api/timed_api.ts b/x-pack/plugins/infra/common/http_api/timed_api.ts new file mode 100644 index 0000000000000..7d0f227f0d0b1 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/timed_api.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ApiResponseTimings { + [timing: string]: number; +} + +export interface TimedApiResponse { + timings: ApiResponseTimings; +} diff --git a/x-pack/plugins/infra/common/log_entry/index.ts b/x-pack/plugins/infra/common/log_entry/index.ts new file mode 100644 index 0000000000000..ba5b67eb7c07c --- /dev/null +++ b/x-pack/plugins/infra/common/log_entry/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_entry'; +export * from './log_entry_list'; diff --git a/x-pack/plugins/infra/common/log_entry/log_entry.ts b/x-pack/plugins/infra/common/log_entry/log_entry.ts new file mode 100644 index 0000000000000..be9de2c8828c5 --- /dev/null +++ b/x-pack/plugins/infra/common/log_entry/log_entry.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeKey } from '../time'; + +export interface LogEntry { + gid: string; + origin: LogEntryOrigin; + fields: LogEntryFields; +} + +export interface LogEntryOrigin { + id: string; + index: string; + type: string; +} + +export interface LogEntryFields extends LogEntryTime { + message: string; +} + +export type LogEntryTime = TimeKey; +// export interface LogEntryTime { +// tiebreaker: number; +// time: number; +// } + +export interface LogEntryFieldsMapping { + message: string; + tiebreaker: string; + time: string; +} + +export function getLogEntryKey(entry: LogEntry) { + return { + gid: entry.gid, + tiebreaker: entry.fields.tiebreaker, + time: entry.fields.time, + }; +} + +export function isEqual(time1: LogEntryTime, time2: LogEntryTime) { + return time1.time === time2.time && time1.tiebreaker === time2.tiebreaker; +} + +export function isLess(time1: LogEntryTime, time2: LogEntryTime) { + return ( + time1.time < time2.time || (time1.time === time2.time && time1.tiebreaker < time2.tiebreaker) + ); +} + +export function isLessOrEqual(time1: LogEntryTime, time2: LogEntryTime) { + return ( + time1.time < time2.time || (time1.time === time2.time && time1.tiebreaker <= time2.tiebreaker) + ); +} + +export function isBetween(min: LogEntryTime, max: LogEntryTime, operand: LogEntryTime) { + return isLessOrEqual(min, operand) && isLessOrEqual(operand, max); +} diff --git a/x-pack/plugins/infra/common/log_entry/log_entry_list.ts b/x-pack/plugins/infra/common/log_entry/log_entry_list.ts new file mode 100644 index 0000000000000..a546d4566fed1 --- /dev/null +++ b/x-pack/plugins/infra/common/log_entry/log_entry_list.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogEntryKey, + isEqual, + isLess, + isLessOrEqual, + LogEntry, + LogEntryTime, +} from './log_entry'; + +export type LogEntryList = LogEntry[]; + +export function getIndexNearLogEntry(logEntries: LogEntryList, key: LogEntryTime, highest = false) { + let minIndex = 0; + let maxIndex = logEntries.length; + let currentIndex: number; + let currentKey: LogEntryTime; + + while (minIndex < maxIndex) { + currentIndex = (minIndex + maxIndex) >>> 1; // tslint:disable-line:no-bitwise + currentKey = getLogEntryKey(logEntries[currentIndex]); + + if ((highest ? isLessOrEqual : isLess)(currentKey, key)) { + minIndex = currentIndex + 1; + } else { + maxIndex = currentIndex; + } + } + + return maxIndex; +} + +export function getIndexOfLogEntry(logEntries: LogEntry[], key: LogEntryTime) { + const index = getIndexNearLogEntry(logEntries, key); + const logEntry = logEntries[index]; + + return logEntry && isEqual(key, getLogEntryKey(logEntry)) ? index : null; +} diff --git a/x-pack/plugins/infra/common/log_search_result/index.ts b/x-pack/plugins/infra/common/log_search_result/index.ts new file mode 100644 index 0000000000000..6795cc1543798 --- /dev/null +++ b/x-pack/plugins/infra/common/log_search_result/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getSearchResultIndexBeforeTime, + getSearchResultIndexAfterTime, + getSearchResultKey, + SearchResult, +} from './log_search_result'; diff --git a/x-pack/plugins/infra/common/log_search_result/log_search_result.ts b/x-pack/plugins/infra/common/log_search_result/log_search_result.ts new file mode 100644 index 0000000000000..a56a9d8e3531c --- /dev/null +++ b/x-pack/plugins/infra/common/log_search_result/log_search_result.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { bisector } from 'd3-array'; + +import { compareToTimeKey, TimeKey } from '../time'; + +export interface SearchResult { + gid: string; + fields: TimeKey; + matches: SearchResultFieldMatches; +} + +export interface SearchResultFieldMatches { + [field: string]: string[]; +} + +export const getSearchResultKey = (result: SearchResult) => + ({ + gid: result.gid, + tiebreaker: result.fields.tiebreaker, + time: result.fields.time, + } as TimeKey); + +const searchResultTimeBisector = bisector(compareToTimeKey(getSearchResultKey)); +export const getSearchResultIndexBeforeTime = searchResultTimeBisector.left; +export const getSearchResultIndexAfterTime = searchResultTimeBisector.right; diff --git a/x-pack/plugins/infra/common/log_search_summary/index.ts b/x-pack/plugins/infra/common/log_search_summary/index.ts new file mode 100644 index 0000000000000..4ba04cb3ea6a4 --- /dev/null +++ b/x-pack/plugins/infra/common/log_search_summary/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SearchSummaryBucket } from './log_search_summary'; diff --git a/x-pack/plugins/infra/common/log_search_summary/log_search_summary.ts b/x-pack/plugins/infra/common/log_search_summary/log_search_summary.ts new file mode 100644 index 0000000000000..72cf643311798 --- /dev/null +++ b/x-pack/plugins/infra/common/log_search_summary/log_search_summary.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResult } from '../log_search_result'; + +export interface SearchSummaryBucket { + start: number; + end: number; + count: number; + representative: SearchResult; +} diff --git a/x-pack/plugins/infra/common/log_summary/index.ts b/x-pack/plugins/infra/common/log_summary/index.ts new file mode 100644 index 0000000000000..3fb97a5cc20d0 --- /dev/null +++ b/x-pack/plugins/infra/common/log_summary/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_summary'; diff --git a/x-pack/plugins/infra/common/log_summary/log_summary.ts b/x-pack/plugins/infra/common/log_summary/log_summary.ts new file mode 100644 index 0000000000000..79d8fcc9fa60f --- /dev/null +++ b/x-pack/plugins/infra/common/log_summary/log_summary.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface LogSummaryBucket { + count: number; + end: number; + start: number; +} + +export type SummaryBucketSize = 'y' | 'M' | 'w' | 'd' | 'h' | 'm' | 's'; diff --git a/x-pack/plugins/infra/common/log_text_scale/index.ts b/x-pack/plugins/infra/common/log_text_scale/index.ts new file mode 100644 index 0000000000000..7fee2bbd398bd --- /dev/null +++ b/x-pack/plugins/infra/common/log_text_scale/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_text_scale'; diff --git a/x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts b/x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts new file mode 100644 index 0000000000000..ec33776f2a764 --- /dev/null +++ b/x-pack/plugins/infra/common/log_text_scale/log_text_scale.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type TextScale = 'small' | 'medium' | 'large'; + +export function getLabelOfTextScale(textScale: TextScale) { + return textScale.charAt(0).toUpperCase() + textScale.slice(1); +} + +export function isTextScale(maybeTextScale: string): maybeTextScale is TextScale { + return ['small', 'medium', 'large'].includes(maybeTextScale); +} diff --git a/x-pack/plugins/infra/common/time/index.ts b/x-pack/plugins/infra/common/time/index.ts new file mode 100644 index 0000000000000..6365ecd1a55b3 --- /dev/null +++ b/x-pack/plugins/infra/common/time/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './time'; +export * from './time_unit'; +export * from './time_scale'; +export * from './time_key'; diff --git a/x-pack/plugins/infra/common/time/time.ts b/x-pack/plugins/infra/common/time/time.ts new file mode 100644 index 0000000000000..dc5ffd1c2c427 --- /dev/null +++ b/x-pack/plugins/infra/common/time/time.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { timeFormat } from 'd3-time-format'; + +const formatDate = timeFormat('%Y-%m-%d %H:%M:%S.%L'); + +export function formatTime(time: number) { + return formatDate(new Date(time)); +} diff --git a/x-pack/plugins/infra/common/time/time_key.ts b/x-pack/plugins/infra/common/time/time_key.ts new file mode 100644 index 0000000000000..58b058ab5e953 --- /dev/null +++ b/x-pack/plugins/infra/common/time/time_key.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ascending, bisector } from 'd3-array'; +import pick from 'lodash/fp/pick'; + +export interface TimeKey { + time: number; + tiebreaker: number; + gid?: string; +} + +export type Comparator = (firstValue: any, secondValue: any) => number; + +export const isTimeKey = (value: any): value is TimeKey => + value && + typeof value === 'object' && + typeof value.time === 'number' && + typeof value.tiebreaker === 'number'; + +export const pickTimeKey = (value: T): TimeKey => + pick(['time', 'tiebreaker'], value); + +export function compareTimeKeys( + firstKey: TimeKey, + secondKey: TimeKey, + compareValues: Comparator = ascending +): number { + const timeComparison = compareValues(firstKey.time, secondKey.time); + + if (timeComparison === 0) { + const tiebreakerComparison = compareValues(firstKey.tiebreaker, secondKey.tiebreaker); + + if ( + tiebreakerComparison === 0 && + typeof firstKey.gid !== 'undefined' && + typeof secondKey.gid !== 'undefined' + ) { + return compareValues(firstKey.gid, secondKey.gid); + } + + return tiebreakerComparison; + } + + return timeComparison; +} + +export const compareToTimeKey = ( + keyAccessor: (value: Value) => TimeKey, + compareValues?: Comparator +) => (value: Value, key: TimeKey) => compareTimeKeys(keyAccessor(value), key, compareValues); + +export const getIndexAtTimeKey = ( + keyAccessor: (value: Value) => TimeKey, + compareValues?: Comparator +) => { + const comparator = compareToTimeKey(keyAccessor, compareValues); + const collectionBisector = bisector(comparator); + + return (collection: Value[], key: TimeKey): number | null => { + const index = collectionBisector.left(collection, key); + + if (index >= collection.length) { + return null; + } + + if (comparator(collection[index], key) !== 0) { + return null; + } + + return index; + }; +}; + +export const timeKeyIsBetween = (min: TimeKey, max: TimeKey, operand: TimeKey) => + compareTimeKeys(min, operand) <= 0 && compareTimeKeys(max, operand) >= 0; diff --git a/x-pack/plugins/infra/common/time/time_scale.ts b/x-pack/plugins/infra/common/time/time_scale.ts new file mode 100644 index 0000000000000..0381f294f81cb --- /dev/null +++ b/x-pack/plugins/infra/common/time/time_scale.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeUnit, timeUnitLabels } from './time_unit'; + +export interface TimeScale { + unit: TimeUnit; + value: number; +} + +export const getMillisOfScale = (scale: TimeScale) => scale.unit * scale.value; + +export const getLabelOfScale = (scale: TimeScale) => `${scale.value}${timeUnitLabels[scale.unit]}`; + +export const decomposeIntoUnits = (time: number, units: TimeUnit[]) => + units.reduce((result, unitMillis) => { + const offset = result.reduce( + (accumulatedOffset, timeScale) => accumulatedOffset + getMillisOfScale(timeScale), + 0 + ); + const value = Math.floor((time - offset) / unitMillis); + + if (value > 0) { + return [ + ...result, + { + unit: unitMillis, + value, + }, + ]; + } else { + return result; + } + }, []); diff --git a/x-pack/plugins/infra/common/time/time_unit.ts b/x-pack/plugins/infra/common/time/time_unit.ts new file mode 100644 index 0000000000000..4273a9fcf2ef3 --- /dev/null +++ b/x-pack/plugins/infra/common/time/time_unit.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum TimeUnit { + Millisecond = 1, + Second = Millisecond * 1000, + Minute = Second * 60, + Hour = Minute * 60, + Day = Hour * 24, + Month = Day * 30, + Year = Month * 12, +} + +export type ElasticsearchTimeUnit = 's' | 'm' | 'h' | 'd' | 'M' | 'y'; + +export const timeUnitLabels = { + [TimeUnit.Millisecond]: 'ms', + [TimeUnit.Second]: 's', + [TimeUnit.Minute]: 'm', + [TimeUnit.Hour]: 'h', + [TimeUnit.Day]: 'd', + [TimeUnit.Month]: 'M', + [TimeUnit.Year]: 'y', +}; + +export const elasticSearchTimeUnits: { + [key: string]: ElasticsearchTimeUnit; +} = { + [TimeUnit.Second]: 's', + [TimeUnit.Minute]: 'm', + [TimeUnit.Hour]: 'h', + [TimeUnit.Day]: 'd', + [TimeUnit.Month]: 'M', + [TimeUnit.Year]: 'y', +}; + +export const getElasticSearchTimeUnit = (scale: TimeUnit): ElasticsearchTimeUnit => + elasticSearchTimeUnits[scale]; diff --git a/x-pack/plugins/infra/common/typed_json.ts b/x-pack/plugins/infra/common/typed_json.ts new file mode 100644 index 0000000000000..ca32bf0b46250 --- /dev/null +++ b/x-pack/plugins/infra/common/typed_json.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonArray extends Array {} + +export interface JsonObject { + [key: string]: JsonValue; +} diff --git a/x-pack/plugins/infra/docs/arch.md b/x-pack/plugins/infra/docs/arch.md new file mode 100644 index 0000000000000..f3d7312a3491d --- /dev/null +++ b/x-pack/plugins/infra/docs/arch.md @@ -0,0 +1,106 @@ +# Adapter Based Architecture + +## Terms + +In this arch, we use 3 main terms to describe the code: + +- **Libs / Domain Libs** - Business logic & data formatting (though complex formatting might call utils) +- **Adapters** - code that directly calls 3rd party APIs and data sources, exposing clean easy to stub APIs +- **Composition Files** - composes adapters into libs based on where the code is running +- **Implementation layer** - The API such as rest endpoints or graphql schema on the server, and the state management / UI on the client + +## Arch Visual Example + +![Arch Visual Overview](/docs/assets/arch.png) + +## Code Guidelines + +### Libs & Domain Libs: + +This term is used to describe the location of business logic. Each use-case in your app should maintain its own lib. + +Now there are 2 types of libs. A "regular lib" would be something like a lib for interacting with Kibana APIs, with something like a parent app adapter. The other is a "domain lib", and would be something like a hosts, or logging lib that might have composed into it an Elasicsearch adapter. + +For the cases on this application, we might have a Logs, Hosts, Containers, Services, ParentApp, and Settings libs, just as an example. Libs should only have 1 Lib per use-case. + +Libs have, composed into them, adapters, as well as access to other libs. The inter-dependencies on other libs and adapters are explicitly expressed in the types of the lib's constructor arguments to provide static type checking and improve testability. In the following example AdapterInterface would define the required interface of an adapter composed into this lib. Likewise LibInterface would declare the inter-dependency this lib has on other libs: + +```ts +new (adapter: AdapterInterface, otherLibs: { lib1: Lib1Interface; lib2: Lib2Interface }): LibInterface +``` + +Libs must not contain code that depends on APIs and behavior specific to the runtime environment. Any such code should be extracted into an adapter. Any code that does not meet this requirement should be inside an adapter. + +### Adapters + +Adapters are the location of any code to interact with any data sources, or 3rd party API / dependency. An example of code that belongs to an adapter would be anything that interacts with Kibana, or Elasticsearch. This would also include things like, for instance, the browser's local storage. + +**The interface exposed by an adapter should be as domain-specific as possible to reduce the risk of leaking abstraction from the "adapted" technology. Therefore a method like `getHosts()` would be preferable to a `runQuery(filterArgs)` method.** This way the output can be well typed and easily stubbed out in an alternate adapter. This will result in vast improvements in testing reliability and code quality. + +Even adapters though should have required dependencies injected into them for as much as is reasonable. Though this is something that is up to the specific adapter as to what is best on a case-by-case basis. + +An app will in most cases have multiple types of each adapter. As an example, a Lib might have an Elasticsearch-backed adapter as well as an adapter backed by an in-memory store, both of which expose the same interface. This way you can compose a lib to use an in-memory adapter to functional or unit tests in order to have isolated tests that are cleaner / faster / more accurate. + +Adapters can at times be composed into another adapter. This behavior though should be kept to a strict minimum. + +**Acceptable:** + +- An Elasticsearch adapter being passed into Hosts, K8, and logging adapters. The Elasticsearch adapter would then never be exposed directly to a lib. + +**Unacceptable:** + +- A K8 adapter being composed into a hosts adapter, but then k8 also being exposed to a lib. + +The former is acceptable only to abstract shared code between adapters. It is clear that this is acceptable because only other adapters use this code. + +The latter being a "code smell" that indicates there is ether too much logic in your adapter that should be in a lib, or the adapters API is insufficient and should be reconsidered. + +### Composition files + +These files will import all libs and their required adapters to instantiate them in the correct order while passing in the respective dependencies. For a contrived but realistic example, a dev_ui composition file that composes an Elasticsearch adapter into Logs, Hosts, and Services libs, and a dev-ui adapter into ParentApp, and a local-storage adapter into Settings. Then another composition file for Kibana might compose other compatible adapters for use with the Kibana APIs. + +composition files simply export a compose method that returns the composed and initialized libs. + +## File structure + +An example structure might be... + +``` +|-- infra-ui + |-- common + | |-- types.ts + | + |-- server + | |-- lib + | | |-- adapters + | | | |-- hosts + | | | | |-- elasticsearch.ts + | | | | |-- fake_data.ts + | | | | + | | | |-- logs + | | | | |-- elasticsearch.ts + | | | | |-- fake_data.ts + | | | | + | | | |-- parent_app + | | | | |-- kibana_angular // if an adapter has more than one file... + | | | | | |-- index.html + | | | | | |-- index.ts + | | | | | + | | | | |-- ui_harness.ts + | | | | + | | |-- domains + | | | |-- hosts.ts + | | | |-- logs.ts + | | | + | | |-- compose + | | | |-- dev.ts + | | | |-- kibana.ts + | | | + | | |-- parent_app.ts // a non-domain lib + | | |-- lib.ts // a file containing lib type defs + |-- public + | | ## SAME STRUCTURE AS SERVER +``` + +Note that in the above adapters have a folder for each adapter type, then inside the implementation of the adapters. The implementation can be a single file, or a directory where index.js is the class that exposes the adapter. +`libs/compose/` contains the composition files diff --git a/x-pack/plugins/infra/docs/arch_client.md b/x-pack/plugins/infra/docs/arch_client.md new file mode 100644 index 0000000000000..2be9de469f0ee --- /dev/null +++ b/x-pack/plugins/infra/docs/arch_client.md @@ -0,0 +1,132 @@ +# Client Architecture + +All rules described in the [server-side architecture documentation](docs/arch.md) apply to the client as well. As shown below, the directory structure additionally accommodates the front-end-specific concepts like components and containers. + +## Apps + +The `apps` folder contains the entry point for the UI code, such as for use in Kibana or testing. + +## Components + +- Components should be stateless wherever possible with pages and containers holding state. +- Small (less than ~30 lines of JSX and less than ~120 lines total) components should simply get their own file. +- If a component gets too large to reason about, and/or needs multiple child components that are only used in this one place, place them all in a folder. +- All components, please use Styled-Components. This also applies to small tweaks to EUI, just use `styled(Component)` and the `attrs` method for always used props. For example: + +```jsx +export const Toolbar = styled(EuiPanel).attrs({ + paddingSize: 'none', + grow: false, +})` + margin: -2px; +`; +``` + +However, components that tweak EUI should go into `/public/components/eui/${componentName}`. + +If using an EUI component that has not yet been typed, types should be placed into `/types/eui.d.ts` + +## Containers (Also: [see GraphQL docs](docs/graphql.md)) + +- HOC's based on Apollo. +- One folder per data type e.g. `host`. Folder name should be singular. +- One file per query type. + +## Pages + +- Ideally one file per page, if more files are needed, move into folder containing the page and a layout file. +- Pages are class based components. +- Should hold most state, and any additional route logic. +- Are the only files where components are wrapped by containers. For example: + +```jsx +// Simple usage +const FancyLogPage = withSearchResults(class FancyLogPage extends React.Component { + render() { + return ( + <> + + + + <> + ); + } +}); +``` + +OR, for more complex scenarios: + +```jsx +// Advanced usage +const ConnectedToolbar = compose( + withTimeMutation, + withCurrentTime +)(Toolbar); + +const ConnectedLogView = compose( + withLogEntries, + withSearchResults, +)(LogView); + +const ConnectedSearchBar = compose( + withSearchMutation +)(SearchBar); + +interface FancyLogPageProps {} + +class FancyLogPage extends React.Component { + render() { + return ( + <> + + + + <> + ); + } +}; +``` + +## Transforms + +- If you need to do some complex data transforms, it is better to put them here than in a utility or lib. Simpler transforms are probably easier to keep in a container. +- One file per transform + +## File structure + +``` +|-- infra-ui + |-- common + | |-- types.ts + | + |-- public + | |-- components // + | | |-- eui // staging area for eui customizations before pushing upstream + | | |-- layout // any layout components should be placed in here + | | |-- button.tsx + | | |-- mega_table // Where mega table is used directly with a data prop, not a composable table + | | |-- index.ts + | | |-- row.tsx + | | |-- table.tsx + | | |-- cell.tsx + | | + | |-- containers + | | |-- host + | | | |-- index.ts + | | | |-- with_all_hosts.ts + | | | |-- transforms + | | | |-- hosts_to_waffel.ts + | | | + | | |-- pod + | | |-- index.ts + | | |-- with_all_pods.ts + | | + | |-- pages + | | |-- home.tsx // the initial page of a plugin is always the `home` page + | | |-- hosts.tsx + | | |-- logging.tsx + | | + | |-- utils // utils folder for what utils folders are for ;) + | | + | |-- lib // covered in [Our code and arch](docs/arch.md) +``` diff --git a/x-pack/plugins/infra/docs/assets/arch.png b/x-pack/plugins/infra/docs/assets/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..878c7d1aa16d44d169cf454269a3f650a99112df GIT binary patch literal 69550 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

??CNcQ%B9@Mgs-3)JySwag`A5mCLGbF+woYq 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/infra/docs/graphql.md b/x-pack/plugins/infra/docs/graphql.md new file mode 100644 index 0000000000000..5584a5ce7c0d1 --- /dev/null +++ b/x-pack/plugins/infra/docs/graphql.md @@ -0,0 +1,53 @@ +# GraphQL In Infra UI + +- The combined graphql schema collected from both the `public` and `server` directories is exported to `common/all.gql_schema.ts` for the purpose of automatic type generation only. + +## Server + +- Under `/server/graphql` there are files for each domain of data's graph schema and resolvers. + - Each file has 2 exports `${domain}Schema` e.g. `fieldsSchema`, and `create${domain}Resolvers` e.g. `createFieldResolvers` +- `/server/infra_server.ts` imports all schema and resolvers and passing the full schema to the server +- Resolvers should be used to call composed libs, rather than directly performing any meaningful amount of data processing. +- Resolvers should, however, only pass the required data into libs; that is to say all args for example would not be passed into a lib unless all were needed. + +## Client + +- Under `/public/containers/${domain}/` there is a file for each container. Each file has two exports, the query name e.g. `AllHosts` and the apollo HOC in the pattern of `with${queryName}` e.g. `withAllHosts`. This is done for two reasons: + + 1. It makes the code uniform, thus easier to reason about later. + 2. If reformatting the data using a transform, it lets us re-type the data clearly. + +- Containers should use the apollo props callback to pass ONLY the props and data needed to children. e.g. + + ```ts + import { Hosts, Pods, HostsAndPods } from '../../common/types'; + + // used to generate the `HostsAndPods` type imported above + export const hostsAndPods = gql` + # ... + `; + + type HostsAndPodsProps = { + hosts: Hosts; + pods: Pods; + } + + export const withHostsAndPods = graphql< + {}, + HostsAndPods.Query, + HostsAndPods.Variables, + HostsAndPodsProps + >(hostsAndPods, { + props: ({ data, ownProps }) => ({ + hosts: hostForMap(data && data.hosts ? data.hosts : []), +  pods: podsFromHosts(data && data.hosts ? data.hosts : []) + ...ownProps, + }), + }); + ``` + + as `ownProps` are the props passed to the wrapped component, they should just be forwarded. + +## Types + +- The command `yarn build-graphql-types` derives the schema, query and mutation types and stores them in `common/types.ts` for use on both the client and server. diff --git a/x-pack/plugins/infra/index.ts b/x-pack/plugins/infra/index.ts new file mode 100644 index 0000000000000..c8cdfa4d9df66 --- /dev/null +++ b/x-pack/plugins/infra/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import JoiNamespace from 'joi'; +import { resolve } from 'path'; + +import { getConfigSchema, initServerWithKibana, KbnServer } from './server/kibana.index'; + +const APP_ID = 'infra'; + +export function infra(kibana: any) { + return new kibana.Plugin({ + id: APP_ID, + configPrefix: 'xpack.infra', + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch'], + uiExports: { + app: { + description: 'Explore your infrastructure', + icon: 'plugins/infra/images/infra_mono_white.svg', + main: 'plugins/infra/app', + title: 'InfraOps', + listed: false, + url: `/app/${APP_ID}#/home`, + }, + home: ['plugins/infra/register_feature'], + links: [ + { + description: 'Explore your infrastructure', + icon: 'plugins/infra/images/infra_mono_white.svg', + id: 'infra:home', + order: 8000, + title: 'InfraOps', + url: `/app/${APP_ID}#/home`, + }, + { + description: 'Explore your logs', + icon: 'plugins/infra/images/logging_mono_white.svg', + id: 'infra:logs', + order: 8001, + title: 'Logs', + url: `/app/${APP_ID}#/logs`, + }, + ], + }, + config(Joi: typeof JoiNamespace) { + return getConfigSchema(Joi); + }, + init(server: KbnServer) { + initServerWithKibana(server); + }, + }); +} diff --git a/x-pack/plugins/infra/package.json b/x-pack/plugins/infra/package.json new file mode 100644 index 0000000000000..ce926e897b681 --- /dev/null +++ b/x-pack/plugins/infra/package.json @@ -0,0 +1,17 @@ +{ + "author": "Elastic", + "name": "infra", + "version": "7.0.0-alpha1", + "scripts": { + "build-graphql-types": "node scripts/generate_types_from_graphql.js" + }, + "devDependencies": { + "@types/boom": "3.2.2", + "@types/lodash": "^4.14.110" + }, + "dependencies": { + "@types/color": "^3.0.0", + "boom": "3.1.1", + "lodash": "^4.17.10" + } +} diff --git a/x-pack/plugins/infra/public/app.ts b/x-pack/plugins/infra/public/app.ts new file mode 100644 index 0000000000000..255c51c9e48ce --- /dev/null +++ b/x-pack/plugins/infra/public/app.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './apps/kibana_app'; diff --git a/x-pack/plugins/infra/public/apps/kibana_app.ts b/x-pack/plugins/infra/public/apps/kibana_app.ts new file mode 100644 index 0000000000000..ac705801175f3 --- /dev/null +++ b/x-pack/plugins/infra/public/apps/kibana_app.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import 'uiExports/autocompleteProviders'; + +import { compose } from '../lib/compose/kibana_compose'; +import { startApp } from './start_app'; +startApp(compose()); diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx new file mode 100644 index 0000000000000..fca8d80153dfc --- /dev/null +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHashHistory } from 'history'; +import React from 'react'; +import { ApolloProvider } from 'react-apollo'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { BehaviorSubject } from 'rxjs'; +import { pluck } from 'rxjs/operators'; +import { ThemeProvider } from 'styled-components'; + +// TODO use theme provided from parentApp when kibana supports it +import { EuiErrorBoundary } from '@elastic/eui'; +import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; +import '@elastic/eui/dist/eui_theme_light.css'; +import { InfraFrontendLibs } from '../lib/lib'; +import { PageRouter } from '../routes'; +import { createStore } from '../store'; + +export async function startApp(libs: InfraFrontendLibs) { + const history = createHashHistory(); + + const libs$ = new BehaviorSubject(libs); + const store = createStore({ + apolloClient: libs$.pipe(pluck('apolloClient')), + observableApi: libs$.pipe(pluck('observableApi')), + }); + + libs.framework.render( + + + + + + + + + + ); +} diff --git a/x-pack/plugins/infra/public/apps/testing_app.ts b/x-pack/plugins/infra/public/apps/testing_app.ts new file mode 100644 index 0000000000000..bcd7d0e592644 --- /dev/null +++ b/x-pack/plugins/infra/public/apps/testing_app.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compose } from '../lib/compose/testing_compose'; +import { startApp } from './start_app'; +startApp(compose()); diff --git a/x-pack/plugins/infra/public/components/auto_sizer.tsx b/x-pack/plugins/infra/public/components/auto_sizer.tsx new file mode 100644 index 0000000000000..674a54338dcf1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/auto_sizer.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import isEqual from 'lodash/fp/isEqual'; +import React from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; + +interface Measurement { + width?: number; + height?: number; +} + +interface Measurements { + bounds: Measurement; + content: Measurement; +} + +interface AutoSizerProps { + detectAnyWindowResize?: boolean; + bounds?: boolean; + content?: boolean; + onResize?: (size: Measurements) => void; + children: ( + args: { measureRef: (instance: HTMLElement | null) => any } & Measurements + ) => React.ReactNode; +} + +interface AutoSizerState { + boundsMeasurement: Measurement; + contentMeasurement: Measurement; +} + +export class AutoSizer extends React.PureComponent { + public element: HTMLElement | null = null; + public resizeObserver: ResizeObserver | null = null; + public windowWidth: number = -1; + + public readonly state = { + boundsMeasurement: { + height: void 0, + width: void 0, + }, + contentMeasurement: { + height: void 0, + width: void 0, + }, + }; + + constructor(props: AutoSizerProps) { + super(props); + if (this.props.detectAnyWindowResize) { + window.addEventListener('resize', this.updateMeasurement); + } + this.resizeObserver = new ResizeObserver(entries => { + entries.forEach(entry => { + if (entry.target === this.element) { + this.measure(entry); + } + }); + }); + } + + public componentWillUnmount() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + if (this.props.detectAnyWindowResize) { + window.removeEventListener('resize', this.updateMeasurement); + } + } + + public measure = (entry: ResizeObserverEntry | null) => { + if (!this.element) { + return; + } + + const { content = true, bounds = false } = this.props; + const { + boundsMeasurement: previousBoundsMeasurement, + contentMeasurement: previousContentMeasurement, + } = this.state; + + const boundsRect = bounds ? this.element.getBoundingClientRect() : null; + const boundsMeasurement = boundsRect + ? { + height: this.element.getBoundingClientRect().height, + width: this.element.getBoundingClientRect().width, + } + : previousBoundsMeasurement; + + if ( + this.props.detectAnyWindowResize && + boundsMeasurement && + boundsMeasurement.width && + this.windowWidth !== -1 && + this.windowWidth > window.innerWidth + ) { + const gap = this.windowWidth - window.innerWidth; + boundsMeasurement.width = boundsMeasurement.width - gap; + } + this.windowWidth = window.innerWidth; + const contentRect = content && entry ? entry.contentRect : null; + const contentMeasurement = + contentRect && entry + ? { + height: entry.contentRect.height, + width: entry.contentRect.width, + } + : previousContentMeasurement; + + if ( + isEqual(boundsMeasurement, previousBoundsMeasurement) && + isEqual(contentMeasurement, previousContentMeasurement) + ) { + return; + } + + requestAnimationFrame(() => { + if (!this.resizeObserver) { + return; + } + + this.setState({ boundsMeasurement, contentMeasurement }); + + if (this.props.onResize) { + this.props.onResize({ + bounds: boundsMeasurement, + content: contentMeasurement, + }); + } + }); + }; + + public render() { + const { children } = this.props; + const { boundsMeasurement, contentMeasurement } = this.state; + + return children({ + bounds: boundsMeasurement, + content: contentMeasurement, + measureRef: this.storeRef, + }); + } + + private updateMeasurement = () => { + window.setTimeout(() => { + this.measure(null); + }, 0); + }; + + private storeRef = (element: HTMLElement | null) => { + if (this.element && this.resizeObserver) { + this.resizeObserver.unobserve(this.element); + } + + if (element && this.resizeObserver) { + this.resizeObserver.observe(element); + } + + this.element = element; + }; +} diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx new file mode 100644 index 0000000000000..982de13ad9685 --- /dev/null +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -0,0 +1,305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFieldSearch, + EuiFieldSearchProps, + EuiOutsideClickDetector, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +import { composeStateUpdaters } from '../../utils/typed_react'; +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: AutocompleteSuggestion[]; + value: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.Component< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Escape': + evt.preventDefault(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); + }; +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, +}); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +const FixedEuiFieldSearch: React.SFC< + React.InputHTMLAttributes & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + } +> = EuiFieldSearch as any; + +const AutocompleteContainer = styled.div` + position: relative; +`; + +const SuggestionsPanel = styled(EuiPanel).attrs({ + paddingSize: 'none', + hasShadow: true, +})` + position: absolute; + width: 100%; + margin-top: 2px; + overflow: hidden; +`; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/index.ts b/x-pack/plugins/infra/public/components/autocomplete_field/index.ts new file mode 100644 index 0000000000000..16def61958d78 --- /dev/null +++ b/x-pack/plugins/infra/public/components/autocomplete_field/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './autocomplete_field'; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx new file mode 100644 index 0000000000000..09112ede6fa11 --- /dev/null +++ b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon } from '@elastic/eui'; +import { tint } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import { AutocompleteSuggestion } from 'ui/autocomplete_providers'; + +interface SuggestionItemProps { + isSelected?: boolean; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + suggestion: AutocompleteSuggestion; +} + +export class SuggestionItem extends React.Component { + public static defaultProps: Partial = { + isSelected: false, + }; + + public render() { + const { isSelected, onClick, onMouseEnter, suggestion } = this.props; + + return ( + + + + + {suggestion.text} + + + ); + } +} + +const SuggestionItemContainer = styled.div<{ + isSelected?: boolean; +}>` + display: flex; + flex-direction: row; + font-size: ${props => props.theme.eui.euiFontSizeS}; + height: ${props => props.theme.eui.euiSizeXl}; + white-space: nowrap; + background-color: ${props => + props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; +`; + +const SuggestionItemField = styled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: ${props => props.theme.eui.euiSizeXl}; + padding: ${props => props.theme.eui.euiSizeXs}; +`; + +const SuggestionItemIconField = SuggestionItemField.extend<{ suggestionType: string }>` + background-color: ${props => tint(0.1, getEuiIconColor(props.theme, props.suggestionType))}; + color: ${props => getEuiIconColor(props.theme, props.suggestionType)}; + flex: 0 0 auto; + justify-content: center; + width: ${props => props.theme.eui.euiSizeXl}; +`; + +const SuggestionItemTextField = SuggestionItemField.extend` + flex: 2 0 0; + font-family: ${props => props.theme.eui.euiCodeFontFamily}; +`; + +const SuggestionItemDescriptionField = SuggestionItemField.extend` + flex: 3 0 0; + + p { + display: inline; + + span { + font-family: ${props => props.theme.eui.euiCodeFontFamily}; + } + } +`; + +const getEuiIconType = (suggestionType: string) => { + switch (suggestionType) { + case 'field': + return 'kqlField'; + case 'value': + return 'kqlValue'; + case 'recentSearch': + return 'search'; + case 'conjunction': + return 'kqlSelector'; + case 'operator': + return 'kqlOperand'; + default: + return 'empty'; + } +}; + +const getEuiIconColor = (theme: any, suggestionType: string): string => { + switch (suggestionType) { + case 'field': + return theme.eui.euiColorVis7; + case 'value': + return theme.eui.euiColorVis0; + case 'operator': + return theme.eui.euiColorVis1; + case 'conjunction': + return theme.eui.euiColorVis2; + case 'recentSearch': + default: + return theme.eui.euiColorMediumShade; + } +}; diff --git a/x-pack/plugins/infra/public/components/empty_page.tsx b/x-pack/plugins/infra/public/components/empty_page.tsx new file mode 100644 index 0000000000000..ed29ad2e6f75f --- /dev/null +++ b/x-pack/plugins/infra/public/components/empty_page.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; + +interface EmptyPageProps { + message: string; + title: string; + actionLabel: string; + actionUrl: string; +} + +export const EmptyPage: React.SFC = ({ + actionLabel, + actionUrl, + message, + title, +}) => ( + {title}} + body={

{message}

} + actions={ + + {actionLabel} + + } + /> +); diff --git a/x-pack/plugins/infra/public/components/eui/index.ts b/x-pack/plugins/infra/public/components/eui/index.ts new file mode 100644 index 0000000000000..f1623ce98c727 --- /dev/null +++ b/x-pack/plugins/infra/public/components/eui/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Toolbar } from './toolbar'; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/index.ts b/x-pack/plugins/infra/public/components/eui/toolbar/index.ts new file mode 100644 index 0000000000000..f1623ce98c727 --- /dev/null +++ b/x-pack/plugins/infra/public/components/eui/toolbar/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Toolbar } from './toolbar'; diff --git a/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx new file mode 100644 index 0000000000000..9d7005091bd75 --- /dev/null +++ b/x-pack/plugins/infra/public/components/eui/toolbar/toolbar.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; + +import styled from 'styled-components'; + +export const Toolbar = styled(EuiPanel).attrs({ + grow: false, + paddingSize: 'none', +})` + border-top: none; + border-right: none; + border-left: none; + border-radius: 0; + padding: ${props => props.theme.eui.euiSizeS} ${props => props.theme.eui.euiSizeL}; + z-index: 1; +`; diff --git a/x-pack/plugins/infra/public/components/header.tsx b/x-pack/plugins/infra/public/components/header.tsx new file mode 100644 index 0000000000000..c9e93ecec7219 --- /dev/null +++ b/x-pack/plugins/infra/public/components/header.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBreadcrumbDefinition, + EuiHeader, + EuiHeaderBreadcrumbs, + EuiHeaderSection, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +interface HeaderProps { + breadcrumbs?: EuiBreadcrumbDefinition[]; +} + +export class Header extends React.PureComponent { + private staticBreadcrumbs = [ + { + href: '#/', + text: 'InfraOps', + }, + ]; + + public render() { + const { breadcrumbs = [] } = this.props; + + return ( + + + + + + ); + } +} + +const HeaderWrapper = styled(EuiHeader)` + height: 29px; +`; diff --git a/x-pack/plugins/infra/public/components/loading/index.tsx b/x-pack/plugins/infra/public/components/loading/index.tsx new file mode 100644 index 0000000000000..143ef5b2ece25 --- /dev/null +++ b/x-pack/plugins/infra/public/components/loading/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiLoadingChart, EuiPanel, EuiText } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +interface InfraLoadingProps { + text: string; + height: number | string; + width: number | string; +} + +export class InfraLoadingPanel extends React.PureComponent { + public render() { + const { height, text, width } = this.props; + return ( + + + + + +

{text}

+
+
+
+
+ ); + } +} + +export const InfraLoadingStaticPanel = styled.div` + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; +`; + +export const InfraLoadingStaticContentPanel = styled.div` + flex: 0 0 auto; + align-self: center; + text-align: center; +`; diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx new file mode 100644 index 0000000000000..606f1fb69aa55 --- /dev/null +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; +import React from 'react'; + +import { FlexPage } from './page'; + +interface LoadingPageProps { + message?: string; +} + +export const LoadingPage = ({ message }: LoadingPageProps) => ( + + + + + + + + {message} + + + + +); diff --git a/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx new file mode 100644 index 0000000000000..2e444460fff9e --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_customization_menu.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +interface LogCustomizationMenuState { + isShown: boolean; +} + +export class LogCustomizationMenu extends React.Component<{}, LogCustomizationMenuState> { + public readonly state = { + isShown: false, + }; + + public show = () => { + this.setState({ + isShown: true, + }); + }; + + public hide = () => { + this.setState({ + isShown: false, + }); + }; + + public toggleVisibility = () => { + this.setState(state => ({ + isShown: !state.isShown, + })); + }; + + public render() { + const { children } = this.props; + const { isShown } = this.state; + + const menuButton = ( + + Customize + + ); + + return ( + + {children} + + ); + } +} + +const CustomizationMenuContent = styled.div` + min-width: 200px; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx new file mode 100644 index 0000000000000..bf524678e7f2b --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/density_chart.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { scaleLinear, scaleTime } from 'd3-scale'; +import { area, curveMonotoneY } from 'd3-shape'; +import max from 'lodash/fp/max'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { SummaryBucket } from './types'; + +interface DensityChartProps { + buckets: SummaryBucket[]; + end: number; + start: number; + width: number; + height: number; +} + +export const DensityChart: React.SFC = ({ + buckets, + start, + end, + width, + height, +}) => { + if (start >= end || height <= 0 || width <= 0 || buckets.length <= 0) { + return null; + } + + const yScale = scaleTime() + .domain([start, end]) + .range([0, height]); + + const xMax = max(buckets.map(bucket => bucket.entriesCount)) || 0; + const xScale = scaleLinear() + .domain([0, xMax]) + .range([0, width / 2]); + + const path = area() + .x0(xScale(0)) + .x1(bucket => xScale(bucket.entriesCount)) + .y(bucket => yScale((bucket.start + bucket.end) / 2)) + .curve(curveMonotoneY); + const pathData = path(buckets); + + return ( + + + + + ); +}; + +const PositiveAreaPath = styled.path` + fill: ${props => props.theme.eui.euiColorLightShade}; +`; + +const NegativeAreaPath = styled.path` + fill: ${props => props.theme.eui.euiColorLightestShade}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx new file mode 100644 index 0000000000000..67981d1b5e9bf --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/highlighted_interval.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import styled from 'styled-components'; + +interface HighlightedIntervalProps { + className?: string; + getPositionOfTime: (time: number) => number; + start: number; + end: number; + width: number; +} + +export const HighlightedInterval: React.SFC = ({ + className, + end, + getPositionOfTime, + start, + width, +}) => { + const yStart = getPositionOfTime(start); + const yEnd = getPositionOfTime(end); + + return ( + + ); +}; + +HighlightedInterval.displayName = 'HighlightedInterval'; + +const HighlightPolygon = styled.polygon` + fill: ${props => props.theme.eui.euiColorPrimary}; + fill-opacity: 0.3; + stroke: ${props => props.theme.eui.euiColorPrimary}; + stroke-width: 1; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/index.ts b/x-pack/plugins/infra/public/components/logging/log_minimap/index.ts new file mode 100644 index 0000000000000..8798a5fd022f2 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LogMinimap } from './log_minimap'; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx new file mode 100644 index 0000000000000..85f47cd4eb1e1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/log_minimap.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { scaleLinear } from 'd3-scale'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { LogEntryTime } from '../../../../common/log_entry'; +// import { SearchSummaryBucket } from '../../../../common/log_search_summary'; +import { DensityChart } from './density_chart'; +import { HighlightedInterval } from './highlighted_interval'; +// import { SearchMarkers } from './search_markers'; +import { TimeRuler } from './time_ruler'; +import { SummaryBucket } from './types'; + +interface LogMinimapProps { + className?: string; + height: number; + highlightedInterval: { + end: number; + start: number; + } | null; + jumpToTarget: (params: LogEntryTime) => any; + reportVisibleInterval: ( + params: { + start: number; + end: number; + bucketsOnPage: number; + pagesBeforeStart: number; + pagesAfterEnd: number; + } + ) => any; + intervalSize: number; + summaryBuckets: SummaryBucket[]; + // searchSummaryBuckets?: SearchSummaryBucket[]; + target: number | null; + width: number; +} + +export class LogMinimap extends React.Component { + public handleClick: React.MouseEventHandler = event => { + const svgPosition = event.currentTarget.getBoundingClientRect(); + const clickedYPosition = event.clientY - svgPosition.top; + const clickedTime = Math.floor(this.getYScale().invert(clickedYPosition)); + + this.props.jumpToTarget({ + tiebreaker: 0, + time: clickedTime, + }); + }; + + public getYScale = () => { + const { height, intervalSize, target } = this.props; + + const domainStart = target ? target - intervalSize / 2 : 0; + const domainEnd = target ? target + intervalSize / 2 : 0; + return scaleLinear() + .domain([domainStart, domainEnd]) + .range([0, height]); + }; + + public getPositionOfTime = (time: number) => { + const { height, intervalSize } = this.props; + + const [minTime] = this.getYScale().domain(); + + return ((time - minTime) * height) / intervalSize; + }; + + public updateVisibleInterval = () => { + const { summaryBuckets, intervalSize } = this.props; + const [minTime, maxTime] = this.getYScale().domain(); + + const firstBucket = summaryBuckets[0]; + const lastBucket = summaryBuckets[summaryBuckets.length - 1]; + + const pagesBeforeStart = firstBucket ? (minTime - firstBucket.start) / intervalSize : 0; + const pagesAfterEnd = lastBucket ? (lastBucket.end - maxTime) / intervalSize : 0; + const bucketsOnPage = firstBucket + ? (maxTime - minTime) / (firstBucket.end - firstBucket.start) + : 0; + + this.props.reportVisibleInterval({ + end: Math.ceil(maxTime), + start: Math.floor(minTime), + bucketsOnPage, + pagesBeforeStart, + pagesAfterEnd, + }); + }; + + public componentDidUpdate(prevProps: LogMinimapProps) { + const hasNewTarget = prevProps.target !== this.props.target; + const hasNewIntervalSize = prevProps.intervalSize !== this.props.intervalSize; + + if (hasNewTarget || hasNewIntervalSize) { + this.updateVisibleInterval(); + } + } + + public render() { + const { + className, + height, + highlightedInterval, + // jumpToTarget, + summaryBuckets, + // searchSummaryBuckets, + width, + } = this.props; + + const [minTime, maxTime] = this.getYScale().domain(); + + return ( + + + + + + {highlightedInterval ? ( + + ) : null} + {/* + + */} + + ); + } +} + +const MinimapBackground = styled.rect` + fill: ${props => props.theme.eui.euiColorLightestShade}; +`; + +const MinimapBorder = styled.line` + stroke: ${props => props.theme.eui.euiColorMediumShade}; + stroke-width: 1px; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx new file mode 100644 index 0000000000000..70cfc5ddac799 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import styled, { keyframes } from 'styled-components'; + +import { LogEntryTime } from '../../../../common/log_entry'; +import { SearchSummaryBucket } from '../../../../common/log_search_summary'; +import { SearchMarkerTooltip } from './search_marker_tooltip'; + +interface SearchMarkerProps { + bucket: SearchSummaryBucket; + height: number; + width: number; + jumpToTarget: (target: LogEntryTime) => void; +} + +interface SearchMarkerState { + hoveredPosition: ClientRect | null; +} + +export class SearchMarker extends React.PureComponent { + public readonly state = { + hoveredPosition: null, + }; + + public handleClick: React.MouseEventHandler = evt => { + evt.stopPropagation(); + + this.props.jumpToTarget(this.props.bucket.representative.fields); + }; + + public handleMouseEnter: React.MouseEventHandler = evt => { + this.setState({ + hoveredPosition: evt.currentTarget.getBoundingClientRect(), + }); + }; + + public handleMouseLeave: React.MouseEventHandler = () => { + this.setState({ + hoveredPosition: null, + }); + }; + + public render() { + const { bucket, height, width } = this.props; + const { hoveredPosition } = this.state; + + const bulge = + bucket.count > 1 ? ( + + ) : ( + <> + + + + ); + + return ( + <> + {hoveredPosition ? ( + + {bucket.count} {bucket.count === 1 ? 'search result' : 'search results'} + + ) : null} + + + {bulge} + + + ); + } +} + +const fadeInAnimation = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const SearchMarkerGroup = styled.g` + animation: ${fadeInAnimation} ${props => props.theme.eui.euiAnimSpeedExtraSlow} ease-in both; +`; + +const SearchMarkerBackgroundRect = styled.rect` + fill: ${props => props.theme.eui.euiColorSecondary}; + opacity: 0; + transition: opacity ${props => props.theme.eui.euiAnimSpeedNormal} ease-in; + + ${SearchMarkerGroup}:hover & { + opacity: 0.2; + } +`; + +const SearchMarkerForegroundRect = styled.rect` + fill: ${props => props.theme.eui.euiColorSecondary}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx new file mode 100644 index 0000000000000..1f995947dfd4e --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/search_marker_tooltip.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { calculatePopoverPosition, EuiPortal } from '@elastic/eui'; +import * as React from 'react'; + +import { AutoSizer } from '../../auto_sizer'; + +interface SearchMarkerTooltipProps { + markerPosition: ClientRect; +} + +export class SearchMarkerTooltip extends React.PureComponent { + public render() { + const { children, markerPosition } = this.props; + + return ( + +
+ + {({ measureRef, bounds: { width, height } }) => { + const { top, left } = + width && height + ? calculatePopoverPosition(markerPosition, { width, height }, 'left', 16, [ + 'left', + ]) + : { + left: -9999, // render off-screen before the first measurement + top: 0, + }; + + return ( +
+ {children} +
+ ); + }} +
+
+
+ ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/search_markers.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/search_markers.tsx new file mode 100644 index 0000000000000..8ad3947cc5a23 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/search_markers.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import classNames from 'classnames'; +import { scaleTime } from 'd3-scale'; +import * as React from 'react'; + +import { LogEntryTime } from '../../../../common/log_entry'; +import { SearchSummaryBucket } from '../../../../common/log_search_summary'; +import { SearchMarker } from './search_marker'; + +interface SearchMarkersProps { + buckets: SearchSummaryBucket[]; + className?: string; + end: number; + start: number; + width: number; + height: number; + jumpToTarget: (target: LogEntryTime) => void; +} + +export class SearchMarkers extends React.PureComponent { + public render() { + const { buckets, start, end, width, height, jumpToTarget, className } = this.props; + const classes = classNames('minimapSearchMarkers', className); + + if (start >= end || height <= 0 || Object.keys(buckets).length <= 0) { + return null; + } + + const yScale = scaleTime() + .domain([start, end]) + .range([0, height]); + + return ( + + {buckets.map(bucket => ( + + + + ))} + + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx new file mode 100644 index 0000000000000..0577dba01e09b --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/time_ruler.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { scaleTime } from 'd3-scale'; +import * as React from 'react'; +import styled from 'styled-components'; + +interface TimeRulerProps { + end: number; + height: number; + start: number; + tickCount: number; + width: number; +} + +export const TimeRuler: React.SFC = ({ end, height, start, tickCount, width }) => { + const yScale = scaleTime() + .domain([start, end]) + .range([0, height]); + + const ticks = yScale.ticks(tickCount); + const formatTick = yScale.tickFormat(); + + return ( + + {ticks.map((tick, tickIndex) => { + const y = yScale(tick); + return ( + + + {formatTick(tick)} + + + + ); + })} + + ); +}; + +TimeRuler.displayName = 'TimeRuler'; + +const TimeRulerTickLabel = styled.text` + font-size: ${props => props.theme.eui.euiFontSizeXs}; + line-height: ${props => props.theme.eui.euiLineHeight}; + color: ${props => props.theme.eui.euiTextColor}; +`; + +const TimeRulerGridLine = styled.line` + stroke: ${props => props.theme.eui.euiColorMediumShade}; + stroke-dasharray: 2, 2; + stroke-opacity: 0.5; + stroke-width: 1px; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap/types.ts b/x-pack/plugins/infra/public/components/logging/log_minimap/types.ts new file mode 100644 index 0000000000000..ac3ea48bc4b16 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface SummaryBucket { + start: number; + end: number; + entriesCount: number; +} diff --git a/x-pack/plugins/infra/public/components/logging/log_minimap_scale_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_minimap_scale_controls.tsx new file mode 100644 index 0000000000000..b195fd3af005a --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_minimap_scale_controls.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow, EuiRadioGroup } from '@elastic/eui'; +import * as React from 'react'; + +interface IntervalSizeDescriptor { + label: string; + intervalSize: number; +} + +interface LogMinimapScaleControlsProps { + availableIntervalSizes: IntervalSizeDescriptor[]; + intervalSize: number; + setIntervalSize: (intervalSize: number) => any; +} + +export class LogMinimapScaleControls extends React.PureComponent { + public handleScaleChange = (intervalSizeDescriptorKey: string) => { + const { availableIntervalSizes, setIntervalSize } = this.props; + const [sizeDescriptor] = availableIntervalSizes.filter( + intervalKeyEquals(intervalSizeDescriptorKey) + ); + + if (sizeDescriptor) { + setIntervalSize(sizeDescriptor.intervalSize); + } + }; + + public render() { + const { availableIntervalSizes, intervalSize } = this.props; + const [currentSizeDescriptor] = availableIntervalSizes.filter(intervalSizeEquals(intervalSize)); + + return ( + + ({ + id: getIntervalSizeDescriptorKey(sizeDescriptor), + label: sizeDescriptor.label, + }))} + onChange={this.handleScaleChange} + idSelected={getIntervalSizeDescriptorKey(currentSizeDescriptor)} + /> + + ); + } +} + +const getIntervalSizeDescriptorKey = (sizeDescriptor: IntervalSizeDescriptor) => + `${sizeDescriptor.intervalSize}`; + +const intervalKeyEquals = (key: string) => (sizeDescriptor: IntervalSizeDescriptor) => + getIntervalSizeDescriptorKey(sizeDescriptor) === key; + +const intervalSizeEquals = (size: number) => (sizeDescriptor: IntervalSizeDescriptor) => + sizeDescriptor.intervalSize === size; diff --git a/x-pack/plugins/infra/public/components/logging/log_search_controls/index.ts b/x-pack/plugins/infra/public/components/logging/log_search_controls/index.ts new file mode 100644 index 0000000000000..bc2800f18e2b9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_search_controls/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LogSearchControls } from './log_search_controls'; diff --git a/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx new file mode 100644 index 0000000000000..5c24a36fac669 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_buttons.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import * as React from 'react'; + +import { LogEntryTime } from '../../../../common/log_entry'; + +interface LogSearchButtonsProps { + className?: string; + jumpToTarget: (target: LogEntryTime) => void; + previousSearchResult: LogEntryTime | null; + nextSearchResult: LogEntryTime | null; +} + +export class LogSearchButtons extends React.PureComponent { + public handleJumpToPreviousSearchResult: React.MouseEventHandler = () => { + const { jumpToTarget, previousSearchResult } = this.props; + + if (previousSearchResult) { + jumpToTarget(previousSearchResult); + } + }; + + public handleJumpToNextSearchResult: React.MouseEventHandler = () => { + const { jumpToTarget, nextSearchResult } = this.props; + + if (nextSearchResult) { + jumpToTarget(nextSearchResult); + } + }; + + public render() { + const { className, previousSearchResult, nextSearchResult } = this.props; + + const classes = classNames('searchButtons', className); + const hasPreviousSearchResult = !!previousSearchResult; + const hasNextSearchResult = !!nextSearchResult; + + return ( + + + + Previous + + + + + Next + + + + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx new file mode 100644 index 0000000000000..d61fde2aaa881 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_controls.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import * as React from 'react'; + +import { LogEntryTime } from '../../../../common/log_entry'; +import { LogSearchButtons } from './log_search_buttons'; +import { LogSearchInput } from './log_search_input'; + +interface LogSearchControlsProps { + className?: string; + clearSearch: () => any; + isLoadingSearchResults: boolean; + previousSearchResult: LogEntryTime | null; + nextSearchResult: LogEntryTime | null; + jumpToTarget: (target: LogEntryTime) => any; + search: (query: string) => any; +} + +export class LogSearchControls extends React.PureComponent { + public render() { + const { + className, + clearSearch, + isLoadingSearchResults, + previousSearchResult, + nextSearchResult, + jumpToTarget, + search, + } = this.props; + + const classes = classNames('searchControls', className); + + return ( + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx new file mode 100644 index 0000000000000..95f4b7bdfc65b --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_search_controls/log_search_input.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFieldSearch } from '@elastic/eui'; +import classNames from 'classnames'; +import * as React from 'react'; +import styled from 'styled-components'; + +interface LogSearchInputProps { + className?: string; + isLoading: boolean; + onSearch: (query: string) => void; + onClear: () => void; +} + +interface LogSearchInputState { + query: string; +} + +export class LogSearchInput extends React.PureComponent { + public readonly state = { + query: '', + }; + + public handleSubmit: React.FormEventHandler = evt => { + evt.preventDefault(); + + const { query } = this.state; + + if (query === '') { + this.props.onClear(); + } else { + this.props.onSearch(this.state.query); + } + }; + + public handleChangeQuery: React.ChangeEventHandler = evt => { + this.setState({ + query: evt.target.value, + }); + }; + + public render() { + const { className, isLoading } = this.props; + const { query } = this.state; + + const classes = classNames('loggingSearchInput', className); + + return ( +
+ + + ); + } +} + +const PlainSearchField = styled(EuiFieldSearch)` + background: transparent; + box-shadow: none; + + &:focus { + box-shadow: inset 0 -2px 0 0 ${props => props.theme.eui.euiColorPrimary}; + } +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx b/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx new file mode 100644 index 0000000000000..95749c62d0756 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_statusbar.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const LogStatusbar = styled(EuiFlexGroup).attrs({ + alignItems: 'center', + gutterSize: 'none', + justifyContent: 'flexEnd', +})` + padding: ${props => props.theme.eui.euiSizeS}; + border-top: ${props => props.theme.eui.euiBorderThin}; + max-height: 48px; + min-height: 48px; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + flex-direction: row; +`; + +export const LogStatusbarItem = EuiFlexItem; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_scale_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_text_scale_controls.tsx new file mode 100644 index 0000000000000..089142891a36e --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_scale_controls.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow, EuiRadioGroup } from '@elastic/eui'; +import * as React from 'react'; + +import { getLabelOfTextScale, isTextScale, TextScale } from '../../../common/log_text_scale'; + +interface LogTextScaleControlsProps { + availableTextScales: TextScale[]; + textScale: TextScale; + setTextScale: (scale: TextScale) => any; +} + +export class LogTextScaleControls extends React.PureComponent { + public setTextScale = (textScale: string) => { + if (isTextScale(textScale)) { + this.props.setTextScale(textScale); + } + }; + + public render() { + const { availableTextScales, textScale } = this.props; + + return ( + + ({ + id: availableTextScale.toString(), + label: getLabelOfTextScale(availableTextScale), + }))} + idSelected={textScale} + onChange={this.setTextScale} + /> + + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/empty_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/empty_view.tsx new file mode 100644 index 0000000000000..d4f0a51a0bc7c --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/empty_view.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import * as React from 'react'; + +interface LogTextStreamEmptyViewProps { + reload: () => void; +} + +export class LogTextStreamEmptyView extends React.PureComponent { + public render() { + const { reload } = this.props; + + return ( + There are no log messages to display.} + titleSize="m" + body={

Try adjusting your filter.

} + actions={ + + Check for new data + + } + /> + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts b/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts new file mode 100644 index 0000000000000..77781b58a3ccd --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ScrollableLogTextStreamView } from './scrollable_log_text_stream_view'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts b/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts new file mode 100644 index 0000000000000..75ee65aa2c3e0 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { bisector } from 'd3-array'; + +import { getLogEntryKey, LogEntry } from '../../../../common/log_entry'; +import { SearchResult } from '../../../../common/log_search_result'; +import { compareToTimeKey, TimeKey } from '../../../../common/time'; + +export type StreamItem = LogEntryStreamItem; + +export interface LogEntryStreamItem { + kind: 'logEntry'; + logEntry: LogEntry; + searchResult: SearchResult | undefined; +} + +export function getStreamItemTimeKey(item: StreamItem) { + switch (item.kind) { + case 'logEntry': + return getLogEntryKey(item.logEntry); + } +} + +export function getStreamItemId(item: StreamItem) { + const { time, tiebreaker, gid } = getStreamItemTimeKey(item); + + return `${time}:${tiebreaker}:${gid}`; +} + +export function parseStreamItemId(id: string) { + const idFragments = id.split(':'); + + return { + gid: idFragments.slice(2).join(':'), + tiebreaker: parseInt(idFragments[1], 10), + time: parseInt(idFragments[0], 10), + }; +} + +const streamItemTimeBisector = bisector(compareToTimeKey(getStreamItemTimeKey)); + +export const getStreamItemBeforeTimeKey = (streamItems: StreamItem[], key: TimeKey) => + streamItems[Math.min(streamItemTimeBisector.left(streamItems, key), streamItems.length - 1)]; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx new file mode 100644 index 0000000000000..82f8057b08fce --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { darken } from 'polished'; +import * as React from 'react'; +import { css } from 'styled-components'; + +import { TextScale } from '../../../../common/log_text_scale'; +import { tintOrShade } from '../../../utils/styles'; +import { LogTextStreamItemField } from './item_field'; + +interface LogTextStreamItemDateFieldProps { + children: string; + hasHighlights: boolean; + isHovered: boolean; + scale: TextScale; +} + +export class LogTextStreamItemDateField extends React.PureComponent< + LogTextStreamItemDateFieldProps, + {} +> { + public render() { + const { children, hasHighlights, isHovered, scale } = this.props; + + return ( + + {children} + + ); + } +} + +const highlightedFieldStyle = css` + background-color: ${props => + tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorSecondary, 0.15)}; + border-color: ${props => props.theme.eui.euiColorSecondary}; +`; + +const hoveredFieldStyle = css` + background-color: ${props => darken(0.05, props.theme.eui.euiColorHighlight)}; + border-color: ${props => darken(0.2, props.theme.eui.euiColorHighlight)}; + color: ${props => props.theme.eui.euiColorFullShade}; +`; + +const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{ + hasHighlights: boolean; + isHovered: boolean; +}>({})` + background-color: ${props => props.theme.eui.euiColorLightestShade}; + border-right: solid 2px ${props => props.theme.eui.euiColorLightShade}; + color: ${props => props.theme.eui.euiColorDarkShade}; + white-space: pre; + + ${props => (props.hasHighlights ? highlightedFieldStyle : '')}; + ${props => (props.isHovered ? hoveredFieldStyle : '')}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx new file mode 100644 index 0000000000000..c9d360fc116a7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import styled from 'styled-components'; + +import { switchProp } from '../../../utils/styles'; + +export const LogTextStreamItemField = styled.div.attrs<{ + scale?: 'small' | 'medium' | 'large'; +}>({})` + font-size: ${props => + switchProp('scale', { + large: props.theme.eui.euiFontSizeM, + medium: props.theme.eui.euiFontSizeS, + small: props.theme.eui.euiFontSizeXs, + [switchProp.default]: props.theme.eui.euiFontSize, + })}; + line-height: ${props => props.theme.eui.euiLineHeight}; + padding: 2px ${props => props.theme.eui.euiSize}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx new file mode 100644 index 0000000000000..1c411adaf4e1c --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { darken } from 'polished'; +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { TextScale } from '../../../../common/log_text_scale'; +import { tintOrShade } from '../../../utils/styles'; +import { LogTextStreamItemField } from './item_field'; + +interface LogTextStreamItemMessageFieldProps { + children: string; + highlights: string[]; + isHovered: boolean; + isWrapped: boolean; + scale: TextScale; +} + +export class LogTextStreamItemMessageField extends React.PureComponent< + LogTextStreamItemMessageFieldProps, + {} +> { + public render() { + const { children, highlights, isHovered, isWrapped, scale } = this.props; + + const hasHighlights = highlights.length > 0; + const content = hasHighlights ? renderHighlightFragments(children, highlights) : children; + return ( + + {content} + + ); + } +} + +const renderHighlightFragments = (text: string, highlights: string[]): React.ReactNode[] => { + const renderedHighlights = highlights.reduce( + ({ lastFragmentEnd, renderedFragments }, highlight) => { + const fragmentStart = text.indexOf(highlight, lastFragmentEnd); + return { + lastFragmentEnd: fragmentStart + highlight.length, + renderedFragments: [ + ...renderedFragments, + text.slice(lastFragmentEnd, fragmentStart), + {highlight}, + ], + }; + }, + { + lastFragmentEnd: 0, + renderedFragments: [], + } as { + lastFragmentEnd: number; + renderedFragments: React.ReactNode[]; + } + ); + + return [...renderedHighlights.renderedFragments, text.slice(renderedHighlights.lastFragmentEnd)]; +}; + +const highlightedFieldStyle = css` + background-color: ${props => + tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorSecondary, 0.15)}; +`; + +const hoveredFieldStyle = css` + background-color: ${props => darken(0.05, props.theme.eui.euiColorHighlight)}; +`; + +const wrappedFieldStyle = css` + overflow: visible; + white-space: pre-wrap; +`; + +const unwrappedFieldStyle = css` + overflow: hidden; + white-space: pre; +`; + +const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs<{ + hasHighlights: boolean; + isHovered: boolean; + isWrapped?: boolean; +}>({})` + flex-grow: 1; + text-overflow: ellipsis; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + + ${props => (props.hasHighlights ? highlightedFieldStyle : '')}; + ${props => (props.isHovered ? hoveredFieldStyle : '')}; + ${props => (props.isWrapped ? wrappedFieldStyle : unwrappedFieldStyle)}; +`; + +const HighlightSpan = styled.span` + display: inline-block; + padding: 0 ${props => props.theme.eui.euiSizeXs}; + background-color: ${props => props.theme.eui.euiColorSecondary}; + color: ${props => props.theme.eui.euiColorGhost}; + font-weight: ${props => props.theme.eui.euiFontWeightMedium}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx new file mode 100644 index 0000000000000..c4a75771fa5a0 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +import { TextScale } from '../../../../common/log_text_scale'; +import { StreamItem } from './item'; +import { LogTextStreamLogEntryItemView } from './log_entry_item_view'; + +interface StreamItemProps { + item: StreamItem; + scale: TextScale; + wrap: boolean; +} + +export const LogTextStreamItemView = React.forwardRef( + ({ item, scale, wrap }, ref) => { + switch (item.kind) { + case 'logEntry': + return ( + + ); + } + } +); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx new file mode 100644 index 0000000000000..973b35d449817 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/loading_item_view.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiProgress, EuiText } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { RelativeTime } from './relative_time'; + +interface LogTextStreamLoadingItemViewProps { + alignment: 'top' | 'bottom'; + className?: string; + hasMore: boolean; + isLoading: boolean; + isStreaming: boolean; + lastStreamingUpdate: number | null; +} + +export class LogTextStreamLoadingItemView extends React.PureComponent< + LogTextStreamLoadingItemViewProps, + {} +> { + public render() { + const { + alignment, + className, + hasMore, + isLoading, + isStreaming, + lastStreamingUpdate, + } = this.props; + + if (isStreaming) { + return ( + + + Streaming new entries + {lastStreamingUpdate ? ( + <> + : last updated {' '} + ago + + ) : null} + + + ); + } else if (isLoading) { + return ( + + Loading additional entries + + ); + } else if (!hasMore) { + return ( + + No additional entries found + + ); + } else { + return null; + } + } +} + +interface ProgressEntryProps { + alignment: 'top' | 'bottom'; + className?: string; + color: 'subdued' | 'primary'; + isLoading: boolean; +} + +// tslint:disable-next-line:max-classes-per-file +class ProgressEntry extends React.PureComponent { + public render() { + const { alignment, children, className, color, isLoading } = this.props; + + return ( + + + {children} + + ); + } +} + +const ProgressEntryWrapper = styled.div` + position: relative; +`; + +const ProgressTextDiv = styled.div` + padding: 8px 16px; +`; + +const AlignedProgress = styled(EuiProgress).attrs<{ + alignment: 'top' | 'bottom'; +}>({})` + top: ${props => (props.alignment === 'top' ? 0 : 'initial')}; + bottom: ${props => (props.alignment === 'top' ? 'initial' : 0)}; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx new file mode 100644 index 0000000000000..2e910cad56785 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import styled from 'styled-components'; + +import { LogEntry } from '../../../../common/log_entry'; +import { SearchResult } from '../../../../common/log_search_result'; +import { TextScale } from '../../../../common/log_text_scale'; +import { formatTime } from '../../../../common/time'; +import { LogTextStreamItemDateField } from './item_date_field'; +import { LogTextStreamItemMessageField } from './item_message_field'; + +interface LogTextStreamLogEntryItemViewProps { + boundingBoxRef?: React.Ref; + logEntry: LogEntry; + searchResult?: SearchResult; + scale: TextScale; + wrap: boolean; +} + +interface LogTextStreamLogEntryItemViewState { + isHovered: boolean; +} + +export class LogTextStreamLogEntryItemView extends React.PureComponent< + LogTextStreamLogEntryItemViewProps, + LogTextStreamLogEntryItemViewState +> { + public readonly state = { + isHovered: false, + }; + + public handleMouseEnter: React.MouseEventHandler = () => { + this.setState({ + isHovered: true, + }); + }; + + public handleMouseLeave: React.MouseEventHandler = () => { + this.setState({ + isHovered: false, + }); + }; + + public render() { + const { boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props; + const { isHovered } = this.state; + + return ( + + + {formatTime(logEntry.fields.time)} + + + {logEntry.fields.message} + + + ); + } +} + +const LogTextStreamLogEntryItemDiv = styled.div` + font-family: ${props => props.theme.eui.euiCodeFontFamily}; + font-size: ${props => props.theme.eui.euiFontSize}; + line-height: ${props => props.theme.eui.euiLineHeight}; + color: ${props => props.theme.eui.euiTextColor}; + overflow: hidden; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: stretch; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_stream_item_view_.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_stream_item_view_.tsx new file mode 100644 index 0000000000000..98bc10a6240d9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_stream_item_view_.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import styled from 'styled-components'; + +import { LogEntry } from '../../../../common/log_entry'; + +interface LogEntryStreamItemViewProps { + boundingBoxRef?: React.Ref<{}>; + item: LogEntry; +} + +export class LogEntryStreamItemView extends React.PureComponent { + public render() { + const { boundingBoxRef, item } = this.props; + + return ( + // @ts-ignore: silence error until styled-components supports React.RefObject + {JSON.stringify(item)} + ); + } +} + +const LogEntryDiv = styled.div` + border-top: 1px solid red; + border-bottom: 1px solid green; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/measurable_item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/measurable_item_view.tsx new file mode 100644 index 0000000000000..4ade3be2bd872 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/measurable_item_view.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +export interface Rect { + top: number; + left: number; + width: number; + height: number; +} + +interface MeasureableProps { + children: (measureRef: React.Ref) => React.ReactNode; + register: (key: any, element: MeasurableItemView | null) => void; + registrationKey: any; +} + +export class MeasurableItemView extends React.PureComponent { + public childRef = React.createRef(); + + public getOffsetRect = (): Rect | null => { + const currentElement = this.childRef.current; + + if (currentElement === null) { + return null; + } + + return { + height: currentElement.offsetHeight, + left: currentElement.offsetLeft, + top: currentElement.offsetTop, + width: currentElement.offsetWidth, + }; + }; + + public componentDidMount() { + this.props.register(this.props.registrationKey, this); + } + + public componentWillUnmount() { + this.props.register(this.props.registrationKey, null); + } + + public componentDidUpdate(prevProps: MeasureableProps) { + if (prevProps.registrationKey !== this.props.registrationKey) { + this.props.register(prevProps.registrationKey, null); + this.props.register(this.props.registrationKey, this); + } + } + + public render() { + return this.props.children(this.childRef); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/relative_time.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/relative_time.tsx new file mode 100644 index 0000000000000..247b444a3b6a6 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/relative_time.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +import { decomposeIntoUnits, getLabelOfScale, TimeUnit } from '../../../../common/time'; + +interface RelativeTimeProps { + time: number; + refreshInterval?: number; +} + +interface RelativeTimeState { + currentTime: number; + timeoutId: number | null; +} + +export class RelativeTime extends React.Component { + public readonly state = { + currentTime: Date.now(), + timeoutId: null, + }; + + public updateCurrentTimeEvery = (refreshInterval: number) => { + const nextTimeoutId = window.setTimeout( + this.updateCurrentTimeEvery.bind(this, refreshInterval), + refreshInterval + ); + + this.setState({ + currentTime: Date.now(), + timeoutId: nextTimeoutId, + }); + }; + + public cancelUpdate = () => { + const { timeoutId } = this.state; + + if (timeoutId) { + window.clearTimeout(timeoutId); + this.setState({ + timeoutId: null, + }); + } + }; + + public componentDidMount() { + const { refreshInterval } = this.props; + + if (refreshInterval && refreshInterval > 0) { + this.updateCurrentTimeEvery(refreshInterval); + } + } + + public componentWillUnmount() { + this.cancelUpdate(); + } + + public render() { + const { time } = this.props; + const { currentTime } = this.state; + const timeDifference = Math.abs(currentTime - time); + + const timeFragments = decomposeIntoUnits(timeDifference, unitThresholds); + + if (timeFragments.length === 0) { + return '0s'; + } else { + return timeFragments.map(getLabelOfScale).join(' '); + } + } +} + +const unitThresholds = [TimeUnit.Day, TimeUnit.Hour, TimeUnit.Minute, TimeUnit.Second]; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx new file mode 100644 index 0000000000000..8b0ededac73c3 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +import { TextScale } from '../../../../common/log_text_scale'; +import { TimeKey } from '../../../../common/time'; +import { callWithoutRepeats } from '../../../utils/handlers'; +import { InfraLoadingPanel } from '../../loading'; +import { LogTextStreamEmptyView } from './empty_view'; +import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item'; +import { LogTextStreamItemView } from './item_view'; +import { LogTextStreamLoadingItemView } from './loading_item_view'; +import { MeasurableItemView } from './measurable_item_view'; +import { VerticalScrollPanel } from './vertical_scroll_panel'; + +interface ScrollableLogTextStreamViewProps { + height: number; + width: number; + items: StreamItem[]; + scale: TextScale; + wrap: boolean; + isReloading: boolean; + isLoadingMore: boolean; + hasMoreBeforeStart: boolean; + hasMoreAfterEnd: boolean; + isStreaming: boolean; + lastLoadedTime: number | null; + target: TimeKey | null; + jumpToTarget: (target: TimeKey) => any; + reportVisibleInterval: ( + params: { + pagesBeforeStart: number; + pagesAfterEnd: number; + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + } + ) => any; +} + +interface ScrollableLogTextStreamViewState { + target: TimeKey | null; + targetId: string | null; +} + +export class ScrollableLogTextStreamView extends React.PureComponent< + ScrollableLogTextStreamViewProps, + ScrollableLogTextStreamViewState +> { + public static getDerivedStateFromProps( + nextProps: ScrollableLogTextStreamViewProps, + prevState: ScrollableLogTextStreamViewState + ): Partial | null { + const hasNewTarget = nextProps.target && nextProps.target !== prevState.target; + const hasItems = nextProps.items.length > 0; + + if (nextProps.isStreaming && hasItems) { + return { + target: nextProps.target, + targetId: getStreamItemId(nextProps.items[nextProps.items.length - 1]), + }; + } else if (hasNewTarget && hasItems) { + return { + target: nextProps.target, + targetId: getStreamItemId(getStreamItemBeforeTimeKey(nextProps.items, nextProps.target!)), + }; + } else if (!nextProps.target || !hasItems) { + return { + target: null, + targetId: null, + }; + } + + return null; + } + + public readonly state = { + target: null, + targetId: null, + }; + + public render() { + const { + items, + height, + width, + scale, + wrap, + isReloading, + isLoadingMore, + hasMoreBeforeStart, + hasMoreAfterEnd, + isStreaming, + lastLoadedTime, + } = this.props; + const { targetId } = this.state; + const hasItems = items.length > 0; + if (isReloading && !hasItems) { + return ; + } else if (!hasItems) { + return ; + } else { + return ( + + {registerChild => ( + <> + + {items.map(item => ( + + {measureRef => ( + + )} + + ))} + + + )} + + ); + } + } + + private handleReload = () => { + const { jumpToTarget, target } = this.props; + + if (target) { + jumpToTarget(target); + } + }; + + // this is actually a method but not recognized as such + // tslint:disable-next-line:member-ordering + private handleVisibleChildrenChange = callWithoutRepeats( + ({ + topChild, + middleChild, + bottomChild, + pagesAbove, + pagesBelow, + }: { + topChild: string; + middleChild: string; + bottomChild: string; + pagesAbove: number; + pagesBelow: number; + }) => { + this.props.reportVisibleInterval({ + endKey: parseStreamItemId(bottomChild), + middleKey: parseStreamItemId(middleChild), + pagesAfterEnd: pagesBelow, + pagesBeforeStart: pagesAbove, + startKey: parseStreamItemId(topChild), + }); + } + ); +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx new file mode 100644 index 0000000000000..db0a6bd29f1d9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/vertical_scroll_panel.tsx @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { bisector } from 'd3-array'; +import sortBy from 'lodash/fp/sortBy'; +import throttle from 'lodash/fp/throttle'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { Rect } from './measurable_item_view'; + +interface VerticalScrollPanelProps { + children?: ( + registerChild: (key: Child, element: MeasurableChild | null) => void + ) => React.ReactNode; + onVisibleChildrenChange?: ( + visibleChildren: { + topChild: Child; + middleChild: Child; + bottomChild: Child; + pagesAbove: number; + pagesBelow: number; + } + ) => void; + target: Child | undefined; + height: number; + width: number; + hideScrollbar?: boolean; +} + +interface VerticalScrollPanelSnapshot { + scrollTarget: Child | undefined; + scrollOffset: number | undefined; +} + +interface MeasurableChild { + getOffsetRect(): Rect | null; +} + +const SCROLL_THROTTLE_INTERVAL = 250; +const ASSUMED_SCROLLBAR_WIDTH = 20; + +export class VerticalScrollPanel extends React.PureComponent< + VerticalScrollPanelProps +> { + public static defaultProps: Partial> = { + hideScrollbar: false, + }; + + public scrollRef = React.createRef(); + public childRefs = new Map(); + public childDimensions = new Map(); + + public handleScroll: React.UIEventHandler = throttle( + SCROLL_THROTTLE_INTERVAL, + () => { + this.reportVisibleChildren(); + } + ); + + public registerChild = (key: any, element: MeasurableChild | null) => { + if (element === null) { + this.childRefs.delete(key); + } else { + this.childRefs.set(key, element); + } + }; + + public updateChildDimensions = () => { + this.childDimensions = new Map( + sortDimensionsByTop( + Array.from(this.childRefs.entries()).reduce( + (accumulatedDimensions, [key, child]) => { + const currentOffsetRect = child.getOffsetRect(); + + if (currentOffsetRect !== null) { + accumulatedDimensions.push([key, currentOffsetRect]); + } + + return accumulatedDimensions; + }, + [] as Array<[any, Rect]> + ) + ) + ); + }; + + public getVisibleChildren = () => { + if (this.scrollRef.current === null || this.childDimensions.size <= 0) { + return; + } + + const { + childDimensions, + props: { height: scrollViewHeight }, + scrollRef: { + current: { scrollTop }, + }, + } = this; + + return getVisibleChildren(Array.from(childDimensions.entries()), scrollViewHeight, scrollTop); + }; + + public getScrollPosition = () => { + if (this.scrollRef.current === null) { + return; + } + + const { + props: { height: scrollViewHeight }, + scrollRef: { + current: { scrollHeight, scrollTop }, + }, + } = this; + + return { + pagesAbove: scrollTop / scrollViewHeight, + pagesBelow: (scrollHeight - scrollTop - scrollViewHeight) / scrollViewHeight, + }; + }; + + public reportVisibleChildren = () => { + const { onVisibleChildrenChange } = this.props; + const visibleChildren = this.getVisibleChildren(); + const scrollPosition = this.getScrollPosition(); + + if (!visibleChildren || !scrollPosition || typeof onVisibleChildrenChange !== 'function') { + return; + } + + onVisibleChildrenChange({ + bottomChild: visibleChildren.bottomChild, + middleChild: visibleChildren.middleChild, + topChild: visibleChildren.topChild, + ...scrollPosition, + }); + }; + + public centerTarget = (target: Child, offset?: number) => { + const { + props: { height: scrollViewHeight }, + childDimensions, + scrollRef, + } = this; + + if (scrollRef.current === null || !target || childDimensions.size <= 0) { + return; + } + + const targetDimensions = childDimensions.get(target); + + if (targetDimensions) { + const targetOffset = typeof offset === 'undefined' ? targetDimensions.height / 2 : offset; + scrollRef.current.scrollTop = targetDimensions.top + targetOffset - scrollViewHeight / 2; + } + }; + + public handleUpdatedChildren = (target: Child | undefined, offset: number | undefined) => { + this.updateChildDimensions(); + if (!!target) { + this.centerTarget(target, offset); + } + this.reportVisibleChildren(); + }; + + public componentDidMount() { + this.handleUpdatedChildren(this.props.target, undefined); + } + + public getSnapshotBeforeUpdate( + prevProps: VerticalScrollPanelProps + ): VerticalScrollPanelSnapshot { + if (prevProps.target !== this.props.target && this.props.target) { + return { + scrollOffset: undefined, + scrollTarget: this.props.target, + }; + } else { + const visibleChildren = this.getVisibleChildren(); + + if (visibleChildren) { + return { + scrollOffset: visibleChildren.middleChildOffset, + scrollTarget: visibleChildren.middleChild, + }; + } + } + + return { + scrollOffset: undefined, + scrollTarget: undefined, + }; + } + + public componentDidUpdate( + prevProps: VerticalScrollPanelProps, + prevState: {}, + snapshot: VerticalScrollPanelSnapshot + ) { + this.handleUpdatedChildren(snapshot.scrollTarget, snapshot.scrollOffset); + } + + public componentWillUnmount() { + this.childRefs.clear(); + } + + public render() { + const { children, height, width, hideScrollbar } = this.props; + const scrollbarOffset = hideScrollbar ? ASSUMED_SCROLLBAR_WIDTH : 0; + + return ( + + {typeof children === 'function' ? children(this.registerChild) : null} + + ); + } +} + +const ScrollPanelWrapper = styled.div.attrs<{ scrollbarOffset?: number }>({})` + overflow-x: hidden; + overflow-y: scroll; + position: relative; + padding-right: ${props => props.scrollbarOffset || 0}px; + + & * { + overflow-anchor: none; + } +`; + +const getVisibleChildren = ( + childDimensions: Array<[Child, Rect]>, + scrollViewHeight: number, + scrollTop: number +) => { + const middleChildIndex = Math.min( + getChildIndexBefore(childDimensions, scrollTop + scrollViewHeight / 2), + childDimensions.length - 1 + ); + + const topChildIndex = Math.min( + getChildIndexBefore(childDimensions, scrollTop, 0, middleChildIndex), + childDimensions.length - 1 + ); + + const bottomChildIndex = Math.min( + getChildIndexBefore(childDimensions, scrollTop + scrollViewHeight, middleChildIndex), + childDimensions.length - 1 + ); + + return { + bottomChild: childDimensions[bottomChildIndex][0], + bottomChildOffset: childDimensions[bottomChildIndex][1].top - scrollTop - scrollViewHeight, + middleChild: childDimensions[middleChildIndex][0], + middleChildOffset: scrollTop + scrollViewHeight / 2 - childDimensions[middleChildIndex][1].top, + topChild: childDimensions[topChildIndex][0], + topChildOffset: childDimensions[topChildIndex][1].top - scrollTop, + }; +}; + +const sortDimensionsByTop = sortBy<[any, Rect]>('1.top'); + +const getChildIndexBefore = bisector<[any, Rect], number>(([key, rect]) => rect.top + rect.height) + .left; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_wrap_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_text_wrap_controls.tsx new file mode 100644 index 0000000000000..7500780961e74 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_wrap_controls.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import * as React from 'react'; + +interface LogTextWrapControlsProps { + wrap: boolean; + setTextWrap: (scale: boolean) => any; +} + +export class LogTextWrapControls extends React.PureComponent { + public toggleWrap = () => { + this.props.setTextWrap(!this.props.wrap); + }; + + public render() { + const { wrap } = this.props; + + return ( + + + + ); + } +} diff --git a/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx new file mode 100644 index 0000000000000..9d719168bcb39 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiDatePicker, EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import moment, { Moment } from 'moment'; +import React from 'react'; +import styled from 'styled-components'; + +const noop = () => undefined; + +interface LogTimeControlsProps { + currentTime: number | null; + startLiveStreaming: (interval: number) => any; + stopLiveStreaming: () => any; + isLiveStreaming: boolean; + jumpToTime: (time: number) => any; +} + +export class LogTimeControls extends React.PureComponent { + public render() { + const { currentTime, isLiveStreaming } = this.props; + + const currentMoment = currentTime ? moment(currentTime) : null; + + if (isLiveStreaming) { + return ( + + + + + + Stop streaming + + + ); + } else { + return ( + + + + + + Stream live + + + ); + } + } + + private handleChangeDate = (date: Moment | null) => { + if (date !== null) { + this.props.jumpToTime(date.valueOf()); + } + }; + + private startLiveStreaming = () => { + this.props.startLiveStreaming(5000); + }; + + private stopLiveStreaming = () => { + this.props.stopLiveStreaming(); + }; +} + +const InlineWrapper = styled.div` + display: inline-block; +`; diff --git a/x-pack/plugins/infra/public/components/metrics/index.tsx b/x-pack/plugins/infra/public/components/metrics/index.tsx new file mode 100644 index 0000000000000..c9475ef4a48f0 --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPageContentBody, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +import { InfraMetricData } from '../../../common/graphql/types'; +import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types'; +import { metricTimeActions } from '../../store'; +import { InfraLoadingPanel } from '../loading'; +import { Section } from './section'; + +interface Props { + metrics: InfraMetricData[]; + layouts: InfraMetricLayout[]; + loading: boolean; + nodeName: string; + onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void; +} + +interface State { + crosshairValue: number | null; +} + +export class Metrics extends React.PureComponent { + public readonly state = { + crosshairValue: null, + }; + + public render() { + if (this.props.loading) { + return ( + + ); + } + return {this.props.layouts.map(this.renderLayout)}; + } + + private renderLayout = (layout: InfraMetricLayout) => { + return ( + + + +

{`${layout.label} Overview`}

+
+
+ {layout.sections.map(this.renderSection(layout))} +
+ ); + }; + + private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => { + let sectionProps = {}; + if (section.type === 'chart') { + const { onChangeRangeTime } = this.props; + sectionProps = { + onChangeRangeTime, + crosshairValue: this.state.crosshairValue, + onCrosshairUpdate: this.onCrosshairUpdate, + }; + } + return ( +
+ ); + }; + + private onCrosshairUpdate = (crosshairValue: number) => { + this.setState({ + crosshairValue, + }); + }; +} diff --git a/x-pack/plugins/infra/public/components/metrics/section.tsx b/x-pack/plugins/infra/public/components/metrics/section.tsx new file mode 100644 index 0000000000000..89170d053b3a9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/section.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { InfraMetricData } from '../../../common/graphql/types'; +import { InfraMetricLayoutSection } from '../../pages/metrics/layouts/types'; +import { metricTimeActions } from '../../store'; +import { sections } from './sections'; + +interface Props { + section: InfraMetricLayoutSection; + metrics: InfraMetricData[]; + onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void; + crosshairValue?: number; + onCrosshairUpdate?: (crosshairValue: number) => void; +} + +export class Section extends React.PureComponent { + public render() { + const metric = this.props.metrics.find(m => m.id === this.props.section.id); + if (!metric) { + return null; + } + let sectionProps = {}; + if (this.props.section.type === 'chart') { + sectionProps = { + onChangeRangeTime: this.props.onChangeRangeTime, + crosshairValue: this.props.crosshairValue, + onCrosshairUpdate: this.props.onCrosshairUpdate, + }; + } + const Component = sections[this.props.section.type]; + return ; + } +} diff --git a/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx new file mode 100644 index 0000000000000..ad8506f5f62ec --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiIcon, EuiPageContentBody, EuiTitle } from '@elastic/eui'; +import { + EuiAreaSeries, + EuiBarSeries, + EuiCrosshairX, + EuiDataPoint, + EuiLineSeries, + EuiSeriesChart, + EuiSeriesChartProps, + EuiSeriesProps, + EuiXAxis, + EuiYAxis, +} from '@elastic/eui/lib/experimental'; +import Color from 'color'; +import { get } from 'lodash'; +import moment from 'moment'; +import React, { ReactText } from 'react'; +import { InfraDataSeries, InfraMetricData } from '../../../../common/graphql/types'; +import { InfraFormatter, InfraFormatterType } from '../../../lib/lib'; +import { + InfraMetricLayoutSection, + InfraMetricLayoutVisualizationType, +} from '../../../pages/metrics/layouts/types'; +import { metricTimeActions } from '../../../store'; +import { createFormatter } from '../../../utils/formatters'; + +const MARGIN_LEFT = 60; +const chartComponentsByType = { + [InfraMetricLayoutVisualizationType.line]: EuiLineSeries, + [InfraMetricLayoutVisualizationType.area]: EuiAreaSeries, + [InfraMetricLayoutVisualizationType.bar]: EuiBarSeries, +}; + +interface Props { + section: InfraMetricLayoutSection; + metric: InfraMetricData; + onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void; + crosshairValue?: number; + onCrosshairUpdate?: (crosshairValue: number) => void; +} + +const isInfraMetricLayoutVisualizationType = ( + subject: any +): subject is InfraMetricLayoutVisualizationType => { + return InfraMetricLayoutVisualizationType[subject] != null; +}; + +const getChartName = (section: InfraMetricLayoutSection, seriesId: string) => { + return get(section, ['visConfig', 'seriesOverrides', seriesId, 'name'], seriesId); +}; + +const getChartColor = (section: InfraMetricLayoutSection, seriesId: string): string | undefined => { + const color = new Color( + get(section, ['visConfig', 'seriesOverrides', seriesId, 'color'], '#999') + ); + return color.hex().toString(); +}; + +const getChartType = (section: InfraMetricLayoutSection, seriesId: string) => { + const value = get(section, ['visConfig', 'type']); + const overrideValue = get(section, ['visConfig', 'seriesOverrides', seriesId, 'type']); + if (isInfraMetricLayoutVisualizationType(overrideValue)) { + return overrideValue; + } + if (isInfraMetricLayoutVisualizationType(value)) { + return value; + } + return InfraMetricLayoutVisualizationType.line; +}; + +const getFormatter = (formatter: InfraFormatterType, formatterTemplate: string) => ( + val: ReactText +) => { + if (val == null) { + return ''; + } + return createFormatter(formatter, formatterTemplate)(val); +}; + +const titleFormatter = (dataPoints: EuiDataPoint[]) => { + if (dataPoints.length > 0) { + const [firstDataPoint] = dataPoints; + const { originalValues } = firstDataPoint; + return { + title: , + value: moment(originalValues.x).format('lll'), + }; + } +}; + +const createItemsFormatter = ( + formatter: InfraFormatter, + labels: string[], + seriesColors: string[] +) => (dataPoints: EuiDataPoint[]) => { + return dataPoints.map(d => { + return { + title: ( + + + {labels[d.seriesIndex]} + + ), + value: formatter(d.y), + }; + }); +}; + +export class ChartSection extends React.PureComponent { + public render() { + const { crosshairValue, section, metric, onCrosshairUpdate } = this.props; + const { visConfig } = section; + const crossHairProps = { + crosshairValue, + onCrosshairUpdate, + }; + const chartProps: EuiSeriesChartProps = { + xType: 'time', + showCrosshair: false, + showDefaultAxis: false, + enableSelectionBrush: true, + onSelectionBrushEnd: this.handleSelectionBrushEnd, + }; + const stacked = visConfig && visConfig.stacked; + if (stacked) { + chartProps.stackBy = 'y'; + } + const bounds = visConfig && visConfig.bounds; + if (bounds) { + chartProps.yDomain = [bounds.min, bounds.max]; + } + if (!metric) { + chartProps.statusText = 'Missing data'; + } + const formatter = get(visConfig, 'formatter', InfraFormatterType.number); + const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}'); + const formatterFunction = getFormatter(formatter, formatterTemplate); + const seriesLabels = get(metric, 'series', [] as InfraDataSeries[]).map(s => + getChartName(section, s.id) + ); + const seriesColors = get(metric, 'series', [] as InfraDataSeries[]).map( + s => getChartColor(section, s.id) || '' + ); + const itemsFormatter = createItemsFormatter(formatterFunction, seriesLabels, seriesColors); + return ( + + +

{section.label}

+
+
+ + + + + {metric && + metric.series.map(series => { + if (!series) { + return null; + } + const data = series.data.map(d => { + return { x: d.timestamp, y: d.value || 0, y0: 0 }; + }); + const chartType = getChartType(section, series.id); + const name = getChartName(section, series.id); + const seriesProps: EuiSeriesProps = { + data, + name, + lineSize: 2, + }; + const color = getChartColor(section, series.id); + if (color) { + seriesProps.color = color; + } + const EuiChartComponent = chartComponentsByType[chartType]; + return ( + + ); + })} + +
+
+ ); + } + + private handleSelectionBrushEnd = (area: Area) => { + const { onChangeRangeTime } = this.props; + const { startX, endX } = area.domainArea; + if (onChangeRangeTime) { + onChangeRangeTime({ + to: endX.valueOf(), + from: startX.valueOf(), + } as metricTimeActions.MetricRangeTimeState); + } + }; +} + +interface DomainArea { + startX: moment.Moment; + endX: moment.Moment; + startY: number; + endY: number; +} + +interface DrawArea { + x0: number; + x1: number; + y0: number; + y1: number; +} + +interface Area { + domainArea: DomainArea; + drawArea: DrawArea; +} diff --git a/x-pack/plugins/infra/public/components/metrics/sections/gauges_section.tsx b/x-pack/plugins/infra/public/components/metrics/sections/gauges_section.tsx new file mode 100644 index 0000000000000..af58fe80230d4 --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/sections/gauges_section.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexItem, + EuiPageContentBody, + EuiPanel, + EuiProgress, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { get, last, max } from 'lodash'; +import React, { ReactText } from 'react'; +import styled from 'styled-components'; +import { InfraMetricData } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; +import { InfraMetricLayoutSection } from '../../../pages/metrics/layouts/types'; +import { createFormatter } from '../../../utils/formatters'; + +interface Props { + section: InfraMetricLayoutSection; + metric: InfraMetricData; +} + +const getFormatter = (section: InfraMetricLayoutSection, seriesId: string) => (val: ReactText) => { + if (val == null) { + return ''; + } + const defaultFormatter = get(section, ['visConfig', 'formatter'], InfraFormatterType.number); + const defaultFormatterTemplate = get(section, ['visConfig', 'formatterTemplate'], '{{value}}'); + const formatter = get( + section, + ['visConfig', 'seriesOverrides', seriesId, 'formatter'], + defaultFormatter + ); + const formatterTemplate = get( + section, + ['visConfig', 'seriesOverrides', seriesId, 'formatterTemplate'], + defaultFormatterTemplate + ); + return createFormatter(formatter, formatterTemplate)(val); +}; + +export class GaugesSection extends React.PureComponent { + public render() { + const { metric, section } = this.props; + return ( + + + + {metric.series.map(series => { + const lastDataPoint = last(series.data); + if (!lastDataPoint) { + return null; + } + const formatter = getFormatter(section, series.id); + const value = formatter(lastDataPoint.value || 0); + const name = get( + section, + ['visConfig', 'seriesOverrides', series.id, 'name'], + series.id + ); + const dataMax = max(series.data.map(d => d.value || 0)); + const gaugeMax = get( + section, + ['visConfig', 'seriesOverrides', series.id, 'gaugeMax'], + dataMax + ); + return ( + + + + {name} + + +

{value}

+
+ +
+
+ ); + })} +
+ +
+ ); + } +} + +const GroupBox = styled.div` + display: flex; + flex-flow: row wrap; + justify-content: space-evenly; +`; diff --git a/x-pack/plugins/infra/public/components/metrics/sections/index.ts b/x-pack/plugins/infra/public/components/metrics/sections/index.ts new file mode 100644 index 0000000000000..7c12ff7520566 --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/sections/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricLayoutSectionType } from '../../../pages/metrics/layouts/types'; +import { ChartSection } from './chart_section'; +import { GaugesSection } from './gauges_section'; + +export const sections = { + [InfraMetricLayoutSectionType.chart]: ChartSection, + [InfraMetricLayoutSectionType.gauges]: GaugesSection, +}; diff --git a/x-pack/plugins/infra/public/components/metrics/time_controls.tsx b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx new file mode 100644 index 0000000000000..f9209fbb16ccd --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import moment, { Moment } from 'moment'; +import React from 'react'; +import styled from 'styled-components'; + +import { RangeDatePicker, RecentlyUsed } from '../range_date_picker'; + +import { metricTimeActions } from '../../store'; + +interface MetricsTimeControlsProps { + currentTimeRange: metricTimeActions.MetricRangeTimeState; + isLiveStreaming?: boolean; + onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void; + startLiveStreaming?: () => void; + stopLiveStreaming?: () => void; +} + +interface MetricsTimeControlsState { + showGoButton: boolean; + to: moment.Moment | undefined; + from: moment.Moment | undefined; + recentlyUsed: RecentlyUsed[]; +} + +export class MetricsTimeControls extends React.Component< + MetricsTimeControlsProps, + MetricsTimeControlsState +> { + public dateRangeRef: React.RefObject = React.createRef(); + public readonly state = { + showGoButton: false, + to: moment().millisecond(this.props.currentTimeRange.to), + from: moment().millisecond(this.props.currentTimeRange.from), + recentlyUsed: [], + }; + public render() { + const { currentTimeRange, isLiveStreaming } = this.props; + const { showGoButton, to, from, recentlyUsed } = this.state; + + const liveStreamingButton = ( + + + {isLiveStreaming ? ( + + Stop refreshing + + ) : ( + + Auto-refresh + + )} + + + Reset + + + ); + + const goColor = from && to && from > to ? 'danger' : 'primary'; + const appendButton = showGoButton ? ( + + + + Go + + + + Cancel + + + ) : ( + liveStreamingButton + ); + + return ( + + + {appendButton} + + ); + } + + private handleChangeDate = ( + from: Moment | undefined, + to: Moment | undefined, + search: boolean + ) => { + const { onChangeRangeTime } = this.props; + const duration = moment.duration(from && to ? from.diff(to) : 0); + const milliseconds = duration.asMilliseconds(); + if (to && from && onChangeRangeTime && search && to > from) { + this.setState({ + showGoButton: false, + to, + from, + }); + onChangeRangeTime({ + to: to && to.valueOf(), + from: from && from.valueOf(), + } as metricTimeActions.MetricRangeTimeState); + } else if (milliseconds !== 0) { + this.setState({ + showGoButton: true, + to, + from, + }); + } + }; + + private searchRangeTime = () => { + const { onChangeRangeTime } = this.props; + const { to, from, recentlyUsed } = this.state; + if (to && from && onChangeRangeTime && to > from) { + this.setState({ + ...this.state, + showGoButton: false, + recentlyUsed: [ + ...recentlyUsed, + ...[ + { + type: 'date-range', + text: [from.format('L LTS'), to.format('L LTS')], + }, + ], + ], + }); + onChangeRangeTime({ + to: to.valueOf(), + from: from.valueOf(), + } as metricTimeActions.MetricRangeTimeState); + } + }; + + private startLiveStreaming = () => { + const { startLiveStreaming } = this.props; + + if (startLiveStreaming) { + startLiveStreaming(); + } + }; + + private stopLiveStreaming = () => { + const { stopLiveStreaming } = this.props; + + if (stopLiveStreaming) { + stopLiveStreaming(); + } + }; + + private cancelSearch = () => { + const { onChangeRangeTime } = this.props; + const to = moment(this.props.currentTimeRange.to); + const from = moment(this.props.currentTimeRange.from); + + this.setState({ + ...this.state, + showGoButton: false, + to, + from, + }); + this.dateRangeRef.current.resetRangeDate(from, to); + if (onChangeRangeTime) { + onChangeRangeTime({ + to: to && to.valueOf(), + from: from && from.valueOf(), + } as metricTimeActions.MetricRangeTimeState); + } + }; + + private resetSearch = () => { + const { onChangeRangeTime } = this.props; + const to = moment(); + const from = moment().subtract(1, 'hour'); + if (onChangeRangeTime) { + onChangeRangeTime({ + to: to.valueOf(), + from: from.valueOf(), + } as metricTimeActions.MetricRangeTimeState); + } + }; +} +const MetricsTimeControlsContainer = styled.div` + display: flex; + justify-content: right; + flex-flow: row wrap; + & > div:first-child { + margin-right: 0.5rem; + } +`; diff --git a/x-pack/plugins/infra/public/components/page.tsx b/x-pack/plugins/infra/public/components/page.tsx new file mode 100644 index 0000000000000..04d69fdb1d9e8 --- /dev/null +++ b/x-pack/plugins/infra/public/components/page.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPage } from '@elastic/eui'; +import styled from 'styled-components'; + +export const ColumnarPage = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + flex: 1 0 0; +`; + +export const PageContent = styled.div` + flex: 1 0 0; + display: flex; + flex-direction: row; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; +`; + +export const FlexPage = styled(EuiPage)` + flex: 1 0 0; +`; diff --git a/x-pack/plugins/infra/public/components/range_date_picker/index.tsx b/x-pack/plugins/infra/public/components/range_date_picker/index.tsx new file mode 100644 index 0000000000000..698b8fde1af72 --- /dev/null +++ b/x-pack/plugins/infra/public/components/range_date_picker/index.tsx @@ -0,0 +1,416 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { find } from 'lodash'; +import moment from 'moment'; +import React, { Fragment } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiDatePicker, + EuiDatePickerRange, + EuiFieldNumber, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiFormControlLayout, + EuiFormRow, + EuiHorizontalRule, + EuiIcon, + EuiLink, + EuiPopover, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +const commonDates = [ + 'Today', + 'Yesterday', + 'This week', + 'Week to date', + 'This month', + 'Month to date', + 'This year', + 'Year to date', +]; + +interface RangeDatePickerProps { + startDate: moment.Moment | undefined; + endDate: moment.Moment | undefined; + onChangeRangeTime: ( + from: moment.Moment | undefined, + to: moment.Moment | undefined, + search: boolean + ) => void; + recentlyUsed: RecentlyUsed[]; + disabled?: boolean; + isLoading?: boolean; + ref?: React.RefObject; +} + +export interface RecentlyUsed { + type: string; + text: string | string[]; +} + +interface RangeDatePickerState { + startDate: moment.Moment | undefined; + endDate: moment.Moment | undefined; + isPopoverOpen: boolean; + recentlyUsed: RecentlyUsed[]; + quickSelectTime: number; + quickSelectUnit: string; +} + +export class RangeDatePicker extends React.PureComponent< + RangeDatePickerProps, + RangeDatePickerState +> { + public readonly state = { + startDate: this.props.startDate, + endDate: this.props.endDate, + isPopoverOpen: false, + recentlyUsed: [], + quickSelectTime: 1, + quickSelectUnit: 'hours', + }; + + public render() { + const { isLoading, disabled } = this.props; + const { startDate, endDate } = this.state; + const quickSelectButton = ( + + + + ); + + const commonlyUsed = this.renderCommonlyUsed(commonDates); + const recentlyUsed = this.renderRecentlyUsed([ + ...this.state.recentlyUsed, + ...this.props.recentlyUsed, + ]); + + const quickSelectPopover = ( + +
+ {this.renderQuickSelect()} + + {commonlyUsed} + + {recentlyUsed} +
+
+ ); + + return ( + + endDate : false} + fullWidth + aria-label="Start date" + disabled={disabled} + shouldCloseOnSelect + showTimeSelect + /> + } + endDateControl={ + endDate : false} + fullWidth + disabled={disabled} + isLoading={isLoading} + aria-label="End date" + shouldCloseOnSelect + showTimeSelect + popperPlacement="top-end" + /> + } + /> + + ); + } + + public resetRangeDate(startDate: moment.Moment, endDate: moment.Moment) { + this.setState({ + ...this.state, + startDate, + endDate, + }); + } + + private handleChangeStart = (date: moment.Moment | null) => { + if (date && this.state.startDate !== date) { + this.props.onChangeRangeTime(date, this.state.endDate, false); + this.setState({ + startDate: date, + }); + } + }; + + private handleChangeEnd = (date: moment.Moment | null) => { + if (date && this.state.endDate !== date) { + this.props.onChangeRangeTime(this.state.startDate, date, false); + this.setState({ + endDate: date, + }); + } + }; + + private onButtonClick = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; + + private closePopover = (type: string, from?: string, to?: string) => { + const { startDate, endDate, recentlyUsed } = this.managedStartEndDateFromType(type, from, to); + this.setState( + { + ...this.state, + isPopoverOpen: false, + startDate, + endDate, + recentlyUsed, + }, + () => { + if (type) { + this.props.onChangeRangeTime(startDate, endDate, true); + } + } + ); + }; + + private managedStartEndDateFromType(type: string, from?: string, to?: string) { + let { startDate, endDate } = this.state; + let recentlyUsed: RecentlyUsed[] = this.state.recentlyUsed; + let textJustUsed = type; + + if (type === 'quick-select') { + textJustUsed = `Last ${this.state.quickSelectTime} ${singularize( + this.state.quickSelectUnit, + this.state.quickSelectTime + )}`; + startDate = moment().subtract(this.state.quickSelectTime, this.state + .quickSelectUnit as moment.unitOfTime.DurationConstructor); + endDate = moment(); + } else if (type === 'Today') { + startDate = moment().startOf('day'); + endDate = moment() + .startOf('day') + .add(24, 'hour'); + } else if (type === 'Yesterday') { + startDate = moment() + .subtract(1, 'day') + .startOf('day'); + endDate = moment() + .subtract(1, 'day') + .startOf('day') + .add(24, 'hour'); + } else if (type === 'This week') { + startDate = moment().startOf('week'); + endDate = moment() + .startOf('week') + .add(1, 'week'); + } else if (type === 'Week to date') { + startDate = moment().subtract(1, 'week'); + endDate = moment(); + } else if (type === 'This month') { + startDate = moment().startOf('month'); + endDate = moment() + .startOf('month') + .add(1, 'month'); + } else if (type === 'Month to date') { + startDate = moment().subtract(1, 'month'); + endDate = moment(); + } else if (type === 'This year') { + startDate = moment().startOf('year'); + endDate = moment() + .startOf('year') + .add(1, 'year'); + } else if (type === 'Year to date') { + startDate = moment().subtract(1, 'year'); + endDate = moment(); + } else if (type === 'date-range' && to && from) { + startDate = moment(from); + endDate = moment(to); + } + + if (textJustUsed !== undefined && !find(recentlyUsed, ['text', textJustUsed])) { + recentlyUsed.unshift({ type, text: textJustUsed }); + recentlyUsed = recentlyUsed.slice(0, 5); + } + + return { + startDate, + endDate, + recentlyUsed, + }; + } + + private renderQuickSelect = () => { + const lastOptions = [ + { value: 'seconds', text: singularize('seconds', this.state.quickSelectTime) }, + { value: 'minutes', text: singularize('minutes', this.state.quickSelectTime) }, + { value: 'hours', text: singularize('hours', this.state.quickSelectTime) }, + { value: 'days', text: singularize('days', this.state.quickSelectTime) }, + { value: 'weeks', text: singularize('weeks', this.state.quickSelectTime) }, + { value: 'months', text: singularize('months', this.state.quickSelectTime) }, + { value: 'years', text: singularize('years', this.state.quickSelectTime) }, + ]; + + return ( + + + Quick select + + + + + + Last + + + + + { + this.onChange('quickSelectTime', arg); + }} + /> + + + + + { + this.onChange('quickSelectUnit', arg); + }} + /> + + + + + this.closePopover('quick-select')} style={{ minWidth: 0 }}> + Apply + + + + + + ); + }; + + private onChange = (stateType: string, args: any) => { + let value = args.currentTarget.value; + + if (stateType === 'quickSelectTime' && value !== '') { + value = parseInt(args.currentTarget.value, 10); + } + this.setState({ + ...this.state, + [stateType]: value, + }); + }; + + private renderCommonlyUsed = (recentlyCommonDates: string[]) => { + const links = recentlyCommonDates.map(date => { + return ( + + this.closePopover(date)}>{date} + + ); + }); + + return ( + + + Commonly used + + + + + {links} + + + + ); + }; + + private renderRecentlyUsed = (recentDates: RecentlyUsed[]) => { + const links = recentDates.map((date: RecentlyUsed) => { + let dateRange; + let dateLink = ( + this.closePopover(date.type)}>{dateRange || date.text} + ); + if (typeof date.text !== 'string') { + dateRange = `${date.text[0]} – ${date.text[1]}`; + dateLink = ( + this.closePopover(date.type, date.text[0], date.text[1])}> + {dateRange || date.type} + + ); + } + + return ( + + {dateLink} + + ); + }); + + return ( + + + Recently used date ranges + + + + + {links} + + + + ); + }; +} + +const singularize = (str: string, qty: number) => (qty === 1 ? str.slice(0, -1) : str); diff --git a/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx b/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx new file mode 100644 index 0000000000000..24ec7bafd391f --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { + InfraFormatter, + InfraWaffleMapBounds, + InfraWaffleMapGradientLegend, + InfraWaffleMapGradientRule, +} from '../../lib/lib'; + +interface Props { + legend: InfraWaffleMapGradientLegend; + bounds: InfraWaffleMapBounds; + formatter: InfraFormatter; +} + +const createTickRender = (bounds: InfraWaffleMapBounds, formatter: InfraFormatter) => ( + rule: InfraWaffleMapGradientRule, + index: number +) => { + const value = rule.value === 0 ? bounds.min : bounds.max * rule.value; + const style = { left: `${rule.value * 100}%` }; + const label = formatter(value); + return ( + + + {label} + + ); +}; + +export const GradientLegend: React.SFC = ({ legend, bounds, formatter }) => { + const maxValue = legend.rules.reduce((acc, rule) => { + return acc < rule.value ? rule.value : acc; + }, 0); + const colorStops = legend.rules.map(rule => { + const percent = (rule.value / maxValue) * 100; + return `${rule.color} ${percent}%`; + }); + const style = { + background: `linear-gradient(to right, ${colorStops})`, + }; + return ( + + {legend.rules.map(createTickRender(bounds, formatter))} + + ); +}; + +const GradientLegendContainer = styled.div` + position: absolute; + height: 10px; + bottom: 0; + left: 0; + right: 0; +`; + +const GradientLegendTick = styled.div` + position: absolute; + bottom: 0; + top: -18px; +`; + +const GradientLegendTickLine = styled.div` + position: absolute; + background-color: ${props => props.theme.eui.euiBorderColor}; + width: 1px; + left: 0; + top: 15px; + bottom: 0; + ${GradientLegendTick}:first-child { + top: 2px; + } + ${GradientLegendTick}:last-child { + top: 2px; + } +`; + +const GradientLegendTickLabel = styled.div` + position: absolute; + font-size: 11px; + text-align: center; + top: 0; + left: 0; + white-space: nowrap; + transform: translate(-50%, 0); + ${GradientLegendTick}:first-child & { + padding-left: 5px; + transform: translate(0, 0); + } + ${GradientLegendTick}:last-child & { + padding-right: 5px; + transform: translate(-100%, 0); + } +`; diff --git a/x-pack/plugins/infra/public/components/waffle/group_name.tsx b/x-pack/plugins/infra/public/components/waffle/group_name.tsx new file mode 100644 index 0000000000000..0d4b91c494f3a --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/group_name.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiLink, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { InfraWaffleMapGroup } from '../../lib/lib'; + +interface Props { + onDrilldown: () => void; + group: InfraWaffleMapGroup; + isChild?: boolean; +} + +export class GroupName extends React.PureComponent { + public render() { + const { group, isChild } = this.props; + const linkStyle = { + fontSize: isChild ? '0.85em' : '1em', + }; + return ( + + + + + + {group.name} + + + + {group.count} + + + ); + } + + private handleClick = (event: React.MouseEvent) => { + // TODO: fill this in with group click handler + event.preventDefault(); + this.props.onDrilldown(); + }; +} + +const GroupNameContainer = styled.div` + position: relative; + text-align: center + font-size: 16px; + margin-bottom: 5px; + top: 20px; + display: flex; + justify-content: center; + padding: 0 10px; +`; + +interface InnerProps { + isChild?: boolean; +} + +const Inner = styled('div')` + border: 1px solid ${props => props.theme.eui.euiBorderColor}; + background-color: ${props => + props.isChild ? props.theme.eui.euiColorLightestShade : props.theme.eui.euiColorEmptyShade}; + border-radius: 4px; + box-shadow: 0px 2px 0px 0px ${props => props.theme.eui.euiBorderColor}; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +`; + +const Name = styled.div` + flex: 1 1 auto; + padding: 6px 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const Count = styled.div` + flex: 0 0 auto; + border-left: 1px solid ${props => props.theme.eui.euiBorderColor}; + padding: 6px 10px; + font-size: 0.85em; + font-weight: normal; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx new file mode 100644 index 0000000000000..5445d18132156 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { InfraNodeType } from '../../../common/graphql/types'; +import { + InfraWaffleMapBounds, + InfraWaffleMapGroupOfGroups, + InfraWaffleMapOptions, +} from '../../lib/lib'; +import { GroupName } from './group_name'; +import { GroupOfNodes } from './group_of_nodes'; + +interface Props { + onDrilldown: () => void; + options: InfraWaffleMapOptions; + group: InfraWaffleMapGroupOfGroups; + formatter: (val: number) => string; + bounds: InfraWaffleMapBounds; + nodeType: InfraNodeType; +} + +export const GroupOfGroups: React.SFC = props => { + return ( + + + + {props.group.groups.map(group => ( + + ))} + + + ); +}; + +const GroupOfGroupsContainer = styled.div` + margin: 0 10px; +`; + +const Groups = styled.div` + display: flex; + background-color: rgba(0, 0, 0, 0.05); + flex-wrap: wrap; + justify-content: center; + padding: 20px 10px 10px; + border-radius: 4px; + border: 1px solid ${props => props.theme.eui.euiBorderColor}; + box-shadow: 0 1px 7px rgba(0, 0, 0, 0.1); +`; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx new file mode 100644 index 0000000000000..d7e3c5d8ed290 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { InfraNodeType } from '../../../common/graphql/types'; +import { + InfraWaffleMapBounds, + InfraWaffleMapGroupOfNodes, + InfraWaffleMapOptions, +} from '../../lib/lib'; +import { GroupName } from './group_name'; +import { Node } from './node'; + +interface Props { + onDrilldown: () => void; + options: InfraWaffleMapOptions; + group: InfraWaffleMapGroupOfNodes; + formatter: (val: number) => string; + isChild: boolean; + bounds: InfraWaffleMapBounds; + nodeType: InfraNodeType; +} + +export const GroupOfNodes: React.SFC = ({ + group, + options, + formatter, + onDrilldown, + isChild = false, + bounds, + nodeType, +}) => { + const width = group.width > 200 ? group.width : 200; + return ( + + + + {group.nodes.map(node => ( + + ))} + + + ); +}; + +const GroupOfNodesContainer = styled.div` + margin: 0 10px; +`; + +const Nodes = styled.div` + display: flex; + background-color: rgba(0, 0, 0, 0.05); + flex-wrap: wrap; + justify-content: center; + padding: 20px 10px 10px; + border-radius: 4px; + border: 1px solid ${props => props.theme.eui.euiBorderColor}; + box-shadow: 0 1px 7px rgba(0, 0, 0, 0.1); +`; diff --git a/x-pack/plugins/infra/public/components/waffle/index.tsx b/x-pack/plugins/infra/public/components/waffle/index.tsx new file mode 100644 index 0000000000000..8503a73eb765a --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/index.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { get, max, min } from 'lodash'; +import React from 'react'; +import styled from 'styled-components'; +import { InfraMetricType, InfraNodeType } from '../../../common/graphql/types'; +import { + isWaffleMapGroupWithGroups, + isWaffleMapGroupWithNodes, +} from '../../containers/waffle/type_guards'; +import { + InfraFormatterType, + InfraWaffleData, + InfraWaffleMapBounds, + InfraWaffleMapGroup, + InfraWaffleMapOptions, +} from '../../lib/lib'; +import { createFormatter } from '../../utils/formatters'; +import { AutoSizer } from '../auto_sizer'; +import { InfraLoadingPanel } from '../loading'; +import { GroupOfGroups } from './group_of_groups'; +import { GroupOfNodes } from './group_of_nodes'; +import { Legend } from './legend'; +import { applyWaffleMapLayout } from './lib/apply_wafflemap_layout'; + +interface Props { + options: InfraWaffleMapOptions; + nodeType: InfraNodeType; + map: InfraWaffleData; + loading: boolean; + reload: () => void; +} + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +const METRIC_FORMATTERS: MetricFormatters = { + [InfraMetricType.count]: { formatter: InfraFormatterType.number, template: '{{value}}' }, + [InfraMetricType.cpu]: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + bounds: { min: 0, max: 1 }, + }, + [InfraMetricType.memory]: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + bounds: { min: 0, max: 1 }, + }, + [InfraMetricType.rx]: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + [InfraMetricType.tx]: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + [InfraMetricType.logRate]: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, +}; + +const extractValuesFromMap = (groups: InfraWaffleMapGroup[], values: number[] = []): number[] => { + return groups.reduce((acc: number[], group: InfraWaffleMapGroup) => { + if (isWaffleMapGroupWithGroups(group)) { + return acc.concat(extractValuesFromMap(group.groups, values)); + } + if (isWaffleMapGroupWithNodes(group)) { + return acc.concat( + group.nodes.map(node => { + return node.metric.value || 0; + }) + ); + } + return acc; + }, values); +}; + +const calculateBoundsFromMap = (map: InfraWaffleData): InfraWaffleMapBounds => { + const values = extractValuesFromMap(map); + return { min: min(values), max: max(values) }; +}; + +export class Waffle extends React.Component { + public render() { + const { loading, map, reload } = this.props; + if (loading) { + return ; + } else if (!loading && map && map.length === 0) { + return ( + There is no data to display.} + titleSize="m" + body={

Try adjusting your time or filter.

} + actions={ + { + reload(); + }} + > + Check for new data + + } + /> + ); + } + const { metric } = this.props.options; + const metricFormatter = get( + METRIC_FORMATTERS, + metric.type, + METRIC_FORMATTERS[InfraMetricType.count] + ); + const bounds = (metricFormatter && metricFormatter.bounds) || calculateBoundsFromMap(map); + return ( + + {({ measureRef, content: { width = 0, height = 0 } }) => { + const groupsWithLayout = applyWaffleMapLayout(map, width, height); + return ( + measureRef(el)}> + + {groupsWithLayout.map(this.renderGroup(bounds))} + + + + ); + }} + + ); + } + + // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. + private formatter = (val: string | number) => { + const { metric } = this.props.options; + const metricFormatter = get( + METRIC_FORMATTERS, + metric.type, + METRIC_FORMATTERS[InfraMetricType.count] + ); + if (val == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(val); + }; + + private handleDrilldown() { + return; + } + + private renderGroup = (bounds: InfraWaffleMapBounds) => (group: InfraWaffleMapGroup) => { + if (isWaffleMapGroupWithGroups(group)) { + return ( + + ); + } + if (isWaffleMapGroupWithNodes(group)) { + return ( + + ); + } + }; +} + +const WaffleMapOuterContiner = styled.div` + flex: 1 0 0; + display: flex; + justify-content: center; + flex-direction: column; + overflow-x: hidden; + overflow-y: auto; +`; + +const WaffleMapInnerContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-content: flex-start; + padding: 10px; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/legend.tsx b/x-pack/plugins/infra/public/components/waffle/legend.tsx new file mode 100644 index 0000000000000..2ce97372a9726 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/legend.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import styled from 'styled-components'; +import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../lib/lib'; +import { GradientLegend } from './gradient_legend'; +import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './lib/type_guards'; +import { StepLegend } from './steps_legend'; +interface Props { + legend: InfraWaffleMapLegend; + bounds: InfraWaffleMapBounds; + formatter: InfraFormatter; +} + +export const Legend: React.SFC = ({ legend, bounds, formatter }) => { + return ( + + {isInfraWaffleMapGradientLegend(legend) && ( + + )} + {isInfraWaffleMapStepLegend(legend) && } + + ); +}; + +const LegendContainer = styled.div` + position: absolute; + bottom: 10px; + left: 10px; + right: 10px; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts b/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts new file mode 100644 index 0000000000000..5f3c06fcfbba7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { first, sortBy } from 'lodash'; +import { + isWaffleMapGroupWithGroups, + isWaffleMapGroupWithNodes, +} from '../../../containers/waffle/type_guards'; +import { InfraWaffleMapGroup } from '../../../lib/lib'; +import { sizeOfSquares } from './size_of_squares'; + +export function getColumns(n: number, w = 1, h = 1) { + const pageRatio = w / h; + const ratio = pageRatio > 1.2 ? 1.2 : pageRatio; + const width = Math.ceil(Math.sqrt(n)); + return Math.ceil(width * ratio); +} + +export function getTotalItems(groups: InfraWaffleMapGroup[]) { + if (!groups) { + return 0; + } + return groups.reduce((acc, group) => { + if (isWaffleMapGroupWithGroups(group)) { + return group.groups.reduce((total, subGroup) => subGroup.nodes.length + total, acc); + } + if (isWaffleMapGroupWithNodes(group)) { + return group.nodes.length + acc; + } + return acc; + }, 0); +} + +export function getLargestCount(groups: InfraWaffleMapGroup[]) { + if (!groups) { + return 0; + } + return groups.reduce((total, group) => { + if (isWaffleMapGroupWithGroups(group)) { + return group.groups.reduce((subTotal, subGroup) => { + if (isWaffleMapGroupWithNodes(subGroup)) { + return subTotal > subGroup.nodes.length ? subTotal : subGroup.nodes.length; + } + return subTotal; + }, total); + } + if (isWaffleMapGroupWithNodes(group)) { + return total > group.nodes.length ? total : group.nodes.length; + } + return total; + }, 0); +} + +const getTotalItemsOfGroup = (group: InfraWaffleMapGroup): number => getTotalItems([group]); + +export function applyWaffleMapLayout( + groups: InfraWaffleMapGroup[], + width: number, + height: number +): InfraWaffleMapGroup[] { + if (groups.length === 0) { + return []; + } + const levels = isWaffleMapGroupWithGroups(first(groups)) ? 2 : 1; + const totalItems = getTotalItems(groups); + const squareSize = Math.round(sizeOfSquares(width, height, totalItems, levels)); + const largestCount = getLargestCount(groups); + return sortBy(groups, getTotalItemsOfGroup) + .reverse() + .map(group => { + if (isWaffleMapGroupWithGroups(group)) { + const columns = getColumns(largestCount, width, height); + const groupOfNodes = group.groups; + const subGroups = sortBy(groupOfNodes, getTotalItemsOfGroup) + .reverse() + .filter(isWaffleMapGroupWithNodes) + .map(subGroup => { + return { + ...subGroup, + count: subGroup.nodes.length, + columns, + width: columns * squareSize, + squareSize, + }; + }); + return { + ...group, + groups: subGroups, + count: getTotalItems([group]), + squareSize, + }; + } + if (isWaffleMapGroupWithNodes(group)) { + const columns = getColumns(Math.max(group.nodes.length, largestCount), width, height); + return { + ...group, + count: group.nodes.length, + squareSize, + width: columns * squareSize, + }; + } + return group; + }); +} diff --git a/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts b/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts new file mode 100644 index 0000000000000..c6bfd45502f3d --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { eq, first, gt, gte, last, lt, lte, sortBy } from 'lodash'; +import { mix } from 'polished'; +import { + InfraWaffleMapBounds, + InfraWaffleMapGradientLegend, + InfraWaffleMapLegend, + InfraWaffleMapRuleOperator, + InfraWaffleMapStepLegend, +} from '../../../lib/lib'; +import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './type_guards'; + +const OPERATOR_TO_FN = { + [InfraWaffleMapRuleOperator.eq]: eq, + [InfraWaffleMapRuleOperator.lt]: lt, + [InfraWaffleMapRuleOperator.lte]: lte, + [InfraWaffleMapRuleOperator.gte]: gte, + [InfraWaffleMapRuleOperator.gt]: gt, +}; + +export const colorFromValue = ( + legend: InfraWaffleMapLegend, + value: number | string, + bounds: InfraWaffleMapBounds, + defaultColor = 'rgba(0, 179, 164, 1)' +): string => { + if (isInfraWaffleMapStepLegend(legend)) { + return calculateStepColor(legend, value, defaultColor); + } + if (isInfraWaffleMapGradientLegend(legend)) { + return calculateGradientColor(legend, value, bounds, defaultColor); + } + return defaultColor; +}; + +const normalizeValue = (min: number, max: number, value: number): number => { + return (value - min) / (max - min); +}; + +export const calculateStepColor = ( + legend: InfraWaffleMapStepLegend, + value: number | string, + defaultColor = 'rgba(0, 179, 164, 1)' +): string => { + return sortBy(legend.rules, 'sortBy').reduce((color: string, rule) => { + const operatorFn = OPERATOR_TO_FN[rule.operator]; + if (operatorFn(value, rule.value)) { + return rule.color; + } + return color; + }, defaultColor); +}; + +export const calculateGradientColor = ( + legend: InfraWaffleMapGradientLegend, + value: number | string, + bounds: InfraWaffleMapBounds, + defaultColor = 'rgba(0, 179, 164, 1)' +): string => { + if (legend.rules.length === 0) { + return defaultColor; + } + if (legend.rules.length === 1) { + return last(legend.rules).color; + } + const { min, max } = bounds; + const sortedRules = sortBy(legend.rules, 'value'); + const normValue = normalizeValue(min, max, Number(value)); + const startRule = sortedRules.reduce((acc, rule) => { + if (rule.value <= normValue) { + return rule; + } + return acc; + }, first(sortedRules)); + const endRule = sortedRules.filter(r => r !== startRule).find(r => r.value >= normValue); + if (!endRule) { + return startRule.color; + } + + const mixValue = normalizeValue(startRule.value, endRule.value, normValue); + + return mix(mixValue, endRule.color, startRule.color); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts b/x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts new file mode 100644 index 0000000000000..d96a376d50581 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SCALE_FACTOR = 0.6; +export const MAX_SIZE = Infinity; +export const MIN_SIZE = 24; + +export function sizeOfSquares( + width: number, + height: number, + totalItems: number, + levels = 1 +): number { + const levelFactor = levels > 1 ? levels * 0.7 : 1; + const scale = SCALE_FACTOR / levelFactor; + const x = width * scale; + const y = height * scale; + const possibleX = Math.ceil(Math.sqrt((totalItems * x) / y)); + let newX; + let newY; + if (Math.floor((possibleX * y) / x) * possibleX < totalItems) { + newX = y / Math.ceil((possibleX * y) / x); + } else { + newX = x / possibleX; + } + const possibleY = Math.ceil(Math.sqrt((totalItems * y) / x)); + if (Math.floor((possibleY * x) / y) * possibleY < totalItems) { + // does not fit + newY = x / Math.ceil((x * possibleY) / y); + } else { + newY = y / possibleY; + } + const size = Math.max(newX, newY); + return Math.min(Math.max(size, MIN_SIZE), MAX_SIZE); +} diff --git a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts new file mode 100644 index 0000000000000..aff16374ae262 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraWaffleMapGradientLegend, + InfraWaffleMapLegendMode, + InfraWaffleMapStepLegend, +} from '../../../lib/lib'; +export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { + return subject.type && subject.type === InfraWaffleMapLegendMode.step; +} +export function isInfraWaffleMapGradientLegend( + subject: any +): subject is InfraWaffleMapGradientLegend { + return subject.type && subject.type === InfraWaffleMapLegendMode.gradient; +} diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/components/waffle/node.tsx new file mode 100644 index 0000000000000..08792a3f36209 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/node.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiToolTip } from '@elastic/eui'; +import { darken, readableColor } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; +import { InfraNodeType } from '../../../server/lib/adapters/nodes'; +import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; +import { colorFromValue } from './lib/color_from_value'; +import { NodeContextMenu } from './node_context_menu'; + +const initialState = { + isPopoverOpen: false, +}; + +type State = Readonly; + +interface Props { + onDrilldown: () => void; + squareSize: number; + options: InfraWaffleMapOptions; + node: InfraWaffleMapNode; + formatter: (val: number) => string; + bounds: InfraWaffleMapBounds; + nodeType: InfraNodeType; +} + +export class Node extends React.PureComponent { + public readonly state: State = initialState; + public render() { + const { nodeType, node, options, squareSize, bounds, formatter } = this.props; + const { isPopoverOpen } = this.state; + const { metric } = node; + const valueMode = squareSize > 110; + const rawValue = (metric && metric.value) || 0; + const color = colorFromValue(options.legend, rawValue, bounds); + const value = formatter(rawValue); + return ( + + + + + + {valueMode && ( + + + {value} + + )} + + + + + + ); + } + + private togglePopover = () => { + this.setState(prevState => ({ isPopoverOpen: !prevState.isPopoverOpen })); + }; + + private closePopover = () => { + this.setState({ isPopoverOpen: false }); + }; +} + +const NodeContainer = styled.div` + position: relative; +`; + +interface ColorProps { + color: string; +} + +const SquareOuter = styled('div')` + position: absolute; + top: 4px; + left: 4px; + bottom: 4px; + right: 4px; + background-color: ${props => darken(0.1, props.color)}; + border-radius: 3px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); +`; + +const SquareInner = styled('div')` + cursor: pointer; + position: absolute; + top: 0; + right: 0; + bottom: 2px; + left: 0; + border-radius: 3px; + background-color: ${props => props.color}; +`; + +const ValueInner = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + line-height: 1.2em; + align-items: center; + align-content: center; + padding: 1em; + overflow: hidden; + flex-wrap: wrap; +`; + +const Value = styled('div')` + font-weight: bold; + font-size: 0.9em; + text-align: center; + width: 100%; + flex: 1 0 auto; + line-height: 1.2em; + color: ${props => readableColor(props.color)}; +`; + +const Label = styled('div')` + text-overflow: ellipsis; + font-size: 0.7em; + margin-bottom: 0.7em; + text-align: center; + width: 100%; + flex: 1 0 auto; + white-space: nowrap; + overflow: hidden; + color: ${props => readableColor(props.color)}; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx new file mode 100644 index 0000000000000..73e774047cea4 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui'; +import React from 'react'; +import { InfraNodeType } from '../../../common/graphql/types'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; +import { + getContainerDetailUrl, + getContainerLogsUrl, + getHostDetailUrl, + getHostLogsUrl, + getPodDetailUrl, + getPodLogsUrl, +} from '../../pages/link_to'; + +interface Props { + options: InfraWaffleMapOptions; + node: InfraWaffleMapNode; + nodeType: InfraNodeType; + isPopoverOpen: boolean; + closePopover: () => void; +} + +export const NodeContextMenu: React.SFC = ({ + options, + children, + node, + isPopoverOpen, + closePopover, + nodeType, +}) => { + const nodeLogsUrl = getNodeLogsUrl(nodeType, node); + const nodeDetailUrl = getNodeDetailUrl(nodeType, node); + const nodeField = options.fields ? options.fields[nodeType] : null; + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: '', + items: [ + ...(nodeLogsUrl + ? [ + { + name: `View logs`, + href: nodeLogsUrl, + }, + ] + : []), + ...(nodeDetailUrl + ? [ + { + name: `View metrics`, + href: nodeDetailUrl, + }, + ] + : []), + ...(nodeField + ? [ + { + name: `View APM Traces`, + href: `../app/apm#/?_g=()&kuery=${nodeField}~20~3A~20~22${node.name}~22`, + }, + ] + : []), + ], + }, + ]; + + return ( + + + + ); +}; + +const getNodeLogsUrl = ( + nodeType: 'host' | 'container' | 'pod', + { path }: InfraWaffleMapNode +): string | undefined => { + if (path.length <= 0) { + return undefined; + } + + const lastPathSegment = path[path.length - 1]; + + switch (nodeType) { + case 'host': + return getHostLogsUrl({ hostname: lastPathSegment.value }); + case 'container': + return getContainerLogsUrl({ containerId: lastPathSegment.value }); + case 'pod': + return getPodLogsUrl({ podId: lastPathSegment.value }); + default: + return undefined; + } +}; + +const getNodeDetailUrl = ( + nodeType: 'host' | 'container' | 'pod', + { path }: InfraWaffleMapNode +): string | undefined => { + if (path.length <= 0) { + return undefined; + } + + const lastPathSegment = path[path.length - 1]; + + switch (nodeType) { + case 'host': + return getHostDetailUrl({ name: lastPathSegment.value }); + case 'container': + return getContainerDetailUrl({ name: lastPathSegment.value }); + case 'pod': + return getPodDetailUrl({ name: lastPathSegment.value }); + default: + return undefined; + } +}; diff --git a/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx b/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx new file mode 100644 index 0000000000000..12964989e1b18 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { darken } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; +import { + InfraFormatter, + InfraWaffleMapRuleOperator, + InfraWaffleMapStepLegend, + InfraWaffleMapStepRule, +} from '../../lib/lib'; + +const OPERATORS = { + [InfraWaffleMapRuleOperator.gte]: '>=', + [InfraWaffleMapRuleOperator.gt]: '>', + [InfraWaffleMapRuleOperator.lte]: '<=', + [InfraWaffleMapRuleOperator.lt]: '<', + [InfraWaffleMapRuleOperator.eq]: '=', +}; + +interface Props { + legend: InfraWaffleMapStepLegend; + formatter: InfraFormatter; +} + +const createStep = (formatter: InfraFormatter) => (rule: InfraWaffleMapStepRule, index: number) => { + const label = + rule.label != null ? rule.label : `${OPERATORS[rule.operator]} ${formatter(rule.value)}`; + const squareStyle = { backgroundColor: darken(0.4, rule.color) }; + const squareInnerStyle = { backgroundColor: rule.color }; + return ( + + + + + {label} + + ); +}; + +export const StepLegend: React.SFC = ({ legend, formatter }) => { + return {legend.rules.map(createStep(formatter))}; +}; + +const StepLegendContainer = styled.div` + display: flex; + padding: 10px; +`; + +const StepContainer = styled.div` + display: flex; + margin-right: 20px + align-items: center; +`; + +const StepSquare = styled.div` + position: relative; + width: 24px; + height: 24px; + flex: 0 0 auto; + margin-right: 5px; + border-radius: 3px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); +`; + +const StepSquareInner = styled.div` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 2px; + border-radius: 3px; +`; + +const StepLabel = styled.div` + font-size: 12px; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx new file mode 100644 index 0000000000000..d07ec7bedc2a1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBadge, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFilterButton, + EuiFilterGroup, + EuiPopover, +} from '@elastic/eui'; +import React from 'react'; +import { InfraNodeType, InfraPathInput, InfraPathType } from '../../../common/graphql/types'; + +interface Props { + nodeType: InfraNodeType; + groupBy: InfraPathInput[]; + onChange: (groupBy: InfraPathInput[]) => void; +} + +const OPTIONS = { + [InfraNodeType.pod]: [ + { text: 'Namespace', type: InfraPathType.terms, field: 'kubernetes.namespace' }, + { text: 'Node', type: InfraPathType.terms, field: 'kubernetes.node.name' }, + ], + [InfraNodeType.container]: [ + { text: 'Host', type: InfraPathType.terms, field: 'host.name' }, + { text: 'Availability Zone', type: InfraPathType.terms, field: 'meta.cloud.availability_zone' }, + { text: 'Machine Type', type: InfraPathType.terms, field: 'meta.cloud.machine_type' }, + { text: 'Project ID', type: InfraPathType.terms, field: 'meta.cloud.project_id' }, + { text: 'Provider', type: InfraPathType.terms, field: 'meta.cloud.provider' }, + ], + [InfraNodeType.host]: [ + { text: 'Availability Zone', type: InfraPathType.terms, field: 'meta.cloud.availability_zone' }, + { text: 'Machine Type', type: InfraPathType.terms, field: 'meta.cloud.machine_type' }, + { text: 'Project ID', type: InfraPathType.terms, field: 'meta.cloud.project_id' }, + { text: 'Cloud Provider', type: InfraPathType.terms, field: 'meta.cloud.provider' }, + ], +}; + +const initialState = { + isPopoverOpen: false, +}; +type State = Readonly; + +export class WaffleGroupByControls extends React.PureComponent { + public readonly state: State = initialState; + public render() { + const { nodeType, groupBy } = this.props; + const options = OPTIONS[nodeType]; + if (!options.length) { + throw Error(`Unable to select group by options for ${nodeType}`); + } + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 'firstPanel', + title: 'Select up to two groupings', + items: options.map(o => { + const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty'; + const panel = { name: o.text, onClick: this.handleClick(o.field), icon }; + return panel; + }), + }, + ]; + const buttonBody = + groupBy.length > 0 + ? groupBy + .map(g => options.find(o => o.field === g.field)) + .filter(o => o != null) + // In this map the `o && o.field` is totally unnecessary but Typescript is + // too stupid to realize that the filter above prevents the next map from being null + .map(o => ( + + {o && o.text} + + )) + : 'All'; + const button = ( + + Group By: {buttonBody} + + ); + + return ( + + + + + + ); + } + + private handleRemove = (field: string) => () => { + const { groupBy } = this.props; + this.props.onChange(groupBy.filter(g => g.field !== field)); + // We need to close the panel after we rmeove the pill icon otherwise + // it will remain open because the click is still captured by the EuiFilterButton + setTimeout(() => this.handleClose()); + }; + + private handleClose = () => { + this.setState({ isPopoverOpen: false }); + }; + + private handleToggle = () => { + this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen })); + }; + + private handleClick = (field: string) => () => { + const { groupBy } = this.props; + if (groupBy.some(g => g.field === field)) { + this.handleRemove(field)(); + } else if (this.props.groupBy.length < 2) { + this.props.onChange([...groupBy, { type: InfraPathType.terms, field }]); + this.handleClose(); + } + }; +} diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_metric_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_metric_controls.tsx new file mode 100644 index 0000000000000..fe9b015e62457 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/waffle_metric_controls.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFilterButton, + EuiFilterGroup, + EuiPopover, +} from '@elastic/eui'; +import React from 'react'; +import { InfraMetricInput, InfraMetricType, InfraNodeType } from '../../../common/graphql/types'; +interface Props { + nodeType: InfraNodeType; + metric: InfraMetricInput; + onChange: (metric: InfraMetricInput) => void; +} + +const OPTIONS = { + [InfraNodeType.pod]: [ + { text: 'CPU Usage', value: InfraMetricType.cpu }, + { text: 'Memory Usage', value: InfraMetricType.memory }, + { text: 'Inbound Traffic', value: InfraMetricType.rx }, + { text: 'Outbound Traffic', value: InfraMetricType.tx }, + ], + [InfraNodeType.container]: [ + { text: 'CPU Usage', value: InfraMetricType.cpu }, + { text: 'Memory Usage', value: InfraMetricType.memory }, + { text: 'Inbound Traffic', value: InfraMetricType.rx }, + { text: 'Outbound Traffic', value: InfraMetricType.tx }, + ], + [InfraNodeType.host]: [ + { text: 'CPU Usage', value: InfraMetricType.cpu }, + { text: 'Memory Usage', value: InfraMetricType.memory }, + { text: 'Load', value: InfraMetricType.load }, + { text: 'Inbound Traffic', value: InfraMetricType.rx }, + { text: 'Outbound Traffic', value: InfraMetricType.tx }, + { text: 'Log Rate', value: InfraMetricType.logRate }, + ], +}; + +const initialState = { + isPopoverOpen: false, +}; +type State = Readonly; + +export class WaffleMetricControls extends React.PureComponent { + public readonly state: State = initialState; + public render() { + const { metric } = this.props; + const options = OPTIONS[this.props.nodeType]; + const value = metric.type; + if (!options.length || !value) { + throw Error('Unable to select options or value for metric.'); + } + const currentLabel = options.find(o => o.value === metric.type); + if (!currentLabel) { + return 'null'; + } + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: '', + items: options.map(o => { + const icon = o.value === metric.type ? 'check' : 'empty'; + const panel = { name: o.text, onClick: this.handleClick(o.value), icon }; + return panel; + }), + }, + ]; + const button = ( + + Metric: {currentLabel.text} + + ); + + return ( + + + + + + ); + } + private handleClose = () => { + this.setState({ isPopoverOpen: false }); + }; + + private handleToggle = () => { + this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen })); + }; + + private handleClick = (value: InfraMetricType) => () => { + this.props.onChange({ type: value }); + this.handleClose(); + }; +} diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_node_type_switcher.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_node_type_switcher.tsx new file mode 100644 index 0000000000000..e1071261a8de6 --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/waffle_node_type_switcher.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiKeyPadMenu, EuiKeyPadMenuItem } from '@elastic/eui'; +import React from 'react'; +import { + InfraMetricInput, + InfraMetricType, + InfraNodeType, + InfraPathInput, +} from '../../../common/graphql/types'; + +interface Props { + nodeType: InfraNodeType; + changeNodeType: (nodeType: InfraNodeType) => void; + changeGroupBy: (groupBy: InfraPathInput[]) => void; + changeMetric: (metric: InfraMetricInput) => void; +} + +export class WaffleNodeTypeSwitcher extends React.PureComponent { + public render() { + return ( + + + + + + + + + + + + ); + } + + private handleClick = (nodeType: InfraNodeType) => () => { + this.props.changeNodeType(nodeType); + this.props.changeGroupBy([]); + this.props.changeMetric({ type: InfraMetricType.cpu }); + }; +} diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx new file mode 100644 index 0000000000000..9453f636b9b4e --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eui'; +import moment, { Moment } from 'moment'; +import React from 'react'; + +interface WaffleTimeControlsProps { + currentTime: number; + isLiveStreaming?: boolean; + onChangeTime?: (time: number) => void; + startLiveStreaming?: () => void; + stopLiveStreaming?: () => void; +} + +export class WaffleTimeControls extends React.Component { + public render() { + const { currentTime, isLiveStreaming } = this.props; + + const currentMoment = moment(currentTime); + + const liveStreamingButton = isLiveStreaming ? ( + + Stop refreshing + + ) : ( + + Auto-refresh + + ); + + return ( + + + + ); + } + + private handleChangeDate = (time: Moment | null) => { + const { onChangeTime } = this.props; + + if (onChangeTime && time) { + onChangeTime(time.valueOf()); + } + }; + + private startLiveStreaming = () => { + const { startLiveStreaming } = this.props; + + if (startLiveStreaming) { + startLiveStreaming(); + } + }; + + private stopLiveStreaming = () => { + const { stopLiveStreaming } = this.props; + + if (stopLiveStreaming) { + stopLiveStreaming(); + } + }; +} diff --git a/x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts b/x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts new file mode 100644 index 0000000000000..53845b463c0b5 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/capabilities/capabilities.gql_query.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const capabilitiesQuery = gql` + query CapabilitiesQuery($sourceId: ID!, $nodeId: String!, $nodeType: InfraNodeType!) { + source(id: $sourceId) { + id + capabilitiesByNode(nodeName: $nodeId, nodeType: $nodeType) { + name + source + } + } + } +`; diff --git a/x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx b/x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx new file mode 100644 index 0000000000000..efdcb0e70e1d8 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/capabilities/with_capabilites.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; + +import React from 'react'; +import { Query } from 'react-apollo'; +import { CapabilitiesQuery, InfraNodeType } from '../../../common/graphql/types'; +import { InfraMetricLayout } from '../../pages/metrics/layouts/types'; +import { capabilitiesQuery } from './capabilities.gql_query'; + +interface WithCapabilitiesProps { + children: (args: WithCapabilitiesArgs) => React.ReactNode; + layouts: InfraMetricLayout[]; + nodeType: InfraNodeType; + nodeId: string; + sourceId: string; +} + +interface WithCapabilitiesArgs { + filteredLayouts: InfraMetricLayout[]; + error?: string | undefined; + loading: boolean; +} + +export const WithCapabilities = ({ + children, + layouts, + nodeType, + nodeId, + sourceId, +}: WithCapabilitiesProps) => { + return ( + + query={capabilitiesQuery} + fetchPolicy="no-cache" + variables={{ + sourceId, + nodeType, + nodeId, + }} + > + {({ data, error, loading }) => { + const capabilities = data && data.source && data.source.capabilitiesByNode; + const filteredLayouts = getFilteredLayouts(layouts, capabilities); + return children({ + filteredLayouts, + error: error && error.message, + loading, + }); + }} + + ); +}; + +const getFilteredLayouts = ( + layouts: InfraMetricLayout[], + capabilities: Array | undefined +): InfraMetricLayout[] => { + if (!capabilities) { + return layouts; + } + + const metricCapabilities: Array = capabilities + .filter(cap => cap && cap.source === 'metrics') + .map(cap => cap && cap.name); + + // After filtering out sections that can't be displayed, a layout may end up empty and can be removed. + const filteredLayouts = layouts + .map(layout => getFilteredLayout(layout, metricCapabilities)) + .filter(layout => layout.sections.length > 0); + return filteredLayouts; +}; + +const getFilteredLayout = ( + layout: InfraMetricLayout, + metricCapabilities: Array +): InfraMetricLayout => { + // A section is only displayed if at least one of its requirements is met + // All others are filtered out. + const filteredSections = layout.sections.filter( + section => _.intersection(section.requires, metricCapabilities).length > 0 + ); + return { ...layout, sections: filteredSections }; +}; diff --git a/x-pack/plugins/infra/public/containers/host/index.ts b/x-pack/plugins/infra/public/containers/host/index.ts new file mode 100644 index 0000000000000..706f05c80ed0d --- /dev/null +++ b/x-pack/plugins/infra/public/containers/host/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AllHosts } from './with_all_hosts'; +export { withAllHosts } from './with_all_hosts'; + +export const hostQueries = { + AllHosts, +}; diff --git a/x-pack/plugins/infra/public/containers/host/with_all_hosts.ts b/x-pack/plugins/infra/public/containers/host/with_all_hosts.ts new file mode 100644 index 0000000000000..de4a7fb22b7bb --- /dev/null +++ b/x-pack/plugins/infra/public/containers/host/with_all_hosts.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// import { graphql } from 'react-apollo'; +// import gql from 'graphql-tag'; +// import { GetAllHosts } from '../../../common/graphql/types'; + +// type ChildProps = { +// hosts: GetAllHosts.Query['hosts']; +// }; + +export const AllHosts = null; + +export const withAllHosts: any = (wrappedComponent: any) => wrappedComponent; +// export const withAllHosts = graphql< +// {}, +// GetAllHosts.Query, +// GetAllHosts.Variables, +// ChildProps +// >(AllHosts, { +// props: ({ data, ownProps }) => { +// return { +// hosts: data && data.hosts ? data.hosts : [], +// ...ownProps, +// }; +// }, +// }); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx new file mode 100644 index 0000000000000..fba0075c3c3b3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { logFilterActions, logFilterSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state'; + +const withLogFilter = connect( + (state: State) => ({ + filterQuery: logFilterSelectors.selectLogFilterQuery(state), + filterQueryDraft: logFilterSelectors.selectLogFilterQueryDraft(state), + isFilterQueryDraftValid: logFilterSelectors.selectIsLogFilterQueryDraftValid(state), + }), + bindPlainActionCreators({ + applyFilterQuery: logFilterActions.applyLogFilterQuery, + applyFilterQueryFromKueryExpression: (expression: string) => + logFilterActions.applyLogFilterQuery({ + kind: 'kuery', + expression, + }), + setFilterQueryDraft: logFilterActions.setLogFilterQueryDraft, + setFilterQueryDraftFromKueryExpression: (expression: string) => + logFilterActions.setLogFilterQueryDraft({ + kind: 'kuery', + expression, + }), + }) +); + +export const WithLogFilter = asChildFunctionRenderer(withLogFilter); + +/** + * Url State + */ + +type LogFilterUrlState = ReturnType; + +export const WithLogFilterUrlState = () => ( + + {({ applyFilterQuery, filterQuery }) => ( + { + if (urlState) { + applyFilterQuery(urlState); + } + }} + onInitialize={urlState => { + if (urlState) { + applyFilterQuery(urlState); + } + }} + /> + )} + +); + +const mapToFilterQuery = (value: any): LogFilterUrlState | undefined => + value && value.kind === 'kuery' && typeof value.expression === 'string' + ? { + kind: value.kind, + expression: value.expression, + } + : undefined; + +export const replaceLogFilterInQueryString = (expression: string) => + replaceStateKeyInQueryString('logFilter', { + kind: 'kuery', + expression, + }); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx new file mode 100644 index 0000000000000..a5ad500eaeb45 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { logMinimapActions, logMinimapSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withLogMinimap = connect( + (state: State) => ({ + availableIntervalSizes, + intervalSize: logMinimapSelectors.selectMinimapIntervalSize(state), + urlState: selectMinimapUrlState(state), + }), + bindPlainActionCreators({ + setIntervalSize: logMinimapActions.setMinimapIntervalSize, + }) +); + +export const WithLogMinimap = asChildFunctionRenderer(withLogMinimap); + +export const availableIntervalSizes = [ + { + label: '1 Year', + intervalSize: 1000 * 60 * 60 * 24 * 365, + }, + { + label: '1 Month', + intervalSize: 1000 * 60 * 60 * 24 * 30, + }, + { + label: '1 Week', + intervalSize: 1000 * 60 * 60 * 24 * 7, + }, + { + label: '1 Day', + intervalSize: 1000 * 60 * 60 * 24, + }, + { + label: '1 Hour', + intervalSize: 1000 * 60 * 60, + }, + { + label: '1 Minute', + intervalSize: 1000 * 60, + }, +]; + +/** + * Url State + */ + +interface LogMinimapUrlState { + intervalSize?: ReturnType; +} + +export const WithLogMinimapUrlState = () => ( + + {({ urlState, setIntervalSize }) => ( + { + if (newUrlState && newUrlState.intervalSize) { + setIntervalSize(newUrlState.intervalSize); + } + }} + onInitialize={newUrlState => { + if (newUrlState && newUrlState.intervalSize) { + setIntervalSize(newUrlState.intervalSize); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): LogMinimapUrlState | undefined => + value + ? { + intervalSize: mapToIntervalSizeUrlState(value.intervalSize), + } + : undefined; + +const mapToIntervalSizeUrlState = (value: any) => + value && typeof value === 'number' ? value : undefined; + +const selectMinimapUrlState = createSelector( + logMinimapSelectors.selectMinimapIntervalSize, + intervalSize => ({ + intervalSize, + }) +); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx new file mode 100644 index 0000000000000..a548ae62facb0 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { pickTimeKey } from '../../../common/time'; +import { logPositionActions, logPositionSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { replaceStateKeyInQueryString, UrlStateContainer } from '../../utils/url_state'; + +export const withLogPosition = connect( + (state: State) => ({ + firstVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), + isAutoReloading: logPositionSelectors.selectIsAutoReloading(state), + lastVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), + targetPosition: logPositionSelectors.selectTargetPosition(state), + urlState: selectPositionUrlState(state), + visibleTimeInterval: logPositionSelectors.selectVisibleTimeInterval(state), + visibleMidpoint: logPositionSelectors.selectVisibleMidpointOrTarget(state), + visibleMidpointTime: logPositionSelectors.selectVisibleMidpointOrTargetTime(state), + }), + bindPlainActionCreators({ + jumpToTargetPosition: logPositionActions.jumpToTargetPosition, + jumpToTargetPositionTime: logPositionActions.jumpToTargetPositionTime, + reportVisiblePositions: logPositionActions.reportVisiblePositions, + reportVisibleSummary: logPositionActions.reportVisibleSummary, + startLiveStreaming: logPositionActions.startAutoReload, + stopLiveStreaming: logPositionActions.stopAutoReload, + }) +); + +export const WithLogPosition = asChildFunctionRenderer(withLogPosition, { + onCleanup: ({ stopLiveStreaming }) => stopLiveStreaming(), +}); + +/** + * Url State + */ + +interface LogPositionUrlState { + position?: ReturnType; + streamLive?: ReturnType; +} + +export const WithLogPositionUrlState = () => ( + + {({ + jumpToTargetPosition, + jumpToTargetPositionTime, + startLiveStreaming, + stopLiveStreaming, + urlState, + }) => ( + { + if (newUrlState && newUrlState.position) { + jumpToTargetPosition(newUrlState.position); + } + if (newUrlState && newUrlState.streamLive) { + startLiveStreaming(5000); + } else if ( + newUrlState && + typeof newUrlState.streamLive !== 'undefined' && + !newUrlState.streamLive + ) { + stopLiveStreaming(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.position) { + jumpToTargetPosition(initialUrlState.position); + } else { + jumpToTargetPositionTime(Date.now()); + } + if (initialUrlState && initialUrlState.streamLive) { + startLiveStreaming(5000); + } + }} + /> + )} + +); + +const selectPositionUrlState = createSelector( + logPositionSelectors.selectVisibleMidpointOrTarget, + logPositionSelectors.selectIsAutoReloading, + (position, streamLive) => ({ + position: position ? pickTimeKey(position) : null, + streamLive, + }) +); + +const mapToUrlState = (value: any): LogPositionUrlState | undefined => + value + ? { + position: mapToPositionUrlState(value.position), + streamLive: mapToStreamLiveUrlState(value.streamLive), + } + : undefined; + +const mapToPositionUrlState = (value: any) => + value && (typeof value.time === 'number' && typeof value.tiebreaker === 'number') + ? pickTimeKey(value) + : undefined; + +const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); + +export const replaceLogPositionInQueryString = (time: number) => + Number.isNaN(time) + ? (value: string) => value + : replaceStateKeyInQueryString('logPosition', { + position: { + time, + tiebreaker: 0, + }, + }); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_search_controls_props.ts b/x-pack/plugins/infra/public/containers/logs/with_log_search_controls_props.ts new file mode 100644 index 0000000000000..e387da9575426 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_search_controls_props.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Temporary Workaround + * This is not a well-designed container. It only exists to enable quick + * migration of the redux-based logging ui into the infra-ui codebase. It will + * be removed during the refactoring to graphql/apollo. + */ +import { connect } from 'react-redux'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; + +import { + // searchActions, + // searchResultsSelectors, + // sharedSelectors, + logPositionActions, + State, +} from '../../store'; + +export const withLogSearchControlsProps = connect( + (state: State) => ({ + // isLoadingSearchResults: searchResultsSelectors.selectIsLoadingSearchResults(state), + // nextSearchResult: sharedSelectors.selectNextSearchResultKey(state), + // previousSearchResult: sharedSelectors.selectPreviousSearchResultKey(state), + }), + bindPlainActionCreators({ + // clearSearch: searchActions.clearSearch, + jumpToTarget: logPositionActions.jumpToTargetPosition, + // search: searchActions.search, + }) +); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx new file mode 100644 index 0000000000000..5d22bc481b783 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { TextScale } from '../../../common/log_text_scale'; +import { logTextviewActions, logTextviewSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +const availableTextScales = ['large', 'medium', 'small'] as TextScale[]; + +export const withLogTextview = connect( + (state: State) => ({ + availableTextScales, + textScale: logTextviewSelectors.selectTextviewScale(state), + urlState: selectTextviewUrlState(state), + wrap: logTextviewSelectors.selectTextviewWrap(state), + }), + bindPlainActionCreators({ + setTextScale: logTextviewActions.setTextviewScale, + setTextWrap: logTextviewActions.setTextviewWrap, + }) +); + +export const WithLogTextview = asChildFunctionRenderer(withLogTextview); + +/** + * Url State + */ + +interface LogTextviewUrlState { + textScale?: ReturnType; + wrap?: ReturnType; +} + +export const WithLogTextviewUrlState = () => ( + + {({ urlState, setTextScale, setTextWrap }) => ( + { + if (newUrlState && newUrlState.textScale) { + setTextScale(newUrlState.textScale); + } + if (newUrlState && typeof newUrlState.wrap !== 'undefined') { + setTextWrap(newUrlState.wrap); + } + }} + onInitialize={newUrlState => { + if (newUrlState && newUrlState.textScale) { + setTextScale(newUrlState.textScale); + } + if (newUrlState && typeof newUrlState.wrap !== 'undefined') { + setTextWrap(newUrlState.wrap); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): LogTextviewUrlState | undefined => + value + ? { + textScale: mapToTextScaleUrlState(value.textScale), + wrap: mapToWrapUrlState(value.wrap), + } + : undefined; + +const mapToTextScaleUrlState = (value: any) => + availableTextScales.includes(value) ? (value as TextScale) : undefined; + +const mapToWrapUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); + +const selectTextviewUrlState = createSelector( + logTextviewSelectors.selectTextviewScale, + logTextviewSelectors.selectTextviewWrap, + (textScale, wrap) => ({ + textScale, + wrap, + }) +); diff --git a/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts new file mode 100644 index 0000000000000..3421f93956bc9 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_stream_items.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { SearchResult } from '../../../common/log_search_result'; +import { logEntriesSelectors, logPositionSelectors, State } from '../../store'; +import { LogEntry, LogEntryMessageSegment } from '../../utils/log_entry'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; + +export const withStreamItems = connect( + (state: State) => ({ + isReloading: logEntriesSelectors.selectIsReloadingEntries(state), + isLoadingMore: logEntriesSelectors.selectIsLoadingMoreEntries(state), + hasMoreBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart(state), + hasMoreAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd(state), + lastLoadedTime: logEntriesSelectors.selectEntriesLastLoadedTime(state), + items: selectItems(state), + }), + {} +); + +export const WithStreamItems = asChildFunctionRenderer(withStreamItems); + +const selectItems = createSelector( + logEntriesSelectors.selectEntries, + logEntriesSelectors.selectIsReloadingEntries, + logPositionSelectors.selectIsAutoReloading, + // searchResultsSelectors.selectSearchResultsById, + (logEntries, isReloading, isAutoReloading /*, searchResults*/) => + isReloading && !isAutoReloading + ? [] + : logEntries.map(logEntry => + createLogEntryStreamItem(logEntry /*, searchResults[logEntry.gid] || null*/) + ) +); + +const createLogEntryStreamItem = (logEntry: LogEntry, searchResult?: SearchResult) => ({ + kind: 'logEntry' as 'logEntry', + logEntry: { + gid: logEntry.gid, + origin: { + id: logEntry.gid, + index: '', + type: '', + }, + fields: { + time: logEntry.key.time, + tiebreaker: logEntry.key.tiebreaker, + message: logEntry.message.map(formatMessageSegment).join(''), + }, + }, + searchResult, +}); + +const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string => + messageSegment.__typename === 'InfraLogMessageFieldSegment' + ? messageSegment.value + : messageSegment.__typename === 'InfraLogMessageConstantSegment' + ? messageSegment.constant + : 'failed to format message'; diff --git a/x-pack/plugins/infra/public/containers/logs/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/with_summary.ts new file mode 100644 index 0000000000000..424da80b460df --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_summary.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { logSummaryActions, logSummarySelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; + +export const withSummary = connect( + (state: State) => ({ + buckets: logSummarySelectors.selectSummaryBuckets(state), + }), + bindPlainActionCreators({ + load: logSummaryActions.loadSummary, + }) +); + +export const WithSummary = asChildFunctionRenderer(withSummary); diff --git a/x-pack/plugins/infra/public/containers/metrics/metrics.gql_query.ts b/x-pack/plugins/infra/public/containers/metrics/metrics.gql_query.ts new file mode 100644 index 0000000000000..2a5cc0219e81a --- /dev/null +++ b/x-pack/plugins/infra/public/containers/metrics/metrics.gql_query.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const metricsQuery = gql` + query MetricsQuery( + $sourceId: ID! + $timerange: InfraTimerangeInput! + $metrics: [InfraMetric!]! + $nodeId: ID! + $nodeType: InfraNodeType! + ) { + source(id: $sourceId) { + id + metrics(nodeId: $nodeId, timerange: $timerange, metrics: $metrics, nodeType: $nodeType) { + id + series { + id + data { + timestamp + value + } + } + } + } + } +`; diff --git a/x-pack/plugins/infra/public/containers/metrics/with_metrics.tsx b/x-pack/plugins/infra/public/containers/metrics/with_metrics.tsx new file mode 100644 index 0000000000000..9f44a24f4893c --- /dev/null +++ b/x-pack/plugins/infra/public/containers/metrics/with_metrics.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Query } from 'react-apollo'; +import { + InfraMetric, + InfraMetricData, + InfraNodeType, + InfraTimerangeInput, + MetricsQuery, +} from '../../../common/graphql/types'; +import { InfraMetricLayout } from '../../pages/metrics/layouts/types'; +import { metricsQuery } from './metrics.gql_query'; + +interface WithMetricsArgs { + metrics: InfraMetricData[]; + error?: string | undefined; + loading: boolean; +} + +interface WithMetricsProps { + children: (args: WithMetricsArgs) => React.ReactNode; + layouts: InfraMetricLayout[]; + nodeType: InfraNodeType; + nodeId: string; + sourceId: string; + timerange: InfraTimerangeInput; +} + +export const WithMetrics = ({ + children, + layouts, + sourceId, + timerange, + nodeType, + nodeId, +}: WithMetricsProps) => { + const metrics = layouts.reduce( + (acc, item) => { + return acc.concat(item.sections.map(s => s.id)); + }, + [] as InfraMetric[] + ); + + return ( + + query={metricsQuery} + fetchPolicy="no-cache" + variables={{ + sourceId, + metrics, + nodeType, + nodeId, + timerange, + }} + > + {({ data, error, loading }) => { + return children({ + metrics: filterOnlyInfraMetricData(data && data.source && data.source.metrics), + error: error && error.message, + loading, + }); + }} + + ); +}; + +const filterOnlyInfraMetricData = ( + metrics: Array | undefined +): InfraMetricData[] => { + if (!metrics) { + return []; + } + return metrics.filter(m => m !== null).map(m => m as InfraMetricData); +}; diff --git a/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx new file mode 100644 index 0000000000000..c2d4fdcc62322 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { metricTimeActions, metricTimeSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withMetricsTime = connect( + (state: State) => ({ + currentTimeRange: metricTimeSelectors.selectRangeTime(state), + isAutoReloading: metricTimeSelectors.selectIsAutoReloading(state), + urlState: selectTimeUrlState(state), + }), + bindPlainActionCreators({ + setRangeTime: metricTimeActions.setRangeTime, + startMetricsAutoReload: metricTimeActions.startMetricsAutoReload, + stopMetricsAutoReload: metricTimeActions.stopMetricsAutoReload, + }) +); + +export const WithMetricsTime = asChildFunctionRenderer(withMetricsTime, { + onCleanup: ({ stopMetricsAutoReload }) => stopMetricsAutoReload(), +}); + +/** + * Url State + */ + +interface MetricTimeUrlState { + timeRange?: ReturnType; + autoReload?: ReturnType; +} + +export const WithMetricsTimeUrlState = () => ( + + {({ setRangeTime, startMetricsAutoReload, stopMetricsAutoReload, urlState }) => ( + { + if (newUrlState && newUrlState.timeRange) { + setRangeTime(newUrlState.timeRange); + } + if (newUrlState && newUrlState.autoReload) { + startMetricsAutoReload(); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + stopMetricsAutoReload(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.timeRange) { + setRangeTime(initialUrlState.timeRange); + } + if (initialUrlState && initialUrlState.autoReload) { + startMetricsAutoReload(); + } + }} + /> + )} + +); + +const selectTimeUrlState = createSelector( + metricTimeSelectors.selectRangeTime, + metricTimeSelectors.selectIsAutoReloading, + (time, autoReload) => ({ + time, + autoReload, + }) +); + +const mapToUrlState = (value: any): MetricTimeUrlState | undefined => + value + ? { + timeRange: mapToTimeUrlState(value.timeRange), + autoReload: mapToAutoReloadUrlState(value.autoReload), + } + : undefined; + +const mapToTimeUrlState = (value: any) => + value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined; + +const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/containers/waffle/index.ts b/x-pack/plugins/infra/public/containers/waffle/index.ts new file mode 100644 index 0000000000000..738a2f7347ce4 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './with_waffle_filters'; +export * from './with_waffle_nodes'; diff --git a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts new file mode 100644 index 0000000000000..6dad5d0b9405b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first, last } from 'lodash'; +import { InfraNode, InfraNodePath } from '../../../common/graphql/types'; +import { + InfraWaffleMapGroup, + InfraWaffleMapGroupOfGroups, + InfraWaffleMapGroupOfNodes, + InfraWaffleMapNode, +} from '../../lib/lib'; +import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards'; + +function createId(path: InfraNodePath[]) { + return path.map(p => p.value).join('/'); +} + +function findOrCreateGroupWithNodes( + groups: InfraWaffleMapGroup[], + path: InfraNodePath[] +): InfraWaffleMapGroupOfNodes { + const id = path.length === 0 ? '__all__' : createId(path); + /** + * If the group is going to be a top level group then we can just + * look for the full id. Otherwise we need to find the parent group and + * then look for the group in it's sub groups. + */ + if (path.length === 2) { + const parentId = first(path).value; + const existingParentGroup = groups.find(g => g.id === parentId); + if (isWaffleMapGroupWithGroups(existingParentGroup)) { + const existingSubGroup = existingParentGroup.groups.find(g => g.id === id); + if (isWaffleMapGroupWithNodes(existingSubGroup)) { + return existingSubGroup; + } + } + } + const existingGroup = groups.find(g => g.id === id); + if (isWaffleMapGroupWithNodes(existingGroup)) { + return existingGroup; + } + return { + id, + name: id === '__all__' ? 'All' : last(path).value, + count: 0, + width: 0, + squareSize: 0, + nodes: [], + }; +} + +function findOrCreateGroupWithGroups( + groups: InfraWaffleMapGroup[], + path: InfraNodePath[] +): InfraWaffleMapGroupOfGroups { + const id = path.length === 0 ? '__all__' : createId(path); + const existingGroup = groups.find(g => g.id === id); + if (isWaffleMapGroupWithGroups(existingGroup)) { + return existingGroup; + } + return { + id, + name: id === '__all__' ? 'All' : last(path).value, + count: 0, + width: 0, + squareSize: 0, + groups: [], + }; +} + +function createWaffleMapNode(node: InfraNode): InfraWaffleMapNode { + return { + id: node.path.map(p => p.value).join('/'), + path: node.path, + name: last(node.path).value, + metric: node.metric, + }; +} + +function withoutGroup(group: InfraWaffleMapGroup) { + return (subject: InfraWaffleMapGroup) => { + return subject.id !== group.id; + }; +} + +export function nodesToWaffleMap(nodes: InfraNode[]): InfraWaffleMapGroup[] { + return nodes.reduce((groups: InfraWaffleMapGroup[], node: InfraNode) => { + const waffleNode = createWaffleMapNode(node); + if (node.path.length === 2) { + const parentGroup = findOrCreateGroupWithNodes( + groups, + node.path.slice(0, node.path.length - 1) + ); + parentGroup.nodes.push(waffleNode); + return groups.filter(withoutGroup(parentGroup)).concat([parentGroup]); + } + if (node.path.length === 3) { + const parentGroup = findOrCreateGroupWithNodes( + groups, + node.path.slice(0, node.path.length - 1) + ); + parentGroup.nodes.push(waffleNode); + const topGroup = findOrCreateGroupWithGroups( + groups, + node.path.slice(0, node.path.length - 2) + ); + topGroup.groups = topGroup.groups.filter(withoutGroup(parentGroup)).concat([parentGroup]); + return groups.filter(withoutGroup(topGroup)).concat([topGroup]); + } + const allGroup = findOrCreateGroupWithNodes(groups, []); + allGroup.nodes.push(waffleNode); + return groups.filter(withoutGroup(allGroup)).concat([allGroup]); + }, []); +} diff --git a/x-pack/plugins/infra/public/containers/waffle/type_guards.ts b/x-pack/plugins/infra/public/containers/waffle/type_guards.ts new file mode 100644 index 0000000000000..3e21e3a56a6c6 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/type_guards.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraWaffleMapGroupOfGroups, InfraWaffleMapGroupOfNodes } from '../../lib/lib'; + +export function isWaffleMapGroupWithNodes(subject: any): subject is InfraWaffleMapGroupOfNodes { + return subject && subject.nodes != null && Array.isArray(subject.nodes); +} + +export function isWaffleMapGroupWithGroups(subject: any): subject is InfraWaffleMapGroupOfGroups { + return subject && subject.groups != null && Array.isArray(subject.groups); +} diff --git a/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts b/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts new file mode 100644 index 0000000000000..8da5b160603e7 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const waffleNodesQuery = gql` + query WaffleNodesQuery( + $sourceId: ID! + $timerange: InfraTimerangeInput! + $filterQuery: String + $metric: InfraMetricInput! + $path: [InfraPathInput!]! + ) { + source(id: $sourceId) { + id + map(timerange: $timerange, filterQuery: $filterQuery) { + nodes(path: $path, metric: $metric) { + path { + value + } + metric { + name + value + } + } + } + } + } +`; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx new file mode 100644 index 0000000000000..80f912574c72b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { sharedSelectors, State, waffleFilterActions, waffleFilterSelectors } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withWaffleFilter = connect( + (state: State) => ({ + filterQuery: waffleFilterSelectors.selectWaffleFilterQuery(state), + filterQueryDraft: waffleFilterSelectors.selectWaffleFilterQueryDraft(state), + filterQueryAsJson: sharedSelectors.selectWaffleFilterQueryAsJson(state), + isFilterQueryDraftValid: waffleFilterSelectors.selectIsWaffleFilterQueryDraftValid(state), + }), + bindPlainActionCreators({ + applyFilterQuery: waffleFilterActions.applyWaffleFilterQuery, + applyFilterQueryFromKueryExpression: (expression: string) => + waffleFilterActions.applyWaffleFilterQuery({ + kind: 'kuery', + expression, + }), + setFilterQueryDraft: waffleFilterActions.setWaffleFilterQueryDraft, + setFilterQueryDraftFromKueryExpression: (expression: string) => + waffleFilterActions.setWaffleFilterQueryDraft({ + kind: 'kuery', + expression, + }), + }) +); + +export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter); + +/** + * Url State + */ + +type WaffleFilterUrlState = ReturnType; + +export const WithWaffleFilterUrlState = () => ( + + {({ applyFilterQuery, filterQuery }) => ( + { + if (urlState) { + applyFilterQuery(urlState); + } + }} + onInitialize={urlState => { + if (urlState) { + applyFilterQuery(urlState); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): WaffleFilterUrlState | undefined => + value && value.kind === 'kuery' && typeof value.expression === 'string' + ? { + kind: value.kind, + expression: value.expression, + } + : undefined; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx new file mode 100644 index 0000000000000..f8a5a7a607917 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Query } from 'react-apollo'; + +import { + InfraMetricInput, + InfraPathInput, + InfraPathType, + InfraTimerangeInput, + WaffleNodesQuery, +} from '../../../common/graphql/types'; +import { InfraNodeType } from '../../../server/lib/adapters/nodes'; +import { InfraWaffleMapGroup } from '../../lib/lib'; +import { nodesToWaffleMap } from './nodes_to_wafflemap'; +import { waffleNodesQuery } from './waffle_nodes.gql_query'; + +interface WithWaffleNodesArgs { + nodes: InfraWaffleMapGroup[]; + loading: boolean; + refetch: () => void; +} + +interface WithWaffleNodesProps { + children: (args: WithWaffleNodesArgs) => React.ReactNode; + filterQuery: string | null | undefined; + metric: InfraMetricInput; + groupBy: InfraPathInput[]; + nodeType: InfraNodeType; + sourceId: string; + timerange: InfraTimerangeInput; +} + +const NODE_TYPE_TO_PATH_TYPE = { + [InfraNodeType.container]: InfraPathType.containers, + [InfraNodeType.host]: InfraPathType.hosts, + [InfraNodeType.pod]: InfraPathType.pods, +}; + +export const WithWaffleNodes = ({ + children, + filterQuery, + metric, + groupBy, + nodeType, + sourceId, + timerange, +}: WithWaffleNodesProps) => ( + + query={waffleNodesQuery} + fetchPolicy="no-cache" + notifyOnNetworkStatusChange + variables={{ + sourceId, + metric, + path: [...groupBy, { type: NODE_TYPE_TO_PATH_TYPE[nodeType] }], + timerange, + filterQuery, + }} + > + {({ data, loading, refetch }) => + children({ + loading, + nodes: + data && data.source && data.source.map && data.source.map.nodes + ? nodesToWaffleMap(data.source.map.nodes) + : [], + refetch, + }) + } + +); diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx new file mode 100644 index 0000000000000..3992d78f71cd0 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { InfraMetricInput, InfraMetricType, InfraPathType } from '../../../common/graphql/types'; +import { InfraNodeType } from '../../../server/lib/adapters/nodes'; +import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store'; +import { initialWaffleOptionsState } from '../../store/local/waffle_options/reducer'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +const selectOptionsUrlState = createSelector( + waffleOptionsSelectors.selectMetric, + waffleOptionsSelectors.selectGroupBy, + waffleOptionsSelectors.selectNodeType, + (metrics, groupBy, nodeType) => ({ + metrics, + groupBy, + nodeType, + }) +); + +export const withWaffleOptions = connect( + (state: State) => ({ + metric: waffleOptionsSelectors.selectMetric(state), + groupBy: waffleOptionsSelectors.selectGroupBy(state), + nodeType: waffleOptionsSelectors.selectNodeType(state), + urlState: selectOptionsUrlState(state), + }), + bindPlainActionCreators({ + changeMetric: waffleOptionsActions.changeMetric, + changeGroupBy: waffleOptionsActions.changeGroupBy, + changeNodeType: waffleOptionsActions.changeNodeType, + }) +); + +export const WithWaffleOptions = asChildFunctionRenderer(withWaffleOptions); + +/** + * Url State + */ + +interface WaffleOptionsUrlState { + metric?: ReturnType; + groupBy?: ReturnType; + nodeType?: ReturnType; +} + +export const WithWaffleOptionsUrlState = () => ( + + {({ changeMetric, urlState, changeGroupBy, changeNodeType }) => ( + { + if (newUrlState && newUrlState.metric) { + changeMetric(newUrlState.metric); + } + if (newUrlState && newUrlState.groupBy) { + changeGroupBy(newUrlState.groupBy); + } + if (newUrlState && newUrlState.nodeType) { + changeNodeType(newUrlState.nodeType); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState) { + changeMetric(initialUrlState.metric || initialWaffleOptionsState.metric); + changeGroupBy(initialUrlState.groupBy || initialWaffleOptionsState.groupBy); + changeNodeType(initialUrlState.nodeType || initialWaffleOptionsState.nodeType); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined => + value + ? { + metric: mapToMetricUrlState(value.metric), + groupBy: mapToGroupByUrlState(value.groupBy), + nodeType: mapToNodeTypeUrlState(value.nodeType), + } + : undefined; + +const isInfraMetricInput = (subject: any): subject is InfraMetricInput => { + return subject != null && subject.type != null && InfraMetricType[subject.type] != null; +}; + +const isInfraPathInput = (subject: any): subject is InfraPathType => { + return subject != null && subject.type != null && InfraPathType[subject.type] != null; +}; + +const mapToMetricUrlState = (subject: any) => { + return subject && isInfraMetricInput(subject) ? subject : undefined; +}; + +const mapToGroupByUrlState = (subject: any) => { + return subject && Array.isArray(subject) && subject.every(isInfraPathInput) ? subject : undefined; +}; + +const mapToNodeTypeUrlState = (subject: any) => { + return subject && InfraNodeType[subject] ? subject : undefined; +}; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx new file mode 100644 index 0000000000000..293f6184af21b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { State, waffleTimeActions, waffleTimeSelectors } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withWaffleTime = connect( + (state: State) => ({ + currentTime: waffleTimeSelectors.selectCurrentTime(state), + currentTimeRange: waffleTimeSelectors.selectCurrentTimeRange(state), + isAutoReloading: waffleTimeSelectors.selectIsAutoReloading(state), + urlState: selectTimeUrlState(state), + }), + bindPlainActionCreators({ + jumpToTime: waffleTimeActions.jumpToTime, + startAutoReload: waffleTimeActions.startAutoReload, + stopAutoReload: waffleTimeActions.stopAutoReload, + }) +); + +export const WithWaffleTime = asChildFunctionRenderer(withWaffleTime, { + onCleanup: ({ stopAutoReload }) => stopAutoReload(), +}); + +/** + * Url State + */ + +interface WaffleTimeUrlState { + time?: ReturnType; + autoReload?: ReturnType; +} + +export const WithWaffleTimeUrlState = () => ( + + {({ jumpToTime, startAutoReload, stopAutoReload, urlState }) => ( + { + if (newUrlState && newUrlState.time) { + jumpToTime(newUrlState.time); + } + if (newUrlState && newUrlState.autoReload) { + startAutoReload(); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + stopAutoReload(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState) { + jumpToTime(initialUrlState.time ? initialUrlState.time : Date.now()); + } + if (initialUrlState && initialUrlState.autoReload) { + startAutoReload(); + } + }} + /> + )} + +); + +const selectTimeUrlState = createSelector( + waffleTimeSelectors.selectCurrentTime, + waffleTimeSelectors.selectIsAutoReloading, + (time, autoReload) => ({ + time, + autoReload, + }) +); + +const mapToUrlState = (value: any): WaffleTimeUrlState | undefined => + value + ? { + time: mapToTimeUrlState(value.time), + autoReload: mapToAutoReloadUrlState(value.autoReload), + } + : undefined; + +const mapToTimeUrlState = (value: any) => (value && typeof value === 'number' ? value : undefined); + +const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx b/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx new file mode 100644 index 0000000000000..a298f3f1c4d7d --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_kibana_chrome.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import chrome from 'ui/chrome'; + +import { RendererFunction } from '../utils/typed_react'; + +interface WithKibanaChromeProps { + children: RendererFunction<{ + basePath: string; + }>; +} + +export const WithKibanaChrome: React.SFC = ({ children }) => + children({ + basePath: chrome.getBasePath(), + }); diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx new file mode 100644 index 0000000000000..b3d062aeed080 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { AutocompleteSuggestion, getAutocompleteProvider } from 'ui/autocomplete_providers'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { sourceSelectors, State } from '../store'; +import { RendererFunction } from '../utils/typed_react'; + +const withIndexPattern = connect((state: State) => ({ + indexPattern: sourceSelectors.selectDerivedIndexPattern(state), +})); + +interface WithKueryAutocompletionLifecycleProps { + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: AutocompleteSuggestion[]; + }>; + indexPattern: StaticIndexPattern; +} + +interface WithKueryAutocompletionLifecycleState { + // lacking cancellation support in the autocompletion api, + // this is used to keep older, slower requests from clobbering newer ones + currentRequest: { + expression: string; + cursorPosition: number; + } | null; + suggestions: AutocompleteSuggestion[]; +} + +export const WithKueryAutocompletion = withIndexPattern( + class WithKueryAutocompletionLifecycle extends React.Component< + WithKueryAutocompletionLifecycleProps, + WithKueryAutocompletionLifecycleState + > { + public readonly state: WithKueryAutocompletionLifecycleState = { + currentRequest: null, + suggestions: [], + }; + + public render() { + const { currentRequest, suggestions } = this.state; + + return this.props.children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions: this.loadSuggestions, + suggestions, + }); + } + + private loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number + ) => { + const { indexPattern } = this.props; + const autocompletionProvider = getAutocompleteProvider('kuery'); + const config = { + get: () => true, + }; + + if (!autocompletionProvider) { + return; + } + + const getSuggestions = autocompletionProvider({ + config, + indexPatterns: [indexPattern], + boolFilter: [], + }); + + this.setState({ + currentRequest: { + expression, + cursorPosition, + }, + suggestions: [], + }); + + const suggestions = await getSuggestions({ + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + }); + + this.setState( + state => + state.currentRequest && + state.currentRequest.expression !== expression && + state.currentRequest.cursorPosition !== cursorPosition + ? state // ignore this result, since a newer request is in flight + : { + ...state, + currentRequest: null, + suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, + } + ); + }; + } +); diff --git a/x-pack/plugins/infra/public/containers/with_options.tsx b/x-pack/plugins/infra/public/containers/with_options.tsx new file mode 100644 index 0000000000000..0e92a6ef43edb --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_options.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import React from 'react'; + +import { InfraMetricType, InfraPathType } from '../../common/graphql/types'; +import { + InfraFormatterType, + InfraOptions, + InfraWaffleMapLegendMode, + // InfraWaffleMapRuleOperator, +} from '../lib/lib'; +import { RendererFunction } from '../utils/typed_react'; + +const initialState = { + options: { + sourceId: 'default', + timerange: { + interval: '1m', + to: moment.utc().valueOf(), + from: moment + .utc() + .subtract(1, 'h') + .valueOf(), + }, + wafflemap: { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + metric: { type: InfraMetricType.cpu }, + path: [{ type: InfraPathType.hosts }], + /* + legend: { + type: InfraWaffleMapLegendMode.step, + rules: [ + { + value: 0, + color: '#00B3A4', + operator: InfraWaffleMapRuleOperator.gte, + label: 'Ok', + }, + { + value: 10000, + color: '#DB1374', + operator: InfraWaffleMapRuleOperator.gte, + label: 'Over 10,000', + }, + ], + }, + */ + legend: { + type: InfraWaffleMapLegendMode.gradient, + rules: [ + { + value: 0, + color: '#D9D9D9', + }, + { + value: 0.65, + color: '#00B3A4', + }, + { + value: 0.8, + color: '#E6C220', + }, + { + value: 1, + color: '#DB1374', + }, + ], + }, + }, + } as InfraOptions, +}; + +interface WithOptionsProps { + children: RendererFunction; +} + +type State = Readonly; + +export const withOptions =

(WrappedComponent: React.ComponentType

) => ( + {args => } +); + +export class WithOptions extends React.Component { + public readonly state: State = initialState; + + public render() { + return this.props.children(this.state.options); + } +} diff --git a/x-pack/plugins/infra/public/containers/with_source.ts b/x-pack/plugins/infra/public/containers/with_source.ts new file mode 100644 index 0000000000000..9674cda59575c --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_source.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; + +import { sourceSelectors, State } from '../store'; +import { asChildFunctionRenderer } from '../utils/typed_react'; + +export const withSource = connect((state: State) => ({ + configuredFields: sourceSelectors.selectSourceFields(state), + logIndicesExist: sourceSelectors.selectSourceLogIndicesExist(state), + metricIndicesExist: sourceSelectors.selectSourceMetricIndicesExist(state), +})); + +export const WithSource = asChildFunctionRenderer(withSource); diff --git a/x-pack/plugins/infra/public/containers/with_state_from_location.tsx b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx new file mode 100644 index 0000000000000..b8c8c85d96631 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; +import omit from 'lodash/fp/omit'; +import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; +import React from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; +import { decode_object, encode_object } from 'rison-node'; +import { Omit } from '../lib/lib'; + +interface AnyObject { + [key: string]: any; +} + +interface WithStateFromLocationOptions { + mapLocationToState: (location: Location) => StateInLocation; + mapStateToLocation: (state: StateInLocation, location: Location) => Location; +} + +type InjectedPropsFromLocation = Partial & { + pushStateInLocation?: (state: StateInLocation) => void; + replaceStateInLocation?: (state: StateInLocation) => void; +}; + +export const withStateFromLocation = ({ + mapLocationToState, + mapStateToLocation, +}: WithStateFromLocationOptions) => < + WrappedComponentProps extends InjectedPropsFromLocation +>( + WrappedComponent: React.ComponentType +) => { + const wrappedName = WrappedComponent.displayName || WrappedComponent.name; + + return withRouter( + class WithStateFromLocation extends React.PureComponent< + RouteComponentProps<{}> & + Omit> + > { + public static displayName = `WithStateFromLocation(${wrappedName})`; + + public render() { + const { location } = this.props; + const otherProps = omit(['location', 'history', 'match', 'staticContext'], this.props); + + const stateFromLocation = mapLocationToState(location); + + return ( + + ); + } + + private pushStateInLocation = (state: StateInLocation) => { + const { history, location } = this.props; + + const newLocation = mapStateToLocation(state, this.props.location); + + if (newLocation !== location) { + history.push(newLocation); + } + }; + + private replaceStateInLocation = (state: StateInLocation) => { + const { history, location } = this.props; + + const newLocation = mapStateToLocation(state, this.props.location); + + if (newLocation !== location) { + history.replace(newLocation); + } + }; + } + ); +}; + +const decodeRisonAppState = (queryValues: { _a?: string }): AnyObject => { + try { + return queryValues && queryValues._a ? decode_object(queryValues._a) : {}; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return {}; + } + throw error; + } +}; + +const encodeRisonAppState = (state: AnyObject) => ({ + _a: encode_object(state), +}); + +export const mapRisonAppLocationToState = ( + mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => state as State +) => (location: Location): State => { + const queryValues = parseQueryString(location.search.substring(1)); + const decodedState = decodeRisonAppState(queryValues); + return mapState(decodedState); +}; + +export const mapStateToRisonAppLocation = ( + mapState: (state: State) => AnyObject = (state: State) => state +) => (state: State, location: Location): Location => { + const previousQueryValues = parseQueryString(location.search.substring(1)); + const previousState = decodeRisonAppState(previousQueryValues); + + const encodedState = encodeRisonAppState({ + ...previousState, + ...mapState(state), + }); + const newQueryValues = stringifyQueryString({ + ...previousQueryValues, + ...encodedState, + }); + return { + ...location, + search: `?${newQueryValues}`, + }; +}; diff --git a/x-pack/plugins/infra/public/images/docker.svg b/x-pack/plugins/infra/public/images/docker.svg new file mode 100644 index 0000000000000..e4499c8ce275d --- /dev/null +++ b/x-pack/plugins/infra/public/images/docker.svg @@ -0,0 +1,12 @@ + + + + Shape + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/infra/public/images/hosts.svg b/x-pack/plugins/infra/public/images/hosts.svg new file mode 100644 index 0000000000000..d04e953d90fa3 --- /dev/null +++ b/x-pack/plugins/infra/public/images/hosts.svg @@ -0,0 +1,12 @@ + + + + picto-server-orange + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/infra/public/images/infra_mono_white.svg b/x-pack/plugins/infra/public/images/infra_mono_white.svg new file mode 100644 index 0000000000000..168ca6c03f18d --- /dev/null +++ b/x-pack/plugins/infra/public/images/infra_mono_white.svg @@ -0,0 +1,15 @@ + + + + infra-mono + Created with Sketch. + + + + + + + + + + diff --git a/x-pack/plugins/infra/public/images/k8.svg b/x-pack/plugins/infra/public/images/k8.svg new file mode 100644 index 0000000000000..2509bd9e8e6b0 --- /dev/null +++ b/x-pack/plugins/infra/public/images/k8.svg @@ -0,0 +1,13 @@ + + + + k8 + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/x-pack/plugins/infra/public/images/logging_mono_white.svg b/x-pack/plugins/infra/public/images/logging_mono_white.svg new file mode 100644 index 0000000000000..0b7363ea16e8e --- /dev/null +++ b/x-pack/plugins/infra/public/images/logging_mono_white.svg @@ -0,0 +1,18 @@ + + + + logging-mono + Created with Sketch. + + + + + + + + + + + + + diff --git a/x-pack/plugins/infra/public/images/services.svg b/x-pack/plugins/infra/public/images/services.svg new file mode 100644 index 0000000000000..52a8db8126ee7 --- /dev/null +++ b/x-pack/plugins/infra/public/images/services.svg @@ -0,0 +1,9 @@ + + + Shape + Created with Sketch. + + + + + \ No newline at end of file diff --git a/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..2d9bab756c104 --- /dev/null +++ b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IModule, IScope } from 'angular'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { UIRoutes as KibanaUIRoutes } from 'ui/routes'; + +import { + InfraBufferedKibanaServiceCall, + InfraFrameworkAdapter, + InfraKibanaAdapterServiceRefs, + InfraKibanaUIConfig, + InfraTimezoneProvider, + InfraUiKibanaAdapterScope, +} from '../../lib'; + +const ROOT_ELEMENT_ID = 'react-infra-root'; +const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs'; + +export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter { + public appState: object; + public dateFormat?: string; + public kbnVersion?: string; + public scaledDateFormat?: string; + public timezone?: string; + + private adapterService: KibanaAdapterServiceProvider; + private timezoneProvider: InfraTimezoneProvider; + private rootComponent: React.ReactElement | null = null; + private breadcrumbsComponent: React.ReactElement | null = null; + + constructor( + uiModule: IModule, + uiRoutes: KibanaUIRoutes, + timezoneProvider: InfraTimezoneProvider + ) { + this.adapterService = new KibanaAdapterServiceProvider(); + this.timezoneProvider = timezoneProvider; + this.appState = {}; + this.register(uiModule, uiRoutes); + } + + public setUISettings = (key: string, value: any) => { + this.adapterService.callOrBuffer(({ config }) => { + config.set(key, value); + }); + }; + + public render = (component: React.ReactElement) => { + this.adapterService.callOrBuffer(() => (this.rootComponent = component)); + }; + + public renderBreadcrumbs = (component: React.ReactElement) => { + this.adapterService.callOrBuffer(() => (this.breadcrumbsComponent = component)); + }; + + private register = (adapterModule: IModule, uiRoutes: KibanaUIRoutes) => { + adapterModule.provider('kibanaAdapter', this.adapterService); + + adapterModule.directive('infraUiKibanaAdapter', () => ({ + controller: ($scope: InfraUiKibanaAdapterScope, $element: JQLite) => ({ + $onDestroy: () => { + const targetRootElement = $element[0].querySelector(`#${ROOT_ELEMENT_ID}`); + const targetBreadcrumbsElement = $element[0].querySelector(`#${ROOT_ELEMENT_ID}`); + + if (targetRootElement) { + ReactDOM.unmountComponentAtNode(targetRootElement); + } + + if (targetBreadcrumbsElement) { + ReactDOM.unmountComponentAtNode(targetBreadcrumbsElement); + } + }, + $onInit: () => { + $scope.topNavMenu = []; + }, + $postLink: () => { + $scope.$watchGroup( + [ + () => this.breadcrumbsComponent, + () => $element[0].querySelector(`#${BREADCRUMBS_ELEMENT_ID}`), + ], + ([breadcrumbsComponent, targetElement]) => { + if (!targetElement) { + return; + } + + if (breadcrumbsComponent) { + ReactDOM.render(breadcrumbsComponent, targetElement); + } else { + ReactDOM.unmountComponentAtNode(targetElement); + } + } + ); + $scope.$watchGroup( + [() => this.rootComponent, () => $element[0].querySelector(`#${ROOT_ELEMENT_ID}`)], + ([rootComponent, targetElement]) => { + if (!targetElement) { + return; + } + + if (rootComponent) { + ReactDOM.render(rootComponent, targetElement); + } else { + ReactDOM.unmountComponentAtNode(targetElement); + } + } + ); + }, + }), + scope: true, + template: ` +

+ `, + })); + + adapterModule.run(( + config: InfraKibanaUIConfig, + kbnVersion: string, + Private: (provider: Provider) => Provider, + // @ts-ignore: inject kibanaAdapter to force eager instatiation + kibanaAdapter: any + ) => { + this.timezone = Private(this.timezoneProvider)(); + this.kbnVersion = kbnVersion; + this.dateFormat = config.get('dateFormat'); + this.scaledDateFormat = config.get('dateFormat:scaled'); + }); + + uiRoutes.enable(); + + uiRoutes.otherwise({ + reloadOnSearch: false, + template: + '', + }); + }; +} + +// tslint:disable-next-line: max-classes-per-file +class KibanaAdapterServiceProvider { + public serviceRefs: InfraKibanaAdapterServiceRefs | null = null; + public bufferedCalls: Array> = []; + + public $get($rootScope: IScope, config: InfraKibanaUIConfig) { + this.serviceRefs = { + config, + rootScope: $rootScope, + }; + + this.applyBufferedCalls(this.bufferedCalls); + + return this; + } + + public callOrBuffer(serviceCall: (serviceRefs: InfraKibanaAdapterServiceRefs) => void) { + if (this.serviceRefs !== null) { + this.applyBufferedCalls([serviceCall]); + } else { + this.bufferedCalls.push(serviceCall); + } + } + + public applyBufferedCalls( + bufferedCalls: Array> + ) { + if (!this.serviceRefs) { + return; + } + + this.serviceRefs.rootScope.$apply(() => { + bufferedCalls.forEach(serviceCall => { + if (!this.serviceRefs) { + return; + } + return serviceCall(this.serviceRefs); + }); + }); + } +} diff --git a/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts new file mode 100644 index 0000000000000..da56c7b97b226 --- /dev/null +++ b/x-pack/plugins/infra/public/lib/adapters/framework/testing_framework_adapter.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraFrameworkAdapter } from '../../lib'; + +export class InfraTestingFrameworkAdapter implements InfraFrameworkAdapter { + public appState?: object; + public dateFormat?: string; + public kbnVersion?: string; + public scaledDateFormat?: string; + public timezone?: string; + + constructor() { + this.appState = {}; + } + + public render() { + return; + } + public renderBreadcrumbs() { + return; + } + public setUISettings() { + return; + } +} diff --git a/x-pack/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts b/x-pack/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts new file mode 100644 index 0000000000000..157a7ebe9fedf --- /dev/null +++ b/x-pack/plugins/infra/public/lib/adapters/observable_api/kibana_observable_api.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajax } from 'rxjs/ajax'; +import { map } from 'rxjs/operators'; + +import { + InfraObservableApi, + InfraObservableApiPostParams, + InfraObservableApiResponse, +} from '../../lib'; + +export class InfraKibanaObservableApiAdapter implements InfraObservableApi { + private basePath: string; + private defaultHeaders: { + [headerName: string]: string; + }; + + constructor({ basePath, xsrfToken }: { basePath: string; xsrfToken: string }) { + this.basePath = basePath; + this.defaultHeaders = { + 'kbn-version': xsrfToken, + }; + } + + public post = ({ + url, + body, + }: InfraObservableApiPostParams): InfraObservableApiResponse => + ajax({ + body: body ? JSON.stringify(body) : undefined, + headers: { + ...this.defaultHeaders, + 'Content-Type': 'application/json', + }, + method: 'POST', + responseType: 'json', + timeout: 30000, + url: `${this.basePath}/api/${url}`, + withCredentials: true, + }).pipe(map(({ response, status }) => ({ response, status }))); +} diff --git a/x-pack/plugins/infra/public/lib/compose/kibana_compose.ts b/x-pack/plugins/infra/public/lib/compose/kibana_compose.ts new file mode 100644 index 0000000000000..3ce48d9a31b96 --- /dev/null +++ b/x-pack/plugins/infra/public/lib/compose/kibana_compose.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import chrome from 'ui/chrome'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +import uiRoutes from 'ui/routes'; +// @ts-ignore: path dynamic for kibana +import { timezoneProvider } from 'ui/vis/lib/timezone'; + +import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; + +import introspectionQueryResultData from '../../../common/graphql/introspection.json'; +import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { InfraFrontendLibs } from '../lib'; + +import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { ApolloLink } from 'apollo-link'; +import { HttpLink } from 'apollo-link-http'; +import { withClientState } from 'apollo-link-state'; + +export function compose(): InfraFrontendLibs { + const cache = new InMemoryCache({ + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }); + + const observableApi = new InfraKibanaObservableApiAdapter({ + basePath: chrome.getBasePath(), + xsrfToken: chrome.getXsrfToken(), + }); + + const graphQLOptions = { + cache, + link: ApolloLink.from([ + withClientState({ + cache, + resolvers: {}, + }), + new HttpLink({ + credentials: 'same-origin', + headers: { + 'kbn-xsrf': chrome.getXsrfToken(), + }, + uri: `${chrome.getBasePath()}/api/infra/graphql`, + }), + ]), + }; + + const apolloClient = new ApolloClient(graphQLOptions); + + const infraModule = uiModules.get('app/infa'); + + const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + + const libs: InfraFrontendLibs = { + apolloClient, + framework, + observableApi, + }; + return libs; +} diff --git a/x-pack/plugins/infra/public/lib/compose/testing_compose.ts b/x-pack/plugins/infra/public/lib/compose/testing_compose.ts new file mode 100644 index 0000000000000..14fd66d378121 --- /dev/null +++ b/x-pack/plugins/infra/public/lib/compose/testing_compose.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import chrome from 'ui/chrome'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +import uiRoutes from 'ui/routes'; +// @ts-ignore: path dynamic for kibana +import { timezoneProvider } from 'ui/vis/lib/timezone'; + +import { InMemoryCache } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { SchemaLink } from 'apollo-link-schema'; +import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; +import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; +import { InfraFrontendLibs } from '../lib'; + +export function compose(): InfraFrontendLibs { + const infraModule = uiModules.get('app/infa'); + const observableApi = new InfraKibanaObservableApiAdapter({ + basePath: chrome.getBasePath(), + xsrfToken: chrome.getXsrfToken(), + }); + const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const typeDefs = ` + Query {} +`; + + const mocks = { + Mutation: () => undefined, + Query: () => undefined, + }; + + const schema = makeExecutableSchema({ typeDefs }); + addMockFunctionsToSchema({ + mocks, + schema, + }); + + const cache = new InMemoryCache((window as any).__APOLLO_CLIENT__); + + const apolloClient = new ApolloClient({ + cache, + link: new SchemaLink({ schema }), + }); + + const libs: InfraFrontendLibs = { + apolloClient, + framework, + observableApi, + }; + return libs; +} diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts new file mode 100644 index 0000000000000..a4fb71131b292 --- /dev/null +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IModule, IScope } from 'angular'; +import { NormalizedCacheObject } from 'apollo-cache-inmemory'; +import ApolloClient from 'apollo-client'; +import { AxiosRequestConfig } from 'axios'; +import React from 'react'; +import { Observable } from 'rxjs'; +import { + InfraMetricInput, + InfraNodeMetric, + InfraNodePath, + InfraPathInput, + InfraTimerangeInput, + SourceQuery, +} from '../../common/graphql/types'; + +export interface InfraFrontendLibs { + framework: InfraFrameworkAdapter; + apolloClient: InfraApolloClient; + observableApi: InfraObservableApi; +} + +export type InfraTimezoneProvider = () => string; + +export type InfraApolloClient = ApolloClient; + +export interface InfraFrameworkAdapter { + // Insstance vars + appState?: object; + dateFormat?: string; + kbnVersion?: string; + scaledDateFormat?: string; + timezone?: string; + + // Methods + setUISettings(key: string, value: any): void; + render(component: React.ReactElement): void; + renderBreadcrumbs(component: React.ReactElement): void; +} + +export interface InfraFramworkAdapterConstructable { + new (uiModule: IModule, timezoneProvider: InfraTimezoneProvider): InfraFrameworkAdapter; +} + +// TODO: replace AxiosRequestConfig with something more defined +export type InfraRequestConfig = AxiosRequestConfig; + +export interface InfraApiAdapter { + get(url: string, config?: InfraRequestConfig | undefined): Promise; + post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise; + delete(url: string, config?: InfraRequestConfig | undefined): Promise; + put(url: string, data?: any, config?: InfraRequestConfig | undefined): Promise; +} + +export interface InfraObservableApiPostParams { + url: string; + body?: RequestBody; +} + +export type InfraObservableApiResponse = Observable<{ + status: number; + response: BodyType; +}>; + +export interface InfraObservableApi { + post( + params: InfraObservableApiPostParams + ): InfraObservableApiResponse; +} + +export interface InfraUiKibanaAdapterScope extends IScope { + breadcrumbs: any[]; + topNavMenu: any[]; +} + +export interface InfraKibanaUIConfig { + get(key: string): any; + set(key: string, value: any): Promise; +} + +export interface InfraKibanaAdapterServiceRefs { + config: InfraKibanaUIConfig; + rootScope: IScope; +} + +export type InfraBufferedKibanaServiceCall = (serviceRefs: ServiceRefs) => void; + +export interface InfraField { + name: string; + type: string; + searchable: boolean; + aggregatable: boolean; +} + +export type InfraWaffleData = InfraWaffleMapGroup[]; + +export interface InfraWaffleMapNode { + id: string; + name: string; + path: InfraNodePath[]; + metric: InfraNodeMetric; +} + +export type InfraWaffleMapGroup = InfraWaffleMapGroupOfNodes | InfraWaffleMapGroupOfGroups; + +export interface InfraWaffleMapGroupBase { + id: string; + name: string; + count: number; + width: number; + squareSize: number; +} + +export interface InfraWaffleMapGroupOfGroups extends InfraWaffleMapGroupBase { + groups: InfraWaffleMapGroupOfNodes[]; +} + +export interface InfraWaffleMapGroupOfNodes extends InfraWaffleMapGroupBase { + nodes: InfraWaffleMapNode[]; +} + +export interface InfraWaffleMapStepRule { + value: number; + operator: InfraWaffleMapRuleOperator; + color: string; + label?: string; +} + +export interface InfraWaffleMapGradientRule { + value: number; + color: string; +} + +export enum InfraWaffleMapLegendMode { + step = 'step', + gradient = 'gradient', +} + +export interface InfraWaffleMapStepLegend { + type: InfraWaffleMapLegendMode.step; + rules: InfraWaffleMapStepRule[]; +} + +export interface InfraWaffleMapGradientLegend { + type: InfraWaffleMapLegendMode.gradient; + rules: InfraWaffleMapGradientRule[]; +} + +export type InfraWaffleMapLegend = InfraWaffleMapStepLegend | InfraWaffleMapGradientLegend; + +export enum InfraWaffleMapRuleOperator { + gt = 'gt', + gte = 'gte', + lt = 'lt', + lte = 'lte', + eq = 'eq', +} + +export interface InfraWaffleMapOptions { + fields?: SourceQuery.Fields | null; + formatter: InfraFormatterType; + formatTemplate: string; + metric: InfraMetricInput; + path: InfraPathInput[]; + legend: InfraWaffleMapLegend; +} + +export interface InfraOptions { + sourceId: string; + timerange: InfraTimerangeInput; + wafflemap: InfraWaffleMapOptions; +} + +export type Omit = Pick>; + +export interface InfraWaffleMapBounds { + min: number; + max: number; +} + +export type InfraFormatter = (value: string | number) => string; +export enum InfraFormatterType { + number = 'number', + abbreviatedNumber = 'abbreviatedNumber', + bytes = 'bytes', + bits = 'bits', + percent = 'percent', +} + +export enum InfraWaffleMapDataFormat { + bytesDecimal = 'bytesDecimal', + bytesBinaryIEC = 'bytesBinaryIEC', + bytesBinaryJEDEC = 'bytesBinaryJEDEC', + bitsDecimal = 'bitsDecimal', + bitsBinaryIEC = 'bitsBinaryIEC', + bitsBinaryJEDEC = 'bitsBinaryJEDEC', + abbreviatedNumber = 'abbreviatedNumber', +} diff --git a/x-pack/plugins/infra/public/pages/404.tsx b/x-pack/plugins/infra/public/pages/404.tsx new file mode 100644 index 0000000000000..956bf90e84927 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/404.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +export class NotFoundPage extends React.PureComponent { + public render() { + return
No content found
; + } +} diff --git a/x-pack/plugins/infra/public/pages/error.tsx b/x-pack/plugins/infra/public/pages/error.tsx new file mode 100644 index 0000000000000..75b158c42c5a1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/error.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + EuiCallOut, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { Header } from '../components/header'; +import { ColumnarPage, PageContent } from '../components/page'; + +const DetailPageContent = styled(PageContent)` + overflow: auto; + background-color: ${props => props.theme.eui.euiColorLightestShade}; +`; + +interface Props { + message: string; +} + +export const Error: React.SFC = ({ message }) => { + return ( + +
+ + + + + ); +}; + +export const ErrorPageBody: React.SFC<{ message: string }> = ({ message }) => { + return ( + + + + + +

Oops!

+
+
+
+ + +

Please click the back button and try again.

+
+
+
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/home/index.tsx b/x-pack/plugins/infra/public/pages/home/index.tsx new file mode 100644 index 0000000000000..7a4d7e674de2b --- /dev/null +++ b/x-pack/plugins/infra/public/pages/home/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { HomePageContent } from './page_content'; +import { HomeToolbar } from './toolbar'; + +import { EmptyPage } from '../../components/empty_page'; +import { Header } from '../../components/header'; +import { ColumnarPage } from '../../components/page'; + +import { WithWaffleFilterUrlState } from '../../containers/waffle/with_waffle_filters'; +import { WithWaffleOptionsUrlState } from '../../containers/waffle/with_waffle_options'; +import { WithWaffleTimeUrlState } from '../../containers/waffle/with_waffle_time'; +import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { WithSource } from '../../containers/with_source'; + +export class HomePage extends React.PureComponent { + public render() { + return ( + + + {({ metricIndicesExist }) => + metricIndicesExist || metricIndicesExist === null ? ( + <> + + + +
+ + + + ) : ( + + {({ basePath }) => ( + + )} + + ) + } + + + ); + } +} diff --git a/x-pack/plugins/infra/public/pages/home/page_content.tsx b/x-pack/plugins/infra/public/pages/home/page_content.tsx new file mode 100644 index 0000000000000..37a6854c0198a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/home/page_content.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { PageContent } from '../../components/page'; +import { Waffle } from '../../components/waffle'; + +import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters'; +import { WithWaffleNodes } from '../../containers/waffle/with_waffle_nodes'; +import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options'; +import { WithWaffleTime } from '../../containers/waffle/with_waffle_time'; +import { WithOptions } from '../../containers/with_options'; +import { WithSource } from '../../containers/with_source'; + +export const HomePageContent: React.SFC = () => ( + + + {({ configuredFields }) => ( + + {({ wafflemap, sourceId }) => ( + + {({ filterQueryAsJson }) => ( + + {({ currentTimeRange, isAutoReloading }) => ( + + {({ metric, groupBy, nodeType }) => ( + + {({ nodes, loading, refetch }) => ( + 0 && isAutoReloading ? false : loading} + nodeType={nodeType} + options={{ ...wafflemap, metric, fields: configuredFields }} + reload={refetch} + /> + )} + + )} + + )} + + )} + + )} + + )} + + +); diff --git a/x-pack/plugins/infra/public/pages/home/toolbar.tsx b/x-pack/plugins/infra/public/pages/home/toolbar.tsx new file mode 100644 index 0000000000000..5abccd9f9763d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/home/toolbar.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +import { AutocompleteField } from '../../components/autocomplete_field'; +import { Toolbar } from '../../components/eui/toolbar'; +import { WaffleTimeControls } from '../../components/waffle/waffle_time_controls'; + +import { InfraNodeType } from '../../../common/graphql/types'; +import { WaffleGroupByControls } from '../../components/waffle/waffle_group_by_controls'; +import { WaffleMetricControls } from '../../components/waffle/waffle_metric_controls'; +import { WaffleNodeTypeSwitcher } from '../../components/waffle/waffle_node_type_switcher'; +import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters'; +import { WithWaffleOptions } from '../../containers/waffle/with_waffle_options'; +import { WithWaffleTime } from '../../containers/waffle/with_waffle_time'; +import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; + +const TITLES = { + [InfraNodeType.host]: 'Hosts', + [InfraNodeType.pod]: 'Kubernetes Pods', + [InfraNodeType.container]: 'Docker Containers', +}; + +export const HomeToolbar: React.SFC = () => ( + + + + + {({ nodeType }) => ( + +

{TITLES[nodeType]}

+
+ )} +
+ +

Showing the last 1 minute of data from the time period

+
+
+ + {({ nodeType, changeNodeType, changeGroupBy, changeMetric }) => ( + + + + )} + +
+ + + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + {({ + applyFilterQueryFromKueryExpression, + filterQueryDraft, + isFilterQueryDraftValid, + setFilterQueryDraftFromKueryExpression, + }) => ( + + )} + + )} + + + + {({ changeMetric, changeGroupBy, groupBy, metric, nodeType }) => ( + + + + + + + + + )} + + + + {({ currentTime, isAutoReloading, jumpToTime, startAutoReload, stopAutoReload }) => ( + + )} + + + +
+); diff --git a/x-pack/plugins/infra/public/pages/link_to/index.ts b/x-pack/plugins/infra/public/pages/link_to/index.ts new file mode 100644 index 0000000000000..b9c5590102353 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LinkToPage } from './link_to'; +export { getContainerLogsUrl, RedirectToContainerLogs } from './redirect_to_container_logs'; +export { getHostLogsUrl, RedirectToHostLogs } from './redirect_to_host_logs'; +export { getPodLogsUrl, RedirectToPodLogs } from './redirect_to_pod_logs'; +export { getHostDetailUrl, RedirectToHostDetail } from './redirect_to_host_detail'; +export { getContainerDetailUrl, RedirectToContainerDetail } from './redirect_to_container_detail'; +export { getPodDetailUrl, RedirectToPodDetail } from './redirect_to_pod_detail'; diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx new file mode 100644 index 0000000000000..c3ca0cdcfc3a5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/link_to.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; + +import { RedirectToContainerDetail } from './redirect_to_container_detail'; +import { RedirectToContainerLogs } from './redirect_to_container_logs'; +import { RedirectToHostDetail } from './redirect_to_host_detail'; +import { RedirectToHostLogs } from './redirect_to_host_logs'; +import { RedirectToPodDetail } from './redirect_to_pod_detail'; +import { RedirectToPodLogs } from './redirect_to_pod_logs'; + +interface LinkToPageProps { + match: RouteMatch<{}>; +} + +export class LinkToPage extends React.Component { + public render() { + const { match } = this.props; + + return ( + + + + + + + + + + ); + } +} diff --git a/x-pack/plugins/infra/public/pages/link_to/query_params.ts b/x-pack/plugins/infra/public/pages/link_to/query_params.ts new file mode 100644 index 0000000000000..8ee492ebc010d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/query_params.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; + +import { getParamFromQueryString, getQueryStringFromLocation } from '../../utils/url_state'; + +export const getTimeFromLocation = (location: Location) => { + const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'time'); + return timeParam ? parseFloat(timeParam) : NaN; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_detail.tsx new file mode 100644 index 0000000000000..f6d6b7d8dab16 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_detail.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +export const RedirectToContainerDetail = ({ match }: RouteComponentProps<{ name: string }>) => ( + +); + +export const getContainerDetailUrl = ({ name }: { name: string }) => + `#/link-to/container-detail/${name}`; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_logs.tsx new file mode 100644 index 0000000000000..d6d4d89eb168e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_container_logs.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import compose from 'lodash/fp/compose'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +import { LoadingPage } from '../../components/loading_page'; +import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; +import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; +import { WithSource } from '../../containers/with_source'; +import { getTimeFromLocation } from './query_params'; + +export const RedirectToContainerLogs = ({ + match, + location, +}: RouteComponentProps<{ containerId: string }>) => ( + + {({ configuredFields }) => { + if (!configuredFields) { + return ; + } + + const searchString = compose( + replaceLogFilterInQueryString(`${configuredFields.container}: ${match.params.containerId}`), + replaceLogPositionInQueryString(getTimeFromLocation(location)) + )(''); + + return ; + }} + +); + +export const getContainerLogsUrl = ({ + containerId, + time, +}: { + containerId: string; + time?: number; +}) => ['#/link-to/container-logs/', containerId, ...(time ? [`?time=${time}`] : [])].join(''); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail.tsx new file mode 100644 index 0000000000000..dcce4e3cb5e64 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +export const RedirectToHostDetail = ({ match }: RouteComponentProps<{ name: string }>) => ( + +); + +export const getHostDetailUrl = ({ name }: { name: string }) => `#/link-to/host-detail/${name}`; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_logs.tsx new file mode 100644 index 0000000000000..290fdb56033e2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_logs.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import compose from 'lodash/fp/compose'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +import { LoadingPage } from '../../components/loading_page'; +import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; +import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; +import { WithSource } from '../../containers/with_source'; +import { getTimeFromLocation } from './query_params'; + +export const RedirectToHostLogs = ({ + match, + location, +}: RouteComponentProps<{ hostname: string }>) => ( + + {({ configuredFields }) => { + if (!configuredFields) { + return ; + } + + const searchString = compose( + replaceLogFilterInQueryString(`${configuredFields.host}: ${match.params.hostname}`), + replaceLogPositionInQueryString(getTimeFromLocation(location)) + )(''); + + return ; + }} + +); + +export const getHostLogsUrl = ({ hostname, time }: { hostname: string; time?: number }) => + ['#/link-to/host-logs/', hostname, ...(time ? [`?time=${time}`] : [])].join(''); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_detail.tsx new file mode 100644 index 0000000000000..0de7b7ca0f579 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_detail.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +export const RedirectToPodDetail = ({ match }: RouteComponentProps<{ name: string }>) => ( + +); + +export const getPodDetailUrl = ({ name }: { name: string }) => `#/link-to/pod-detail/${name}`; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_logs.tsx new file mode 100644 index 0000000000000..0ba15ee346f4c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_pod_logs.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import compose from 'lodash/fp/compose'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; + +import { LoadingPage } from '../../components/loading_page'; +import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; +import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; +import { WithSource } from '../../containers/with_source'; +import { getTimeFromLocation } from './query_params'; + +export const RedirectToPodLogs = ({ match, location }: RouteComponentProps<{ podId: string }>) => ( + + {({ configuredFields }) => { + if (!configuredFields) { + return ; + } + + const searchString = compose( + replaceLogFilterInQueryString(`${configuredFields.pod}: ${match.params.podId}`), + replaceLogPositionInQueryString(getTimeFromLocation(location)) + )(''); + + return ; + }} + +); + +export const getPodLogsUrl = ({ podId, time }: { podId: string; time?: number }) => + ['#/link-to/pod-logs/', podId, ...(time ? [`?time=${time}`] : [])].join(''); diff --git a/x-pack/plugins/infra/public/pages/logs/index.ts b/x-pack/plugins/infra/public/pages/logs/index.ts new file mode 100644 index 0000000000000..c2ae23acdc9b0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LogsPage } from './logs'; diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx new file mode 100644 index 0000000000000..1b001d7404785 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { LogsPageContent } from './page_content'; +import { LogsToolbar } from './toolbar'; + +import { EmptyPage } from '../../components/empty_page'; +import { Header } from '../../components/header'; +import { ColumnarPage } from '../../components/page'; + +import { WithLogFilterUrlState } from '../../containers/logs/with_log_filter'; +import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap'; +import { WithLogPositionUrlState } from '../../containers/logs/with_log_position'; +import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview'; +import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; +import { WithSource } from '../../containers/with_source'; + +export class LogsPage extends React.Component { + public render() { + return ( + + + {({ logIndicesExist }) => + logIndicesExist || logIndicesExist === null ? ( + <> + + + + +
+ + + + ) : ( + + {({ basePath }) => ( + + )} + + ) + } + + + ); + } +} diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx new file mode 100644 index 0000000000000..b30eed4b9c039 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; + +import { AutoSizer } from '../../components/auto_sizer'; +import { LogMinimap } from '../../components/logging/log_minimap'; +import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream'; +import { PageContent } from '../../components/page'; +import { WithLogMinimap } from '../../containers/logs/with_log_minimap'; +import { WithLogPosition } from '../../containers/logs/with_log_position'; +import { WithLogTextview } from '../../containers/logs/with_log_textview'; +import { WithStreamItems } from '../../containers/logs/with_stream_items'; +import { WithSummary } from '../../containers/logs/with_summary'; + +export const LogsPageContent: React.SFC = () => ( + + + {({ measureRef, content: { width = 0, height = 0 } }) => ( + + + {({ textScale, wrap }) => ( + + {({ + isAutoReloading, + jumpToTargetPosition, + reportVisiblePositions, + targetPosition, + }) => ( + + {({ + hasMoreAfterEnd, + hasMoreBeforeStart, + isLoadingMore, + isReloading, + items, + lastLoadedTime, + }) => ( + + )} + + )} + + )} + + + )} + + + {({ measureRef, content: { width = 0, height = 0 } }) => { + return ( + + + {({ intervalSize }) => ( + + {({ buckets }) => ( + + {({ + jumpToTargetPosition, + reportVisibleSummary, + visibleMidpointTime, + visibleTimeInterval, + }) => ( + + )} + + )} + + )} + + + ); + }} + + +); + +const LogPageEventStreamColumn = styled.div` + flex: 1 0 0; + overflow: hidden; + display: flex; + flex-direction: column; +`; + +const LogPageMinimapColumn = styled.div` + flex: 1 0 0; + overflow: hidden; + min-width: 100px; + max-width: 100px; + display: flex; + flex-direction: column; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/toolbar.tsx new file mode 100644 index 0000000000000..7c7c7004c4674 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/toolbar.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { AutocompleteField } from '../../components/autocomplete_field'; +import { Toolbar } from '../../components/eui'; +import { LogCustomizationMenu } from '../../components/logging/log_customization_menu'; +import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls'; +import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls'; +import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls'; +import { LogTimeControls } from '../../components/logging/log_time_controls'; +import { WithLogFilter } from '../../containers/logs/with_log_filter'; +import { WithLogMinimap } from '../../containers/logs/with_log_minimap'; +import { WithLogPosition } from '../../containers/logs/with_log_position'; +import { WithLogTextview } from '../../containers/logs/with_log_textview'; +import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; + +export const LogsToolbar: React.SFC = () => ( + + + + + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + + {({ + applyFilterQueryFromKueryExpression, + /* filterQuery,*/ + filterQueryDraft, + isFilterQueryDraftValid, + setFilterQueryDraftFromKueryExpression, + }) => ( + + )} + + )} + + + + + + {({ availableIntervalSizes, intervalSize, setIntervalSize }) => ( + + )} + + + {({ availableTextScales, textScale, setTextScale, setTextWrap, wrap }) => ( + <> + + + + )} + + + + + + {({ + visibleMidpointTime, + isAutoReloading, + jumpToTargetPositionTime, + startLiveStreaming, + stopLiveStreaming, + }) => ( + + )} + + + + +); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx new file mode 100644 index 0000000000000..d6fef00e11c46 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { + EuiHideFor, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageSideBar, + EuiShowFor, + EuiSideNav, + EuiTitle, +} from '@elastic/eui'; +import styled, { withTheme } from 'styled-components'; +import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types'; +import { AutoSizer } from '../../components/auto_sizer'; +import { Header } from '../../components/header'; +import { Metrics } from '../../components/metrics'; +import { MetricsTimeControls } from '../../components/metrics/time_controls'; +import { ColumnarPage, PageContent } from '../../components/page'; +import { WithCapabilities } from '../../containers/capabilities/with_capabilites'; +import { WithMetrics } from '../../containers/metrics/with_metrics'; +import { + WithMetricsTime, + WithMetricsTimeUrlState, +} from '../../containers/metrics/with_metrics_time'; +import { WithOptions } from '../../containers/with_options'; +import { Error, ErrorPageBody } from '../error'; +import { layoutCreators } from './layouts'; +import { InfraMetricLayoutSection } from './layouts/types'; + +const DetailPageContent = styled(PageContent)` + overflow: auto; + background-color: ${props => props.theme.eui.euiColorLightestShade}; +`; + +const EuiPageContentWithRelative = styled(EuiPageContent)` + position: relative; +`; + +interface Props { + theme: { eui: any }; + match: { + params: { + type: string; + node: string; + }; + }; +} + +class MetricDetailPage extends React.PureComponent { + public readonly state = { + isSideNavOpenOnMobile: false, + }; + + public render() { + const nodeName = this.props.match.params.node; + const nodeType = this.props.match.params.type as InfraNodeType; + const layoutCreator = layoutCreators[nodeType]; + if (!layoutCreator) { + return ; + } + const layouts = layoutCreator(this.props.theme); + const breadcrumbs = [{ text: nodeName }]; + + return ( + +
+ + + + {({ sourceId }) => ( + + {({ + currentTimeRange, + isAutoReloading, + setRangeTime, + startMetricsAutoReload, + stopMetricsAutoReload, + }) => ( + + {({ filteredLayouts }) => { + return ( + + {({ metrics, error, loading }) => { + if (error) { + return ; + } + const sideNav = filteredLayouts.map(item => { + return { + name: item.label, + id: item.id, + items: item.sections.map(section => ({ + id: section.id as string, + name: section.label, + onClick: this.handleClick(section), + })), + }; + }); + return ( + + + + + + + + + + + + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

{nodeName}

+
+
+ +
+
+
+ + + 0 && isAutoReloading + ? false + : loading + } + onChangeRangeTime={setRangeTime} + /> + +
+
+ ); + }} +
+
+ ); + }} +
+ ); + }} +
+ )} +
+ )} +
+
+ + ); + } + + private handleClick = (section: InfraMetricLayoutSection) => () => { + const id = section.linkToId || section.id; + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + } + }; + + private toggleOpenOnMobile = () => { + this.setState({ + isSideNavOpenOnMobile: !this.state.isSideNavOpenOnMobile, + }); + }; +} + +export const MetricDetail = withTheme(MetricDetailPage); + +const EuiSideNavContainer = styled.div` + position: fixed; + z-index: 1; + height: 88vh; + background-color: #f5f5f5; + padding-left: 16px; + margin-left: -16px; + overflow-y: auto; + overflow-x: hidden; +`; + +const MetricsDetailsPageColumn = styled.div` + flex: 1 0 0; + display: flex; + flex-direction: column; +`; + +const MetricsTitleTimeRangeContainer = styled.div` + display: flex; + flex-flow: row wrap; + justify-content: space-between; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts new file mode 100644 index 0000000000000..d7f6cee2400fa --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/container.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; +import { nginxLayoutCreator } from './nginx'; +import { + InfraMetricLayoutCreator, + InfraMetricLayoutSectionType, + InfraMetricLayoutVisualizationType, +} from './types'; + +export const containerLayoutCreator: InfraMetricLayoutCreator = theme => [ + { + id: 'containerOverview', + label: 'Container Overview', + sections: [ + { + id: InfraMetric.containerOverview, + label: 'Overview', + requires: ['docker.cpu', 'docker.memory', 'docker.network'], + type: InfraMetricLayoutSectionType.gauges, + visConfig: { + seriesOverrides: { + cpu: { + name: 'CPU Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + memory: { + name: 'Memory Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + rx: { + name: 'Inbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + tx: { + name: 'Outbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + }, + }, + }, + { + id: InfraMetric.containerCpuUsage, + label: 'CPU Usage', + requires: ['docker.cpu'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + stacked: true, + type: InfraMetricLayoutVisualizationType.area, + formatter: InfraFormatterType.percent, + seriesOverrides: { + cpu: { color: theme.eui.euiColorVis1 }, + }, + }, + }, + { + id: InfraMetric.containerMemory, + label: 'Memory Usage', + requires: ['docker.memory'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + stacked: true, + type: InfraMetricLayoutVisualizationType.area, + formatter: InfraFormatterType.percent, + seriesOverrides: { + memory: { color: theme.eui.euiColorVis1 }, + }, + }, + }, + { + id: InfraMetric.containerNetworkTraffic, + label: 'Network Traffic', + requires: ['docker.network'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + rx: { color: theme.eui.euiColorVis1, name: 'in' }, + tx: { color: theme.eui.euiColorVis2, name: 'out' }, + }, + }, + }, + { + id: InfraMetric.containerDiskIOOps, + label: 'Disk IO (Ops)', + requires: ['docker.diskio'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.number, + formatterTemplate: '{{value}}/s', + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + read: { color: theme.eui.euiColorVis1, name: 'reads' }, + write: { color: theme.eui.euiColorVis2, name: 'writes' }, + }, + }, + }, + { + id: InfraMetric.containerDiskIOBytes, + label: 'Disk IO (Bytes)', + requires: ['docker.diskio'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bytes, + formatterTemplate: '{{value}}/s', + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + read: { color: theme.eui.euiColorVis1, name: 'reads' }, + write: { color: theme.eui.euiColorVis2, name: 'writes' }, + }, + }, + }, + ], + }, + ...nginxLayoutCreator(theme), +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts new file mode 100644 index 0000000000000..ce9a5a01247ce --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/host.ts @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; +import { nginxLayoutCreator } from './nginx'; +import { + InfraMetricLayoutCreator, + InfraMetricLayoutSectionType, + InfraMetricLayoutVisualizationType, +} from './types'; + +export const hostLayoutCreator: InfraMetricLayoutCreator = theme => [ + { + id: 'hostOverview', + label: 'Host', + sections: [ + { + id: InfraMetric.hostSystemOverview, + linkToId: 'hostOverview', + label: 'Overview', + requires: ['system.cpu', 'system.load', 'system.memory', 'system.network'], + type: InfraMetricLayoutSectionType.gauges, + visConfig: { + seriesOverrides: { + cpu: { + name: 'CPU Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + load: { name: 'Load (5m)', color: theme.eui.euiColorFullShade }, + memory: { + name: 'Memory Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + rx: { + name: 'Inbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + tx: { + name: 'Outbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + }, + }, + }, + { + id: InfraMetric.hostCpuUsage, + label: 'CPU Usage', + requires: ['system.cpu'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + stacked: true, + type: InfraMetricLayoutVisualizationType.area, + formatter: InfraFormatterType.percent, + bounds: { min: 0, max: 1 }, + seriesOverrides: { + user: { color: theme.eui.euiColorVis0 }, + system: { color: theme.eui.euiColorVis2 }, + steal: { color: theme.eui.euiColorVis9 }, + irq: { color: theme.eui.euiColorVis4 }, + softirq: { color: theme.eui.euiColorVis6 }, + iowait: { color: theme.eui.euiColorVis7 }, + nice: { color: theme.eui.euiColorVis5 }, + }, + }, + }, + { + id: InfraMetric.hostLoad, + label: 'Load', + requires: ['system.load'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + seriesOverrides: { + load_1m: { color: theme.eui.euiColorVis0, name: '1m' }, + load_5m: { color: theme.eui.euiColorVis1, name: '5m' }, + load_15m: { color: theme.eui.euiColorVis3, name: '15m' }, + }, + }, + }, + { + id: InfraMetric.hostMemoryUsage, + label: 'MemoryUsage', + requires: ['system.memory'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + stacked: true, + formatter: InfraFormatterType.bytes, + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + used: { color: theme.eui.euiColorVis2 }, + free: { color: theme.eui.euiColorVis0 }, + cache: { color: theme.eui.euiColorVis1 }, + }, + }, + }, + { + id: InfraMetric.hostNetworkTraffic, + label: 'Network Traffic', + requires: ['system.network'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + rx: { color: theme.eui.euiColorVis1, name: 'in' }, + tx: { color: theme.eui.euiColorVis2, name: 'out' }, + }, + }, + }, + ], + }, + { + id: 'k8sOverview', + label: 'Kubernetes', + sections: [ + { + id: InfraMetric.hostK8sOverview, + linkToId: 'k8sOverview', + label: 'Overview', + requires: ['kubernetes.node'], + type: InfraMetricLayoutSectionType.gauges, + visConfig: { + seriesOverrides: { + cpucap: { + name: 'CPU Capacity', + color: 'secondary', + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + load: { name: 'Load (5m)', color: 'secondary' }, + memorycap: { + name: 'Memory Capacity', + color: 'secondary', + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + podcap: { + name: 'Pod Capacity', + color: 'secondary', + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + diskcap: { + name: 'Disk Capacity', + color: 'secondary', + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + }, + }, + }, + { + id: InfraMetric.hostK8sCpuCap, + label: 'Node CPU Capacity', + requires: ['kubernetes.node'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.abbreviatedNumber, + seriesOverrides: { + capacity: { color: theme.eui.euiColorVis2 }, + used: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + { + id: InfraMetric.hostK8sMemoryCap, + label: 'Node Memory Capacity', + requires: ['kubernetes.node'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bytes, + seriesOverrides: { + capacity: { color: theme.eui.euiColorVis2 }, + used: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + { + id: InfraMetric.hostK8sDiskCap, + label: 'Node Disk Capacity', + requires: ['kubernetes.node'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bytes, + seriesOverrides: { + capacity: { color: theme.eui.euiColorVis2 }, + used: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + { + id: InfraMetric.hostK8sPodCap, + label: 'Node Pod Capacity', + requires: ['kubernetes.node'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.number, + seriesOverrides: { + capacity: { color: theme.eui.euiColorVis2 }, + used: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + ], + }, + ...nginxLayoutCreator(theme), +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/index.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/index.ts new file mode 100644 index 0000000000000..d81bde64e230b --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { containerLayoutCreator } from './container'; +import { hostLayoutCreator } from './host'; +import { podLayoutCreator } from './pod'; +import { InfraMetricLayoutCreator } from './types'; + +interface Layouts { + [key: string]: InfraMetricLayoutCreator; +} + +export const layoutCreators: Layouts = { + host: hostLayoutCreator, + pod: podLayoutCreator, + container: containerLayoutCreator, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts new file mode 100644 index 0000000000000..f03580feb72c0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/nginx.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; +import { + InfraMetricLayoutCreator, + InfraMetricLayoutSectionType, + InfraMetricLayoutVisualizationType, +} from './types'; + +export const nginxLayoutCreator: InfraMetricLayoutCreator = theme => [ + { + id: 'nginxOverview', + label: 'Nginx', + requires: ['nginx'], + sections: [ + { + id: InfraMetric.nginxHits, + label: 'Hits', + requires: ['nginx.access'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.abbreviatedNumber, + stacked: true, + seriesOverrides: { + '200s': { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.bar }, + '300s': { color: theme.eui.euiColorVis5, type: InfraMetricLayoutVisualizationType.bar }, + '400s': { color: theme.eui.euiColorVis2, type: InfraMetricLayoutVisualizationType.bar }, + '500s': { color: theme.eui.euiColorVis9, type: InfraMetricLayoutVisualizationType.bar }, + }, + }, + }, + { + id: InfraMetric.nginxRequestRate, + label: 'Request Rate', + requires: ['nginx.statusstub'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.abbreviatedNumber, + formatterTemplate: '{{value}}/s', + seriesOverrides: { + rate: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + { + id: InfraMetric.nginxActiveConnections, + label: 'Active Connections', + requires: ['nginx.statusstub'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.abbreviatedNumber, + seriesOverrides: { + connections: { + color: theme.eui.euiColorVis1, + type: InfraMetricLayoutVisualizationType.bar, + }, + }, + }, + }, + { + id: InfraMetric.nginxRequestsPerConnection, + label: 'Requests per Connections', + requires: ['nginx.statusstub'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.abbreviatedNumber, + seriesOverrides: { + reqPerConns: { + color: theme.eui.euiColorVis1, + type: InfraMetricLayoutVisualizationType.bar, + name: 'reqs per conn', + }, + }, + }, + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts new file mode 100644 index 0000000000000..bc58608737115 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/pod.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; +import { nginxLayoutCreator } from './nginx'; +import { + InfraMetricLayoutCreator, + InfraMetricLayoutSectionType, + InfraMetricLayoutVisualizationType, +} from './types'; + +export const podLayoutCreator: InfraMetricLayoutCreator = theme => [ + { + id: 'podOverview', + label: 'Pod Overview', + sections: [ + { + id: InfraMetric.podOverview, + label: 'Pod Overview', + requires: ['kubernetes.pod'], + type: InfraMetricLayoutSectionType.gauges, + visConfig: { + seriesOverrides: { + cpu: { + name: 'CPU Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + memory: { + name: 'Memory Usage', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.percent, + gaugeMax: 1, + }, + rx: { + name: 'Inbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + tx: { + name: 'Outbound (RX)', + color: theme.eui.euiColorFullShade, + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + }, + }, + }, + }, + { + id: InfraMetric.podCpuUsage, + label: 'CPU Usage', + requires: ['kubernetes.pod'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.percent, + seriesOverrides: { + cpu: { color: theme.eui.euiColorVis1, type: InfraMetricLayoutVisualizationType.area }, + }, + }, + }, + { + id: InfraMetric.podMemoryUsage, + label: 'Memory Usage', + requires: ['kubernetes.pod'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.percent, + seriesOverrides: { + memory: { + color: theme.eui.euiColorVis1, + type: InfraMetricLayoutVisualizationType.area, + }, + }, + }, + }, + { + id: InfraMetric.podNetworkTraffic, + label: 'Network Traffic', + requires: ['kubernetes.pod'], + type: InfraMetricLayoutSectionType.chart, + visConfig: { + formatter: InfraFormatterType.bits, + formatterTemplate: '{{value}}/s', + type: InfraMetricLayoutVisualizationType.area, + seriesOverrides: { + rx: { color: theme.eui.euiColorVis1, name: 'in' }, + tx: { color: theme.eui.euiColorVis2, name: 'out' }, + }, + }, + }, + ], + }, + ...nginxLayoutCreator(theme), +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/layouts/types.ts b/x-pack/plugins/infra/public/pages/metrics/layouts/types.ts new file mode 100644 index 0000000000000..d45e505a7d788 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/layouts/types.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../common/graphql/types'; +import { InfraFormatterType } from '../../../lib/lib'; + +export enum InfraMetricLayoutVisualizationType { + line = 'line', + area = 'area', + bar = 'bar', +} + +export enum InfraMetricLayoutSectionType { + chart = 'chart', + gauges = 'gauges', +} + +interface SeriesOverrides { + type?: InfraMetricLayoutVisualizationType; + color: string; + name?: string; + formatter?: InfraFormatterType; + formatterTemplate?: string; +} + +interface SeriesOverrideObject { + [name: string]: SeriesOverrides | undefined; +} + +export interface InfraMetricLayoutVisualizationConfig { + stacked?: boolean; + type?: InfraMetricLayoutVisualizationType; + formatter?: InfraFormatterType; + formatterTemplate?: string; + bounds?: { min: number; max: number }; + seriesOverrides: SeriesOverrideObject; +} + +export interface InfraMetricLayoutSection { + id: InfraMetric; + linkToId?: string; + label: string; + requires: string[]; + visConfig: InfraMetricLayoutVisualizationConfig; + type: InfraMetricLayoutSectionType; +} + +export interface InfraMetricLayout { + id: string; + label: string; + sections: InfraMetricLayoutSection[]; +} + +export type InfraMetricLayoutCreator = (theme: { eui: any }) => InfraMetricLayout[]; diff --git a/x-pack/plugins/infra/public/register_feature.ts b/x-pack/plugins/infra/public/register_feature.ts new file mode 100644 index 0000000000000..ca1260798b83c --- /dev/null +++ b/x-pack/plugins/infra/public/register_feature.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FeatureCatalogueCategory, + FeatureCatalogueRegistryProvider, +} from 'ui/registry/feature_catalogue'; + +const APP_ID = 'infra'; + +FeatureCatalogueRegistryProvider.register(() => ({ + id: 'infraops', + title: 'InfraOps', + description: + 'Explore infrastructure metrics and logs for common servers, containers, and services.', + icon: 'infraApp', + path: `/app/${APP_ID}#home`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, +})); + +FeatureCatalogueRegistryProvider.register(() => ({ + id: 'infralogging', + title: 'Logs', + description: + 'Stream logs in real time or scroll through historical views in a console-like experience.', + icon: 'loggingApp', + path: `/app/${APP_ID}#logs`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, +})); diff --git a/x-pack/plugins/infra/public/routes.tsx b/x-pack/plugins/infra/public/routes.tsx new file mode 100644 index 0000000000000..ca23ebae2279f --- /dev/null +++ b/x-pack/plugins/infra/public/routes.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { History } from 'history'; +import React from 'react'; +import { Redirect, Route, Router, Switch } from 'react-router-dom'; + +import { NotFoundPage } from './pages/404'; +import { HomePage } from './pages/home'; +import { LinkToPage } from './pages/link_to'; +import { LogsPage } from './pages/logs'; +import { MetricDetail } from './pages/metrics'; + +interface RouterProps { + history: History; +} + +export const PageRouter: React.SFC = ({ history }) => { + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/store/actions.ts b/x-pack/plugins/infra/public/store/actions.ts new file mode 100644 index 0000000000000..ee9a2858f1c34 --- /dev/null +++ b/x-pack/plugins/infra/public/store/actions.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + logFilterActions, + logMinimapActions, + logPositionActions, + logTextviewActions, + metricTimeActions, + waffleFilterActions, + waffleTimeActions, + waffleOptionsActions, +} from './local'; +export { logEntriesActions, logSummaryActions } from './remote'; diff --git a/x-pack/plugins/infra/public/store/epics.ts b/x-pack/plugins/infra/public/store/epics.ts new file mode 100644 index 0000000000000..4df8e1368ca01 --- /dev/null +++ b/x-pack/plugins/infra/public/store/epics.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineEpics } from 'redux-observable'; + +import { createLocalEpic } from './local'; +import { createRemoteEpic } from './remote'; + +export const createRootEpic = () => + combineEpics(createLocalEpic(), createRemoteEpic()); diff --git a/x-pack/plugins/infra/public/store/index.ts b/x-pack/plugins/infra/public/store/index.ts new file mode 100644 index 0000000000000..025da41ec40d5 --- /dev/null +++ b/x-pack/plugins/infra/public/store/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './actions'; +export * from './epics'; +export * from './reducer'; +export * from './selectors'; +export { createStore } from './store'; diff --git a/x-pack/plugins/infra/public/store/local/actions.ts b/x-pack/plugins/infra/public/store/local/actions.ts new file mode 100644 index 0000000000000..8b9e0c9f5b58a --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/actions.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { logFilterActions } from './log_filter'; +export { logMinimapActions } from './log_minimap'; +export { logPositionActions } from './log_position'; +export { logTextviewActions } from './log_textview'; +export { metricTimeActions } from './metric_time'; +export { waffleFilterActions } from './waffle_filter'; +export { waffleTimeActions } from './waffle_time'; +export { waffleOptionsActions } from './waffle_options'; diff --git a/x-pack/plugins/infra/public/store/local/epic.ts b/x-pack/plugins/infra/public/store/local/epic.ts new file mode 100644 index 0000000000000..274a74b1627c5 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/epic.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineEpics } from 'redux-observable'; + +import { createLogPositionEpic } from './log_position'; +import { createMetricTimeEpic } from './metric_time'; +import { createWaffleTimeEpic } from './waffle_time'; + +export const createLocalEpic = () => + combineEpics( + createLogPositionEpic(), + createWaffleTimeEpic(), + createMetricTimeEpic() + ); diff --git a/x-pack/plugins/infra/public/store/local/index.ts b/x-pack/plugins/infra/public/store/local/index.ts new file mode 100644 index 0000000000000..c2843320bfd0c --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './actions'; +export * from './epic'; +export * from './reducer'; +export * from './selectors'; diff --git a/x-pack/plugins/infra/public/store/local/log_filter/actions.ts b/x-pack/plugins/infra/public/store/local/log_filter/actions.ts new file mode 100644 index 0000000000000..acf28ab6092c6 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_filter/actions.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { FilterQuery } from './reducer'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/log_filter'); + +export const setLogFilterQueryDraft = actionCreator('SET_LOG_FILTER_QUERY_DRAFT'); + +export const applyLogFilterQuery = actionCreator('APPLY_LOG_FILTER_QUERY'); diff --git a/x-pack/plugins/infra/public/store/local/log_filter/index.ts b/x-pack/plugins/infra/public/store/local/log_filter/index.ts new file mode 100644 index 0000000000000..369f5f013d669 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_filter/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logFilterActions from './actions'; +import * as logFilterSelectors from './selectors'; + +export { logFilterActions, logFilterSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/log_filter/reducer.ts b/x-pack/plugins/infra/public/store/local/log_filter/reducer.ts new file mode 100644 index 0000000000000..c541293b6f273 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_filter/reducer.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { applyLogFilterQuery, setLogFilterQueryDraft } from './actions'; + +export interface KueryFilterQuery { + kind: 'kuery'; + expression: string; +} + +export type FilterQuery = KueryFilterQuery; + +export interface LogFilterState { + filterQuery: KueryFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; +} + +export const initialLogFilterState: LogFilterState = { + filterQuery: null, + filterQueryDraft: null, +}; + +export const logFilterReducer = reducerWithInitialState(initialLogFilterState) + .case(setLogFilterQueryDraft, (state, filterQueryDraft) => ({ + ...state, + filterQueryDraft, + })) + .case(applyLogFilterQuery, (state, filterQuery) => ({ + ...state, + filterQuery, + filterQueryDraft: filterQuery, + })) + .build(); diff --git a/x-pack/plugins/infra/public/store/local/log_filter/selectors.ts b/x-pack/plugins/infra/public/store/local/log_filter/selectors.ts new file mode 100644 index 0000000000000..b8e75e8080cfd --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_filter/selectors.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { fromKueryExpression } from 'ui/kuery'; + +import { LogFilterState } from './reducer'; + +export const selectLogFilterQuery = (state: LogFilterState) => state.filterQuery; + +export const selectLogFilterQueryDraft = (state: LogFilterState) => state.filterQueryDraft; + +export const selectIsLogFilterQueryDraftValid = createSelector( + selectLogFilterQueryDraft, + filterQueryDraft => { + if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { + try { + fromKueryExpression(filterQueryDraft.expression); + } catch (err) { + return false; + } + } + + return true; + } +); diff --git a/x-pack/plugins/infra/public/store/local/log_minimap/actions.ts b/x-pack/plugins/infra/public/store/local/log_minimap/actions.ts new file mode 100644 index 0000000000000..019b1034b2bd9 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_minimap/actions.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/log_minimap'); + +export const setMinimapIntervalSize = actionCreator('SET_MINIMAP_INTERVAL_SIZE'); diff --git a/x-pack/plugins/infra/public/store/local/log_minimap/index.ts b/x-pack/plugins/infra/public/store/local/log_minimap/index.ts new file mode 100644 index 0000000000000..f75f5a8688f1c --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_minimap/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logMinimapActions from './actions'; +import * as logMinimapSelectors from './selectors'; + +export { logMinimapActions, logMinimapSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/log_minimap/reducer.ts b/x-pack/plugins/infra/public/store/local/log_minimap/reducer.ts new file mode 100644 index 0000000000000..e9bac97d05a11 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_minimap/reducer.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { setMinimapIntervalSize } from './actions'; + +export interface LogMinimapState { + intervalSize: number; +} + +export const initialLogMinimapState: LogMinimapState = { + intervalSize: 1000 * 60 * 60 * 24, +}; + +export const logMinimapReducer = reducerWithInitialState(initialLogMinimapState) + .case(setMinimapIntervalSize, (state, intervalSize) => ({ + intervalSize, + })) + .build(); diff --git a/x-pack/plugins/infra/public/store/local/log_minimap/selectors.ts b/x-pack/plugins/infra/public/store/local/log_minimap/selectors.ts new file mode 100644 index 0000000000000..76abd4152f125 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_minimap/selectors.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogMinimapState } from './reducer'; + +export const selectMinimapIntervalSize = (state: LogMinimapState) => state.intervalSize; diff --git a/x-pack/plugins/infra/public/store/local/log_position/actions.ts b/x-pack/plugins/infra/public/store/local/log_position/actions.ts new file mode 100644 index 0000000000000..0c677f17dd7ac --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_position/actions.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { TimeKey } from '../../../../common/time'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/log_position'); + +export const jumpToTargetPosition = actionCreator('JUMP_TO_TARGET_POSITION'); + +export const jumpToTargetPositionTime = (time: number) => + jumpToTargetPosition({ + tiebreaker: 0, + time, + }); + +export interface ReportVisiblePositionsPayload { + pagesAfterEnd: number; + pagesBeforeStart: number; + endKey: TimeKey | null; + middleKey: TimeKey | null; + startKey: TimeKey | null; +} + +export const reportVisiblePositions = actionCreator( + 'REPORT_VISIBLE_POSITIONS' +); + +export interface ReportVisibleSummaryPayload { + start: number; + end: number; + bucketsOnPage: number; + pagesBeforeStart: number; + pagesAfterEnd: number; +} + +export const reportVisibleSummary = actionCreator( + 'REPORT_VISIBLE_SUMMARY' +); + +export const startAutoReload = actionCreator('START_AUTO_RELOAD'); + +export const stopAutoReload = actionCreator('STOP_AUTO_RELOAD'); diff --git a/x-pack/plugins/infra/public/store/local/log_position/epic.ts b/x-pack/plugins/infra/public/store/local/log_position/epic.ts new file mode 100644 index 0000000000000..01b9b6eb0bb42 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_position/epic.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { timer } from 'rxjs'; +import { exhaustMap, filter, map, takeUntil } from 'rxjs/operators'; + +import { jumpToTargetPositionTime, startAutoReload, stopAutoReload } from './actions'; + +export const createLogPositionEpic = (): Epic => action$ => + action$.pipe( + filter(startAutoReload.match), + exhaustMap(({ payload }) => + timer(0, payload).pipe( + map(() => jumpToTargetPositionTime(Date.now())), + takeUntil(action$.pipe(filter(stopAutoReload.match))) + ) + ) + ); diff --git a/x-pack/plugins/infra/public/store/local/log_position/index.ts b/x-pack/plugins/infra/public/store/local/log_position/index.ts new file mode 100644 index 0000000000000..17e06348d18b2 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_position/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logPositionActions from './actions'; +import * as logPositionSelectors from './selectors'; + +export { logPositionActions, logPositionSelectors }; +export * from './epic'; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/log_position/reducer.ts b/x-pack/plugins/infra/public/store/local/log_position/reducer.ts new file mode 100644 index 0000000000000..c87cb432c0442 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_position/reducer.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { TimeKey } from '../../../../common/time'; +import { + jumpToTargetPosition, + reportVisiblePositions, + reportVisibleSummary, + startAutoReload, + stopAutoReload, +} from './actions'; + +interface ManualTargetPositionUpdatePolicy { + policy: 'manual'; +} + +interface IntervalTargetPositionUpdatePolicy { + policy: 'interval'; + interval: number; +} + +type TargetPositionUpdatePolicy = + | ManualTargetPositionUpdatePolicy + | IntervalTargetPositionUpdatePolicy; + +export interface LogPositionState { + targetPosition: TimeKey | null; + updatePolicy: TargetPositionUpdatePolicy; + visiblePositions: { + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + }; + visibleSummary: { + start: number | null; + end: number | null; + }; +} + +export const initialLogPositionState: LogPositionState = { + targetPosition: null, + updatePolicy: { + policy: 'manual', + }, + visiblePositions: { + endKey: null, + middleKey: null, + startKey: null, + }, + visibleSummary: { + start: null, + end: null, + }, +}; + +const targetPositionReducer = reducerWithInitialState(initialLogPositionState.targetPosition).case( + jumpToTargetPosition, + (state, target) => target +); + +const targetPositionUpdatePolicyReducer = reducerWithInitialState( + initialLogPositionState.updatePolicy +) + .case(startAutoReload, (state, interval) => ({ + policy: 'interval', + interval, + })) + .case(stopAutoReload, () => ({ + policy: 'manual', + })); + +const visiblePositionReducer = reducerWithInitialState( + initialLogPositionState.visiblePositions +).case(reportVisiblePositions, (state, { startKey, middleKey, endKey }) => ({ + endKey, + middleKey, + startKey, +})); + +const visibleSummaryReducer = reducerWithInitialState(initialLogPositionState.visibleSummary).case( + reportVisibleSummary, + (state, { start, end }) => ({ + start, + end, + }) +); + +export const logPositionReducer = combineReducers({ + targetPosition: targetPositionReducer, + updatePolicy: targetPositionUpdatePolicyReducer, + visiblePositions: visiblePositionReducer, + visibleSummary: visibleSummaryReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/log_position/selectors.ts b/x-pack/plugins/infra/public/store/local/log_position/selectors.ts new file mode 100644 index 0000000000000..dd26905d4005f --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_position/selectors.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { LogPositionState } from './reducer'; + +export const selectTargetPosition = (state: LogPositionState) => state.targetPosition; + +export const selectIsAutoReloading = (state: LogPositionState) => + state.updatePolicy.policy === 'interval'; + +export const selectFirstVisiblePosition = (state: LogPositionState) => + state.visiblePositions.startKey ? state.visiblePositions.startKey : null; + +export const selectMiddleVisiblePosition = (state: LogPositionState) => + state.visiblePositions.middleKey ? state.visiblePositions.middleKey : null; + +export const selectLastVisiblePosition = (state: LogPositionState) => + state.visiblePositions.endKey ? state.visiblePositions.endKey : null; + +export const selectVisibleMidpointOrTarget = createSelector( + selectMiddleVisiblePosition, + selectTargetPosition, + (middleVisiblePosition, targetPosition) => { + if (middleVisiblePosition) { + return middleVisiblePosition; + } else if (targetPosition) { + return targetPosition; + } else { + return null; + } + } +); + +export const selectVisibleMidpointOrTargetTime = createSelector( + selectVisibleMidpointOrTarget, + visibleMidpointOrTarget => (visibleMidpointOrTarget ? visibleMidpointOrTarget.time : null) +); + +export const selectVisibleTimeInterval = createSelector( + selectFirstVisiblePosition, + selectLastVisiblePosition, + (firstVisiblePosition, lastVisiblePosition) => + firstVisiblePosition && lastVisiblePosition + ? { + start: firstVisiblePosition.time, + end: lastVisiblePosition.time, + } + : null +); + +export const selectVisibleSummary = (state: LogPositionState) => state.visibleSummary; diff --git a/x-pack/plugins/infra/public/store/local/log_textview/actions.ts b/x-pack/plugins/infra/public/store/local/log_textview/actions.ts new file mode 100644 index 0000000000000..7aa63c8bb7fdd --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_textview/actions.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { TextScale } from '../../../../common/log_text_scale'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/log_textview'); + +export const setTextviewScale = actionCreator('SET_TEXTVIEW_SCALE'); + +export const setTextviewWrap = actionCreator('SET_TEXTVIEW_WRAP'); diff --git a/x-pack/plugins/infra/public/store/local/log_textview/index.ts b/x-pack/plugins/infra/public/store/local/log_textview/index.ts new file mode 100644 index 0000000000000..cebc58c7c3275 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_textview/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logTextviewActions from './actions'; +import * as logTextviewSelectors from './selectors'; + +export { logTextviewActions, logTextviewSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/log_textview/reducer.ts b/x-pack/plugins/infra/public/store/local/log_textview/reducer.ts new file mode 100644 index 0000000000000..45ff35cdc4af7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_textview/reducer.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { TextScale } from '../../../../common/log_text_scale'; +import { setTextviewScale, setTextviewWrap } from './actions'; + +export interface LogTextviewState { + scale: TextScale; + wrap: boolean; +} + +export const initialLogTextviewState: LogTextviewState = { + scale: 'medium', + wrap: true, +}; + +const textviewScaleReducer = reducerWithInitialState(initialLogTextviewState.scale).case( + setTextviewScale, + (state, scale) => scale +); + +const textviewWrapReducer = reducerWithInitialState(initialLogTextviewState.wrap).case( + setTextviewWrap, + (state, wrap) => wrap +); + +export const logTextviewReducer = combineReducers({ + scale: textviewScaleReducer, + wrap: textviewWrapReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/log_textview/selectors.ts b/x-pack/plugins/infra/public/store/local/log_textview/selectors.ts new file mode 100644 index 0000000000000..ee701afd4cbcb --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_textview/selectors.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogTextviewState } from './reducer'; + +export const selectTextviewScale = (state: LogTextviewState) => state.scale; + +export const selectTextviewWrap = (state: LogTextviewState) => state.wrap; diff --git a/x-pack/plugins/infra/public/store/local/metric_time/actions.ts b/x-pack/plugins/infra/public/store/local/metric_time/actions.ts new file mode 100644 index 0000000000000..8dec727c895c0 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/actions.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time'); + +export interface MetricRangeTimeState { + to: number; + from: number; + interval: string; +} + +export const setRangeTime = actionCreator('SET_RANGE_TIME'); + +export const startMetricsAutoReload = actionCreator('START_METRICS_AUTO_RELOAD'); + +export const stopMetricsAutoReload = actionCreator('STOP_METRICS_AUTO_RELOAD'); diff --git a/x-pack/plugins/infra/public/store/local/metric_time/epic.ts b/x-pack/plugins/infra/public/store/local/metric_time/epic.ts new file mode 100644 index 0000000000000..aaecdc42a215b --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/epic.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { timer } from 'rxjs'; +import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators'; + +import { setRangeTime, startMetricsAutoReload, stopMetricsAutoReload } from './actions'; + +interface MetricTimeEpicDependencies { + selectMetricTimeUpdatePolicyInterval: (state: State) => number | null; + selectMetricRangeFromTimeRange: (state: State) => number | null; +} + +export const createMetricTimeEpic = (): Epic< + Action, + Action, + State, + MetricTimeEpicDependencies +> => ( + action$, + state$, + { selectMetricTimeUpdatePolicyInterval, selectMetricRangeFromTimeRange } +) => { + const updateInterval$ = state$.pipe( + map(selectMetricTimeUpdatePolicyInterval), + filter(isNotNull) + ); + + const range$ = state$.pipe( + map(selectMetricRangeFromTimeRange), + filter(isNotNull) + ); + + return action$.pipe( + filter(startMetricsAutoReload.match), + withLatestFrom(updateInterval$, range$), + exhaustMap(([action, updateInterval, range]) => + timer(0, updateInterval).pipe( + map(() => + setRangeTime({ + from: moment() + .subtract(range, 'ms') + .valueOf(), + to: moment().valueOf(), + interval: '1m', + }) + ), + takeUntil(action$.pipe(filter(stopMetricsAutoReload.match))) + ) + ) + ); +}; + +const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/local/metric_time/index.ts b/x-pack/plugins/infra/public/store/local/metric_time/index.ts new file mode 100644 index 0000000000000..1df7b682d1314 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as metricTimeActions from './actions'; +import * as metricTimeSelectors from './selectors'; + +export { metricTimeActions, metricTimeSelectors }; +export * from './epic'; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts b/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts new file mode 100644 index 0000000000000..00a4d7d311d0d --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { + MetricRangeTimeState, + setRangeTime, + startMetricsAutoReload, + stopMetricsAutoReload, +} from './actions'; + +interface ManualTimeUpdatePolicy { + policy: 'manual'; +} + +interface IntervalTimeUpdatePolicy { + policy: 'interval'; + interval: number; +} + +type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy; + +export interface MetricTimeState { + timeRange: MetricRangeTimeState; + updatePolicy: TimeUpdatePolicy; +} + +export const initialMetricTimeState: MetricTimeState = { + timeRange: { + from: moment() + .subtract(1, 'hour') + .valueOf(), + to: moment().valueOf(), + interval: '>=1m', + }, + updatePolicy: { + policy: 'manual', + }, +}; + +const timeRangeReducer = reducerWithInitialState(initialMetricTimeState.timeRange).case( + setRangeTime, + (state, { to, from }) => ({ ...state, to, from }) +); + +const updatePolicyReducer = reducerWithInitialState(initialMetricTimeState.updatePolicy) + .case(startMetricsAutoReload, () => ({ + policy: 'interval', + interval: 5000, + })) + .case(stopMetricsAutoReload, () => ({ + policy: 'manual', + })); + +export const metricTimeReducer = combineReducers({ + timeRange: timeRangeReducer, + updatePolicy: updatePolicyReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts b/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts new file mode 100644 index 0000000000000..cac7ac2edca05 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { MetricTimeState } from './reducer'; + +export const selectRangeTime = (state: MetricTimeState) => state.timeRange; + +export const selectIsAutoReloading = (state: MetricTimeState) => + state.updatePolicy.policy === 'interval'; + +export const selectTimeUpdatePolicyInterval = (state: MetricTimeState) => + state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null; + +export const selectRangeFromTimeRange = (state: MetricTimeState) => { + const { to, from } = state.timeRange; + return to - from; +}; diff --git a/x-pack/plugins/infra/public/store/local/reducer.ts b/x-pack/plugins/infra/public/store/local/reducer.ts new file mode 100644 index 0000000000000..59e890b748d5e --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/reducer.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; + +import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter'; +import { initialLogMinimapState, logMinimapReducer, LogMinimapState } from './log_minimap'; +import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position'; +import { initialLogTextviewState, logTextviewReducer, LogTextviewState } from './log_textview'; +import { initialMetricTimeState, metricTimeReducer, MetricTimeState } from './metric_time'; +import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter'; +import { + initialWaffleOptionsState, + waffleOptionsReducer, + WaffleOptionsState, +} from './waffle_options'; +import { initialWaffleTimeState, waffleTimeReducer, WaffleTimeState } from './waffle_time'; + +export interface LocalState { + logFilter: LogFilterState; + logMinimap: LogMinimapState; + logPosition: LogPositionState; + logTextview: LogTextviewState; + metricTime: MetricTimeState; + waffleFilter: WaffleFilterState; + waffleTime: WaffleTimeState; + waffleMetrics: WaffleOptionsState; +} + +export const initialLocalState: LocalState = { + logFilter: initialLogFilterState, + logMinimap: initialLogMinimapState, + logPosition: initialLogPositionState, + logTextview: initialLogTextviewState, + metricTime: initialMetricTimeState, + waffleFilter: initialWaffleFilterState, + waffleTime: initialWaffleTimeState, + waffleMetrics: initialWaffleOptionsState, +}; + +export const localReducer = combineReducers({ + logFilter: logFilterReducer, + logMinimap: logMinimapReducer, + logPosition: logPositionReducer, + logTextview: logTextviewReducer, + metricTime: metricTimeReducer, + waffleFilter: waffleFilterReducer, + waffleTime: waffleTimeReducer, + waffleMetrics: waffleOptionsReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/selectors.ts b/x-pack/plugins/infra/public/store/local/selectors.ts new file mode 100644 index 0000000000000..85188e144ade1 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/selectors.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { globalizeSelectors } from '../../utils/typed_redux'; +import { logFilterSelectors as innerLogFilterSelectors } from './log_filter'; +import { logMinimapSelectors as innerLogMinimapSelectors } from './log_minimap'; +import { logPositionSelectors as innerLogPositionSelectors } from './log_position'; +import { logTextviewSelectors as innerLogTextviewSelectors } from './log_textview'; +import { metricTimeSelectors as innerMetricTimeSelectors } from './metric_time'; +import { LocalState } from './reducer'; +import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter'; +import { waffleOptionsSelectors as innerWaffleOptionsSelectors } from './waffle_options'; +import { waffleTimeSelectors as innerWaffleTimeSelectors } from './waffle_time'; + +export const logFilterSelectors = globalizeSelectors( + (state: LocalState) => state.logFilter, + innerLogFilterSelectors +); + +export const logMinimapSelectors = globalizeSelectors( + (state: LocalState) => state.logMinimap, + innerLogMinimapSelectors +); + +export const logPositionSelectors = globalizeSelectors( + (state: LocalState) => state.logPosition, + innerLogPositionSelectors +); + +export const logTextviewSelectors = globalizeSelectors( + (state: LocalState) => state.logTextview, + innerLogTextviewSelectors +); + +export const metricTimeSelectors = globalizeSelectors( + (state: LocalState) => state.metricTime, + innerMetricTimeSelectors +); + +export const waffleFilterSelectors = globalizeSelectors( + (state: LocalState) => state.waffleFilter, + innerWaffleFilterSelectors +); + +export const waffleTimeSelectors = globalizeSelectors( + (state: LocalState) => state.waffleTime, + innerWaffleTimeSelectors +); + +export const waffleOptionsSelectors = globalizeSelectors( + (state: LocalState) => state.waffleMetrics, + innerWaffleOptionsSelectors +); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts new file mode 100644 index 0000000000000..3f2c8839308c1 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_filter/actions.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { FilterQuery } from './reducer'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_filter'); + +export const setWaffleFilterQueryDraft = actionCreator( + 'SET_WAFFLE_FILTER_QUERY_DRAFT' +); + +export const applyWaffleFilterQuery = actionCreator('APPLY_WAFFLE_FILTER_QUERY'); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts new file mode 100644 index 0000000000000..558314f2aeda8 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_filter/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as waffleFilterActions from './actions'; +import * as waffleFilterSelectors from './selectors'; + +export { waffleFilterActions, waffleFilterSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts new file mode 100644 index 0000000000000..0f6be7524e081 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_filter/reducer.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { applyWaffleFilterQuery, setWaffleFilterQueryDraft } from './actions'; + +export interface KueryFilterQuery { + kind: 'kuery'; + expression: string; +} + +export type FilterQuery = KueryFilterQuery; + +export interface WaffleFilterState { + filterQuery: KueryFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; +} + +export const initialWaffleFilterState: WaffleFilterState = { + filterQuery: null, + filterQueryDraft: null, +}; + +export const waffleFilterReducer = reducerWithInitialState(initialWaffleFilterState) + .case(setWaffleFilterQueryDraft, (state, filterQueryDraft) => ({ + ...state, + filterQueryDraft, + })) + .case(applyWaffleFilterQuery, (state, filterQuery) => ({ + ...state, + filterQuery, + filterQueryDraft: filterQuery, + })) + .build(); diff --git a/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts b/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts new file mode 100644 index 0000000000000..ddad03b44d120 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_filter/selectors.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { fromKueryExpression } from 'ui/kuery'; + +import { WaffleFilterState } from './reducer'; + +export const selectWaffleFilterQuery = (state: WaffleFilterState) => state.filterQuery; + +export const selectWaffleFilterQueryDraft = (state: WaffleFilterState) => state.filterQueryDraft; + +export const selectIsWaffleFilterQueryDraftValid = createSelector( + selectWaffleFilterQueryDraft, + filterQueryDraft => { + if (filterQueryDraft && filterQueryDraft.kind === 'kuery') { + try { + fromKueryExpression(filterQueryDraft.expression); + } catch (err) { + return false; + } + } + + return true; + } +); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts new file mode 100644 index 0000000000000..04c0a1d112306 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import { InfraMetricInput, InfraPathInput } from '../../../../common/graphql/types'; +import { InfraNodeType } from '../../../../server/lib/adapters/nodes'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_options'); + +export const changeMetric = actionCreator('CHANGE_METRIC'); +export const changeGroupBy = actionCreator('CHANGE_GROUP_BY'); +export const changeNodeType = actionCreator('CHANGE_NODE_TYPE'); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/index.ts b/x-pack/plugins/infra/public/store/local/waffle_options/index.ts new file mode 100644 index 0000000000000..3ecf108eb49d4 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_options/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as waffleOptionsActions from './actions'; +import * as waffleOptionsSelectors from './selector'; + +export { waffleOptionsActions, waffleOptionsSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts new file mode 100644 index 0000000000000..c736aa47de39c --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers'; + +import { + InfraMetricInput, + InfraMetricType, + InfraPathInput, +} from '../../../../common/graphql/types'; +import { InfraNodeType } from '../../../../server/lib/adapters/nodes'; +import { changeGroupBy, changeMetric, changeNodeType } from './actions'; + +export interface WaffleOptionsState { + metric: InfraMetricInput; + groupBy: InfraPathInput[]; + nodeType: InfraNodeType; +} + +export const initialWaffleOptionsState: WaffleOptionsState = { + metric: { type: InfraMetricType.cpu }, + groupBy: [], + nodeType: InfraNodeType.host, +}; + +const currentMetricReducer = reducerWithInitialState(initialWaffleOptionsState.metric).case( + changeMetric, + (current, target) => target +); + +const currentGroupByReducer = reducerWithInitialState(initialWaffleOptionsState.groupBy).case( + changeGroupBy, + (current, target) => target +); + +const currentNodeTypeReducer = reducerWithInitialState(initialWaffleOptionsState.nodeType).case( + changeNodeType, + (current, target) => target +); + +export const waffleOptionsReducer = combineReducers({ + metric: currentMetricReducer, + groupBy: currentGroupByReducer, + nodeType: currentNodeTypeReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts b/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts new file mode 100644 index 0000000000000..6889cd6150ab7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { WaffleOptionsState } from './reducer'; + +export const selectMetric = (state: WaffleOptionsState) => state.metric; +export const selectGroupBy = (state: WaffleOptionsState) => state.groupBy; +export const selectNodeType = (state: WaffleOptionsState) => state.nodeType; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts new file mode 100644 index 0000000000000..fe79f2f536a93 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_time/actions.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time'); + +export const jumpToTime = actionCreator('JUMP_TO_TIME'); + +export const startAutoReload = actionCreator('START_AUTO_RELOAD'); + +export const stopAutoReload = actionCreator('STOP_AUTO_RELOAD'); diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts b/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts new file mode 100644 index 0000000000000..d9c1c825f1a25 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_time/epic.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { timer } from 'rxjs'; +import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators'; + +import { jumpToTime, startAutoReload, stopAutoReload } from './actions'; + +interface WaffleTimeEpicDependencies { + selectWaffleTimeUpdatePolicyInterval: (state: State) => number | null; +} + +export const createWaffleTimeEpic = (): Epic< + Action, + Action, + State, + WaffleTimeEpicDependencies +> => (action$, state$, { selectWaffleTimeUpdatePolicyInterval }) => { + const updateInterval$ = state$.pipe( + map(selectWaffleTimeUpdatePolicyInterval), + filter(isNotNull) + ); + + return action$.pipe( + filter(startAutoReload.match), + withLatestFrom(updateInterval$), + exhaustMap(([action, updateInterval]) => + timer(0, updateInterval).pipe( + map(() => jumpToTime(Date.now())), + takeUntil(action$.pipe(filter(stopAutoReload.match))) + ) + ) + ); +}; + +const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/index.ts b/x-pack/plugins/infra/public/store/local/waffle_time/index.ts new file mode 100644 index 0000000000000..2b99a6d6d5760 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_time/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as waffleTimeActions from './actions'; +import * as waffleTimeSelectors from './selectors'; + +export { waffleTimeActions, waffleTimeSelectors }; +export * from './epic'; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts new file mode 100644 index 0000000000000..026e5decf5d37 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_time/reducer.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { jumpToTime, startAutoReload, stopAutoReload } from './actions'; + +interface ManualTimeUpdatePolicy { + policy: 'manual'; +} + +interface IntervalTimeUpdatePolicy { + policy: 'interval'; + interval: number; +} + +type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy; + +export interface WaffleTimeState { + currentTime: number; + updatePolicy: TimeUpdatePolicy; +} + +export const initialWaffleTimeState: WaffleTimeState = { + currentTime: Date.now(), + updatePolicy: { + policy: 'manual', + }, +}; + +const currentTimeReducer = reducerWithInitialState(initialWaffleTimeState.currentTime).case( + jumpToTime, + (currentTime, targetTime) => targetTime +); + +const updatePolicyReducer = reducerWithInitialState(initialWaffleTimeState.updatePolicy) + .case(startAutoReload, () => ({ + policy: 'interval', + interval: 5000, + })) + .case(stopAutoReload, () => ({ + policy: 'manual', + })); + +export const waffleTimeReducer = combineReducers({ + currentTime: currentTimeReducer, + updatePolicy: updatePolicyReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts b/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts new file mode 100644 index 0000000000000..122988637ae6e --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/waffle_time/selectors.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { WaffleTimeState } from './reducer'; + +export const selectCurrentTime = (state: WaffleTimeState) => state.currentTime; + +export const selectIsAutoReloading = (state: WaffleTimeState) => + state.updatePolicy.policy === 'interval'; + +export const selectTimeUpdatePolicyInterval = (state: WaffleTimeState) => + state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null; + +export const selectCurrentTimeRange = createSelector(selectCurrentTime, currentTime => ({ + from: currentTime - 1000 * 60 * 10, + interval: '5m', + to: currentTime, +})); diff --git a/x-pack/plugins/infra/public/store/reducer.ts b/x-pack/plugins/infra/public/store/reducer.ts new file mode 100644 index 0000000000000..65b225d019603 --- /dev/null +++ b/x-pack/plugins/infra/public/store/reducer.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; + +import { initialLocalState, localReducer, LocalState } from './local'; +import { initialRemoteState, remoteReducer, RemoteState } from './remote'; + +export interface State { + local: LocalState; + remote: RemoteState; +} + +export const initialState: State = { + local: initialLocalState, + remote: initialRemoteState, +}; + +export const reducer = combineReducers({ + local: localReducer, + remote: remoteReducer, +}); diff --git a/x-pack/plugins/infra/public/store/remote/actions.ts b/x-pack/plugins/infra/public/store/remote/actions.ts new file mode 100644 index 0000000000000..49c5d7209f7ef --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/actions.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { logEntriesActions } from './log_entries'; +export { logSummaryActions } from './log_summary'; +export { sourceActions } from './source'; diff --git a/x-pack/plugins/infra/public/store/remote/epic.ts b/x-pack/plugins/infra/public/store/remote/epic.ts new file mode 100644 index 0000000000000..22eb3c68c75c4 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/epic.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineEpics } from 'redux-observable'; + +import { createLogEntriesEpic } from './log_entries'; +import { createLogSummaryEpic } from './log_summary'; +import { createSourceEpic } from './source'; + +export const createRemoteEpic = () => + combineEpics( + createLogEntriesEpic(), + createLogSummaryEpic(), + createSourceEpic() + ); diff --git a/x-pack/plugins/infra/public/store/remote/index.ts b/x-pack/plugins/infra/public/store/remote/index.ts new file mode 100644 index 0000000000000..c2843320bfd0c --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './actions'; +export * from './epic'; +export * from './reducer'; +export * from './selectors'; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts b/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts new file mode 100644 index 0000000000000..f02fa12beb55a --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/actions.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loadEntriesActionCreators } from './operations/load'; +import { loadMoreEntriesActionCreators } from './operations/load_more'; + +export const loadEntries = loadEntriesActionCreators.resolve; +export const loadMoreEntries = loadMoreEntriesActionCreators.resolve; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts b/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts new file mode 100644 index 0000000000000..43f1d2078ae8f --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/epic.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { combineEpics, Epic, EpicWithState } from 'redux-observable'; +import { merge } from 'rxjs'; +import { exhaustMap, filter, map, withLatestFrom } from 'rxjs/operators'; + +import { logFilterActions, logPositionActions } from '../..'; +import { pickTimeKey, TimeKey, timeKeyIsBetween } from '../../../../common/time'; +import { loadEntries, loadMoreEntries } from './actions'; +import { loadEntriesEpic } from './operations/load'; +import { loadMoreEntriesEpic } from './operations/load_more'; + +const LOAD_CHUNK_SIZE = 200; +const DESIRED_BUFFER_PAGES = 2; + +interface ManageEntriesDependencies { + selectLogEntriesStart: (state: State) => TimeKey | null; + selectLogEntriesEnd: (state: State) => TimeKey | null; + selectHasMoreLogEntriesBeforeStart: (state: State) => boolean; + selectHasMoreLogEntriesAfterEnd: (state: State) => boolean; + selectIsAutoReloadingLogEntries: (state: State) => boolean; + selectIsLoadingLogEntries: (state: State) => boolean; + selectLogFilterQueryAsJson: (state: State) => string | null; + selectVisibleLogMidpointOrTarget: (state: State) => TimeKey | null; +} + +export const createLogEntriesEpic = () => + combineEpics( + createEntriesEffectsEpic(), + loadEntriesEpic as EpicWithState, + loadMoreEntriesEpic as EpicWithState + ); + +export const createEntriesEffectsEpic = (): Epic< + Action, + Action, + State, + ManageEntriesDependencies +> => ( + action$, + state$, + { + selectLogEntriesStart, + selectLogEntriesEnd, + selectHasMoreLogEntriesBeforeStart, + selectHasMoreLogEntriesAfterEnd, + selectIsAutoReloadingLogEntries, + selectIsLoadingLogEntries, + selectLogFilterQueryAsJson, + selectVisibleLogMidpointOrTarget, + } +) => { + const filterQuery$ = state$.pipe(map(selectLogFilterQueryAsJson)); + const visibleMidpointOrTarget$ = state$.pipe( + map(selectVisibleLogMidpointOrTarget), + filter(isNotNull), + map(pickTimeKey) + ); + + const shouldLoadAroundNewPosition$ = action$.pipe( + filter(logPositionActions.jumpToTargetPosition.match), + withLatestFrom(state$), + filter(([{ payload }, state]) => { + const entriesStart = selectLogEntriesStart(state); + const entriesEnd = selectLogEntriesEnd(state); + + return entriesStart && entriesEnd + ? !timeKeyIsBetween(entriesStart, entriesEnd, payload) + : true; + }), + map(([{ payload }]) => pickTimeKey(payload)) + ); + + const shouldLoadWithNewFilter$ = action$.pipe( + filter(logFilterActions.applyLogFilterQuery.match), + withLatestFrom(filterQuery$, (filterQuery, filterQueryString) => filterQueryString) + ); + + const shouldLoadMoreBefore$ = action$.pipe( + filter(logPositionActions.reportVisiblePositions.match), + filter(({ payload: { pagesBeforeStart } }) => pagesBeforeStart < DESIRED_BUFFER_PAGES), + withLatestFrom(state$), + filter( + ([action, state]) => + !selectIsAutoReloadingLogEntries(state) && + !selectIsLoadingLogEntries(state) && + selectHasMoreLogEntriesBeforeStart(state) + ), + map(([action, state]) => selectLogEntriesStart(state)), + filter(isNotNull), + map(pickTimeKey) + ); + + const shouldLoadMoreAfter$ = action$.pipe( + filter(logPositionActions.reportVisiblePositions.match), + filter(({ payload: { pagesAfterEnd } }) => pagesAfterEnd < DESIRED_BUFFER_PAGES), + withLatestFrom(state$), + filter( + ([action, state]) => + !selectIsAutoReloadingLogEntries(state) && + !selectIsLoadingLogEntries(state) && + selectHasMoreLogEntriesAfterEnd(state) + ), + map(([action, state]) => selectLogEntriesEnd(state)), + filter(isNotNull), + map(pickTimeKey) + ); + + return merge( + shouldLoadAroundNewPosition$.pipe( + withLatestFrom(filterQuery$), + exhaustMap(([timeKey, filterQuery]) => [ + loadEntries({ + sourceId: 'default', + timeKey, + countBefore: LOAD_CHUNK_SIZE, + countAfter: LOAD_CHUNK_SIZE, + filterQuery, + }), + ]) + ), + shouldLoadWithNewFilter$.pipe( + withLatestFrom(visibleMidpointOrTarget$), + exhaustMap(([filterQuery, timeKey]) => [ + loadEntries({ + sourceId: 'default', + timeKey, + countBefore: LOAD_CHUNK_SIZE, + countAfter: LOAD_CHUNK_SIZE, + filterQuery, + }), + ]) + ), + shouldLoadMoreAfter$.pipe( + withLatestFrom(filterQuery$), + exhaustMap(([timeKey, filterQuery]) => [ + loadMoreEntries({ + sourceId: 'default', + timeKey, + countBefore: 0, + countAfter: LOAD_CHUNK_SIZE, + filterQuery, + }), + ]) + ), + shouldLoadMoreBefore$.pipe( + withLatestFrom(filterQuery$), + exhaustMap(([timeKey, filterQuery]) => [ + loadMoreEntries({ + sourceId: 'default', + timeKey, + countBefore: LOAD_CHUNK_SIZE, + countAfter: 0, + filterQuery, + }), + ]) + ) + ); +}; + +const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/index.ts b/x-pack/plugins/infra/public/store/remote/log_entries/index.ts new file mode 100644 index 0000000000000..8e00425526935 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logEntriesActions from './actions'; +import * as logEntriesSelectors from './selectors'; + +export { logEntriesActions, logEntriesSelectors }; +export * from './epic'; +export * from './reducer'; +export { initialLogEntriesState, LogEntriesState } from './state'; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/operations/load.ts b/x-pack/plugins/infra/public/store/remote/log_entries/operations/load.ts new file mode 100644 index 0000000000000..5a92afd89024c --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/operations/load.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogEntries as LogEntriesQuery } from '../../../../../common/graphql/types'; +import { + createGraphqlOperationActionCreators, + createGraphqlOperationReducer, + createGraphqlQueryEpic, +} from '../../../../utils/remote_state/remote_graphql_state'; +import { initialLogEntriesState } from '../state'; +import { logEntriesQuery } from './log_entries.gql_query'; + +const operationKey = 'load'; + +export const loadEntriesActionCreators = createGraphqlOperationActionCreators< + LogEntriesQuery.Query, + LogEntriesQuery.Variables +>('log_entries', operationKey); + +export const loadEntriesReducer = createGraphqlOperationReducer( + operationKey, + initialLogEntriesState, + loadEntriesActionCreators, + (state, action) => action.payload.result.data.source.logEntriesAround +); + +export const loadEntriesEpic = createGraphqlQueryEpic(logEntriesQuery, loadEntriesActionCreators); diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/operations/load_more.ts b/x-pack/plugins/infra/public/store/remote/log_entries/operations/load_more.ts new file mode 100644 index 0000000000000..0f58f38e52b11 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/operations/load_more.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogEntries as LogEntriesQuery } from '../../../../../common/graphql/types'; +import { + getLogEntryIndexAfterTime, + getLogEntryIndexBeforeTime, + getLogEntryKey, +} from '../../../../utils/log_entry'; +import { + createGraphqlOperationActionCreators, + createGraphqlOperationReducer, + createGraphqlQueryEpic, +} from '../../../../utils/remote_state/remote_graphql_state'; +import { initialLogEntriesState } from '../state'; +import { logEntriesQuery } from './log_entries.gql_query'; + +const operationKey = 'load_more'; + +export const loadMoreEntriesActionCreators = createGraphqlOperationActionCreators< + LogEntriesQuery.Query, + LogEntriesQuery.Variables +>('log_entries', operationKey); + +export const loadMoreEntriesReducer = createGraphqlOperationReducer( + operationKey, + initialLogEntriesState, + loadMoreEntriesActionCreators, + (state, action) => { + const logEntriesAround = action.payload.result.data.source.logEntriesAround; + const newEntries = logEntriesAround.entries; + const oldEntries = state && state.entries ? state.entries : []; + const oldStart = state && state.start ? state.start : null; + const oldEnd = state && state.end ? state.end : null; + + if (newEntries.length <= 0) { + return state; + } + + if ((action.payload.params.countBefore || 0) > 0) { + const lastLogEntry = newEntries[newEntries.length - 1]; + const prependAtIndex = getLogEntryIndexAfterTime(oldEntries, getLogEntryKey(lastLogEntry)); + return { + start: logEntriesAround.start, + end: oldEnd, + hasMoreBefore: logEntriesAround.hasMoreBefore, + hasMoreAfter: state ? state.hasMoreAfter : logEntriesAround.hasMoreAfter, + entries: [...newEntries, ...oldEntries.slice(prependAtIndex)], + }; + } else if ((action.payload.params.countAfter || 0) > 0) { + const firstLogEntry = newEntries[0]; + const appendAtIndex = getLogEntryIndexBeforeTime(oldEntries, getLogEntryKey(firstLogEntry)); + return { + start: oldStart, + end: logEntriesAround.end, + hasMoreBefore: state ? state.hasMoreBefore : logEntriesAround.hasMoreBefore, + hasMoreAfter: logEntriesAround.hasMoreAfter, + entries: [...oldEntries.slice(0, appendAtIndex), ...newEntries], + }; + } else { + return state; + } + } +); + +export const loadMoreEntriesEpic = createGraphqlQueryEpic( + logEntriesQuery, + loadMoreEntriesActionCreators +); diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts b/x-pack/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts new file mode 100644 index 0000000000000..3bffc43644418 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/operations/log_entries.gql_query.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +import { sharedFragments } from '../../../../../common/graphql/shared'; + +export const logEntriesQuery = gql` + query LogEntries( + $sourceId: ID = "default" + $timeKey: InfraTimeKeyInput! + $countBefore: Int = 0 + $countAfter: Int = 0 + $filterQuery: String + ) { + source(id: $sourceId) { + id + logEntriesAround( + key: $timeKey + countBefore: $countBefore + countAfter: $countAfter + filterQuery: $filterQuery + ) { + start { + ...InfraTimeKeyFields + } + end { + ...InfraTimeKeyFields + } + hasMoreBefore + hasMoreAfter + entries { + gid + key { + time + tiebreaker + } + message { + ... on InfraLogMessageFieldSegment { + field + value + } + ... on InfraLogMessageConstantSegment { + constant + } + } + } + } + } + } + + ${sharedFragments.InfraTimeKey} +`; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/reducer.ts b/x-pack/plugins/infra/public/store/remote/log_entries/reducer.ts new file mode 100644 index 0000000000000..c0d60c4d336de --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/reducer.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import reduceReducers from 'reduce-reducers'; +import { Reducer } from 'redux'; + +import { loadEntriesReducer } from './operations/load'; +import { loadMoreEntriesReducer } from './operations/load_more'; +import { LogEntriesState } from './state'; + +export const logEntriesReducer = reduceReducers( + loadEntriesReducer, + loadMoreEntriesReducer +) as Reducer; diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/selectors.ts b/x-pack/plugins/infra/public/store/remote/log_entries/selectors.ts new file mode 100644 index 0000000000000..7520803f93ac7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/selectors.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state'; +import { LogEntriesRemoteState } from './state'; + +const entriesGraphlStateSelectors = createGraphqlStateSelectors(); + +export const selectEntries = createSelector( + entriesGraphlStateSelectors.selectData, + data => (data ? data.entries : []) +); + +export const selectIsLoadingEntries = entriesGraphlStateSelectors.selectIsLoading; + +export const selectIsReloadingEntries = createSelector( + entriesGraphlStateSelectors.selectIsLoading, + entriesGraphlStateSelectors.selectLoadingProgressOperationInfo, + (isLoading, operationInfo) => + isLoading && operationInfo ? operationInfo.operationKey === 'load' : false +); + +export const selectIsLoadingMoreEntries = createSelector( + entriesGraphlStateSelectors.selectIsLoading, + entriesGraphlStateSelectors.selectLoadingProgressOperationInfo, + (isLoading, operationInfo) => + isLoading && operationInfo ? operationInfo.operationKey === 'load_more' : false +); + +export const selectEntriesStart = createSelector( + entriesGraphlStateSelectors.selectData, + data => (data && data.start ? data.start : null) +); + +export const selectEntriesEnd = createSelector( + entriesGraphlStateSelectors.selectData, + data => (data && data.end ? data.end : null) +); + +export const selectHasMoreBeforeStart = createSelector( + entriesGraphlStateSelectors.selectData, + data => (data ? data.hasMoreBefore : true) +); + +export const selectHasMoreAfterEnd = createSelector( + entriesGraphlStateSelectors.selectData, + data => (data ? data.hasMoreAfter : true) +); + +export const selectEntriesLastLoadedTime = entriesGraphlStateSelectors.selectLoadingResultTime; + +export const selectEntriesStartLoadingState = entriesGraphlStateSelectors.selectLoadingState; + +export const selectEntriesEndLoadingState = entriesGraphlStateSelectors.selectLoadingState; + +export const selectFirstEntry = createSelector( + selectEntries, + entries => (entries.length > 0 ? entries[0] : null) +); + +export const selectLastEntry = createSelector( + selectEntries, + entries => (entries.length > 0 ? entries[entries.length - 1] : null) +); + +export const selectLoadedEntriesTimeInterval = createSelector( + entriesGraphlStateSelectors.selectData, + data => ({ + end: data && data.end ? data.end.time : null, + start: data && data.start ? data.start.time : null, + }) +); diff --git a/x-pack/plugins/infra/public/store/remote/log_entries/state.ts b/x-pack/plugins/infra/public/store/remote/log_entries/state.ts new file mode 100644 index 0000000000000..6f8675b1efa83 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_entries/state.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogEntries as LogEntriesQuery } from '../../../../common/graphql/types'; +import { + createGraphqlInitialState, + GraphqlState, +} from '../../../utils/remote_state/remote_graphql_state'; + +export type LogEntriesRemoteState = LogEntriesQuery.LogEntriesAround; +export type LogEntriesState = GraphqlState; + +export const initialLogEntriesState = createGraphqlInitialState(); diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/actions.ts b/x-pack/plugins/infra/public/store/remote/log_summary/actions.ts new file mode 100644 index 0000000000000..4cb77a16aeecb --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/actions.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loadSummaryActionCreators } from './operations/load'; + +export const loadSummary = loadSummaryActionCreators.resolve; diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/epic.ts b/x-pack/plugins/infra/public/store/remote/log_summary/epic.ts new file mode 100644 index 0000000000000..8bf8b07f973be --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/epic.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { combineEpics, Epic, EpicWithState } from 'redux-observable'; +import { merge } from 'rxjs'; +import { exhaustMap, filter, map, withLatestFrom } from 'rxjs/operators'; + +import { logFilterActions, logPositionActions } from '../..'; +import { loadSummary } from './actions'; +import { loadSummaryEpic } from './operations/load'; + +const LOAD_BUCKETS_PER_PAGE = 100; +const MINIMUM_BUCKETS_PER_PAGE = 90; +const MINIMUM_BUFFER_PAGES = 0.5; + +interface ManageSummaryDependencies { + selectLogFilterQueryAsJson: (state: State) => string | null; + selectVisibleLogSummary: ( + state: State + ) => { + start: number | null; + end: number | null; + }; +} + +export const createLogSummaryEpic = () => + combineEpics(createSummaryEffectsEpic(), loadSummaryEpic as EpicWithState< + typeof loadSummaryEpic, + State + >); + +export const createSummaryEffectsEpic = (): Epic< + Action, + Action, + State, + ManageSummaryDependencies +> => (action$, state$, { selectLogFilterQueryAsJson, selectVisibleLogSummary }) => { + const filterQuery$ = state$.pipe(map(selectLogFilterQueryAsJson)); + const summaryInterval$ = state$.pipe( + map(selectVisibleLogSummary), + map(({ start, end }) => (start && end ? getLoadParameters(start, end) : null)), + filter(isNotNull) + ); + + const shouldLoadBetweenNewInterval$ = action$.pipe( + filter(logPositionActions.reportVisibleSummary.match), + filter( + ({ payload: { bucketsOnPage, pagesBeforeStart, pagesAfterEnd } }) => + bucketsOnPage < MINIMUM_BUCKETS_PER_PAGE || + pagesBeforeStart < MINIMUM_BUFFER_PAGES || + pagesAfterEnd < MINIMUM_BUFFER_PAGES + ), + map(({ payload: { start, end } }) => getLoadParameters(start, end)) + ); + + const shouldLoadWithNewFilter$ = action$.pipe( + filter(logFilterActions.applyLogFilterQuery.match), + withLatestFrom(filterQuery$, (filterQuery, filterQueryString) => filterQueryString) + ); + + return merge( + shouldLoadBetweenNewInterval$.pipe( + withLatestFrom(filterQuery$), + exhaustMap(([{ start, end, bucketSize }, filterQuery]) => [ + loadSummary({ + start, + end, + sourceId: 'default', + bucketSize, + filterQuery, + }), + ]) + ), + shouldLoadWithNewFilter$.pipe( + withLatestFrom(summaryInterval$), + exhaustMap(([filterQuery, { start, end, bucketSize }]) => [ + loadSummary({ + start, + end, + sourceId: 'default', + bucketSize: (end - start) / LOAD_BUCKETS_PER_PAGE, + filterQuery, + }), + ]) + ) + ); +}; + +const getLoadParameters = (start: number, end: number) => ({ + start: start - (end - start), + end: end + (end - start), + bucketSize: (end - start) / LOAD_BUCKETS_PER_PAGE, +}); + +const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/index.ts b/x-pack/plugins/infra/public/store/remote/log_summary/index.ts new file mode 100644 index 0000000000000..110d65e958f82 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as logSummaryActions from './actions'; +import * as logSummarySelectors from './selectors'; + +export { logSummaryActions, logSummarySelectors }; +export * from './epic'; +export * from './reducer'; +export { initialLogSummaryState, LogSummaryState } from './state'; diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/operations/load.ts b/x-pack/plugins/infra/public/store/remote/log_summary/operations/load.ts new file mode 100644 index 0000000000000..be3303586a568 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/operations/load.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogSummary as LogSummaryQuery } from '../../../../../common/graphql/types'; +import { + createGraphqlOperationActionCreators, + createGraphqlOperationReducer, + createGraphqlQueryEpic, +} from '../../../../utils/remote_state/remote_graphql_state'; +import { initialLogSummaryState } from '../state'; +import { logSummaryQuery } from './log_summary.gql_query'; + +const operationKey = 'load'; + +export const loadSummaryActionCreators = createGraphqlOperationActionCreators< + LogSummaryQuery.Query, + LogSummaryQuery.Variables +>('log_summary', operationKey); + +export const loadSummaryReducer = createGraphqlOperationReducer( + operationKey, + initialLogSummaryState, + loadSummaryActionCreators, + (state, action) => action.payload.result.data.source.logSummaryBetween +); + +export const loadSummaryEpic = createGraphqlQueryEpic(logSummaryQuery, loadSummaryActionCreators); diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/operations/log_summary.gql_query.ts b/x-pack/plugins/infra/public/store/remote/log_summary/operations/log_summary.gql_query.ts new file mode 100644 index 0000000000000..a8bd808e51947 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/operations/log_summary.gql_query.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const logSummaryQuery = gql` + query LogSummary( + $sourceId: ID = "default" + $start: Float! + $end: Float! + $bucketSize: Float! + $filterQuery: String + ) { + source(id: $sourceId) { + id + logSummaryBetween( + start: $start + end: $end + bucketSize: $bucketSize + filterQuery: $filterQuery + ) { + start + end + buckets { + start + end + entriesCount + } + } + } + } +`; diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/reducer.ts b/x-pack/plugins/infra/public/store/remote/log_summary/reducer.ts new file mode 100644 index 0000000000000..4ca900ceb20cd --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/reducer.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import reduceReducers from 'reduce-reducers'; +import { Reducer } from 'redux'; + +import { loadSummaryReducer } from './operations/load'; +import { LogSummaryState } from './state'; + +export const logSummaryReducer = reduceReducers( + loadSummaryReducer /*, loadMoreSummaryReducer*/ +) as Reducer; diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/selectors.ts b/x-pack/plugins/infra/public/store/remote/log_summary/selectors.ts new file mode 100644 index 0000000000000..52975f6306cd5 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/selectors.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state'; +import { LogSummaryRemoteState } from './state'; + +const summaryGraphlStateSelectors = createGraphqlStateSelectors(); + +export const selectSummaryBuckets = createSelector( + summaryGraphlStateSelectors.selectData, + data => (data ? data.buckets : []) +); diff --git a/x-pack/plugins/infra/public/store/remote/log_summary/state.ts b/x-pack/plugins/infra/public/store/remote/log_summary/state.ts new file mode 100644 index 0000000000000..d6c10e0a2f2b1 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/log_summary/state.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogSummary as LogSummaryQuery } from '../../../../common/graphql/types'; +import { + createGraphqlInitialState, + GraphqlState, +} from '../../../utils/remote_state/remote_graphql_state'; + +export type LogSummaryRemoteState = LogSummaryQuery.LogSummaryBetween; +export type LogSummaryState = GraphqlState; + +export const initialLogSummaryState: LogSummaryState = createGraphqlInitialState< + LogSummaryRemoteState +>(); diff --git a/x-pack/plugins/infra/public/store/remote/reducer.ts b/x-pack/plugins/infra/public/store/remote/reducer.ts new file mode 100644 index 0000000000000..703e6d426f031 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/reducer.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; +import { initialLogEntriesState, logEntriesReducer, LogEntriesState } from './log_entries'; +import { initialLogSummaryState, logSummaryReducer, LogSummaryState } from './log_summary'; +import { initialSourceState, sourceReducer, SourceState } from './source'; + +export interface RemoteState { + logEntries: LogEntriesState; + logSummary: LogSummaryState; + source: SourceState; +} + +export const initialRemoteState = { + logEntries: initialLogEntriesState, + logSummary: initialLogSummaryState, + source: initialSourceState, +}; + +export const remoteReducer = combineReducers({ + logEntries: logEntriesReducer, + logSummary: logSummaryReducer, + source: sourceReducer, +}); diff --git a/x-pack/plugins/infra/public/store/remote/selectors.ts b/x-pack/plugins/infra/public/store/remote/selectors.ts new file mode 100644 index 0000000000000..1d48af7bf5058 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/selectors.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { globalizeSelectors } from '../../utils/typed_redux'; +import { logEntriesSelectors as innerLogEntriesSelectors } from './log_entries'; +import { logSummarySelectors as innerLogSummarySelectors } from './log_summary'; +import { RemoteState } from './reducer'; +import { sourceSelectors as innerSourceSelectors } from './source'; + +export const logEntriesSelectors = globalizeSelectors( + (state: RemoteState) => state.logEntries, + innerLogEntriesSelectors +); + +export const logSummarySelectors = globalizeSelectors( + (state: RemoteState) => state.logSummary, + innerLogSummarySelectors +); + +export const sourceSelectors = globalizeSelectors( + (state: RemoteState) => state.source, + innerSourceSelectors +); diff --git a/x-pack/plugins/infra/public/store/remote/source/actions.ts b/x-pack/plugins/infra/public/store/remote/source/actions.ts new file mode 100644 index 0000000000000..741b547b895e2 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/actions.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loadSourceActionCreators } from './operations/load'; + +export const loadSource = loadSourceActionCreators.resolve; diff --git a/x-pack/plugins/infra/public/store/remote/source/epic.ts b/x-pack/plugins/infra/public/store/remote/source/epic.ts new file mode 100644 index 0000000000000..373bd7f0298df --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/epic.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { combineEpics, Epic, EpicWithState } from 'redux-observable'; +import { of } from 'rxjs'; + +import { loadSource } from './actions'; +import { loadSourceEpic } from './operations/load'; + +export const createSourceEpic = () => + combineEpics(createSourceEffectsEpic(), loadSourceEpic as EpicWithState< + typeof loadSourceEpic, + State + >); + +export const createSourceEffectsEpic = (): Epic => action$ => { + return of(loadSource({ sourceId: 'default' })); +}; diff --git a/x-pack/plugins/infra/public/store/remote/source/index.ts b/x-pack/plugins/infra/public/store/remote/source/index.ts new file mode 100644 index 0000000000000..c12b516f3d6a8 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as sourceActions from './actions'; +import * as sourceSelectors from './selectors'; + +export { sourceActions, sourceSelectors }; +export * from './epic'; +export * from './reducer'; +export { initialSourceState, SourceState } from './state'; diff --git a/x-pack/plugins/infra/public/store/remote/source/operations/load.ts b/x-pack/plugins/infra/public/store/remote/source/operations/load.ts new file mode 100644 index 0000000000000..44f8fbee8ca34 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/operations/load.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SourceQuery } from '../../../../../common/graphql/types'; +import { + createGraphqlOperationActionCreators, + createGraphqlOperationReducer, + createGraphqlQueryEpic, +} from '../../../../utils/remote_state/remote_graphql_state'; +import { initialSourceState } from '../state'; +import { sourceQuery } from './query_source.gql_query'; + +const operationKey = 'load'; + +export const loadSourceActionCreators = createGraphqlOperationActionCreators< + SourceQuery.Query, + SourceQuery.Variables +>('source', operationKey); + +export const loadSourceReducer = createGraphqlOperationReducer( + operationKey, + initialSourceState, + loadSourceActionCreators, + (state, action) => action.payload.result.data.source +); + +export const loadSourceEpic = createGraphqlQueryEpic(sourceQuery, loadSourceActionCreators); diff --git a/x-pack/plugins/infra/public/store/remote/source/operations/query_source.gql_query.ts b/x-pack/plugins/infra/public/store/remote/source/operations/query_source.gql_query.ts new file mode 100644 index 0000000000000..f619b5175c6d6 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/operations/query_source.gql_query.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const sourceQuery = gql` + query SourceQuery($sourceId: ID = "default") { + source(id: $sourceId) { + configuration { + metricAlias + logAlias + fields { + container + host + pod + } + } + status { + indexFields { + name + type + searchable + aggregatable + } + logIndicesExist + metricIndicesExist + } + } + } +`; diff --git a/x-pack/plugins/infra/public/store/remote/source/reducer.ts b/x-pack/plugins/infra/public/store/remote/source/reducer.ts new file mode 100644 index 0000000000000..811ff72c5ac9e --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/reducer.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import reduceReducers from 'reduce-reducers'; +import { Reducer } from 'redux'; + +import { loadSourceReducer } from './operations/load'; +import { SourceState } from './state'; + +export const sourceReducer = reduceReducers(loadSourceReducer) as Reducer; diff --git a/x-pack/plugins/infra/public/store/remote/source/selectors.ts b/x-pack/plugins/infra/public/store/remote/source/selectors.ts new file mode 100644 index 0000000000000..0ab3ee62745f5 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/selectors.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { createGraphqlStateSelectors } from '../../../utils/remote_state/remote_graphql_state'; +import { SourceRemoteState } from './state'; + +const sourceStatusGraphqlStateSelectors = createGraphqlStateSelectors(); + +export const selectSource = sourceStatusGraphqlStateSelectors.selectData; + +export const selectSourceConfiguration = createSelector( + selectSource, + source => (source ? source.configuration : null) +); + +export const selectSourceLogAlias = createSelector( + selectSourceConfiguration, + configuration => (configuration ? configuration.logAlias : null) +); + +export const selectSourceMetricAlias = createSelector( + selectSourceConfiguration, + configuration => (configuration ? configuration.metricAlias : null) +); + +export const selectSourceFields = createSelector( + selectSourceConfiguration, + configuration => (configuration ? configuration.fields : null) +); + +export const selectSourceStatus = createSelector( + selectSource, + source => (source ? source.status : null) +); + +export const selectSourceLogIndicesExist = createSelector( + selectSourceStatus, + sourceStatus => (sourceStatus ? sourceStatus.logIndicesExist : null) +); + +export const selectSourceMetricIndicesExist = createSelector( + selectSourceStatus, + sourceStatus => (sourceStatus ? sourceStatus.metricIndicesExist : null) +); + +export const selectSourceIndexFields = createSelector( + selectSourceStatus, + sourceStatus => (sourceStatus ? sourceStatus.indexFields : []) +); + +export const selectDerivedIndexPattern = createSelector( + selectSourceIndexFields, + selectSourceLogAlias, + selectSourceMetricAlias, + (indexFields, logAlias, metricAlias) => ({ + fields: indexFields, + title: `${logAlias},${metricAlias}`, + }) +); diff --git a/x-pack/plugins/infra/public/store/remote/source/state.ts b/x-pack/plugins/infra/public/store/remote/source/state.ts new file mode 100644 index 0000000000000..15eef7426dbb7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/remote/source/state.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SourceQuery } from '../../../../common/graphql/types'; +import { + createGraphqlInitialState, + GraphqlState, +} from '../../../utils/remote_state/remote_graphql_state'; + +export type SourceRemoteState = SourceQuery.Source; +export type SourceState = GraphqlState; + +export const initialSourceState = createGraphqlInitialState(); diff --git a/x-pack/plugins/infra/public/store/selectors.ts b/x-pack/plugins/infra/public/store/selectors.ts new file mode 100644 index 0000000000000..bbe1a4455e43e --- /dev/null +++ b/x-pack/plugins/infra/public/store/selectors.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { fromKueryExpression, toElasticsearchQuery } from 'ui/kuery'; + +import { getLogEntryAtTime } from '../utils/log_entry'; +import { globalizeSelectors } from '../utils/typed_redux'; +import { + logFilterSelectors as localLogFilterSelectors, + logMinimapSelectors as localLogMinimapSelectors, + logPositionSelectors as localLogPositionSelectors, + logTextviewSelectors as localLogTextviewSelectors, + metricTimeSelectors as localMetricTimeSelectors, + waffleFilterSelectors as localWaffleFilterSelectors, + waffleOptionsSelectors as localWaffleOptionsSelectors, + waffleTimeSelectors as localWaffleTimeSelectors, +} from './local'; +import { State } from './reducer'; +import { + logEntriesSelectors as remoteLogEntriesSelectors, + logSummarySelectors as remoteLogSummarySelectors, + sourceSelectors as remoteSourceSelectors, +} from './remote'; + +/** + * local selectors + */ + +const selectLocal = (state: State) => state.local; + +export const logFilterSelectors = globalizeSelectors(selectLocal, localLogFilterSelectors); +export const logMinimapSelectors = globalizeSelectors(selectLocal, localLogMinimapSelectors); +export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPositionSelectors); +export const logTextviewSelectors = globalizeSelectors(selectLocal, localLogTextviewSelectors); +export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTimeSelectors); +export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors); +export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors); +export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors); + +/** + * remote selectors + */ + +const selectRemote = (state: State) => state.remote; + +export const logEntriesSelectors = globalizeSelectors(selectRemote, remoteLogEntriesSelectors); +export const logSummarySelectors = globalizeSelectors(selectRemote, remoteLogSummarySelectors); +export const sourceSelectors = globalizeSelectors(selectRemote, remoteSourceSelectors); + +/** + * shared selectors + */ + +export const sharedSelectors = { + selectFirstVisibleLogEntry: createSelector( + logEntriesSelectors.selectEntries, + logPositionSelectors.selectFirstVisiblePosition, + (entries, firstVisiblePosition) => + firstVisiblePosition ? getLogEntryAtTime(entries, firstVisiblePosition) : null + ), + selectMiddleVisibleLogEntry: createSelector( + logEntriesSelectors.selectEntries, + logPositionSelectors.selectMiddleVisiblePosition, + (entries, middleVisiblePosition) => + middleVisiblePosition ? getLogEntryAtTime(entries, middleVisiblePosition) : null + ), + selectLastVisibleLogEntry: createSelector( + logEntriesSelectors.selectEntries, + logPositionSelectors.selectLastVisiblePosition, + (entries, lastVisiblePosition) => + lastVisiblePosition ? getLogEntryAtTime(entries, lastVisiblePosition) : null + ), + selectLogFilterQueryAsJson: createSelector( + logFilterSelectors.selectLogFilterQuery, + sourceSelectors.selectDerivedIndexPattern, + (filterQuery, indexPattern) => { + try { + return filterQuery + ? JSON.stringify( + toElasticsearchQuery(fromKueryExpression(filterQuery.expression), indexPattern) + ) + : null; + } catch (err) { + return null; + } + } + ), + selectWaffleFilterQueryAsJson: createSelector( + waffleFilterSelectors.selectWaffleFilterQuery, + sourceSelectors.selectDerivedIndexPattern, + (filterQuery, indexPattern) => { + try { + return filterQuery + ? JSON.stringify( + toElasticsearchQuery(fromKueryExpression(filterQuery.expression), indexPattern) + ) + : null; + } catch (err) { + return null; + } + } + ), +}; diff --git a/x-pack/plugins/infra/public/store/store.ts b/x-pack/plugins/infra/public/store/store.ts new file mode 100644 index 0000000000000..d18ef02875059 --- /dev/null +++ b/x-pack/plugins/infra/public/store/store.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action, applyMiddleware, compose, createStore as createBasicStore } from 'redux'; +import { createEpicMiddleware } from 'redux-observable'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { + createRootEpic, + initialState, + logEntriesSelectors, + logPositionSelectors, + metricTimeSelectors, + reducer, + sharedSelectors, + State, + waffleTimeSelectors, +} from '.'; +import { InfraApolloClient, InfraObservableApi } from '../lib/lib'; + +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose; + } +} + +export interface StoreDependencies { + apolloClient: Observable; + observableApi: Observable; +} + +export function createStore({ apolloClient, observableApi }: StoreDependencies) { + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + const middlewareDependencies = { + postToApi$: observableApi.pipe(map(({ post }) => post)), + apolloClient$: apolloClient, + selectIsLoadingLogEntries: logEntriesSelectors.selectIsLoadingEntries, + selectLogEntriesEnd: logEntriesSelectors.selectEntriesEnd, + selectLogEntriesStart: logEntriesSelectors.selectEntriesStart, + selectHasMoreLogEntriesAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd, + selectHasMoreLogEntriesBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart, + selectIsAutoReloadingLogEntries: logPositionSelectors.selectIsAutoReloading, + selectLogFilterQueryAsJson: sharedSelectors.selectLogFilterQueryAsJson, + selectLogTargetPosition: logPositionSelectors.selectTargetPosition, + selectVisibleLogMidpointOrTarget: logPositionSelectors.selectVisibleMidpointOrTarget, + selectVisibleLogSummary: logPositionSelectors.selectVisibleSummary, + selectWaffleTimeUpdatePolicyInterval: waffleTimeSelectors.selectTimeUpdatePolicyInterval, + selectMetricTimeUpdatePolicyInterval: metricTimeSelectors.selectTimeUpdatePolicyInterval, + selectMetricRangeFromTimeRange: metricTimeSelectors.selectRangeFromTimeRange, + }; + + const epicMiddleware = createEpicMiddleware( + { + dependencies: middlewareDependencies, + } + ); + + const store = createBasicStore( + reducer, + initialState, + composeEnhancers(applyMiddleware(epicMiddleware)) + ); + + epicMiddleware.run(createRootEpic()); + + return store; +} diff --git a/x-pack/plugins/infra/public/utils/formatters/data.ts b/x-pack/plugins/infra/public/utils/formatters/data.ts new file mode 100644 index 0000000000000..07b404c84f983 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/formatters/data.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraWaffleMapDataFormat } from '../../lib/lib'; +import { formatNumber } from './number'; + +/** + * The labels are derived from these two Wikipedia articles. + * https://en.wikipedia.org/wiki/Kilobit + * https://en.wikipedia.org/wiki/Kilobyte + */ +const LABELS = { + [InfraWaffleMapDataFormat.bytesDecimal]: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + [InfraWaffleMapDataFormat.bytesBinaryIEC]: [ + 'b', + 'Kib', + 'Mib', + 'Gib', + 'Tib', + 'Pib', + 'Eib', + 'Zib', + 'Yib', + ], + [InfraWaffleMapDataFormat.bytesBinaryJEDEC]: ['B', 'KB', 'MB', 'GB'], + [InfraWaffleMapDataFormat.bitsDecimal]: [ + 'bit', + 'kbit', + 'Mbit', + 'Gbit', + 'Tbit', + 'Pbit', + 'Ebit', + 'Zbit', + 'Ybit', + ], + [InfraWaffleMapDataFormat.bitsBinaryIEC]: [ + 'bit', + 'Kibit', + 'Mibit', + 'Gibit', + 'Tibit', + 'Pibit', + 'Eibit', + 'Zibit', + 'Yibit', + ], + [InfraWaffleMapDataFormat.bitsBinaryJEDEC]: ['bit', 'Kbit', 'Mbit', 'Gbit'], + [InfraWaffleMapDataFormat.abbreviatedNumber]: ['', 'K', 'M', 'B', 'T'], +}; + +const BASES = { + [InfraWaffleMapDataFormat.bytesDecimal]: 1000, + [InfraWaffleMapDataFormat.bytesBinaryIEC]: 1024, + [InfraWaffleMapDataFormat.bytesBinaryJEDEC]: 1024, + [InfraWaffleMapDataFormat.bitsDecimal]: 1000, + [InfraWaffleMapDataFormat.bitsBinaryIEC]: 1024, + [InfraWaffleMapDataFormat.bitsBinaryJEDEC]: 1024, + [InfraWaffleMapDataFormat.abbreviatedNumber]: 1000, +}; + +export const createDataFormatter = (format: InfraWaffleMapDataFormat) => (val: number) => { + const labels = LABELS[format]; + const base = BASES[format]; + const power = Math.min(Math.floor(Math.log(Math.abs(val)) / Math.log(base)), labels.length - 1); + if (power < 0) { + return `${formatNumber(val)}${labels[0]}`; + } + return `${formatNumber(val / Math.pow(base, power))}${labels[power]}`; +}; diff --git a/x-pack/plugins/infra/public/utils/formatters/index.ts b/x-pack/plugins/infra/public/utils/formatters/index.ts new file mode 100644 index 0000000000000..864890a43f957 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/formatters/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Mustache from 'mustache'; +import { InfraFormatterType, InfraWaffleMapDataFormat } from '../../lib/lib'; +import { createDataFormatter } from './data'; +import { formatNumber } from './number'; +import { formatPercent } from './percent'; + +export const FORMATTERS = { + [InfraFormatterType.number]: formatNumber, + [InfraFormatterType.abbreviatedNumber]: createDataFormatter( + InfraWaffleMapDataFormat.abbreviatedNumber + ), + [InfraFormatterType.bytes]: createDataFormatter(InfraWaffleMapDataFormat.bytesDecimal), + [InfraFormatterType.bits]: createDataFormatter(InfraWaffleMapDataFormat.bitsDecimal), + [InfraFormatterType.percent]: formatPercent, +}; + +export const createFormatter = (format: InfraFormatterType, template: string = '{{value}}') => ( + val: string | number +) => { + if (val == null) { + return ''; + } + const fmtFn = FORMATTERS[format]; + const value = fmtFn(Number(val)); + return Mustache.render(template, { value }); +}; diff --git a/x-pack/plugins/infra/public/utils/formatters/number.ts b/x-pack/plugins/infra/public/utils/formatters/number.ts new file mode 100644 index 0000000000000..db4ad4190bd73 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/formatters/number.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const formatNumber = (val: number) => { + return Number(val).toLocaleString('en', { + maximumFractionDigits: 1, + }); +}; diff --git a/x-pack/plugins/infra/public/utils/formatters/percent.ts b/x-pack/plugins/infra/public/utils/formatters/percent.ts new file mode 100644 index 0000000000000..c1cd9cd089d17 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/formatters/percent.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { formatNumber } from './number'; +export const formatPercent = (val: number) => { + return `${formatNumber(val * 100)}%`; +}; diff --git a/x-pack/plugins/infra/public/utils/handlers.ts b/x-pack/plugins/infra/public/utils/handlers.ts new file mode 100644 index 0000000000000..4678718169b94 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/handlers.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import isEqual from 'lodash/fp/isEqual'; + +export function callWithoutRepeats( + func: (...args: any[]) => T, + isArgsEqual: (firstArgs: any, secondArgs: any) => boolean = isEqual +) { + let previousArgs: any[]; + let previousResult: T; + + return (...args: any[]) => { + if (!isArgsEqual(args, previousArgs)) { + previousArgs = args; + previousResult = func(...args); + } + + return previousResult; + }; +} diff --git a/x-pack/plugins/infra/public/utils/loading_state/index.ts b/x-pack/plugins/infra/public/utils/loading_state/index.ts new file mode 100644 index 0000000000000..d4a1b8e52ad00 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/loading_state/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { initialLoadingState, LoadingState } from './loading_state'; + +export { isManualLoadingPolicy, isIntervalLoadingPolicy, LoadingPolicy } from './loading_policy'; + +export { + createRunningProgressReducer, + createIdleProgressReducer, + isIdleLoadingProgress, + isRunningLoadingProgress, + LoadingProgress, +} from './loading_progress'; + +export { + createFailureResult, + createFailureResultReducer, + createSuccessResult, + createSuccessResultReducer, + getTimeOrDefault, + isExhaustedLoadingResult, + isFailureLoadingResult, + isSuccessLoadingResult, + isUninitializedLoadingResult, + LoadingResult, +} from './loading_result'; diff --git a/x-pack/plugins/infra/public/utils/loading_state/loading_policy.ts b/x-pack/plugins/infra/public/utils/loading_state/loading_policy.ts new file mode 100644 index 0000000000000..6a129e7b79c87 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/loading_state/loading_policy.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface ManualLoadingPolicy { + policy: 'manual'; +} + +interface IntervalLoadingPolicy { + policy: 'interval'; + delayMillis: number; +} + +export type LoadingPolicy = ManualLoadingPolicy | IntervalLoadingPolicy; + +export const isManualLoadingPolicy = ( + loadingPolicy: LoadingPolicy +): loadingPolicy is ManualLoadingPolicy => loadingPolicy.policy === 'manual'; + +export const isIntervalLoadingPolicy = ( + loadingPolicy: LoadingPolicy +): loadingPolicy is IntervalLoadingPolicy => loadingPolicy.policy === 'interval'; diff --git a/x-pack/plugins/infra/public/utils/loading_state/loading_progress.ts b/x-pack/plugins/infra/public/utils/loading_state/loading_progress.ts new file mode 100644 index 0000000000000..8e2b09ee5f1f5 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/loading_state/loading_progress.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface IdleLoadingProgress { + progress: 'idle'; +} + +interface RunningLoadingProgress { + progress: 'running'; + time: number; + parameters: Parameters; +} + +export type LoadingProgress = IdleLoadingProgress | RunningLoadingProgress; + +export const isIdleLoadingProgress =

( + loadingProgress: LoadingProgress

+): loadingProgress is IdleLoadingProgress => loadingProgress.progress === 'idle'; + +export const isRunningLoadingProgress =

( + loadingProgress: LoadingProgress

+): loadingProgress is RunningLoadingProgress

=> loadingProgress.progress === 'running'; + +export const createIdleProgressReducer = () => ( + state: LoadingProgress +): IdleLoadingProgress => ({ + progress: 'idle', +}); + +export const createRunningProgressReducer = () => ( + state: LoadingProgress, + parameters: Parameters +): RunningLoadingProgress => ({ + parameters, + progress: 'running', + time: Date.now(), +}); diff --git a/x-pack/plugins/infra/public/utils/loading_state/loading_result.ts b/x-pack/plugins/infra/public/utils/loading_state/loading_result.ts new file mode 100644 index 0000000000000..e48b04743c811 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/loading_state/loading_result.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface UninitializedLoadingResult { + result: 'uninitialized'; +} + +interface SuccessLoadingResult { + result: 'success'; + time: number; + isExhausted: boolean; + parameters: Parameters; +} + +interface FailureLoadingResult { + result: 'failure'; + time: number; + reason: string; + parameters: Parameters; +} + +export type LoadingResult = + | UninitializedLoadingResult + | SuccessLoadingResult + | FailureLoadingResult; + +export const isUninitializedLoadingResult =

( + loadingResult: LoadingResult

+): loadingResult is UninitializedLoadingResult => loadingResult.result === 'uninitialized'; + +export const isSuccessLoadingResult =

( + loadingResult: LoadingResult

+): loadingResult is SuccessLoadingResult

=> loadingResult.result === 'success'; + +export const isFailureLoadingResult =

( + loadingResult: LoadingResult

+): loadingResult is FailureLoadingResult

=> loadingResult.result === 'failure'; + +export const isExhaustedLoadingResult =

(loadingResult: LoadingResult

) => + isSuccessLoadingResult(loadingResult) && loadingResult.isExhausted; + +interface GetTimeOrDefaultT { +

(loadingResult: LoadingResult

): number | null; + (loadingResult: LoadingResult

, defaultValue: T): number | T; + (loadingResult: LoadingResult

, defaultValue?: T): number | T | null; +} + +export const getTimeOrDefault: GetTimeOrDefaultT = ( + loadingResult: LoadingResult

, + defaultValue?: T +) => (isUninitializedLoadingResult(loadingResult) ? defaultValue || null : loadingResult.time); + +export const createSuccessResult = ( + parameters: Parameters, + isExhausted: boolean +): SuccessLoadingResult => ({ + isExhausted, + parameters, + result: 'success', + time: Date.now(), +}); + +export const createSuccessResultReducer = ( + isExhausted: (params: Parameters, result: Payload) => boolean +) => ( + state: LoadingResult, + { params, result }: { params: Parameters; result: Payload } +): SuccessLoadingResult => createSuccessResult(params, isExhausted(params, result)); + +export const createFailureResult = ( + parameters: Parameters, + reason: string +): FailureLoadingResult => ({ + parameters, + reason, + result: 'failure', + time: Date.now(), +}); + +export const createFailureResultReducer = ( + convertErrorToString: (error: ErrorPayload) => string = error => `${error}` +) => ( + state: LoadingResult, + { params, error }: { params: Parameters; error: ErrorPayload } +): FailureLoadingResult => createFailureResult(params, convertErrorToString(error)); diff --git a/x-pack/plugins/infra/public/utils/loading_state/loading_state.ts b/x-pack/plugins/infra/public/utils/loading_state/loading_state.ts new file mode 100644 index 0000000000000..d5066dd4c9007 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/loading_state/loading_state.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LoadingPolicy } from './loading_policy'; +import { LoadingProgress } from './loading_progress'; +import { LoadingResult } from './loading_result'; + +export interface LoadingState { + current: LoadingProgress; + last: LoadingResult; + policy: LoadingPolicy; +} + +export const initialLoadingState: LoadingState = { + current: { + progress: 'idle', + }, + last: { + result: 'uninitialized', + }, + policy: { + policy: 'manual', + }, +}; diff --git a/x-pack/plugins/infra/public/utils/log_entry/index.ts b/x-pack/plugins/infra/public/utils/log_entry/index.ts new file mode 100644 index 0000000000000..66cc5108b6692 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/log_entry/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_entry'; diff --git a/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts b/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts new file mode 100644 index 0000000000000..d4eac795612b4 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/log_entry/log_entry.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { bisector } from 'd3-array'; + +import { LogEntries as LogEntriesQuery } from '../../../common/graphql/types'; +import { compareToTimeKey, getIndexAtTimeKey, TimeKey } from '../../../common/time'; + +export type LogEntry = LogEntriesQuery.Entries; + +export type LogEntryMessageSegment = LogEntriesQuery.Message; + +export const getLogEntryKey = (entry: LogEntry) => entry.key; + +const logEntryTimeBisector = bisector(compareToTimeKey(getLogEntryKey)); + +export const getLogEntryIndexBeforeTime = logEntryTimeBisector.left; +export const getLogEntryIndexAfterTime = logEntryTimeBisector.right; +export const getLogEntryIndexAtTime = getIndexAtTimeKey(getLogEntryKey); + +export const getLogEntryAtTime = (entries: LogEntry[], time: TimeKey) => { + const entryIndex = getLogEntryIndexAtTime(entries, time); + + return entryIndex !== null ? entries[entryIndex] : null; +}; diff --git a/x-pack/plugins/infra/public/utils/memoize_last.ts b/x-pack/plugins/infra/public/utils/memoize_last.ts new file mode 100644 index 0000000000000..fbab4ce22cf6e --- /dev/null +++ b/x-pack/plugins/infra/public/utils/memoize_last.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface MemoizedCall { + args: any[]; + returnValue: any; + this: any; +} + +// A symbol expressing, that the memoized function has never been called +const neverCalled: unique symbol = Symbol(); +type NeverCalled = typeof neverCalled; + +/** + * A simple memoize function, that only stores the last returned value + * and uses the identity of all passed parameters as a cache key. + */ +function memoizeLast any>(func: T): T { + let prevCall: MemoizedCall | NeverCalled = neverCalled; + + // We need to use a `function` here for proper this passing. + // tslint:disable-next-line:only-arrow-functions + const memoizedFunction = function(this: any, ...args: any[]) { + if ( + prevCall !== neverCalled && + prevCall.this === this && + prevCall.args.length === args.length && + prevCall.args.every((arg, index) => arg === args[index]) + ) { + return prevCall.returnValue; + } + + prevCall = { + args, + this: this, + returnValue: func.apply(this, args), + }; + + return prevCall.returnValue; + } as T; + + return memoizedFunction; +} + +export { memoizeLast }; diff --git a/x-pack/plugins/infra/public/utils/remote_state/remote_graphql_state.ts b/x-pack/plugins/infra/public/utils/remote_state/remote_graphql_state.ts new file mode 100644 index 0000000000000..6a03def5d068d --- /dev/null +++ b/x-pack/plugins/infra/public/utils/remote_state/remote_graphql_state.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ApolloError, ApolloQueryResult } from 'apollo-client'; +import { DocumentNode } from 'graphql'; +import { Action as ReduxAction } from 'redux'; +import { Epic } from 'redux-observable'; +import { from, Observable } from 'rxjs'; +import { catchError, filter, map, startWith, switchMap, withLatestFrom } from 'rxjs/operators'; +import { Action, ActionCreator, actionCreatorFactory, Failure, Success } from 'typescript-fsa'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { createSelector } from 'reselect'; +import { InfraApolloClient } from '../../lib/lib'; +import { + isFailureLoadingResult, + isIdleLoadingProgress, + isRunningLoadingProgress, + isSuccessLoadingResult, + isUninitializedLoadingResult, + LoadingPolicy, + LoadingProgress, + LoadingResult, +} from '../loading_state'; + +export interface GraphqlState { + current: LoadingProgress>; + last: LoadingResult>; + data: State | undefined; +} + +interface OperationInfo { + operationKey: string; + variables: Variables; +} + +type ResolveDonePayload = Success>; +type ResolveFailedPayload = Failure; + +interface OperationActionCreators { + resolve: ActionCreator; + resolveStarted: ActionCreator; + resolveDone: ActionCreator>; + resolveFailed: ActionCreator>; +} + +export const createGraphqlInitialState = (initialData?: State): GraphqlState => ({ + current: { + progress: 'idle', + }, + last: { + result: 'uninitialized', + }, + data: initialData, +}); + +export const createGraphqlOperationActionCreators = ( + stateKey: string, + operationKey: string +): OperationActionCreators => { + const actionCreator = actionCreatorFactory(`x-pack/infra/remote/${stateKey}/${operationKey}`); + + const resolve = actionCreator('RESOLVE'); + const resolveEffect = actionCreator.async>('RESOLVE'); + + return { + resolve, + resolveStarted: resolveEffect.started, + resolveDone: resolveEffect.done, + resolveFailed: resolveEffect.failed, + }; +}; + +export const createGraphqlOperationReducer = ( + operationKey: string, + initialState: GraphqlState, + actionCreators: OperationActionCreators, + reduceSuccess: ( + state: State | undefined, + action: Action> + ) => State | undefined = state => state +) => + reducerWithInitialState(initialState) + .caseWithAction(actionCreators.resolveStarted, (state, action) => ({ + ...state, + current: { + progress: 'running', + time: Date.now(), + parameters: { + operationKey, + variables: action.payload, + }, + }, + })) + .caseWithAction(actionCreators.resolveDone, (state, action) => ({ + ...state, + current: { + progress: 'idle', + }, + last: { + result: 'success', + parameters: { + operationKey, + variables: action.payload.params, + }, + time: Date.now(), + isExhausted: false, + }, + data: reduceSuccess(state.data, action), + })) + .caseWithAction(actionCreators.resolveFailed, (state, action) => ({ + ...state, + current: { + progress: 'idle', + }, + last: { + result: 'failure', + reason: `${action.payload}`, + time: Date.now(), + parameters: { + operationKey, + variables: action.payload.params, + }, + }, + })) + .build(); + +export const createGraphqlQueryEpic = ( + graphqlQuery: DocumentNode, + actionCreators: OperationActionCreators +): Epic< + ReduxAction, + ReduxAction, + any, + { + apolloClient$: Observable; + } +> => (action$, state$, { apolloClient$ }) => + action$.pipe( + filter(actionCreators.resolve.match), + withLatestFrom(apolloClient$), + switchMap(([{ payload: variables }, apolloClient]) => + from( + apolloClient.query({ + query: graphqlQuery, + variables, + fetchPolicy: 'no-cache', + }) + ).pipe( + map(result => actionCreators.resolveDone({ params: variables, result })), + catchError(error => [actionCreators.resolveFailed({ params: variables, error })]), + startWith(actionCreators.resolveStarted(variables)) + ) + ) + ); + +export const createGraphqlStateSelectors = ( + selectState: (parentState: any) => GraphqlState = parentState => parentState +) => { + const selectData = createSelector(selectState, state => state.data); + + const selectLoadingProgress = createSelector(selectState, state => state.current); + const selectLoadingProgressOperationInfo = createSelector( + selectLoadingProgress, + progress => (isRunningLoadingProgress(progress) ? progress.parameters : null) + ); + const selectIsLoading = createSelector(selectLoadingProgress, isRunningLoadingProgress); + const selectIsIdle = createSelector(selectLoadingProgress, isIdleLoadingProgress); + + const selectLoadingResult = createSelector(selectState, state => state.last); + const selectLoadingResultOperationInfo = createSelector( + selectLoadingResult, + result => (!isUninitializedLoadingResult(result) ? result.parameters : null) + ); + const selectLoadingResultTime = createSelector( + selectLoadingResult, + result => (!isUninitializedLoadingResult(result) ? result.time : null) + ); + const selectIsUninitialized = createSelector(selectLoadingResult, isUninitializedLoadingResult); + const selectIsSuccess = createSelector(selectLoadingResult, isSuccessLoadingResult); + const selectIsFailure = createSelector(selectLoadingResult, isFailureLoadingResult); + + const selectLoadingState = createSelector( + selectLoadingProgress, + selectLoadingResult, + (loadingProgress, loadingResult) => ({ + current: loadingProgress, + last: loadingResult, + policy: { + policy: 'manual', + } as LoadingPolicy, + }) + ); + + return { + selectData, + selectIsFailure, + selectIsIdle, + selectIsLoading, + selectIsSuccess, + selectIsUninitialized, + selectLoadingProgress, + selectLoadingProgressOperationInfo, + selectLoadingResult, + selectLoadingResultOperationInfo, + selectLoadingResultTime, + selectLoadingState, + }; +}; diff --git a/x-pack/plugins/infra/public/utils/styles.ts b/x-pack/plugins/infra/public/utils/styles.ts new file mode 100644 index 0000000000000..0dd7b47950d05 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/styles.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import get from 'lodash/fp/get'; +import getOr from 'lodash/fp/getOr'; +import { parseToHsl, shade, tint } from 'polished'; + +type PropReader = (props: object, defaultValue?: Default) => Prop; + +const asPropReader = (reader: string | string[] | PropReader) => + typeof reader === 'function' + ? reader + : ( + props: Props, + defaultValue?: Default + ) => getOr(defaultValue, reader as Prop, props); + +export const switchProp = Object.assign( + (propName: string | string[] | PropReader, options: Map | object) => ( + props: object + ) => { + const propValue = asPropReader(propName)(props, switchProp.default); + if (typeof propValue === 'undefined') { + return; + } + return options instanceof Map ? options.get(propValue) : get(propValue, options); + }, + { + default: Symbol('default'), + } +); + +export const ifProp = ( + propName: string | string[] | PropReader, + pass: Pass, + fail: Fail +) => (props: object) => (asPropReader(propName)(props) ? pass : fail); + +export const tintOrShade = (textColor: 'string', color: 'string', fraction: number) => { + return parseToHsl(textColor).lightness > 0.5 ? shade(fraction, color) : tint(fraction, color); +}; diff --git a/x-pack/plugins/infra/public/utils/typed_react.tsx b/x-pack/plugins/infra/public/utils/typed_react.tsx new file mode 100644 index 0000000000000..99c43608a3044 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/typed_react.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import omit from 'lodash/fp/omit'; +import React from 'react'; +import { InferableComponentEnhancerWithProps } from 'react-redux'; + +export type RendererResult = React.ReactElement | null; +export type RendererFunction = (args: RenderArgs) => Result; + +export type ChildFunctionRendererProps = { + children: RendererFunction; + initializeOnMount?: boolean; + resetOnUnmount?: boolean; +} & RenderArgs; + +interface ChildFunctionRendererOptions { + onInitialize?: (props: RenderArgs) => void; + onCleanup?: (props: RenderArgs) => void; +} + +export const asChildFunctionRenderer = ( + hoc: InferableComponentEnhancerWithProps, + { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} +) => + hoc( + class ChildFunctionRenderer extends React.Component> { + public displayName = 'ChildFunctionRenderer'; + + public componentDidMount() { + if (this.props.initializeOnMount && onInitialize) { + onInitialize(this.getRendererArgs()); + } + } + + public componentWillUnmount() { + if (this.props.resetOnUnmount && onCleanup) { + onCleanup(this.getRendererArgs()); + } + } + + public render() { + return this.props.children(this.getRendererArgs()); + } + + private getRendererArgs = () => + omit(['children', 'initializeOnMount', 'resetOnUnmount'], this.props) as Pick< + ChildFunctionRendererProps, + keyof InjectedProps + >; + } + ); + +export type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +export function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} diff --git a/x-pack/plugins/infra/public/utils/typed_redux.ts b/x-pack/plugins/infra/public/utils/typed_redux.ts new file mode 100644 index 0000000000000..391c580902176 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/typed_redux.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { bindActionCreators, Dispatch } from 'redux'; + +/** + * Selectors + */ +export type Selector = (state: State) => Value; + +export interface Selectors { + [selectorName: string]: Selector; +} + +export type GlobalSelectors = { + [selectorName in keyof LocalSelectors]: Selector< + GlobalState, + ReturnType + > +}; + +export const globalizeSelector = < + GlobalState, + LocalSelector extends Selector, + LocalState = any, + Value = any +>( + globalizer: Selector, + selector: LocalSelector +): Selector => (globalState: GlobalState) => selector(globalizer(globalState)); + +export const globalizeSelectors = < + GlobalState, + LocalSelectors extends Selectors, + LocalState = any +>( + globalizer: (globalState: GlobalState) => LocalState, + selectors: LocalSelectors +): GlobalSelectors => { + const globalSelectors = {} as GlobalSelectors; + for (const s in selectors) { + if (selectors.hasOwnProperty(s)) { + globalSelectors[s] = globalizeSelector(globalizer, selectors[s]); + } + } + return globalSelectors; +}; + +/** + * Action Creators + */ +interface ActionCreators { + [key: string]: (arg: any) => any; +} + +type PlainActionCreator = WrappedActionCreator extends () => infer R + ? () => R + : WrappedActionCreator extends (payload: infer A) => infer R ? (payload: A) => R : never; + +export const bindPlainActionCreators = ( + actionCreators: WrappedActionCreators +) => (dispatch: Dispatch) => + bindActionCreators(actionCreators, dispatch) as { + [P in keyof WrappedActionCreators]: PlainActionCreator + }; diff --git a/x-pack/plugins/infra/public/utils/url_state.tsx b/x-pack/plugins/infra/public/utils/url_state.tsx new file mode 100644 index 0000000000000..8ed80fbb2e383 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/url_state.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { History, Location } from 'history'; +import throttle from 'lodash/fp/throttle'; +import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; +import React from 'react'; +import { Route, RouteProps } from 'react-router'; +import { decode, encode, RisonValue } from 'rison-node'; + +interface UrlStateContainerProps { + urlState: UrlState | undefined; + urlStateKey: string; + mapToUrlState?: (value: any) => UrlState | undefined; + onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; + onInitialize?: (urlState: UrlState | undefined) => void; +} + +interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { + location: Location; + history: History; +} + +class UrlStateContainerLifecycle extends React.Component< + UrlStateContainerLifecycleProps +> { + public render() { + return null; + } + + public componentDidUpdate({ + location: prevLocation, + urlState: prevUrlState, + }: UrlStateContainerLifecycleProps) { + const { history, location, urlState } = this.props; + + if (urlState !== prevUrlState) { + this.replaceStateInLocation(urlState); + } + + if (history.action === 'POP' && location !== prevLocation) { + this.handleLocationChange(prevLocation, location); + } + } + + public componentDidMount() { + const { location } = this.props; + + this.handleInitialize(location); + } + + // tslint:disable-next-line:member-ordering this is really a method despite what tslint thinks + private replaceStateInLocation = throttle(1000, (urlState: UrlState | undefined) => { + const { history, location, urlStateKey } = this.props; + + const newLocation = replaceQueryStringInLocation( + location, + replaceStateKeyInQueryString(urlStateKey, urlState)(getQueryStringFromLocation(location)) + ); + + if (newLocation !== location) { + history.replace(newLocation); + } + }); + + private handleInitialize = (location: Location) => { + const { onInitialize, mapToUrlState, urlStateKey } = this.props; + + if (!onInitialize || !mapToUrlState) { + return; + } + + const newUrlStateString = getParamFromQueryString( + getQueryStringFromLocation(location), + urlStateKey + ); + const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + + onInitialize(newUrlState); + }; + + private handleLocationChange = (prevLocation: Location, newLocation: Location) => { + const { onChange, mapToUrlState, urlStateKey } = this.props; + + if (!onChange || !mapToUrlState) { + return; + } + + const previousUrlStateString = getParamFromQueryString( + getQueryStringFromLocation(prevLocation), + urlStateKey + ); + const newUrlStateString = getParamFromQueryString( + getQueryStringFromLocation(newLocation), + urlStateKey + ); + + if (previousUrlStateString !== newUrlStateString) { + const previousUrlState = mapToUrlState(decodeRisonUrlState(previousUrlStateString)); + const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + + if (typeof newUrlState !== 'undefined') { + onChange(newUrlState, previousUrlState); + } + } + }; +} + +export const UrlStateContainer = ( + props: UrlStateContainerProps +) => ( + > + {({ history, location }) => ( + history={history} location={location} {...props} /> + )} + +); + +export const decodeRisonUrlState = (value: string | undefined): RisonValue | undefined => { + try { + return value ? decode(value) : undefined; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return {}; + } + throw error; + } +}; + +const encodeRisonUrlState = (state: any) => encode(state); + +export const getQueryStringFromLocation = (location: Location) => location.search.substring(1); + +export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { + const queryParam = parseQueryString(queryString)[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; +}; + +export const replaceStateKeyInQueryString = ( + stateKey: string, + urlState: UrlState | undefined +) => (queryString: string) => { + const previousQueryValues = parseQueryString(queryString); + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + return stringifyQueryString({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }); +}; + +const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { + if (queryString === getQueryStringFromLocation(location)) { + return location; + } else { + return { + ...location, + search: `?${queryString}`, + }; + } +}; diff --git a/x-pack/plugins/infra/scripts/combined_schema.ts b/x-pack/plugins/infra/scripts/combined_schema.ts new file mode 100644 index 0000000000000..ec3628b2632c9 --- /dev/null +++ b/x-pack/plugins/infra/scripts/combined_schema.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildSchemaFromTypeDefinitions } from 'graphql-tools'; + +import { schemas as serverSchemas } from '../server/graphql'; + +export const schemas = [...serverSchemas]; + +// this default export is used to feed the combined types to the gql-gen tool +// which generates the corresponding typescript types +// tslint:disable-next-line:no-default-export +export default buildSchemaFromTypeDefinitions(schemas); diff --git a/x-pack/plugins/infra/scripts/generate_types_from_graphql.js b/x-pack/plugins/infra/scripts/generate_types_from_graphql.js new file mode 100644 index 0000000000000..5270985905002 --- /dev/null +++ b/x-pack/plugins/infra/scripts/generate_types_from_graphql.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const { join, resolve } = require('path'); +// eslint-disable-next-line import/no-extraneous-dependencies +const { generate } = require('graphql-code-generator'); + +const GRAPHQL_GLOBS = [ + join('public', 'containers', '**', '*.gql_query.ts{,x}'), + join('public', 'store', '**', '*.gql_query.ts{,x}'), + join('common', 'graphql', '**', '*.gql_query.ts{,x}') +]; +const CONFIG_PATH = resolve(__dirname, 'gql_gen.json'); +const OUTPUT_INTROSPECTION_PATH = resolve('common', 'graphql', 'introspection.json'); +const OUTPUT_TYPES_PATH = resolve('common', 'graphql', 'types.ts'); +const SCHEMA_PATH = resolve(__dirname, 'combined_schema.ts'); + +async function main() { + await generate( + { + args: GRAPHQL_GLOBS, + config: CONFIG_PATH, + out: OUTPUT_INTROSPECTION_PATH, + overwrite: true, + require: ['ts-node/register'], + schema: SCHEMA_PATH, + template: 'graphql-codegen-introspection-template', + }, + true + ); + await generate( + { + args: GRAPHQL_GLOBS, + config: CONFIG_PATH, + out: OUTPUT_TYPES_PATH, + overwrite: true, + schema: SCHEMA_PATH, + template: 'graphql-codegen-typescript-template', + }, + true + ); +} + +if (require.main === module) { + main(); +} diff --git a/x-pack/plugins/infra/scripts/gql_gen.json b/x-pack/plugins/infra/scripts/gql_gen.json new file mode 100644 index 0000000000000..87b8233dd1eeb --- /dev/null +++ b/x-pack/plugins/infra/scripts/gql_gen.json @@ -0,0 +1,11 @@ +{ + "flattenTypes": true, + "generatorConfig": {}, + "primitives": { + "String": "string", + "Int": "number", + "Float": "number", + "Boolean": "boolean", + "ID": "string" + } +} diff --git a/x-pack/plugins/infra/server/graphql/capabilities/index.ts b/x-pack/plugins/infra/server/graphql/capabilities/index.ts new file mode 100644 index 0000000000000..3f6f9541eda33 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/capabilities/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createCapabilitiesResolvers } from './resolvers'; +export { capabilitiesSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts b/x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts new file mode 100644 index 0000000000000..fa242f67b230f --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/capabilities/resolvers.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceResolvers } from '../../../common/graphql/types'; +import { InfraResolvedResult, InfraResolverOf } from '../../lib/adapters/framework'; +import { InfraCapabilitiesDomain } from '../../lib/domains/capabilities_domain'; +import { InfraContext } from '../../lib/infra_types'; +import { QuerySourceResolver } from '../sources/resolvers'; + +type InfraSourceCapabilitiesByNodeResolver = InfraResolverOf< + InfraSourceResolvers.CapabilitiesByNodeResolver, + InfraResolvedResult, + InfraContext +>; + +export const createCapabilitiesResolvers = (libs: { + capabilities: InfraCapabilitiesDomain; +}): { + InfraSource: { + capabilitiesByNode: InfraSourceCapabilitiesByNodeResolver; + }; +} => ({ + InfraSource: { + async capabilitiesByNode(source, args, { req }) { + const result = await libs.capabilities.getCapabilities( + req, + source.id, + args.nodeName, + args.nodeType + ); + return result; + }, + }, +}); diff --git a/x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts b/x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts new file mode 100644 index 0000000000000..9a97ff29eeb85 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/capabilities/schema.gql.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const capabilitiesSchema = gql` + "One specific capability available on a node. A capability corresponds to a fileset or metricset" + type InfraNodeCapability { + name: String! + source: String! + } + + extend type InfraSource { + "A hierarchy of capabilities available on nodes" + capabilitiesByNode(nodeName: String!, nodeType: InfraNodeType!): [InfraNodeCapability]! + } +`; diff --git a/x-pack/plugins/infra/server/graphql/index.ts b/x-pack/plugins/infra/server/graphql/index.ts new file mode 100644 index 0000000000000..7fb3a92330352 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { rootSchema } from '../../common/graphql/root/schema.gql'; +import { sharedSchema } from '../../common/graphql/shared/schema.gql'; +import { capabilitiesSchema } from './capabilities/schema.gql'; +import { logEntriesSchema } from './log_entries/schema.gql'; +import { metricsSchema } from './metrics/schema.gql'; +import { nodesSchema } from './nodes/schema.gql'; +import { sourceStatusSchema } from './source_status/schema.gql'; +import { sourcesSchema } from './sources/schema.gql'; + +export const schemas = [ + rootSchema, + sharedSchema, + capabilitiesSchema, + logEntriesSchema, + nodesSchema, + sourcesSchema, + sourceStatusSchema, + metricsSchema, +]; diff --git a/x-pack/plugins/infra/server/graphql/log_entries/index.ts b/x-pack/plugins/infra/server/graphql/log_entries/index.ts new file mode 100644 index 0000000000000..21134862663ec --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/log_entries/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createLogEntriesResolvers } from './resolvers'; diff --git a/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts b/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts new file mode 100644 index 0000000000000..3a16ff0c341db --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraLogMessageConstantSegment, + InfraLogMessageFieldSegment, + InfraLogMessageSegment, + InfraSourceResolvers, +} from '../../../common/graphql/types'; +import { InfraResolvedResult, InfraResolverOf } from '../../lib/adapters/framework'; +import { InfraLogEntriesDomain } from '../../lib/domains/log_entries_domain'; +import { InfraContext } from '../../lib/infra_types'; +import { UsageCollector } from '../../usage/usage_collector'; +import { parseFilterQuery } from '../../utils/serialized_query'; +import { QuerySourceResolver } from '../sources/resolvers'; + +export type InfraSourceLogEntriesAroundResolver = InfraResolverOf< + InfraSourceResolvers.LogEntriesAroundResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceLogEntriesBetweenResolver = InfraResolverOf< + InfraSourceResolvers.LogEntriesBetweenResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceLogSummaryBetweenResolver = InfraResolverOf< + InfraSourceResolvers.LogSummaryBetweenResolver, + InfraResolvedResult, + InfraContext +>; + +export const createLogEntriesResolvers = (libs: { + logEntries: InfraLogEntriesDomain; +}): { + InfraSource: { + logEntriesAround: InfraSourceLogEntriesAroundResolver; + logEntriesBetween: InfraSourceLogEntriesBetweenResolver; + logSummaryBetween: InfraSourceLogSummaryBetweenResolver; + }; + InfraLogMessageSegment: { + __resolveType( + messageSegment: InfraLogMessageSegment + ): 'InfraLogMessageFieldSegment' | 'InfraLogMessageConstantSegment' | null; + }; +} => ({ + InfraSource: { + async logEntriesAround(source, args, { req }) { + const countBefore = args.countBefore || 0; + const countAfter = args.countAfter || 0; + + const { entriesBefore, entriesAfter } = await libs.logEntries.getLogEntriesAround( + req, + source.id, + args.key, + countBefore + 1, + countAfter + 1, + parseFilterQuery(args.filterQuery), + args.highlightQuery || undefined + ); + + const hasMoreBefore = entriesBefore.length > countBefore; + const hasMoreAfter = entriesAfter.length > countAfter; + + const entries = [ + ...(hasMoreBefore ? entriesBefore.slice(1) : entriesBefore), + ...(hasMoreAfter ? entriesAfter.slice(0, -1) : entriesAfter), + ]; + + return { + start: entries.length > 0 ? entries[0].key : null, + end: entries.length > 0 ? entries[entries.length - 1].key : null, + hasMoreBefore, + hasMoreAfter, + filterQuery: args.filterQuery, + highlightQuery: args.highlightQuery, + entries, + }; + }, + async logEntriesBetween(source, args, { req }) { + const entries = await libs.logEntries.getLogEntriesBetween( + req, + source.id, + args.startKey, + args.endKey, + parseFilterQuery(args.filterQuery), + args.highlightQuery || undefined + ); + + return { + start: entries.length > 0 ? entries[0].key : null, + end: entries.length > 0 ? entries[entries.length - 1].key : null, + hasMoreBefore: true, + hasMoreAfter: true, + filterQuery: args.filterQuery, + highlightQuery: args.highlightQuery, + entries, + }; + }, + async logSummaryBetween(source, args, { req }) { + UsageCollector.countLogs(); + const buckets = await libs.logEntries.getLogSummaryBucketsBetween( + req, + source.id, + args.start, + args.end, + args.bucketSize, + parseFilterQuery(args.filterQuery) + ); + + return { + start: buckets.length > 0 ? buckets[0].start : null, + end: buckets.length > 0 ? buckets[buckets.length - 1].end : null, + buckets, + }; + }, + }, + InfraLogMessageSegment: { + __resolveType: (messageSegment: InfraLogMessageSegment) => { + if (isConstantSegment(messageSegment)) { + return 'InfraLogMessageConstantSegment'; + } + + if (isFieldSegment(messageSegment)) { + return 'InfraLogMessageFieldSegment'; + } + + return null; + }, + }, +}); + +const isConstantSegment = ( + segment: InfraLogMessageSegment +): segment is InfraLogMessageConstantSegment => 'constant' in segment; + +const isFieldSegment = (segment: InfraLogMessageSegment): segment is InfraLogMessageFieldSegment => + 'field' in segment && 'value' in segment && 'highlights' in segment; diff --git a/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts b/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts new file mode 100644 index 0000000000000..e4947767ffbb1 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const logEntriesSchema = gql` + "A segment of the log entry message that was derived from a field" + type InfraLogMessageFieldSegment { + "The field the segment was derived from" + field: String! + "The segment's message" + value: String! + "A list of highlighted substrings of the value" + highlights: [String!]! + } + + "A segment of the log entry message that was derived from a field" + type InfraLogMessageConstantSegment { + "The segment's message" + constant: String! + } + + "A segment of the log entry message" + union InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessageConstantSegment + + "A log entry" + type InfraLogEntry { + "A unique representation of the log entry's position in the event stream" + key: InfraTimeKey! + "The log entry's id" + gid: String! + "The source id" + source: String! + "A list of the formatted log entry segments" + message: [InfraLogMessageSegment!]! + } + + "A log summary bucket" + type InfraLogSummaryBucket { + "The start timestamp of the bucket" + start: Float! + "The end timestamp of the bucket" + end: Float! + "The number of entries inside the bucket" + entriesCount: Int! + } + + "A consecutive sequence of log entries" + type InfraLogEntryInterval { + "The key corresponding to the start of the interval covered by the entries" + start: InfraTimeKey + "The key corresponding to the end of the interval covered by the entries" + end: InfraTimeKey + "Whether there are more log entries available before the start" + hasMoreBefore: Boolean! + "Whether there are more log entries available after the end" + hasMoreAfter: Boolean! + "The query the log entries were filtered by" + filterQuery: String + "The query the log entries were highlighted with" + highlightQuery: String + "A list of the log entries" + entries: [InfraLogEntry!]! + } + + "A consecutive sequence of log summary buckets" + type InfraLogSummaryInterval { + "The millisecond timestamp corresponding to the start of the interval covered by the summary" + start: Float + "The millisecond timestamp corresponding to the end of the interval covered by the summary" + end: Float + "The query the log entries were filtered by" + filterQuery: String + "A list of the log entries" + buckets: [InfraLogSummaryBucket!]! + } + + extend type InfraSource { + "A consecutive span of log entries surrounding a point in time" + logEntriesAround( + "The sort key that corresponds to the point in time" + key: InfraTimeKeyInput! + "The maximum number of preceding to return" + countBefore: Int = 0 + "The maximum number of following to return" + countAfter: Int = 0 + "The query to filter the log entries by" + filterQuery: String + "The query to highlight the log entries with" + highlightQuery: String + ): InfraLogEntryInterval! + "A consecutive span of log entries within an interval" + logEntriesBetween( + "The sort key that corresponds to the start of the interval" + startKey: InfraTimeKeyInput! + "The sort key that corresponds to the end of the interval" + endKey: InfraTimeKeyInput! + "The query to filter the log entries by" + filterQuery: String + "The query to highlight the log entries with" + highlightQuery: String + ): InfraLogEntryInterval! + "A consecutive span of summary buckets within an interval" + logSummaryBetween( + "The millisecond timestamp that corresponds to the start of the interval" + start: Float! + "The millisecond timestamp that corresponds to the end of the interval" + end: Float! + "The size of each bucket in milliseconds" + bucketSize: Float! + "The query to filter the log entries by" + filterQuery: String + ): InfraLogSummaryInterval! + } +`; diff --git a/x-pack/plugins/infra/server/graphql/metrics/index.ts b/x-pack/plugins/infra/server/graphql/metrics/index.ts new file mode 100644 index 0000000000000..d7a789c4cda74 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/metrics/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createMetricResolvers } from './resolvers'; +export { metricsSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/server/graphql/metrics/resolvers.ts b/x-pack/plugins/infra/server/graphql/metrics/resolvers.ts new file mode 100644 index 0000000000000..e7403f7ce3fcb --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/metrics/resolvers.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceResolvers } from '../../../common/graphql/types'; +import { InfraResolvedResult, InfraResolverOf } from '../../lib/adapters/framework'; +import { InfraMetricsDomain } from '../../lib/domains/metrics_domain'; +import { InfraContext } from '../../lib/infra_types'; +import { UsageCollector } from '../../usage/usage_collector'; +import { QuerySourceResolver } from '../sources/resolvers'; + +type InfraSourceMetricsResolver = InfraResolverOf< + InfraSourceResolvers.MetricsResolver, + InfraResolvedResult, + InfraContext +>; + +interface ResolverDeps { + metrics: InfraMetricsDomain; +} + +export const createMetricResolvers = ( + libs: ResolverDeps +): { + InfraSource: { + metrics: InfraSourceMetricsResolver; + }; +} => ({ + InfraSource: { + async metrics(source, args, { req }) { + UsageCollector.countNode(args.nodeType); + const options = { + nodeId: args.nodeId, + nodeType: args.nodeType, + timerange: args.timerange, + metrics: args.metrics, + sourceConfiguration: source.configuration, + }; + return libs.metrics.getMetrics(req, options); + }, + }, +}); diff --git a/x-pack/plugins/infra/server/graphql/metrics/schema.gql.ts b/x-pack/plugins/infra/server/graphql/metrics/schema.gql.ts new file mode 100644 index 0000000000000..3218bffe95945 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/metrics/schema.gql.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const metricsSchema: any = gql` + enum InfraMetric { + hostSystemOverview + hostCpuUsage + hostFilesystem + hostK8sOverview + hostK8sCpuCap + hostK8sDiskCap + hostK8sMemoryCap + hostK8sPodCap + hostLoad + hostMemoryUsage + hostNetworkTraffic + podOverview + podCpuUsage + podMemoryUsage + podLogUsage + podNetworkTraffic + containerOverview + containerCpuKernel + containerCpuUsage + containerDiskIOOps + containerDiskIOBytes + containerMemory + containerNetworkTraffic + nginxHits + nginxRequestRate + nginxActiveConnections + nginxRequestsPerConnection + } + + type InfraMetricData { + id: InfraMetric + series: [InfraDataSeries!]! + } + + type InfraDataSeries { + id: ID! + data: [InfraDataPoint!]! + } + + type InfraDataPoint { + timestamp: Float! + value: Float + } + + extend type InfraSource { + metrics( + nodeId: ID! + nodeType: InfraNodeType! + timerange: InfraTimerangeInput! + metrics: [InfraMetric!]! + ): [InfraMetricData!]! + } +`; diff --git a/x-pack/plugins/infra/server/graphql/nodes/index.ts b/x-pack/plugins/infra/server/graphql/nodes/index.ts new file mode 100644 index 0000000000000..d61651c7b263b --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/nodes/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createNodeResolvers } from './resolvers'; +export { nodesSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/server/graphql/nodes/resolvers.ts b/x-pack/plugins/infra/server/graphql/nodes/resolvers.ts new file mode 100644 index 0000000000000..867cfc4dd49ed --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/nodes/resolvers.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraResponseResolvers, InfraSourceResolvers } from '../../../common/graphql/types'; +import { + InfraResolvedResult, + InfraResolverOf, + InfraResolverWithoutFields, +} from '../../lib/adapters/framework'; +import { InfraNodeRequestOptions } from '../../lib/adapters/nodes'; +import { extractGroupByAndNodeFromPath } from '../../lib/adapters/nodes/extract_group_by_and_node_from_path'; +import { InfraNodesDomain } from '../../lib/domains/nodes_domain'; +import { InfraContext } from '../../lib/infra_types'; +import { UsageCollector } from '../../usage/usage_collector'; +import { parseFilterQuery } from '../../utils/serialized_query'; +import { QuerySourceResolver } from '../sources/resolvers'; + +type InfraSourceMapResolver = InfraResolverWithoutFields< + InfraSourceResolvers.MapResolver, + InfraResolvedResult, + InfraContext, + 'nodes' +>; + +interface QueryMapResponse extends InfraSourceResolvers.MapArgs { + source: InfraResolvedResult; +} + +type InfraNodesResolver = InfraResolverOf< + InfraResponseResolvers.NodesResolver, + QueryMapResponse, + InfraContext +>; + +interface NodesResolversDeps { + nodes: InfraNodesDomain; +} + +export const createNodeResolvers = ( + libs: NodesResolversDeps +): { + InfraSource: { + map: InfraSourceMapResolver; + }; + InfraResponse: { + nodes: InfraNodesResolver; + }; +} => ({ + InfraSource: { + async map(source, args) { + return { + source, + timerange: args.timerange, + filterQuery: args.filterQuery, + }; + }, + }, + InfraResponse: { + async nodes(mapResponse, args, { req }) { + const { source, timerange, filterQuery } = mapResponse; + const { groupBy, nodeType } = extractGroupByAndNodeFromPath(args.path); + UsageCollector.countNode(nodeType); + const options: InfraNodeRequestOptions = { + filterQuery: parseFilterQuery(filterQuery), + nodeType, + groupBy, + sourceConfiguration: source.configuration, + metric: args.metric, + timerange, + }; + + return await libs.nodes.getNodes(req, options); + }, + }, +}); diff --git a/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts b/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts new file mode 100644 index 0000000000000..33de32f100805 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const nodesSchema: any = gql` + type InfraNodeMetric { + name: InfraMetricType! + value: Float! + } + + type InfraNodePath { + value: String! + } + + type InfraNode { + path: [InfraNodePath!]! + metric: InfraNodeMetric! + } + + input InfraTimerangeInput { + "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan." + interval: String! + "The end of the timerange" + to: Float! + "The beginning of the timerange" + from: Float! + } + + enum InfraOperator { + gt + gte + lt + lte + eq + } + + enum InfraMetricType { + count + cpu + load + memory + tx + rx + logRate + } + + input InfraMetricInput { + "The type of metric" + type: InfraMetricType! + } + + enum InfraPathType { + terms + filters + hosts + pods + containers + } + + input InfraPathInput { + "The type of path" + type: InfraPathType! + "The label to use in the results for the group by for the terms group by" + label: String + "The field to group by from a terms aggregation, this is ignored by the filter type" + field: String + "The fitlers for the filter group by" + filters: [InfraPathFilterInput!] + } + + "A group by filter" + input InfraPathFilterInput { + "The label for the filter, this will be used as the group name in the final results" + label: String! + "The query string query" + query: String! + } + + type InfraResponse { + nodes(path: [InfraPathInput!]!, metric: InfraMetricInput!): [InfraNode!]! + } + + extend type InfraSource { + "A hierarchy of hosts, pods, containers, services or arbitrary groups" + map(timerange: InfraTimerangeInput!, filterQuery: String): InfraResponse + } +`; diff --git a/x-pack/plugins/infra/server/graphql/source_status/index.ts b/x-pack/plugins/infra/server/graphql/source_status/index.ts new file mode 100644 index 0000000000000..abc91fa3815c8 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/source_status/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSourceStatusResolvers } from './resolvers'; diff --git a/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts b/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts new file mode 100644 index 0000000000000..4ae516a9c3988 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/source_status/resolvers.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraIndexType, InfraSourceStatusResolvers } from '../../../common/graphql/types'; +import { InfraResolvedResult, InfraResolverOf } from '../../lib/adapters/framework'; +import { InfraFieldsDomain } from '../../lib/domains/fields_domain'; +import { InfraContext } from '../../lib/infra_types'; +import { InfraSourceStatus } from '../../lib/source_status'; +import { QuerySourceResolver } from '../sources/resolvers'; + +export type InfraSourceStatusMetricAliasExistsResolver = InfraResolverOf< + InfraSourceStatusResolvers.MetricAliasExistsResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusMetricIndicesExistResolver = InfraResolverOf< + InfraSourceStatusResolvers.MetricIndicesExistResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusMetricIndicesResolver = InfraResolverOf< + InfraSourceStatusResolvers.MetricIndicesResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusLogAliasExistsResolver = InfraResolverOf< + InfraSourceStatusResolvers.LogAliasExistsResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusLogIndicesExistResolver = InfraResolverOf< + InfraSourceStatusResolvers.LogIndicesExistResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusLogIndicesResolver = InfraResolverOf< + InfraSourceStatusResolvers.LogIndicesResolver, + InfraResolvedResult, + InfraContext +>; + +export type InfraSourceStatusIndexFieldsResolver = InfraResolverOf< + InfraSourceStatusResolvers.IndexFieldsResolver, + InfraResolvedResult, + InfraContext +>; + +export const createSourceStatusResolvers = (libs: { + sourceStatus: InfraSourceStatus; + fields: InfraFieldsDomain; +}): { + InfraSourceStatus: { + metricAliasExists: InfraSourceStatusMetricAliasExistsResolver; + metricIndicesExist: InfraSourceStatusMetricIndicesExistResolver; + metricIndices: InfraSourceStatusMetricIndicesResolver; + logAliasExists: InfraSourceStatusLogAliasExistsResolver; + logIndicesExist: InfraSourceStatusLogIndicesExistResolver; + logIndices: InfraSourceStatusLogIndicesResolver; + indexFields: InfraSourceStatusIndexFieldsResolver; + }; +} => ({ + InfraSourceStatus: { + async metricAliasExists(source, args, { req }) { + return await libs.sourceStatus.hasMetricAlias(req, source.id); + }, + async metricIndicesExist(source, args, { req }) { + return await libs.sourceStatus.hasMetricIndices(req, source.id); + }, + async metricIndices(source, args, { req }) { + return await libs.sourceStatus.getMetricIndexNames(req, source.id); + }, + async logAliasExists(source, args, { req }) { + return await libs.sourceStatus.hasLogAlias(req, source.id); + }, + async logIndicesExist(source, args, { req }) { + return await libs.sourceStatus.hasLogIndices(req, source.id); + }, + async logIndices(source, args, { req }) { + return await libs.sourceStatus.getLogIndexNames(req, source.id); + }, + async indexFields(source, args, { req }) { + const fields = await libs.fields.getFields( + req, + source.id, + args.indexType || InfraIndexType.ANY + ); + return fields; + }, + }, +}); diff --git a/x-pack/plugins/infra/server/graphql/source_status/schema.gql.ts b/x-pack/plugins/infra/server/graphql/source_status/schema.gql.ts new file mode 100644 index 0000000000000..10acb9650b108 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/source_status/schema.gql.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const sourceStatusSchema = gql` + "A descriptor of a field in an index" + type InfraIndexField { + "The name of the field" + name: String! + "The type of the field's values as recognized by Kibana" + type: String! + "Whether the field's values can be efficiently searched for" + searchable: Boolean! + "Whether the field's values can be aggregated" + aggregatable: Boolean! + } + + extend type InfraSourceStatus { + "Whether the configured metric alias exists" + metricAliasExists: Boolean! + "Whether the configured log alias exists" + logAliasExists: Boolean! + "Whether the configured alias or wildcard pattern resolve to any metric indices" + metricIndicesExist: Boolean! + "Whether the configured alias or wildcard pattern resolve to any log indices" + logIndicesExist: Boolean! + "The list of indices in the metric alias" + metricIndices: [String!]! + "The list of indices in the log alias" + logIndices: [String!]! + "The list of fields defined in the index mappings" + indexFields(indexType: InfraIndexType = ANY): [InfraIndexField!]! + } +`; diff --git a/x-pack/plugins/infra/server/graphql/sources/index.ts b/x-pack/plugins/infra/server/graphql/sources/index.ts new file mode 100644 index 0000000000000..ee187d8c31bec --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/sources/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSourcesResolvers } from './resolvers'; +export { sourcesSchema } from './schema.gql'; diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts new file mode 100644 index 0000000000000..5a3ab5acb76d0 --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceResolvers, QueryResolvers } from '../../../common/graphql/types'; +import { InfraResolvedResult, InfraResolverWithFields } from '../../lib/adapters/framework'; +import { InfraContext } from '../../lib/infra_types'; +import { InfraSourceStatus } from '../../lib/source_status'; +import { InfraSources } from '../../lib/sources'; + +export type QuerySourceResolver = InfraResolverWithFields< + QueryResolvers.SourceResolver, + null, + InfraContext, + 'id' | 'configuration' +>; + +export type QueryAllSourcesResolver = InfraResolverWithFields< + QueryResolvers.AllSourcesResolver, + null, + InfraContext, + 'id' | 'configuration' +>; + +export type InfraSourceStatusResolver = InfraResolverWithFields< + InfraSourceResolvers.StatusResolver, + InfraResolvedResult, + InfraContext, + never +>; + +interface SourcesResolversDeps { + sources: InfraSources; + sourceStatus: InfraSourceStatus; +} + +export const createSourcesResolvers = ( + libs: SourcesResolversDeps +): { + Query: { + source: QuerySourceResolver; + allSources: QueryAllSourcesResolver; + }; + InfraSource: { + status: InfraSourceStatusResolver; + }; +} => ({ + Query: { + async source(root, args) { + const requestedSourceConfiguration = await libs.sources.getConfiguration(args.id); + + return { + id: args.id, + configuration: requestedSourceConfiguration, + }; + }, + async allSources() { + const sourceConfigurations = await libs.sources.getAllConfigurations(); + + return Object.entries(sourceConfigurations).map(([sourceName, sourceConfiguration]) => ({ + id: sourceName, + configuration: sourceConfiguration, + })); + }, + }, + InfraSource: { + async status(source) { + return source; + }, + }, +}); diff --git a/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts new file mode 100644 index 0000000000000..952862f76173d --- /dev/null +++ b/x-pack/plugins/infra/server/graphql/sources/schema.gql.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const sourcesSchema = gql` + "A source of infrastructure data" + type InfraSource { + "The id of the source" + id: ID! + "The raw configuration of the source" + configuration: InfraSourceConfiguration! + "The status of the source" + status: InfraSourceStatus! + } + + "The status of an infrastructure data source" + type InfraSourceStatus + + "A set of configuration options for an infrastructure data source" + type InfraSourceConfiguration { + "The alias to read metric data from" + metricAlias: String! + "The alias to read log data from" + logAlias: String! + "The field mapping to use for this source" + fields: InfraSourceFields! + } + + "A mapping of semantic fields to their document counterparts" + type InfraSourceFields { + "The field to identify a container by" + container: String! + "The fields to identify a host by" + host: String! + "The fields that may contain the log event message. The first field found win." + message: [String!]! + "The field to identify a pod by" + pod: String! + "The field to use as a tiebreaker for log events that have identical timestamps" + tiebreaker: String! + "The field to use as a timestamp for metrics and logs" + timestamp: String! + } + + extend type Query { + "Get an infrastructure data source by id" + source("The id of the source" id: ID!): InfraSource! + "Get a list of all infrastructure data sources" + allSources: [InfraSource!]! + } +`; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts new file mode 100644 index 0000000000000..eb68e44af6423 --- /dev/null +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IResolvers, makeExecutableSchema } from 'graphql-tools'; +import { schemas } from './graphql'; +import { createCapabilitiesResolvers } from './graphql/capabilities'; +import { createLogEntriesResolvers } from './graphql/log_entries'; +import { createMetricResolvers } from './graphql/metrics/resolvers'; +import { createNodeResolvers } from './graphql/nodes'; +import { createSourceStatusResolvers } from './graphql/source_status'; +import { createSourcesResolvers } from './graphql/sources'; +import { InfraBackendLibs } from './lib/infra_types'; +import { initLegacyLoggingRoutes } from './logging_legacy'; + +export const initInfraServer = (libs: InfraBackendLibs) => { + const schema = makeExecutableSchema({ + resolvers: [ + createCapabilitiesResolvers(libs) as IResolvers, + createLogEntriesResolvers(libs) as IResolvers, + createNodeResolvers(libs) as IResolvers, + createSourcesResolvers(libs) as IResolvers, + createSourceStatusResolvers(libs) as IResolvers, + createMetricResolvers(libs) as IResolvers, + ], + typeDefs: schemas, + }); + + libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema); + + initLegacyLoggingRoutes(libs.framework); +}; diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts new file mode 100644 index 0000000000000..c916203648024 --- /dev/null +++ b/x-pack/plugins/infra/server/kibana.index.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import JoiNamespace from 'joi'; +import { initInfraServer } from './infra_server'; +import { compose } from './lib/compose/kibana'; +import { UsageCollector } from './usage/usage_collector'; + +export interface KbnServer extends Server { + usage: any; +} + +export const initServerWithKibana = (kbnServer: KbnServer) => { + const libs = compose(kbnServer); + initInfraServer(libs); + + // Register a function with server to manage the collection of usage stats + kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); +}; + +export const getConfigSchema = (Joi: typeof JoiNamespace) => { + const InfraDefaultSourceConfigSchema = Joi.object({ + metricAlias: Joi.string(), + logAlias: Joi.string(), + fields: Joi.object({ + container: Joi.string(), + host: Joi.string(), + message: Joi.array() + .items(Joi.string()) + .single(), + pod: Joi.string(), + tiebreaker: Joi.string(), + timestamp: Joi.string(), + }), + }); + + const InfraSourceConfigSchema = InfraDefaultSourceConfigSchema.keys({ + metricAlias: Joi.reach(InfraDefaultSourceConfigSchema, 'metricAlias').required(), + logAlias: Joi.reach(InfraDefaultSourceConfigSchema, 'logAlias').required(), + }); + + const InfraRootConfigSchema = Joi.object({ + enabled: Joi.boolean().default(true), + query: Joi.object({ + partitionSize: Joi.number(), + partitionFactor: Joi.number(), + }).default(), + sources: Joi.object() + .keys({ + default: InfraDefaultSourceConfigSchema, + }) + .pattern(/.*/, InfraSourceConfigSchema) + .default(), + }).default(); + + return InfraRootConfigSchema; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts new file mode 100644 index 0000000000000..1486d0d9e2e38 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/capabilities/adapter_types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceConfiguration } from '../../sources'; +import { InfraCapabilityAggregationBucket, InfraFrameworkRequest } from '../framework'; + +export interface InfraCapabilitiesAdapter { + getMetricCapabilities( + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeName: string, + nodeType: string + ): Promise; + getLogCapabilities( + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeName: string, + nodeType: string + ): Promise; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts new file mode 100644 index 0000000000000..049fb864dcbc0 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/capabilities/elasticsearch_capabilities_adapter.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceConfiguration } from '../../sources'; +import { + InfraBackendFrameworkAdapter, + InfraCapabilityAggregationBucket, + InfraCapabilityAggregationResponse, + InfraFrameworkRequest, +} from '../framework'; +import { InfraCapabilitiesAdapter } from './adapter_types'; + +export class ElasticsearchCapabilitiesAdapter implements InfraCapabilitiesAdapter { + private framework: InfraBackendFrameworkAdapter; + constructor(framework: InfraBackendFrameworkAdapter) { + this.framework = framework; + } + + public async getMetricCapabilities( + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeName: string, + nodeType: 'host' | 'container' | 'pod' + ): Promise { + const idFieldName = getIdFieldName(sourceConfiguration, nodeType); + const metricQuery = { + index: sourceConfiguration.metricAlias, + body: { + query: { + bool: { + filter: { + term: { [idFieldName]: nodeName }, + }, + }, + }, + size: 0, + aggs: { + metrics: { + terms: { + field: 'metricset.module', + size: 1000, + }, + aggs: { + names: { + terms: { + field: 'metricset.name', + size: 1000, + }, + }, + }, + }, + }, + }, + }; + + const response = await this.framework.callWithRequest< + any, + { metrics?: InfraCapabilityAggregationResponse } + >(req, 'search', metricQuery); + + return response.aggregations && response.aggregations.metrics + ? response.aggregations.metrics.buckets + : []; + } + + public async getLogCapabilities( + req: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + nodeName: string, + nodeType: 'host' | 'container' | 'pod' + ): Promise { + const idFieldName = getIdFieldName(sourceConfiguration, nodeType); + const logQuery = { + index: sourceConfiguration.logAlias, + body: { + query: { + bool: { + filter: { + term: { [idFieldName]: nodeName }, + }, + }, + }, + size: 0, + aggs: { + metrics: { + terms: { + field: 'fileset.module', + size: 1000, + }, + aggs: { + names: { + terms: { + field: 'fileset.name', + size: 1000, + }, + }, + }, + }, + }, + }, + }; + + const response = await this.framework.callWithRequest< + any, + { metrics?: InfraCapabilityAggregationResponse } + >(req, 'search', logQuery); + + return response.aggregations && response.aggregations.metrics + ? response.aggregations.metrics.buckets + : []; + } +} + +const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => { + switch (nodeType) { + case 'host': + return sourceConfiguration.fields.host; + case 'container': + return sourceConfiguration.fields.container; + default: + return sourceConfiguration.fields.pod; + } +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts b/x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/capabilities/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/configuration/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/configuration/adapter_types.ts new file mode 100644 index 0000000000000..e086f67092af3 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/configuration/adapter_types.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface InfraConfigurationAdapter { + get(): Promise; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/configuration/index.ts b/x-pack/plugins/infra/server/lib/adapters/configuration/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/configuration/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts new file mode 100644 index 0000000000000..ed7c88fc994da --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraConfigurationAdapter } from './adapter_types'; + +export class InfraInmemoryConfigurationAdapter + implements InfraConfigurationAdapter { + constructor(private readonly configuration: Configuration) {} + + public async get() { + return this.configuration; + } +} diff --git a/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts b/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts new file mode 100644 index 0000000000000..4d87878e9aa87 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraKibanaConfigurationAdapter } from './kibana_configuration_adapter'; + +describe('the InfraKibanaConfigurationAdapter', () => { + test('queries the xpack.infra configuration of the server', async () => { + const mockConfig = { + get: jest.fn(), + }; + + const configurationAdapter = new InfraKibanaConfigurationAdapter({ + config: () => mockConfig, + }); + + await configurationAdapter.get(); + + expect(mockConfig.get).toBeCalledWith('xpack.infra'); + }); + + test('applies the query defaults', async () => { + const configurationAdapter = new InfraKibanaConfigurationAdapter({ + config: () => ({ + get: () => ({}), + }), + }); + + const configuration = await configurationAdapter.get(); + + expect(configuration).toMatchObject({ + query: { + partitionSize: expect.any(Number), + partitionFactor: expect.any(Number), + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts new file mode 100644 index 0000000000000..62b01b3eac2a3 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +import { InfraConfigurationAdapter } from './adapter_types'; + +export class InfraKibanaConfigurationAdapter + implements InfraConfigurationAdapter { + private readonly server: ServerWithConfig; + + constructor(server: any) { + if (!isServerWithConfig(server)) { + throw new Error('Failed to find configuration on server.'); + } + + this.server = server; + } + + public async get() { + const config = this.server.config(); + + if (!isKibanaConfiguration(config)) { + throw new Error('Failed to access configuration of server.'); + } + + const configuration = config.get('xpack.infra') || {}; + const configurationWithDefaults = { + enabled: true, + query: { + partitionSize: 75, + partitionFactor: 1.2, + ...(configuration.query || {}), + }, + sources: {}, + ...configuration, + } as Configuration; + + // we assume this to be the configuration because Kibana would have already validated it + return configurationWithDefaults; + } +} + +interface ServerWithConfig { + config(): any; +} + +function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig { + return ( + Joi.validate( + maybeServer, + Joi.object({ + config: Joi.func().required(), + }).unknown() + ).error === null + ); +} + +interface KibanaConfiguration { + get(key: string): any; +} + +function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration { + return ( + Joi.validate( + maybeConfiguration, + Joi.object({ + get: Joi.func().required(), + }).unknown() + ).error === null + ); +} diff --git a/x-pack/plugins/infra/server/lib/adapters/fields/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/fields/adapter_types.ts new file mode 100644 index 0000000000000..4749999f22d26 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/fields/adapter_types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraFrameworkRequest } from '../framework'; + +export interface FieldsAdapter { + getIndexFields(req: InfraFrameworkRequest, indices: string[]): Promise; +} + +export interface IndexFieldDescriptor { + name: string; + type: string; + searchable: boolean; + aggregatable: boolean; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts new file mode 100644 index 0000000000000..37d242359b87b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { FieldsAdapter, IndexFieldDescriptor } from './adapter_types'; + +export class FrameworkFieldsAdapter implements FieldsAdapter { + private framework: InfraBackendFrameworkAdapter; + + constructor(framework: InfraBackendFrameworkAdapter) { + this.framework = framework; + } + + public async getIndexFields( + request: InfraFrameworkRequest, + indices: string[] + ): Promise { + const indexPatternsService = this.framework.getIndexPatternsService(request); + const response = await indexPatternsService.getFieldsForWildcard({ + pattern: indices, + }); + return response; + } +} diff --git a/x-pack/plugins/infra/server/lib/adapters/fields/index.ts b/x-pack/plugins/infra/server/lib/adapters/fields/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/fields/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts new file mode 100644 index 0000000000000..17cf5064aeb4b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { GraphQLSchema } from 'graphql'; +import { IRouteAdditionalConfigurationOptions, IStrictReply } from 'hapi'; +import { InfraMetricModel } from '../metrics/adapter_types'; + +export * from '../../../../common/graphql/typed_resolvers'; +import { JsonObject } from '../../../../common/typed_json'; + +export const internalInfraFrameworkRequest = Symbol('internalInfraFrameworkRequest'); + +export interface InfraBackendFrameworkAdapter { + version: string; + exposeStaticDir(urlPath: string, dir: string): void; + registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; + registerRoute( + route: InfraFrameworkRouteOptions + ): void; + callWithRequest( + req: InfraFrameworkRequest, + method: 'search', + options?: object + ): Promise>; + callWithRequest( + req: InfraFrameworkRequest, + method: 'msearch', + options?: object + ): Promise>; + callWithRequest( + req: InfraFrameworkRequest, + method: 'fieldCaps', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'indices.existsAlias', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: 'indices.getAlias' | 'indices.get', + options?: object + ): Promise; + callWithRequest( + req: InfraFrameworkRequest, + method: string, + options?: object + ): Promise; + getIndexPatternsService(req: InfraFrameworkRequest): InfraFrameworkIndexPatternsService; + makeTSVBRequest( + req: InfraFrameworkRequest, + model: InfraMetricModel, + timerange: { min: number; max: number }, + filters: JsonObject[] + ): Promise; +} + +export interface InfraFrameworkRequest< + InternalRequest extends InfraWrappableRequest = InfraWrappableRequest +> { + [internalInfraFrameworkRequest]: InternalRequest; + payload: InternalRequest['payload']; + params: InternalRequest['params']; + query: InternalRequest['query']; +} + +export interface InfraWrappableRequest { + payload: Payload; + params: Params; + query: Query; +} + +export interface InfraFrameworkPluginOptions { + register: any; + options: any; +} + +export interface InfraFrameworkRouteOptions< + RouteRequest extends InfraWrappableRequest, + RouteResponse +> { + path: string; + method: string | string[]; + vhost?: string; + handler: InfraFrameworkRouteHandler; + config?: Pick< + IRouteAdditionalConfigurationOptions, + Exclude + >; +} + +export type InfraFrameworkRouteHandler< + RouteRequest extends InfraWrappableRequest, + RouteResponse +> = (request: InfraFrameworkRequest, reply: IStrictReply) => void; + +export interface InfraDatabaseResponse { + took: number; + timeout: boolean; +} + +export interface InfraDatabaseSearchResponse + extends InfraDatabaseResponse { + aggregations?: Aggregations; + hits: { + total: number; + hits: Hit[]; + }; +} + +export interface InfraDatabaseMultiResponse extends InfraDatabaseResponse { + responses: Array>; +} + +export interface InfraDatabaseFieldCapsResponse extends InfraDatabaseResponse { + fields: InfraFieldsResponse; +} + +export interface InfraDatabaseGetIndicesResponse { + [indexName: string]: { + aliases: { + [aliasName: string]: any; + }; + }; +} + +export type SearchHit = SearchResponse['hits']['hits'][0]; + +export interface SortedSearchHit extends SearchHit { + sort: any[]; + _source: { + [field: string]: any; + }; +} + +export interface InfraDateRangeAggregationBucket { + from?: number; + to?: number; + doc_count: number; + key: string; +} + +export interface InfraDateRangeAggregationResponse { + buckets: InfraDateRangeAggregationBucket[]; +} + +export interface InfraCapabilityAggregationBucket { + key: string; + names?: { + buckets: InfraCapabilityAggregationBucket[]; + }; +} + +export interface InfraCapabilityAggregationResponse { + buckets: InfraCapabilityAggregationBucket[]; +} + +export interface InfraFieldsResponse { + [name: string]: InfraFieldDef; +} + +export interface InfraFieldDetails { + searchable: boolean; + aggregatable: boolean; + type: string; +} + +export interface InfraFieldDef { + [type: string]: InfraFieldDetails; +} + +interface InfraFrameworkIndexFieldDescriptor { + name: string; + type: string; + searchable: boolean; + aggregatable: boolean; + readFromDocValues: boolean; +} + +export interface InfraFrameworkIndexPatternsService { + getFieldsForWildcard(options: { + pattern: string | string[]; + }): Promise; +} + +export interface InfraTSVBResponse { + [key: string]: InfraTSVBPanel; +} + +export interface InfraTSVBPanel { + id: string; + series: InfraTSVBSeries[]; +} + +export interface InfraTSVBSeries { + id: string; + data: InfraTSVBDataPoint[]; +} + +export type InfraTSVBDataPoint = [number, number]; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts b/x-pack/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts new file mode 100644 index 0000000000000..9277a9619fc4e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as GraphiQL from 'apollo-server-module-graphiql'; +import Boom from 'boom'; +import { IReply, Request, Server } from 'hapi'; + +import { GraphQLOptions, runHttpQuery } from 'apollo-server-core'; + +export interface IRegister { + (server: Server, options: any, next: () => void): void; + attributes: { + name: string; + version?: string; + }; +} + +export type HapiOptionsFunction = (req?: Request) => GraphQLOptions | Promise; + +export interface HapiPluginOptions { + path: string; + vhost?: string; + route?: any; + graphqlOptions: GraphQLOptions | HapiOptionsFunction; +} + +export const graphqlHapi: IRegister = Object.assign( + (server: Server, options: HapiPluginOptions, next: () => void) => { + if (!options || !options.graphqlOptions) { + throw new Error('Apollo Server requires options.'); + } + + server.route({ + config: options.route || {}, + handler: async (request: Request, reply: IReply) => { + try { + const gqlResponse = await runHttpQuery([request], { + method: request.method.toUpperCase(), + options: options.graphqlOptions, + query: request.method === 'post' ? request.payload : request.query, + }); + + return reply(gqlResponse).type('application/json'); + } catch (error) { + if ('HttpQueryError' !== error.name) { + const queryError = Boom.wrap(error); + + queryError.output.payload.message = error.message; + + return reply(queryError); + } + + if (error.isGraphQLError === true) { + return reply(error.message) + .code(error.statusCode) + .type('application/json'); + } + + const genericError = Boom.create(error.statusCode, error.message); + + if (error.headers) { + Object.keys(error.headers).forEach(header => { + genericError.output.headers[header] = error.headers[header]; + }); + } + + // Boom hides the error when status code is 500 + + genericError.output.payload.message = error.message; + + throw genericError; + } + }, + method: ['GET', 'POST'], + path: options.path || '/graphql', + vhost: options.vhost || undefined, + }); + + return next(); + }, + { + attributes: { + name: 'graphql', + }, + } +); + +export type HapiGraphiQLOptionsFunction = ( + req?: Request +) => GraphiQL.GraphiQLData | Promise; + +export interface HapiGraphiQLPluginOptions { + path: string; + + route?: any; + + graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction; +} + +export const graphiqlHapi: IRegister = Object.assign( + (server: Server, options: HapiGraphiQLPluginOptions) => { + if (!options || !options.graphiqlOptions) { + throw new Error('Apollo Server GraphiQL requires options.'); + } + + server.route({ + config: options.route || {}, + handler: async (request: Request, reply: IReply) => { + const graphiqlString = await GraphiQL.resolveGraphiQLString( + request.query, + options.graphiqlOptions, + request + ); + + return reply(graphiqlString).type('text/html'); + }, + method: 'GET', + path: options.path || '/graphiql', + }); + }, + { + attributes: { + name: 'graphiql', + }, + } +); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/index.ts b/x-pack/plugins/infra/server/lib/adapters/framework/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/framework/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..c8e97c89911c8 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GraphQLSchema } from 'graphql'; +import { IStrictReply, Request, Server } from 'hapi'; + +import { InfraMetricModel } from '../metrics/adapter_types'; +import { + InfraBackendFrameworkAdapter, + InfraFrameworkIndexPatternsService, + InfraFrameworkRequest, + InfraFrameworkRouteOptions, + InfraTSVBResponse, + InfraWrappableRequest, + internalInfraFrameworkRequest, +} from './adapter_types'; +import { graphiqlHapi, graphqlHapi } from './apollo_server_hapi'; + +export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFrameworkAdapter { + public version: string; + private server: Server; + + constructor(hapiServer: Server) { + this.server = hapiServer; + this.version = hapiServer.plugins.kibana.status.plugin.version; + } + + public exposeStaticDir(urlPath: string, dir: string): void { + this.server.route({ + handler: { + directory: { + path: dir, + }, + }, + method: 'GET', + path: urlPath, + }); + } + + public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { + this.server.register({ + options: { + graphqlOptions: (req: Request) => ({ + context: { req: wrapRequest(req) }, + schema, + }), + path: routePath, + }, + register: graphqlHapi, + }); + + this.server.register({ + options: { + graphiqlOptions: { + endpointURL: routePath, + passHeader: `'kbn-version': '${this.version}'`, + }, + path: `${routePath}/graphiql`, + }, + register: graphiqlHapi, + }); + } + + public registerRoute( + route: InfraFrameworkRouteOptions + ) { + const wrappedHandler = (request: any, reply: IStrictReply) => + route.handler(wrapRequest(request), reply); + + this.server.route({ + handler: wrappedHandler, + method: route.method, + path: route.path, + }); + } + + public async callWithRequest(req: InfraFrameworkRequest, ...rest: any[]) { + const internalRequest = req[internalInfraFrameworkRequest]; + const { elasticsearch } = internalRequest.server.plugins; + const { callWithRequest } = elasticsearch.getCluster('data'); + const fields = await callWithRequest(internalRequest, ...rest); + return fields; + } + + public getIndexPatternsService( + request: InfraFrameworkRequest + ): InfraFrameworkIndexPatternsService { + if (!isServerWithIndexPatternsServiceFactory(this.server)) { + throw new Error('Failed to access indexPatternsService for the request'); + } + return this.server.indexPatternsServiceFactory({ + callCluster: async (method: string, args: [object], ...rest: any[]) => { + const fieldCaps = await this.callWithRequest( + request, + method, + { ...args, allowNoIndices: true }, + ...rest + ); + return fieldCaps; + }, + }); + } + + public async makeTSVBRequest( + req: InfraFrameworkRequest, + model: InfraMetricModel, + timerange: { min: number; max: number }, + filters: any[] + ) { + const internalRequest = req[internalInfraFrameworkRequest]; + const server = internalRequest.server; + return new Promise((resolve, reject) => { + const request = { + url: '/api/metrics/vis/data', + method: 'POST', + headers: internalRequest.headers, + payload: { + timerange, + panels: [model], + filters, + }, + }; + server.inject(request, res => { + if (res.statusCode !== 200) { + return reject(res); + } + resolve(res.result); + }); + }); + } +} + +export function wrapRequest( + req: InternalRequest +): InfraFrameworkRequest { + const { params, payload, query } = req; + + return { + [internalInfraFrameworkRequest]: req, + params, + payload, + query, + }; +} + +interface ServerWithIndexPatternsServiceFactory extends Server { + indexPatternsServiceFactory(options: { + callCluster: (...args: any[]) => any; + }): InfraFrameworkIndexPatternsService; +} + +const isServerWithIndexPatternsServiceFactory = ( + server: Server +): server is ServerWithIndexPatternsServiceFactory => + typeof (server as any).indexPatternsServiceFactory === 'function'; diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/adapter_types.ts new file mode 100644 index 0000000000000..41bc2aa258807 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/adapter_types.ts @@ -0,0 +1,5 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/index.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/index.ts new file mode 100644 index 0000000000000..41bc2aa258807 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/index.ts @@ -0,0 +1,5 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts new file mode 100644 index 0000000000000..2dc6fd2551ec4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { timeMilliseconds } from 'd3-time'; +import get from 'lodash/fp/get'; +import has from 'lodash/fp/has'; +import zip from 'lodash/fp/zip'; + +import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time'; +import { + LogEntriesAdapter, + LogEntryDocument, + LogEntryQuery, +} from '../../domains/log_entries_domain'; +import { InfraSourceConfiguration } from '../../sources'; +import { + InfraDateRangeAggregationBucket, + InfraDateRangeAggregationResponse, + InfraFrameworkRequest, + SortedSearchHit, +} from '../framework'; +import { InfraBackendFrameworkAdapter } from '../framework'; + +const DAY_MILLIS = 24 * 60 * 60 * 1000; +const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS); + +export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { + constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + + public async getAdjacentLogEntryDocuments( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + fields: string[], + start: TimeKey, + direction: 'asc' | 'desc', + maxCount: number, + filterQuery: LogEntryQuery, + highlightQuery: string + ): Promise { + if (maxCount <= 0) { + return []; + } + + const intervals = getLookupIntervals(start.time, direction); + + let documents: LogEntryDocument[] = []; + for (const [intervalStart, intervalEnd] of intervals) { + if (documents.length >= maxCount) { + break; + } + + const documentsInInterval = await this.getLogEntryDocumentsBetween( + request, + sourceConfiguration, + fields, + intervalStart, + intervalEnd, + documents.length > 0 ? documents[documents.length - 1].key : start, + maxCount - documents.length, + filterQuery, + highlightQuery + ); + + documents = [...documents, ...documentsInInterval]; + } + + return direction === 'asc' ? documents : documents.reverse(); + } + + public async getContainedLogEntryDocuments( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + fields: string[], + start: TimeKey, + end: TimeKey, + filterQuery: LogEntryQuery, + highlightQuery: string + ): Promise { + const documents = await this.getLogEntryDocumentsBetween( + request, + sourceConfiguration, + fields, + start.time, + end.time, + start, + 10000, + filterQuery, + highlightQuery + ); + + return documents.filter(document => compareTimeKeys(document.key, end) < 0); + } + + public async getContainedLogSummaryBuckets( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + start: number, + end: number, + bucketSize: number, + filterQuery: LogEntryQuery + ): Promise { + const bucketIntervalStarts = timeMilliseconds(new Date(start), new Date(end), bucketSize); + + const query = { + allowNoIndices: true, + index: sourceConfiguration.logAlias, + ignoreUnavailable: true, + body: { + aggregations: { + count_by_date: { + date_range: { + field: sourceConfiguration.fields.timestamp, + ranges: bucketIntervalStarts.map(bucketIntervalStart => ({ + from: bucketIntervalStart.getTime(), + to: bucketIntervalStart.getTime() + bucketSize, + })), + }, + }, + }, + query: { + bool: { + filter: [ + ...createQueryFilterClauses(filterQuery), + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: start, + lte: end, + }, + }, + }, + ], + }, + }, + size: 0, + }, + }; + + const response = await this.framework.callWithRequest< + any, + { count_by_date?: InfraDateRangeAggregationResponse } + >(request, 'search', query); + + return response.aggregations && response.aggregations.count_by_date + ? response.aggregations.count_by_date.buckets + : []; + } + + private async getLogEntryDocumentsBetween( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + fields: string[], + start: number, + end: number, + after: TimeKey | null, + maxCount: number, + filterQuery?: LogEntryQuery, + highlightQuery?: string + ): Promise { + if (maxCount <= 0) { + return []; + } + + const sortDirection: 'asc' | 'desc' = start <= end ? 'asc' : 'desc'; + + const startRange = { + [sortDirection === 'asc' ? 'gte' : 'lte']: start, + }; + const endRange = + end === Infinity + ? {} + : { + [sortDirection === 'asc' ? 'lte' : 'gte']: end, + }; + + const highlightClause = highlightQuery + ? { + highlight: { + boundary_scanner: 'word', + fields: fields.reduce( + (highlightFieldConfigs, fieldName) => ({ + ...highlightFieldConfigs, + [fieldName]: {}, + }), + {} + ), + fragment_size: 1, + number_of_fragments: 100, + post_tags: [''], + pre_tags: [''], + }, + } + : {}; + + const searchAfterClause = isTimeKey(after) + ? { + search_after: [after.time, after.tiebreaker], + } + : {}; + + const query = { + allowNoIndices: true, + index: sourceConfiguration.logAlias, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + ...createQueryFilterClauses(filterQuery), + { + range: { + [sourceConfiguration.fields.timestamp]: { + ...startRange, + ...endRange, + }, + }, + }, + ], + }, + }, + ...highlightClause, + ...searchAfterClause, + _source: fields, + size: maxCount, + sort: [ + { [sourceConfiguration.fields.timestamp]: sortDirection }, + { [sourceConfiguration.fields.tiebreaker]: sortDirection }, + ], + track_total_hits: false, + }, + }; + + const response = await this.framework.callWithRequest( + request, + 'search', + query + ); + const hits = response.hits.hits; + const documents = hits.map(convertHitToLogEntryDocument(fields)); + + return documents; + } +} + +function getLookupIntervals(start: number, direction: 'asc' | 'desc'): Array<[number, number]> { + const offsetSign = direction === 'asc' ? 1 : -1; + const translatedOffsets = LOOKUP_OFFSETS.map(offset => start + offset * offsetSign); + const intervals = zip(translatedOffsets.slice(0, -1), translatedOffsets.slice(1)) as Array< + [number, number] + >; + return intervals; +} + +const convertHitToLogEntryDocument = (fields: string[]) => ( + hit: SortedSearchHit +): LogEntryDocument => ({ + gid: hit._id, + fields: fields.reduce( + (flattenedFields, fieldName) => + has(fieldName, hit._source) + ? { + ...flattenedFields, + [fieldName]: get(fieldName, hit._source), + } + : flattenedFields, + {} as { [fieldName: string]: string | number | boolean | null } + ), + key: { + time: hit.sort[0], + tiebreaker: hit.sort[1], + }, +}); + +const createQueryFilterClauses = (filterQuery: LogEntryQuery | undefined) => + filterQuery ? [filterQuery] : []; diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/adapter_types.ts new file mode 100644 index 0000000000000..18a81e518c931 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/adapter_types.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraMetric, + InfraMetricData, + InfraNodeType, + InfraTimerangeInput, +} from '../../../../common/graphql/types'; + +import { InfraSourceConfiguration } from '../../sources'; +import { InfraFrameworkRequest } from '../framework'; + +export interface InfraMetricsRequestOptions { + nodeId: string; + nodeType: InfraNodeType; + sourceConfiguration: InfraSourceConfiguration; + timerange: InfraTimerangeInput; + metrics: InfraMetric[]; +} + +export interface InfraMetricsAdapter { + getMetrics( + req: InfraFrameworkRequest, + options: InfraMetricsRequestOptions + ): Promise; +} + +export enum InfraMetricModelMetricType { + avg = 'avg', + max = 'max', + min = 'min', + calculation = 'calculation', + cardinality = 'cardinality', + series_agg = 'series_agg', + positive_only = 'positive_only', + derivative = 'derivative', + count = 'count', +} + +export interface InfraMetricModel { + id: string; + requires: string[]; + index_pattern: string | string[]; + interval: string; + time_field: string; + type: string; + series: InfraMetricModelSeries[]; + filter?: string; + map_field_to?: string; +} + +export interface InfraMetricModelSeries { + id: string; + metrics: InfraMetricModelMetric[]; + split_mode: string; + terms_field?: string; + terms_size?: number; + terms_order_by?: string; +} + +export interface InfraMetricModelBasicMetric { + id: string; + field: string; + type: InfraMetricModelMetricType; +} + +export interface InfraMetricModelSeriesAgg { + id: string; + function: string; + type: InfraMetricModelMetricType.series_agg; +} + +export interface InfraMetricModelDerivative { + id: string; + field: string; + unit: string; + type: InfraMetricModelMetricType; +} + +export interface InfraMetricModelBucketScriptVariable { + field: string; + id: string; + name: string; +} + +export interface InfraMetricModelCount { + id: string; + type: InfraMetricModelMetricType.count; +} + +export interface InfraMetricModelBucketScript { + id: string; + script: string; + type: InfraMetricModelMetricType.calculation; + variables: InfraMetricModelBucketScriptVariable[]; +} + +export type InfraMetricModelMetric = + | InfraMetricModelCount + | InfraMetricModelBasicMetric + | InfraMetricModelBucketScript + | InfraMetricModelDerivative + | InfraMetricModelSeriesAgg; + +export type InfraMetricModelCreator = ( + timeField: string, + indexPattern: string | string[], + interval: string +) => InfraMetricModel; diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/index.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts new file mode 100644 index 0000000000000..bb7f7f6d2dc93 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { flatten } from 'lodash'; +import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../../common/graphql/types'; +import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; +import { checkValidNode } from './lib/check_valid_node'; +import { metricModels } from './models'; + +export class KibanaMetricsAdapter implements InfraMetricsAdapter { + private framework: InfraBackendFrameworkAdapter; + + constructor(framework: InfraBackendFrameworkAdapter) { + this.framework = framework; + } + + public async getMetrics( + req: InfraFrameworkRequest, + options: InfraMetricsRequestOptions + ): Promise { + const fields = { + [InfraNodeType.host]: options.sourceConfiguration.fields.host, + [InfraNodeType.container]: options.sourceConfiguration.fields.container, + [InfraNodeType.pod]: options.sourceConfiguration.fields.pod, + }; + const indexPattern = [ + options.sourceConfiguration.metricAlias, + options.sourceConfiguration.logAlias, + ]; + const timeField = options.sourceConfiguration.fields.timestamp; + const interval = options.timerange.interval; + const nodeField = fields[options.nodeType]; + const timerange = { + min: options.timerange.from, + max: options.timerange.to, + }; + + const search = (searchOptions: object) => + this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions); + + const validNode = await checkValidNode(search, indexPattern, nodeField, options.nodeId); + if (!validNode) { + throw new Error(`${options.nodeId} does not exist.`); + } + + const requests = options.metrics.map(metricId => { + const model = metricModels[metricId](timeField, indexPattern, interval); + const filters = [{ match: { [nodeField]: options.nodeId } }]; + return this.framework.makeTSVBRequest(req, model, timerange, filters); + }); + return Promise.all(requests) + .then(results => { + return results.map(result => { + const metricIds = Object.keys(result).filter(k => k !== 'type'); + return metricIds.map((id: string) => { + const infraMetricId: InfraMetric = (InfraMetric as any)[id]; + if (!infraMetricId) { + throw new Error(`${id} is not a valid InfraMetric`); + } + const panel = result[infraMetricId]; + return { + id: infraMetricId, + series: panel.series.map(series => { + return { + id: series.id, + data: series.data.map(point => ({ timestamp: point[0], value: point[1] })), + }; + }), + }; + }); + }); + }) + .then(result => flatten(result)); + } +} diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/lib/check_valid_node.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/lib/check_valid_node.ts new file mode 100644 index 0000000000000..a9c6d36560b6c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/lib/check_valid_node.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraDatabaseSearchResponse } from '../../framework'; + +export const checkValidNode = async ( + search: (options: object) => Promise>, + indexPattern: string | string[], + field: string, + id: string +): Promise => { + const params = { + index: indexPattern, + terminateAfter: 1, + body: { + size: 0, + query: { + match: { + [field]: id, + }, + }, + }, + }; + return (await search(params)).hits.total > 0; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_kernel.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_kernel.ts new file mode 100644 index 0000000000000..3fd1144b6770b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_kernel.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerCpuKernel: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'containerCpuKernel', + requires: ['docker.cpu'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'kernel', + split_mode: 'everything', + metrics: [ + { + field: 'docker.cpu.kernel.pct', + id: 'avg-cpu-kernel', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_usage.ts new file mode 100644 index 0000000000000..bdb52b8a9d7b5 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_cpu_usage.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerCpuUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'containerCpuUsage', + requires: ['docker.cpu'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'docker.cpu.total.pct', + id: 'avg-cpu-total', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_disk_io_bytes.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_disk_io_bytes.ts new file mode 100644 index 0000000000000..a310886ab9a0f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_disk_io_bytes.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerDiskIOBytes: InfraMetricModelCreator = ( + timeField, + indexPattern, + interval +) => ({ + id: 'containerDiskIOBytes', + requires: ['docker.disk'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'docker.diskio.read.bytes', + id: 'avg-diskio-bytes', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'docker.diskio.write.bytes', + id: 'avg-diskio-bytes', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_diskio_ops.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_diskio_ops.ts new file mode 100644 index 0000000000000..d09250ab1c577 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_diskio_ops.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerDiskIOOps: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'containerDiskIOOps', + requires: ['docker.disk'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'docker.diskio.read.ops', + id: 'avg-diskio-ops', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'docker.diskio.write.ops', + id: 'avg-diskio-ops', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_memory.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_memory.ts new file mode 100644 index 0000000000000..9844fde182fe7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_memory.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerMemory: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'containerMemory', + requires: ['docker.memory'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'memory', + split_mode: 'everything', + metrics: [ + { + field: 'docker.memory.usage.pct', + id: 'avg-memory', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_network_traffic.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_network_traffic.ts new file mode 100644 index 0000000000000..9c1ea8920f218 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_network_traffic.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerNetworkTraffic: InfraMetricModelCreator = ( + timeField, + indexPattern, + interval +) => ({ + id: 'containerNetworkTraffic', + requires: ['docker.network'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'docker.network.out.bytes', + id: 'avg-network-out', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'docker.network.in.bytes', + id: 'avg-network-in', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'invert-posonly-deriv-max-network-in', + script: 'params.rate * -1', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'avg-network-in', + id: 'var-rate', + name: 'rate', + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_overview.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_overview.ts new file mode 100644 index 0000000000000..24d1a93540104 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/container/container_overview.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const containerOverview: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'containerOverview', + requires: ['docker'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'docker.cpu.total.pct', + id: 'avg-cpu-total', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'memory', + split_mode: 'everything', + metrics: [ + { + field: 'docker.memory.usage.pct', + id: 'avg-memory', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'docker.network.out.bytes', + id: 'avg-network-out', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'docker.network.in.bytes', + id: 'avg-network-in', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_cpu_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_cpu_usage.ts new file mode 100644 index 0000000000000..3d35bc59aa4df --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_cpu_usage.ts @@ -0,0 +1,249 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostCpuUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostCpuUsage', + requires: ['system.cpu'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'user', + metrics: [ + { + field: 'system.cpu.user.pct', + id: 'avg-cpu-user', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-user', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'system', + metrics: [ + { + field: 'system.cpu.system.pct', + id: 'avg-cpu-system', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-system', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'steal', + metrics: [ + { + field: 'system.cpu.steal.pct', + id: 'avg-cpu-steal', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'avg-cpu-steal', + id: 'var-avg', + name: 'avg', + }, + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'irq', + metrics: [ + { + field: 'system.cpu.irq.pct', + id: 'avg-cpu-irq', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-irq', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'softirq', + metrics: [ + { + field: 'system.cpu.softirq.pct', + id: 'avg-cpu-softirq', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-softirq', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'iowait', + metrics: [ + { + field: 'system.cpu.iowait.pct', + id: 'avg-cpu-iowait', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-iowait', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'nice', + metrics: [ + { + field: 'system.cpu.nice.pct', + id: 'avg-cpu-nice', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-avg-cores', + script: 'params.avg / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + { + field: 'avg-cpu-nice', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_filesystem.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_filesystem.ts new file mode 100644 index 0000000000000..4e0dc01ab3923 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_filesystem.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostFilesystem: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostFilesystem', + requires: ['system.filesystem'], + filter: 'system.filesystem.device_name:\\/*', + index_pattern: indexPattern, + time_field: timeField, + interval, + type: 'timeseries', + series: [ + { + id: 'used', + metrics: [ + { + field: 'system.filesystem.used.pct', + id: 'avg-filesystem-used', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'terms', + terms_field: 'system.filesystem.device_name', + terms_order_by: 'used', + terms_size: 5, + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_cpu_cap.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_cpu_cap.ts new file mode 100644 index 0000000000000..685b95614425a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_cpu_cap.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostK8sCpuCap: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostK8sCpuCap', + map_field_to: 'kubernetes.node.name', + requires: ['kubernetes.node'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'capacity', + metrics: [ + { + field: 'kubernetes.node.cpu.allocatable.cores', + id: 'max-cpu-cap', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-nanocores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + id: 'var-cores', + field: 'max-cpu-cap', + name: 'cores', + }, + ], + script: 'params.cores * 1000000000', + }, + ], + split_mode: 'everything', + }, + { + id: 'used', + metrics: [ + { + field: 'kubernetes.node.cpu.usage.nanocores', + id: 'avg-cpu-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_disk_cap.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_disk_cap.ts new file mode 100644 index 0000000000000..27fc70e2e00e7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_disk_cap.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostK8sDiskCap: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostK8sDiskCap', + map_field_to: 'kubernetes.node.name', + requires: ['kubernetes.node'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'capacity', + metrics: [ + { + field: 'kubernetes.node.fs.capacity.bytes', + id: 'max-fs-cap', + type: InfraMetricModelMetricType.max, + }, + ], + split_mode: 'everything', + }, + { + id: 'used', + metrics: [ + { + field: 'kubernetes.node.fs.used.bytes', + id: 'avg-fs-used', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_memory_cap.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_memory_cap.ts new file mode 100644 index 0000000000000..7e064f7256dbd --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_memory_cap.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostK8sMemoryCap: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostK8sMemoryCap', + map_field_to: 'kubernetes.node.name', + requires: ['kubernetes.node'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'capacity', + metrics: [ + { + field: 'kubernetes.node.memory.allocatable.bytes', + id: 'max-memory-cap', + type: InfraMetricModelMetricType.max, + }, + ], + split_mode: 'everything', + }, + { + id: 'used', + metrics: [ + { + field: 'kubernetes.node.memory.usage.bytes', + id: 'avg-memory-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_overview.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_overview.ts new file mode 100644 index 0000000000000..cd015d4312fd2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_overview.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostK8sOverview: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostK8sOverview', + requires: ['kubernetes'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'gauge', + series: [ + { + id: 'cpucap', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.node.cpu.allocatable.cores', + id: 'max-cpu-cap', + type: InfraMetricModelMetricType.max, + }, + { + field: 'kubernetes.node.cpu.usage.nanocores', + id: 'avg-cpu-usage', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'calc-used-cap', + script: 'params.used / (params.cap * 1000000000)', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-cpu-cap', + id: 'var-cap', + name: 'cap', + }, + { + field: 'avg-cpu-usage', + id: 'var-used', + name: 'used', + }, + ], + }, + ], + }, + { + id: 'diskcap', + metrics: [ + { + field: 'kubernetes.node.fs.capacity.bytes', + id: 'max-fs-cap', + type: InfraMetricModelMetricType.max, + }, + { + field: 'kubernetes.node.fs.used.bytes', + id: 'avg-fs-used', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'calc-used-cap', + script: 'params.used / params.cap', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-fs-cap', + id: 'var-cap', + name: 'cap', + }, + { + field: 'avg-fs-used', + id: 'var-used', + name: 'used', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'memorycap', + metrics: [ + { + field: 'kubernetes.node.memory.allocatable.bytes', + id: 'max-memory-cap', + type: InfraMetricModelMetricType.max, + }, + { + field: 'kubernetes.node.memory.usage.bytes', + id: 'avg-memory-usage', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'calc-used-cap', + script: 'params.used / params.cap', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-memory-cap', + id: 'var-cap', + name: 'cap', + }, + { + field: 'avg-memory-usage', + id: 'var-used', + name: 'used', + }, + ], + }, + ], + split_mode: 'everything', + }, + { + id: 'podcap', + metrics: [ + { + field: 'kubernetes.node.pod.capacity.total', + id: 'max-pod-cap', + type: InfraMetricModelMetricType.max, + }, + { + field: 'kubernetes.pod.name', + id: 'card-pod-name', + type: InfraMetricModelMetricType.cardinality, + }, + { + id: 'calc-used-cap', + script: 'params.used / params.cap', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'max-pod-cap', + id: 'var-cap', + name: 'cap', + }, + { + field: 'card-pod-name', + id: 'var-used', + name: 'used', + }, + ], + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_pod_cap.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_pod_cap.ts new file mode 100644 index 0000000000000..76e37e7d5f8b8 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_k8s_pod_cap.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostK8sPodCap: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostK8sPodCap', + requires: ['kubernetes.node'], + map_field_to: 'kubernetes.node.name', + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + + series: [ + { + id: 'capacity', + metrics: [ + { + field: 'kubernetes.node.pod.allocatable.total', + id: 'max-pod-cap', + type: InfraMetricModelMetricType.max, + }, + ], + split_mode: 'everything', + }, + { + id: 'used', + metrics: [ + { + field: 'kubernetes.pod.name', + id: 'avg-pod', + type: InfraMetricModelMetricType.cardinality, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_load.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_load.ts new file mode 100644 index 0000000000000..3ee043c5676d5 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_load.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostLoad: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostLoad', + requires: ['system.cpu'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'load_1m', + metrics: [ + { + field: 'system.load.1', + id: 'avg-load-1m', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + { + id: 'load_5m', + metrics: [ + { + field: 'system.load.5', + id: 'avg-load-5m', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + { + id: 'load_15m', + metrics: [ + { + field: 'system.load.15', + id: 'avg-load-15m', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_memory_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_memory_usage.ts new file mode 100644 index 0000000000000..862381df1280d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_memory_usage.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostMemoryUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostMemoryUsage', + requires: ['system.memory'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'free', + metrics: [ + { + field: 'system.memory.free', + id: 'avg-memory-free', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + { + id: 'used', + metrics: [ + { + field: 'system.memory.actual.used.bytes', + id: 'avg-memory-used', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + { + id: 'cache', + metrics: [ + { + field: 'system.memory.actual.used.bytes', + id: 'avg-memory-actual-used', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.memory.used.bytes', + id: 'avg-memory-used', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'calc-used-actual', + script: 'params.used - params.actual', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'avg-memory-actual-used', + id: 'var-actual', + name: 'actual', + }, + { + field: 'avg-memory-used', + id: 'var-used', + name: 'used', + }, + ], + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_network_traffic.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_network_traffic.ts new file mode 100644 index 0000000000000..98d10a96b3fe1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_network_traffic.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostNetworkTraffic: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostNetworkTraffic', + requires: ['system.network'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'tx', + metrics: [ + { + field: 'system.network.out.bytes', + id: 'max-net-out', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-net-out', + id: 'deriv-max-net-out', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-out', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-net-out' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + { + function: 'sum', + id: 'seriesagg-sum', + type: InfraMetricModelMetricType.series_agg, + }, + ], + split_mode: 'terms', + terms_field: 'system.network.name', + }, + { + id: 'rx', + label: 'Inbound (RX)', + metrics: [ + { + field: 'system.network.in.bytes', + id: 'max-net-in', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-net-in', + id: 'deriv-max-net-in', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-in', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-net-in' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + { + id: 'calc-invert-rate', + script: 'params.rate * -1', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'posonly-deriv-max-net-in', + id: 'var-rate', + name: 'rate', + }, + ], + }, + { + function: 'sum', + id: 'seriesagg-sum', + type: InfraMetricModelMetricType.series_agg, + }, + ], + split_mode: 'terms', + terms_field: 'system.network.name', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_system_overview.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_system_overview.ts new file mode 100644 index 0000000000000..8244fec21e5a9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/host/host_system_overview.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const hostSystemOverview: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'hostSystemOverview', + requires: ['system.cpu', 'system.memory', 'system.load', 'system.network'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'gauge', + series: [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'system.cpu.user.pct', + id: 'avg-cpu-user', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'system.cpu.cores', + id: 'max-cpu-cores', + type: InfraMetricModelMetricType.max, + }, + { + field: 'system.cpu.system.pct', + id: 'avg-cpu-system', + type: InfraMetricModelMetricType.avg, + }, + { + id: 'calc-user-system-cores', + script: '(params.users + params.system) / params.cores', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'avg-cpu-user', + id: 'var-users', + name: 'users', + }, + { + field: 'avg-cpu-system', + id: 'var-system', + name: 'system', + }, + { + field: 'max-cpu-cores', + id: 'var-cores', + name: 'cores', + }, + ], + }, + ], + }, + { + id: 'load', + split_mode: 'everything', + metrics: [ + { + field: 'system.load.5', + id: 'avg-load-5m', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'memory', + split_mode: 'everything', + metrics: [ + { + field: 'system.memory.actual.used.pct', + id: 'avg-memory-actual-used', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'rx', + split_mode: 'terms', + terms_field: 'system.network.name', + metrics: [ + { + field: 'system.network.in.bytes', + id: 'max-net-in', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-net-in', + id: 'deriv-max-net-in', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-in', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-net-in' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + { + function: 'sum', + id: 'seriesagg-sum', + type: InfraMetricModelMetricType.series_agg, + }, + ], + }, + { + id: 'tx', + split_mode: 'terms', + terms_field: 'system.network.name', + metrics: [ + { + field: 'system.network.out.bytes', + id: 'max-net-out', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-net-out', + id: 'deriv-max-net-out', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-out', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-net-out' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + { + function: 'sum', + id: 'seriesagg-sum', + type: InfraMetricModelMetricType.series_agg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/index.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/index.ts new file mode 100644 index 0000000000000..0a1c6fab14a84 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetric } from '../../../../../common/graphql/types'; +import { InfraMetricModelCreator } from '../adapter_types'; + +import { hostCpuUsage } from './host/host_cpu_usage'; +import { hostFilesystem } from './host/host_filesystem'; +import { hostK8sCpuCap } from './host/host_k8s_cpu_cap'; +import { hostK8sDiskCap } from './host/host_k8s_disk_cap'; +import { hostK8sMemoryCap } from './host/host_k8s_memory_cap'; +import { hostK8sOverview } from './host/host_k8s_overview'; +import { hostK8sPodCap } from './host/host_k8s_pod_cap'; +import { hostLoad } from './host/host_load'; +import { hostMemoryUsage } from './host/host_memory_usage'; +import { hostNetworkTraffic } from './host/host_network_traffic'; +import { hostSystemOverview } from './host/host_system_overview'; + +import { podCpuUsage } from './pod/pod_cpu_usage'; +import { podLogUsage } from './pod/pod_log_usage'; +import { podMemoryUsage } from './pod/pod_memory_usage'; +import { podNetworkTraffic } from './pod/pod_network_traffic'; +import { podOverview } from './pod/pod_overview'; + +import { containerCpuKernel } from './container/container_cpu_kernel'; +import { containerCpuUsage } from './container/container_cpu_usage'; +import { containerDiskIOBytes } from './container/container_disk_io_bytes'; +import { containerDiskIOOps } from './container/container_diskio_ops'; +import { containerMemory } from './container/container_memory'; +import { containerNetworkTraffic } from './container/container_network_traffic'; +import { containerOverview } from './container/container_overview'; +import { nginxActiveConnections } from './nginx/nginx_active_connections'; +import { nginxHits } from './nginx/nginx_hits'; +import { nginxRequestRate } from './nginx/nginx_request_rate'; +import { nginxRequestsPerConnection } from './nginx/nginx_requests_per_connection'; + +interface InfraMetricModels { + [key: string]: InfraMetricModelCreator; +} + +export const metricModels: InfraMetricModels = { + [InfraMetric.hostSystemOverview]: hostSystemOverview, + [InfraMetric.hostCpuUsage]: hostCpuUsage, + [InfraMetric.hostFilesystem]: hostFilesystem, + [InfraMetric.hostK8sOverview]: hostK8sOverview, + [InfraMetric.hostK8sCpuCap]: hostK8sCpuCap, + [InfraMetric.hostK8sDiskCap]: hostK8sDiskCap, + [InfraMetric.hostK8sMemoryCap]: hostK8sMemoryCap, + [InfraMetric.hostK8sPodCap]: hostK8sPodCap, + [InfraMetric.hostLoad]: hostLoad, + [InfraMetric.hostMemoryUsage]: hostMemoryUsage, + [InfraMetric.hostNetworkTraffic]: hostNetworkTraffic, + + [InfraMetric.podOverview]: podOverview, + [InfraMetric.podCpuUsage]: podCpuUsage, + [InfraMetric.podMemoryUsage]: podMemoryUsage, + [InfraMetric.podLogUsage]: podLogUsage, + [InfraMetric.podNetworkTraffic]: podNetworkTraffic, + + [InfraMetric.containerCpuKernel]: containerCpuKernel, + [InfraMetric.containerCpuUsage]: containerCpuUsage, + [InfraMetric.containerDiskIOBytes]: containerDiskIOBytes, + [InfraMetric.containerDiskIOOps]: containerDiskIOOps, + [InfraMetric.containerNetworkTraffic]: containerNetworkTraffic, + [InfraMetric.containerMemory]: containerMemory, + [InfraMetric.containerOverview]: containerOverview, + [InfraMetric.nginxHits]: nginxHits, + [InfraMetric.nginxRequestRate]: nginxRequestRate, + [InfraMetric.nginxActiveConnections]: nginxActiveConnections, + [InfraMetric.nginxRequestsPerConnection]: nginxRequestsPerConnection, +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_active_connections.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_active_connections.ts new file mode 100644 index 0000000000000..befc776661f74 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_active_connections.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const nginxActiveConnections: InfraMetricModelCreator = ( + timeField, + indexPattern, + interval +) => ({ + id: 'nginxActiveConnections', + requires: ['nginx.stubstatus'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'connections', + metrics: [ + { + field: 'nginx.stubstatus.active', + id: 'avg-active', + type: InfraMetricModelMetricType.avg, + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_hits.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_hits.ts new file mode 100644 index 0000000000000..2f24c504db184 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_hits.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const nginxHits: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'nginxHits', + requires: ['nginx.access'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: '200s', + metrics: [ + { + id: 'count-200', + type: InfraMetricModelMetricType.count, + }, + ], + split_mode: 'filter', + filter: 'nginx.access.response_code:[200 TO 299]', + }, + { + id: '300s', + metrics: [ + { + id: 'count-300', + type: InfraMetricModelMetricType.count, + }, + ], + split_mode: 'filter', + filter: 'nginx.access.response_code:[300 TO 399]', + }, + { + id: '400s', + metrics: [ + { + id: 'count-400', + type: InfraMetricModelMetricType.count, + }, + ], + split_mode: 'filter', + filter: 'nginx.access.response_code:[400 TO 499]', + }, + { + id: '500s', + metrics: [ + { + id: 'count-500', + type: InfraMetricModelMetricType.count, + }, + ], + split_mode: 'filter', + filter: 'nginx.access.response_code:[500 TO 599]', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_request_rate.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_request_rate.ts new file mode 100644 index 0000000000000..aca3d5432c837 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_request_rate.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const nginxRequestRate: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'nginxRequestRate', + requires: ['nginx.stubstatus'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'rate', + metrics: [ + { + field: 'nginx.stubstatus.requests', + id: 'max-requests', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-requests', + id: 'derv-max-requests', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-derv-max-requests', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'derv-max-requests' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_requests_per_connection.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_requests_per_connection.ts new file mode 100644 index 0000000000000..03d8a9458d21a --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/nginx/nginx_requests_per_connection.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const nginxRequestsPerConnection: InfraMetricModelCreator = ( + timeField, + indexPattern, + interval +) => ({ + id: 'nginxRequestsPerConnection', + requires: ['nginx.stubstatus'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'reqPerConns', + metrics: [ + { + field: 'nginx.stubstatus.handled', + id: 'max-handled', + type: InfraMetricModelMetricType.max, + }, + { + field: 'nginx.stubstatus.requests', + id: 'max-requests', + type: InfraMetricModelMetricType.max, + }, + { + id: 'reqs-per-connection', + type: InfraMetricModelMetricType.calculation, + variables: [ + { id: 'var-handled', name: 'handled', field: 'max-handled' }, + { id: 'var-requests', name: 'requests', field: 'max-requests' }, + ], + script: + 'params.handled > 0.0 && params.requests > 0.0 ? params.handled / params.requests : 0.0', + }, + ], + split_mode: 'everything', + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_cpu_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_cpu_usage.ts new file mode 100644 index 0000000000000..7b766e3dbda43 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_cpu_usage.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const podCpuUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'podCpuUsage', + requires: ['kubernetes.pod'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.cpu.usage.node.pct', + id: 'avg-cpu-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_log_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_log_usage.ts new file mode 100644 index 0000000000000..19ce51226919e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_log_usage.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const podLogUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'podLogUsage', + requires: ['kubernetes.pod'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'logs', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.container.logs.used.bytes', + id: 'avg-log-used', + type: InfraMetricModelMetricType.avg, + }, + { + field: 'kubernetes.container.logs.capacity.bytes', + id: 'max-log-cap', + type: InfraMetricModelMetricType.max, + }, + { + id: 'calc-usage-limit', + script: 'params.usage / params.limit', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'avg-log-userd', + id: 'var-usage', + name: 'usage', + }, + { + field: 'max-log-cap', + id: 'var-limit', + name: 'limit', + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_memory_usage.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_memory_usage.ts new file mode 100644 index 0000000000000..a8ff514877a32 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_memory_usage.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const podMemoryUsage: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'podMemoryUsage', + requires: ['kubernetes.pod'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'memory', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.memory.usage.node.pct', + id: 'avg-memory-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_network_traffic.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_network_traffic.ts new file mode 100644 index 0000000000000..d83695d4590f2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_network_traffic.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const podNetworkTraffic: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'podNetworkTraffic', + requires: ['kubernetes.pod'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.network.tx.bytes', + id: 'max-network-tx', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-network-tx', + id: 'deriv-max-network-tx', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-tx', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-network-tx' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.network.rx.bytes', + id: 'max-network-rx', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-network-rx', + id: 'deriv-max-network-rx', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-net-tx', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-network-tx' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + { + id: 'invert-posonly-deriv-max-network-rx', + script: 'params.rate * -1', + type: InfraMetricModelMetricType.calculation, + variables: [ + { + field: 'posonly-deriv-max-network-rx', + id: 'var-rate', + name: 'rate', + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_overview.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_overview.ts new file mode 100644 index 0000000000000..c94def091d4c9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/models/pod/pod_overview.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricModelCreator, InfraMetricModelMetricType } from '../../adapter_types'; + +export const podOverview: InfraMetricModelCreator = (timeField, indexPattern, interval) => ({ + id: 'podOverview', + requires: ['kubernetes.pod'], + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series: [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.cpu.usage.node.pct', + id: 'avg-cpu-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'memory', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.memory.usage.node.pct', + id: 'avg-memory-usage', + type: InfraMetricModelMetricType.avg, + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.network.rx.bytes', + id: 'max-network-rx', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-network-rx', + id: 'deriv-max-network-rx', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-network-rx', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-network-rx' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + ], + }, + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'kubernetes.pod.network.tx.bytes', + id: 'max-network-tx', + type: InfraMetricModelMetricType.max, + }, + { + field: 'max-network-tx', + id: 'deriv-max-network-tx', + type: InfraMetricModelMetricType.derivative, + unit: '1s', + }, + { + id: 'posonly-deriv-max-network-tx', + type: InfraMetricModelMetricType.calculation, + variables: [{ id: 'var-rate', name: 'rate', field: 'deriv-max-network-tx' }], + script: 'params.rate > 0.0 ? params.rate : 0.0', + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/adapter_types.ts new file mode 100644 index 0000000000000..9d43317efe084 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/adapter_types.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraMetricInput, + InfraNode, + InfraPathFilterInput, + InfraPathInput, + InfraPathType, + InfraTimerangeInput, +} from '../../../../common/graphql/types'; +import { JsonObject } from '../../../../common/typed_json'; +import { InfraSourceConfiguration } from '../../sources'; +import { InfraFrameworkRequest } from '../framework'; + +export interface InfraNodesAdapter { + getNodes(req: InfraFrameworkRequest, options: InfraNodeRequestOptions): Promise; +} + +export interface InfraHostsFieldsObject { + name?: any; + metrics?: any; + groups?: [any]; +} + +export type InfraESQuery = + | InfraESBoolQuery + | InfraESRangeQuery + | InfraESExistsQuery + | InfraESQueryStringQuery + | InfraESMatchQuery + | JsonObject; + +export interface InfraESExistsQuery { + exists: { field: string }; +} + +export interface InfraESQueryStringQuery { + query_string: { + query: string; + analyze_wildcard: boolean; + }; +} + +export interface InfraESRangeQuery { + range: { + [name: string]: { + gte: number; + lte: number; + format: string; + }; + }; +} + +export interface InfraESMatchQuery { + match: { + [name: string]: { + query: string; + }; + }; +} + +export interface InfraESBoolQuery { + bool: { + must?: InfraESQuery[]; + should?: InfraESQuery[]; + filter?: InfraESQuery[]; + }; +} + +export interface InfraESMSearchHeader { + index: string[] | string; +} + +export interface InfraESSearchBody { + query?: object; + aggregations?: object; + aggs?: object; + size?: number; +} + +export type InfraESMSearchBody = InfraESSearchBody | InfraESMSearchHeader; + +export interface InfraNodeRequestOptions { + nodeType: InfraNodeType; + sourceConfiguration: InfraSourceConfiguration; + timerange: InfraTimerangeInput; + groupBy: InfraPathInput[]; + metric: InfraMetricInput; + filterQuery: InfraESQuery | undefined; +} + +export enum InfraNodesKey { + hosts = 'hosts', + pods = 'pods', + containers = 'containers', +} + +export enum InfraNodeType { + host = 'host', + pod = 'pod', + container = 'container', +} + +export interface InfraNodesAggregations { + waffle: { + nodes: { + buckets: InfraBucket[]; + }; + }; +} + +export type InfraProcessorTransformer = (doc: T) => T; + +export type InfraProcessorChainFn = ( + next: InfraProcessorTransformer +) => InfraProcessorTransformer; + +export type InfraProcessor = (options: O) => InfraProcessorChainFn; + +export interface InfraProcesorRequestOptions { + nodeType: InfraNodeType; + nodeOptions: InfraNodeRequestOptions; + partitionId: number; + numberOfPartitions: number; + nodeField: string; +} + +export interface InfraGroupByFilters { + id: string /** The UUID for the group by object */; + type: InfraPathType /** The type of aggregation to use to bucket the groups */; + label?: + | string + | null /** The label to use in the results for the group by for the terms group by */; + filters: InfraPathFilterInput[] /** The filters to use for the group by aggregation, this is ignored by the terms group by */; +} + +export interface InfraGroupByTerms { + id: string /** The UUID for the group by object */; + type: InfraPathType /** The type of aggregation to use to bucket the groups */; + label?: + | string + | null /** The label to use in the results for the group by for the terms group by */; + field: string; +} + +export interface InfraBucketWithKey { + key: string | number; + doc_count: number; +} + +export interface InfraBucketWithAggs { + [name: string]: { + buckets: InfraBucket[]; + }; +} + +export interface InfraBucketWithValues { + [name: string]: { value: number; normalized_value?: number }; +} + +export type InfraBucket = InfraBucketWithAggs & InfraBucketWithKey & InfraBucketWithValues; + +export interface InfraGroupWithNodes { + name: string; + nodes: InfraNode[]; +} + +export interface InfraGroupWithSubGroups { + name: string; + groups: InfraGroupWithNodes[]; +} + +export type InfraNodeGroup = InfraGroupWithNodes | InfraGroupWithSubGroups; + +export interface InfraNodesResponse { + total?: number; +} + +export interface InfraGroupsResponse { + total: number; + groups: InfraNodeGroup[]; +} + +export interface InfraNodesOnlyResponse { + total: number; + nodes: InfraNode[]; +} + +export interface InfraAvgAgg { + avg: { field: string }; +} + +export interface InfraMaxAgg { + max: { field: string }; +} + +export interface InfraDerivativeAgg { + derivative: { + buckets_path: string; + gap_policy: string; + unit: string; + }; +} + +export interface InfraCumulativeSumAgg { + cumulative_sum: { + buckets_path: string; + }; +} + +export interface InfraBucketScriptAgg { + bucket_script: { + buckets_path: { [key: string]: string }; + script: { + source: string; + lang: string; + }; + gap_policy: string; + }; +} + +export type InfraAgg = + | InfraBucketScriptAgg + | InfraDerivativeAgg + | InfraAvgAgg + | InfraMaxAgg + | InfraCumulativeSumAgg; +export interface InfraNodeMetricAgg { + [key: string]: InfraAgg; +} + +export type InfraNodeMetricFn = (nodeType: InfraNodeType) => InfraNodeMetricAgg | undefined; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/constants.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/constants.ts new file mode 100644 index 0000000000000..75d8464e8efe5 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: Make NODE_REQUEST_PARTITION_SIZE configurable from kibana.yml +export const NODE_REQUEST_PARTITION_SIZE = 75; +export const NODE_REQUEST_PARTITION_FACTOR = 1.2; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/elasticsearch_nodes_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/elasticsearch_nodes_adapter.ts new file mode 100644 index 0000000000000..97459ad0cc04b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/elasticsearch_nodes_adapter.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { + InfraBucket, + InfraNodeRequestOptions, + InfraNodesAdapter, + InfraNodesAggregations, +} from './adapter_types'; + +import { InfraNode } from '../../../../common/graphql/types'; +import { calculateCardinalityOfNodeField } from './lib/calculate_cardinality'; +import { createPartitionBodies } from './lib/create_partition_bodies'; +import { processNodes } from './lib/process_nodes'; + +export class ElasticsearchNodesAdapter implements InfraNodesAdapter { + private framework: InfraBackendFrameworkAdapter; + constructor(framework: InfraBackendFrameworkAdapter) { + this.framework = framework; + } + + public async getNodes( + req: InfraFrameworkRequest, + options: InfraNodeRequestOptions + ): Promise { + const search = (searchOptions: object) => + this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions); + const msearch = (msearchOptions: object) => + this.framework.callWithRequest<{}, Aggregation>(req, 'msearch', msearchOptions); + + const nodeField = options.sourceConfiguration.fields[options.nodeType]; + const totalNodes = await calculateCardinalityOfNodeField(search, nodeField, options); + + if (totalNodes === 0) { + return []; + } + + const body = createPartitionBodies(totalNodes, options.nodeType, nodeField, options); + + const response = await msearch({ + body, + }); + + if (response && response.responses) { + const nodeBuckets: InfraBucket[] = response.responses.reduce( + (current: InfraBucket[], resp) => { + if (!resp.aggregations) { + return current; + } + const buckets = resp.aggregations.waffle.nodes.buckets; + return current.concat(buckets); + }, + [] + ); + return processNodes(options, nodeBuckets); + } + + return []; + } +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/extract_group_by_and_node_from_path.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/extract_group_by_and_node_from_path.ts new file mode 100644 index 0000000000000..60fa22b4116e4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/extract_group_by_and_node_from_path.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { InfraPathInput, InfraPathType } from '../../../../common/graphql/types'; +import { InfraNodeType } from './adapter_types'; + +const getNodeType = (type: InfraPathType): InfraNodeType => { + switch (type) { + case InfraPathType.pods: + return InfraNodeType.pod; + case InfraPathType.containers: + return InfraNodeType.container; + case InfraPathType.hosts: + return InfraNodeType.host; + default: + throw new Error('Invalid InfraPathType'); + } +}; + +const isEntityType = (path: InfraPathInput) => { + if (!path) { + return false; + } + switch (path.type) { + case InfraPathType.containers: + case InfraPathType.hosts: + case InfraPathType.pods: + return true; + default: + return false; + } +}; + +const moreThenOneEntityType = (path: InfraPathInput[]) => { + return path.filter(isEntityType).length > 1; +}; + +export function extractGroupByAndNodeFromPath(path: InfraPathInput[]) { + if (moreThenOneEntityType(path)) { + throw new Error('There can be only one entity type in the path.'); + } + if (path.length > 3) { + throw new Error('The path can only have a maximum of 3 elements.'); + } + const nodePart = path[path.length - 1]; + if (!isEntityType(nodePart)) { + throw new Error( + 'The last element in the path should be either a "hosts", "containers" or "pods" path type.' + ); + } + const nodeType = getNodeType(nodePart.type); + const groupBy = path.slice(0, path.length - 1); + return { groupBy, nodeType }; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/index.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/index.ts new file mode 100644 index 0000000000000..4e09b5d0e9e2d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './adapter_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/calculate_cardinality.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/calculate_cardinality.ts new file mode 100644 index 0000000000000..b1707727ff616 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/calculate_cardinality.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraDatabaseSearchResponse } from '../../framework'; +import { InfraESQuery, InfraNodeRequestOptions } from '../adapter_types'; +import { createQuery } from './create_query'; + +interface CardinalityOfFieldParams { + size: number; + query: InfraESQuery; + aggs: { + nodeCount: { cardinality: { field: string } }; + }; +} + +interface CardinalityAggregation { + nodeCount: { value: number }; +} + +export async function calculateCardinalityOfNodeField( + search: (options: object) => Promise>, + nodeField: string, + options: InfraNodeRequestOptions +): Promise { + const { sourceConfiguration }: InfraNodeRequestOptions = options; + const body: CardinalityOfFieldParams = { + aggs: { + nodeCount: { + cardinality: { field: nodeField }, + }, + }, + query: createQuery(options), + size: 0, + }; + + const resp = await search({ + body, + index: [sourceConfiguration.logAlias, sourceConfiguration.metricAlias], + }); + + if (resp.aggregations) { + return resp.aggregations.nodeCount.value; + } + + return 0; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/convert_nodes_response_to_groups.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/convert_nodes_response_to_groups.ts new file mode 100644 index 0000000000000..9ee00be17b917 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/convert_nodes_response_to_groups.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { InfraNode } from '../../../../../common/graphql/types'; +import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types'; +import { extractGroupPaths } from './extract_group_paths'; + +export function convertNodesResponseToGroups( + options: InfraNodeRequestOptions, + nodes: InfraBucket[] +): InfraNode[] { + let results: InfraNode[] = []; + nodes.forEach((node: InfraBucket) => { + const nodesWithPaths = extractGroupPaths(options, node); + results = results.concat(nodesWithPaths); + }); + return results; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_base_path.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_base_path.ts new file mode 100644 index 0000000000000..f8480686cf48e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_base_path.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraPathInput } from '../../../../../common/graphql/types'; +export const createBasePath = (groupBy: InfraPathInput[]) => { + const basePath = ['aggs', 'waffle', 'aggs', 'nodes', 'aggs']; + return groupBy.reduce((acc, group, index) => { + return acc.concat([`path_${index}`, `aggs`]); + }, basePath); +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts new file mode 100644 index 0000000000000..a2d072e0897d7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { last } from 'lodash'; +import { isNumber } from 'lodash'; +import moment from 'moment'; +import { InfraNode, InfraNodeMetric } from '../../../../../common/graphql/types'; +import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types'; +import { getBucketSizeInSeconds } from './get_bucket_size_in_seconds'; + +// TODO: Break these function into seperate files and expand beyond just documnet count +// In the code below it looks like overkill to split these three functions out +// but in reality the create metrics functions will be different per node type. + +const findLastFullBucket = ( + bucket: InfraBucket, + bucketSize: number, + options: InfraNodeRequestOptions +): InfraBucket | undefined => { + const { buckets } = bucket.timeseries; + const to = moment.utc(options.timerange.to); + return buckets.reduce((current, item) => { + const itemKey = isNumber(item.key) ? item.key : parseInt(item.key, 10); + const date = moment.utc(itemKey + bucketSize * 1000); + if (!date.isAfter(to) && item.doc_count > 0) { + return item; + } + return current; + }, last(buckets)); +}; + +function createNodeMetrics( + options: InfraNodeRequestOptions, + node: InfraBucket, + bucket: InfraBucket +): InfraNodeMetric { + const { timerange, metric } = options; + const bucketSize = getBucketSizeInSeconds(timerange.interval); + const lastBucket = findLastFullBucket(bucket, bucketSize, options); + if (!lastBucket) { + throw new Error('Date histogram returned an empty set of buckets.'); + } + const metricObj = lastBucket[metric.type]; + const value = (metricObj && (metricObj.normalized_value || metricObj.value)) || 0; + return { + name: metric.type, + value, + }; +} + +export function createNodeItem( + options: InfraNodeRequestOptions, + node: InfraBucket, + bucket: InfraBucket +): InfraNode { + return { + metric: createNodeMetrics(options, node, bucket), + path: [{ value: node.key }], + } as InfraNode; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_request_body.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_request_body.ts new file mode 100644 index 0000000000000..733e260c5e874 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_request_body.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraESSearchBody, InfraProcesorRequestOptions } from '../adapter_types'; +import { createLastNProcessor } from '../processors/last'; + +export function createNodeRequestBody(options: InfraProcesorRequestOptions): InfraESSearchBody { + const requestProcessor = createLastNProcessor(options); + const doc = {}; + const body = requestProcessor(doc); + return body; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_partition_bodies.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_partition_bodies.ts new file mode 100644 index 0000000000000..83b0ccec28c96 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_partition_bodies.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { times } from 'lodash'; + +import { InfraMetricType } from '../../../../../common/graphql/types'; +import { + InfraESMSearchBody, + InfraNodeRequestOptions, + InfraNodeType, + InfraProcesorRequestOptions, +} from '../adapter_types'; +import { NODE_REQUEST_PARTITION_SIZE } from '../constants'; +import { createNodeRequestBody } from './create_node_request_body'; + +export function createPartitionBodies( + totalNodes: number, + nodeType: InfraNodeType, + nodeField: string, + nodeOptions: InfraNodeRequestOptions +): InfraESMSearchBody[] { + const { sourceConfiguration }: InfraNodeRequestOptions = nodeOptions; + const bodies: InfraESMSearchBody[] = []; + const numberOfPartitions: number = Math.ceil(totalNodes / NODE_REQUEST_PARTITION_SIZE); + const indices = + nodeOptions.metric.type === InfraMetricType.logRate + ? [sourceConfiguration.logAlias] + : [sourceConfiguration.logAlias, sourceConfiguration.metricAlias]; + times( + numberOfPartitions, + (partitionId: number): void => { + const processorOptions: InfraProcesorRequestOptions = { + nodeType, + nodeField, + nodeOptions, + numberOfPartitions, + partitionId, + }; + bodies.push({ + index: indices, + }); + bodies.push(createNodeRequestBody(processorOptions)); + } + ); + return bodies; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_query.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_query.ts new file mode 100644 index 0000000000000..0c56172c4c4d0 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_query.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraPathFilterInput, InfraPathInput } from '../../../../../common/graphql/types'; + +import { + InfraESBoolQuery, + InfraESQuery, + InfraESRangeQuery, + InfraNodeRequestOptions, +} from '../adapter_types'; + +import { isGroupByFilters, isGroupByTerms } from './type_guards'; + +export function createQuery(options: InfraNodeRequestOptions): InfraESQuery { + const { timerange, sourceConfiguration, groupBy, filterQuery }: InfraNodeRequestOptions = options; + const mustClause: InfraESQuery[] = []; + const shouldClause: InfraESQuery[] = []; + const filterClause: InfraESQuery[] = []; + + const rangeFilter: InfraESRangeQuery = { + range: { + [sourceConfiguration.fields.timestamp]: { + format: 'epoch_millis', + gte: timerange.from, + lte: timerange.to, + }, + }, + }; + + filterClause.push(rangeFilter); + + if (groupBy) { + groupBy.forEach( + (group: InfraPathInput): void => { + if (isGroupByTerms(group) && group.field) { + mustClause.push({ + exists: { + field: group.field, + }, + }); + } + if (isGroupByFilters(group) && group.filters) { + group.filters!.forEach( + (groupFilter: InfraPathFilterInput | null): void => { + if (groupFilter != null && groupFilter.query) { + shouldClause.push({ + query_string: { + analyze_wildcard: true, + query: groupFilter.query, + }, + }); + } + } + ); + } + } + ); + } + + if (filterQuery) { + mustClause.push(filterQuery); + } + + const query: InfraESBoolQuery = { + bool: { + filter: filterClause, + must: mustClause, + should: shouldClause, + }, + }; + + return query; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/extract_group_paths.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/extract_group_paths.ts new file mode 100644 index 0000000000000..016a109e6e5a2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/extract_group_paths.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNode, InfraPathInput } from '../../../../../common/graphql/types'; +import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types'; +import { createNodeItem } from './create_node_item'; + +export interface InfraPathItem { + path: string[]; + nodeItem: InfraNode; +} + +export function extractGroupPaths( + options: InfraNodeRequestOptions, + node: InfraBucket +): InfraNode[] { + const { groupBy } = options; + const secondGroup: InfraPathInput = groupBy[1]; + const paths: InfraNode[] = node.path_0.buckets.reduce( + (acc: InfraNode[], bucket: InfraBucket, index: number): InfraNode[] => { + const key: string = (bucket.key || index).toString(); + if (secondGroup) { + return acc.concat( + bucket.path_1.buckets.map( + (b: InfraBucket): InfraNode => { + const innerNode = createNodeItem(options, node, b); + const nodePath = [ + { value: bucket.key.toString() }, + { value: b.key.toString() }, + ].concat(innerNode.path); + return { + ...innerNode, + path: nodePath, + }; + } + ) + ); + } + const nodeItem = createNodeItem(options, node, bucket); + const path = [{ value: key }].concat(nodeItem.path); + return acc.concat({ + ...nodeItem, + path, + }); + }, + [] + ); + return paths; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/get_bucket_size_in_seconds.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/get_bucket_size_in_seconds.ts new file mode 100644 index 0000000000000..667f2d2f35745 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/get_bucket_size_in_seconds.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const intervalUnits = ['y', 'M', 'w', 'd', 'h', 'm', 's', 'ms']; +const INTERVAL_STRING_RE = new RegExp('^([0-9\\.]*)\\s*(' + intervalUnits.join('|') + ')$'); + +interface UnitsToSeconds { + [unit: string]: number; +} + +const units: UnitsToSeconds = { + ms: 0.001, + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 86400 * 7, + M: 86400 * 30, + y: 86400 * 356, +}; + +export const getBucketSizeInSeconds = (interval: string): number => { + const matches = interval.match(INTERVAL_STRING_RE); + if (matches) { + return parseFloat(matches[1]) * units[matches[2]]; + } + throw new Error('Invalid interval string format.'); +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/process_nodes.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/process_nodes.ts new file mode 100644 index 0000000000000..ef55628cb744d --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/process_nodes.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNode } from '../../../../../common/graphql/types'; +import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types'; +import { convertNodesResponseToGroups } from './convert_nodes_response_to_groups'; +import { createNodeItem } from './create_node_item'; + +export function processNodes(options: InfraNodeRequestOptions, nodes: any[]): InfraNode[] { + if (options.groupBy.length === 0) { + // If there are NO group by options then we need to return a + // nodes only response + const nodeResults: InfraNode[] = nodes.map( + (node: InfraBucket): InfraNode => { + return createNodeItem(options, node, node); + } + ); + return nodeResults; + } + + // Return a grouped response + return convertNodesResponseToGroups(options, nodes); +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/type_guards.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/type_guards.ts new file mode 100644 index 0000000000000..14423afbe0ff3 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/type_guards.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraPathInput, InfraPathType } from '../../../../../common/graphql/types'; +import { InfraGroupByFilters, InfraGroupByTerms } from '../adapter_types'; + +export function isGroupByFilters(value: InfraPathInput): value is InfraGroupByFilters { + return value.type === InfraPathType.filters; +} + +export function isGroupByTerms(value: InfraPathInput): value is InfraGroupByTerms { + return value.type === InfraPathType.terms; +} diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/count.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/count.ts new file mode 100644 index 0000000000000..3e64c50887956 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/count.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +export const count: InfraNodeMetricFn = (nodeType: InfraNodeType) => { + return { + count: { + bucket_script: { + buckets_path: { count: '_count' }, + script: { + source: 'count * 1', + lang: 'expression', + }, + gap_policy: 'skip', + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/cpu.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/cpu.ts new file mode 100644 index 0000000000000..4b3d64018d09e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/cpu.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +const FIELDS = { + [InfraNodeType.host]: 'system.cpu.user.pct', + [InfraNodeType.pod]: 'kubernetes.pod.cpu.usage.node.pct', + [InfraNodeType.container]: 'docker.cpu.user.pct', +}; + +export const cpu: InfraNodeMetricFn = (nodeType: InfraNodeType) => { + const field = FIELDS[nodeType]; + return { cpu: { avg: { field } } }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/index.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/index.ts new file mode 100644 index 0000000000000..da520d1d64941 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricType } from '../../../../../common/graphql/types'; +import { count } from './count'; +import { cpu } from './cpu'; +import { load } from './load'; +import { logRate } from './log_rate'; +import { memory } from './memory'; +import { rx } from './rx'; +import { tx } from './tx'; + +export const metricAggregationCreators = { + [InfraMetricType.count]: count, + [InfraMetricType.cpu]: cpu, + [InfraMetricType.memory]: memory, + [InfraMetricType.rx]: rx, + [InfraMetricType.tx]: tx, + [InfraMetricType.load]: load, + [InfraMetricType.logRate]: logRate, +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/load.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/load.ts new file mode 100644 index 0000000000000..6b68fd5931b95 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/load.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +const FIELDS = { + [InfraNodeType.host]: 'system.load.5', + [InfraNodeType.pod]: '', + [InfraNodeType.container]: '', +}; + +export const load: InfraNodeMetricFn = (nodeType: InfraNodeType) => { + const field = FIELDS[nodeType]; + if (field) { + return { load: { avg: { field } } }; + } +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/log_rate.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/log_rate.ts new file mode 100644 index 0000000000000..d447997e47734 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/log_rate.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +export const logRate: InfraNodeMetricFn = (nodeType: InfraNodeType) => { + return { + count: { + bucket_script: { + buckets_path: { count: '_count' }, + script: { + source: 'count * 1', + lang: 'expression', + }, + gap_policy: 'skip', + }, + }, + cumsum: { + cumulative_sum: { + buckets_path: 'count', + }, + }, + logRate: { + derivative: { + buckets_path: 'cumsum', + gap_policy: 'skip', + unit: '1s', + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/memory.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/memory.ts new file mode 100644 index 0000000000000..175a9c7e96920 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/memory.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +const FIELDS = { + [InfraNodeType.host]: 'system.memory.actual.used.pct', + [InfraNodeType.pod]: 'kubernetes.pod.memory.usage.node.pct', + [InfraNodeType.container]: 'docker.memory.usage.pct', +}; + +export const memory: InfraNodeMetricFn = (nodeType: InfraNodeType) => { + const field = FIELDS[nodeType]; + return { memory: { avg: { field } } }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rate.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rate.ts new file mode 100644 index 0000000000000..9a315a9145992 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rate.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeMetricFn, InfraNodeType } from '../adapter_types'; + +interface Fields { + [InfraNodeType.container]: string; + [InfraNodeType.pod]: string; + [InfraNodeType.host]: string; +} + +export const rate = (id: string, fields: Fields): InfraNodeMetricFn => ( + nodeType: InfraNodeType +) => { + const field = fields[nodeType]; + if (field) { + return { + [`${id}_max`]: { max: { field } }, + [`${id}_deriv`]: { + derivative: { + buckets_path: `${id}_max`, + gap_policy: 'skip', + unit: '1s', + }, + }, + [id]: { + bucket_script: { + buckets_path: { value: `${id}_deriv[normalized_value]` }, + script: { + source: 'params.value > 0.0 ? params.value : 0.0', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, + }; + } +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rx.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rx.ts new file mode 100644 index 0000000000000..ab797a7b97a1b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/rx.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { InfraNodeType } from '../adapter_types'; +import { rate } from './rate'; + +const FIELDS = { + [InfraNodeType.host]: 'system.network.in.bytes', + [InfraNodeType.pod]: 'kubernetes.pod.network.rx.bytes', + [InfraNodeType.container]: 'docker.network.in.bytes', +}; + +export const rx = rate('rx', FIELDS); diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/tx.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/tx.ts new file mode 100644 index 0000000000000..32ea4d3e9d303 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/metric_aggregation_creators/tx.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeType } from '../adapter_types'; +import { rate } from './rate'; + +const FIELDS = { + [InfraNodeType.host]: 'system.network.out.bytes', + [InfraNodeType.pod]: 'kubernetes.pod.network.tx.bytes', + [InfraNodeType.container]: 'docker.network.out.bytes', +}; + +export const tx = rate('tx', FIELDS); diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/field_filter_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/field_filter_processor.ts new file mode 100644 index 0000000000000..0cd448d58b257 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/field_filter_processor.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; + +import { InfraESSearchBody, InfraProcesorRequestOptions } from '../../adapter_types'; + +export const fieldsFilterProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + /* + TODO: Need to add the filter logic to find all the fields the user is requesting + and then add an exists filter for each. That way we are only looking at documents + that have the correct fields. This is because we are having to run a partioned + terms agg at the top level. Normally we wouldn't need to do this because they would + get filter out natually. + */ + set(result, 'aggs.waffle.filter.match_all', {}); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/group_by_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/group_by_processor.ts new file mode 100644 index 0000000000000..0bd07cbeeb66e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/group_by_processor.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; + +import { InfraPathFilterInput, InfraPathInput } from '../../../../../../common/graphql/types'; +import { + InfraESQueryStringQuery, + InfraESSearchBody, + InfraProcesorRequestOptions, +} from '../../adapter_types'; +import { isGroupByFilters, isGroupByTerms } from '../../lib/type_guards'; + +export const groupByProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + const { groupBy } = options.nodeOptions; + let aggs = {}; + set(result, 'aggs.waffle.aggs.nodes.aggs', aggs); + groupBy.forEach((grouping: InfraPathInput, index: number) => { + if (isGroupByTerms(grouping)) { + const termsAgg = { + aggs: {}, + terms: { + field: grouping.field, + size: 10, + }, + }; + set(aggs, `path_${index}`, termsAgg); + aggs = termsAgg.aggs; + } + + if (grouping && isGroupByFilters(grouping)) { + const filtersAgg = { + aggs: {}, + filters: { + filters: grouping.filters!.map( + (filter: InfraPathFilterInput): InfraESQueryStringQuery => { + return { + query_string: { + analyze_wildcard: true, + query: (filter && filter.query) || '*', + }, + }; + } + ), + }, + }; + set(aggs, `${grouping.id}`, filtersAgg); + aggs = filtersAgg.aggs; + } + }); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/nodes_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/nodes_processor.ts new file mode 100644 index 0000000000000..b161e4b04b50e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/nodes_processor.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; + +import { InfraESSearchBody, InfraNodeType, InfraProcesorRequestOptions } from '../../adapter_types'; +import { NODE_REQUEST_PARTITION_FACTOR, NODE_REQUEST_PARTITION_SIZE } from '../../constants'; + +const nodeTypeToField = (options: InfraProcesorRequestOptions): string => { + const { fields } = options.nodeOptions.sourceConfiguration; + switch (options.nodeType) { + case InfraNodeType.pod: + return fields.pod; + case InfraNodeType.container: + return fields.container; + default: + return fields.host; + } +}; + +export const nodesProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + const field = nodeTypeToField(options); + + set(result, 'aggs.waffle.aggs.nodes.terms', { + field, + include: { + num_partitions: options.numberOfPartitions, + partition: options.partitionId, + }, + order: { _key: 'asc' }, + size: NODE_REQUEST_PARTITION_SIZE * NODE_REQUEST_PARTITION_FACTOR, + }); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/query_procssor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/query_procssor.ts new file mode 100644 index 0000000000000..cc35e93c73aa2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/common/query_procssor.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; + +import { InfraESSearchBody, InfraProcesorRequestOptions } from '../../adapter_types'; +import { createQuery } from '../../lib/create_query'; + +export const queryProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + set(result, 'size', 0); + set(result, 'query', createQuery(options.nodeOptions)); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts new file mode 100644 index 0000000000000..31ef88ed3229c --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; +import moment from 'moment'; +import { InfraESSearchBody, InfraProcesorRequestOptions } from '../../adapter_types'; +import { createBasePath } from '../../lib/create_base_path'; +import { getBucketSizeInSeconds } from '../../lib/get_bucket_size_in_seconds'; + +export function getBucketKey(value: number, interval: number, offset = 0) { + return Math.floor((value - offset) / interval) * interval + offset; +} + +export const calculateOffsetInSeconds = (end: number, interval: number) => { + const bucketKey = getBucketKey(end, interval); + return Math.floor(end - interval - bucketKey); +}; + +export const dateHistogramProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + const { timerange, sourceConfiguration, groupBy } = options.nodeOptions; + const bucketSizeInSeconds = getBucketSizeInSeconds(timerange.interval); + const boundsMin = moment + .utc(timerange.from) + .subtract(5 * bucketSizeInSeconds, 's') + .valueOf(); + const path = createBasePath(groupBy).concat('timeseries'); + const bucketOffset = calculateOffsetInSeconds(timerange.from, bucketSizeInSeconds); + const offset = `${Math.floor(bucketOffset)}s`; + set(result, path, { + date_histogram: { + field: sourceConfiguration.fields.timestamp, + interval: timerange.interval, + min_doc_count: 0, + offset, + extended_bounds: { + min: boundsMin, + max: timerange.to, + }, + }, + aggs: {}, + }); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/index.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/index.ts new file mode 100644 index 0000000000000..28fdb432ebe8e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pipe } from 'lodash/fp'; +import { + InfraESSearchBody, + InfraProcesorRequestOptions, + InfraProcessorTransformer, +} from '../../adapter_types'; +import { fieldsFilterProcessor } from '../common/field_filter_processor'; +import { groupByProcessor } from '../common/group_by_processor'; +import { nodesProcessor } from '../common/nodes_processor'; +import { queryProcessor } from '../common/query_procssor'; +import { dateHistogramProcessor } from './date_histogram_processor'; +import { metricBucketsProcessor } from './metric_buckets_processor'; + +export const createLastNProcessor = ( + options: InfraProcesorRequestOptions +): InfraProcessorTransformer => { + return pipe( + fieldsFilterProcessor(options), + nodesProcessor(options), + queryProcessor(options), + groupByProcessor(options), + dateHistogramProcessor(options), + metricBucketsProcessor(options) + ); +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/metric_buckets_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/metric_buckets_processor.ts new file mode 100644 index 0000000000000..57e8cf6012617 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/metric_buckets_processor.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, set } from 'lodash'; +import { InfraESSearchBody, InfraProcesorRequestOptions } from '../../adapter_types'; +import { createBasePath } from '../../lib/create_base_path'; +import { metricAggregationCreators } from '../../metric_aggregation_creators'; + +export const metricBucketsProcessor = (options: InfraProcesorRequestOptions) => { + return (doc: InfraESSearchBody) => { + const result = cloneDeep(doc); + const { metric, groupBy } = options.nodeOptions; + const path = createBasePath(groupBy).concat(['timeseries', 'aggs']); + const aggregationCreator = metricAggregationCreators[metric.type]; + const aggs = aggregationCreator(options.nodeType); + set(result, path, aggs); + return result; + }; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts new file mode 100644 index 0000000000000..5b5af97880659 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceStatusAdapter } from '../../source_status'; +import { + InfraBackendFrameworkAdapter, + InfraDatabaseGetIndicesResponse, + InfraFrameworkRequest, +} from '../framework'; + +export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusAdapter { + constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + + public async getIndexNames(request: InfraFrameworkRequest, aliasName: string) { + const indexMaps = await Promise.all([ + this.framework + .callWithRequest(request, 'indices.getAlias', { + name: aliasName, + filterPath: '*.settings.index.uuid', // to keep the response size as small as possible + }) + .catch(withDefaultIfNotFound({})), + this.framework + .callWithRequest(request, 'indices.get', { + index: aliasName, + filterPath: '*.settings.index.uuid', // to keep the response size as small as possible + }) + .catch(withDefaultIfNotFound({})), + ]); + + return indexMaps.reduce( + (indexNames, indexMap) => [...indexNames, ...Object.keys(indexMap)], + [] as string[] + ); + } + + public async hasAlias(request: InfraFrameworkRequest, aliasName: string) { + return await this.framework.callWithRequest(request, 'indices.existsAlias', { + name: aliasName, + }); + } + + public async hasIndices(request: InfraFrameworkRequest, indexNames: string) { + return (await this.getIndexNames(request, indexNames)).length > 0; + } +} + +const withDefaultIfNotFound = (defaultValue: DefaultValue) => ( + error: any +): DefaultValue => { + if (error && error.status === 404) { + return defaultValue; + } + throw error; +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/source_status/index.ts b/x-pack/plugins/infra/server/lib/adapters/source_status/index.ts new file mode 100644 index 0000000000000..f5adfe190f805 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/source_status/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { InfraElasticsearchSourceStatusAdapter } from './elasticsearch_source_status_adapter'; diff --git a/x-pack/plugins/infra/server/lib/adapters/sources/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/sources/adapter_types.ts new file mode 100644 index 0000000000000..76f17f99acc71 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/sources/adapter_types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceConfiguration } from '../../sources'; + +export type PartialInfraSourceConfigurations = { + default?: PartialInfraDefaultSourceConfiguration; +} & { + [sourceId: string]: PartialInfraSourceConfiguration; +}; + +export type PartialInfraDefaultSourceConfiguration = { + fields?: Partial; +} & Partial>>; + +export type PartialInfraSourceConfiguration = { + fields?: Partial; +} & Pick>; diff --git a/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.test.ts b/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.test.ts new file mode 100644 index 0000000000000..c5ba6aed8b591 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraInmemoryConfigurationAdapter } from '../configuration/inmemory_configuration_adapter'; +import { PartialInfraSourceConfiguration } from './adapter_types'; +import { InfraConfigurationSourcesAdapter } from './configuration_sources_adapter'; + +describe('the InfraConfigurationSourcesAdapter', () => { + test('adds the default source when no sources are configured', async () => { + const sourcesAdapter = new InfraConfigurationSourcesAdapter( + new InfraInmemoryConfigurationAdapter({ sources: {} }) + ); + + expect(await sourcesAdapter.getAll()).toMatchObject({ + default: { + metricAlias: expect.any(String), + logAlias: expect.any(String), + fields: { + container: expect.any(String), + host: expect.any(String), + message: expect.arrayContaining([expect.any(String)]), + pod: expect.any(String), + tiebreaker: expect.any(String), + timestamp: expect.any(String), + }, + }, + }); + }); + + test('adds missing aliases to default source when they are missing from the configuration', async () => { + const sourcesAdapter = new InfraConfigurationSourcesAdapter( + new InfraInmemoryConfigurationAdapter({ + sources: { + default: {} as PartialInfraSourceConfiguration, + }, + }) + ); + + expect(await sourcesAdapter.getAll()).toMatchObject({ + default: { + metricAlias: expect.any(String), + logAlias: expect.any(String), + }, + }); + }); + + test('adds missing fields to default source when they are missing from the configuration', async () => { + const sourcesAdapter = new InfraConfigurationSourcesAdapter( + new InfraInmemoryConfigurationAdapter({ + sources: { + default: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'DIFFERENT_CONTAINER_FIELD', + }, + } as PartialInfraSourceConfiguration, + }, + }) + ); + + expect(await sourcesAdapter.getAll()).toMatchObject({ + default: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'DIFFERENT_CONTAINER_FIELD', + host: expect.any(String), + message: expect.arrayContaining([expect.any(String)]), + pod: expect.any(String), + tiebreaker: expect.any(String), + timestamp: expect.any(String), + }, + }, + }); + }); + + test('adds missing fields to non-default sources when they are missing from the configuration', async () => { + const sourcesAdapter = new InfraConfigurationSourcesAdapter( + new InfraInmemoryConfigurationAdapter({ + sources: { + sourceOne: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'DIFFERENT_CONTAINER_FIELD', + }, + }, + }, + }) + ); + + expect(await sourcesAdapter.getAll()).toMatchObject({ + sourceOne: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'DIFFERENT_CONTAINER_FIELD', + host: expect.any(String), + message: expect.arrayContaining([expect.any(String)]), + pod: expect.any(String), + tiebreaker: expect.any(String), + timestamp: expect.any(String), + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.ts new file mode 100644 index 0000000000000..4b8956b5f9985 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/sources/configuration_sources_adapter.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraSourceConfigurations, InfraSourcesAdapter } from '../../sources'; +import { InfraConfigurationAdapter } from '../configuration'; +import { PartialInfraSourceConfigurations } from './adapter_types'; + +interface ConfigurationWithSources { + sources?: PartialInfraSourceConfigurations; +} + +export class InfraConfigurationSourcesAdapter implements InfraSourcesAdapter { + private readonly configuration: InfraConfigurationAdapter; + + constructor(configuration: InfraConfigurationAdapter) { + this.configuration = configuration; + } + + public async getAll() { + const sourceConfigurations = (await this.configuration.get()).sources || { + default: DEFAULT_SOURCE, + }; + const sourceConfigurationsWithDefault = { + ...sourceConfigurations, + default: { + ...DEFAULT_SOURCE, + ...(sourceConfigurations.default || {}), + }, + } as PartialInfraSourceConfigurations; + + return Object.entries(sourceConfigurationsWithDefault).reduce( + (result, [sourceId, sourceConfiguration]) => + ({ + ...result, + [sourceId]: { + ...sourceConfiguration, + fields: { + ...DEFAULT_FIELDS, + ...(sourceConfiguration.fields || {}), + }, + }, + } as InfraSourceConfigurations), + {} + ); + } +} + +const DEFAULT_FIELDS = { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', +}; + +const DEFAULT_SOURCE = { + metricAlias: 'metricbeat-*', + logAlias: 'filebeat-*', + fields: DEFAULT_FIELDS, +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/sources/index.ts b/x-pack/plugins/infra/server/lib/adapters/sources/index.ts new file mode 100644 index 0000000000000..dcd9262d7ebd2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/adapters/sources/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { InfraConfigurationSourcesAdapter } from './configuration_sources_adapter'; diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts new file mode 100644 index 0000000000000..23c5c3a45bd23 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; + +import { ElasticsearchCapabilitiesAdapter } from '../adapters/capabilities/elasticsearch_capabilities_adapter'; +import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter'; +import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; +import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; +import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; +import { ElasticsearchNodesAdapter } from '../adapters/nodes/elasticsearch_nodes_adapter'; +import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; +import { InfraConfigurationSourcesAdapter } from '../adapters/sources/configuration_sources_adapter'; +import { InfraCapabilitiesDomain } from '../domains/capabilities_domain'; +import { InfraFieldsDomain } from '../domains/fields_domain'; +import { InfraLogEntriesDomain } from '../domains/log_entries_domain'; +import { InfraMetricsDomain } from '../domains/metrics_domain'; +import { InfraNodesDomain } from '../domains/nodes_domain'; +import { InfraBackendLibs, InfraConfiguration, InfraDomainLibs } from '../infra_types'; +import { InfraSourceStatus } from '../source_status'; +import { InfraSources } from '../sources'; + +export function compose(server: Server): InfraBackendLibs { + const configuration = new InfraKibanaConfigurationAdapter(server); + const framework = new InfraKibanaBackendFrameworkAdapter(server); + const sources = new InfraSources(new InfraConfigurationSourcesAdapter(configuration)); + const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { + sources, + }); + + const domainLibs: InfraDomainLibs = { + capabilities: new InfraCapabilitiesDomain(new ElasticsearchCapabilitiesAdapter(framework), { + sources, + }), + fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { + sources, + }), + logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + sources, + }), + nodes: new InfraNodesDomain(new ElasticsearchNodesAdapter(framework)), + metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), + }; + + const libs: InfraBackendLibs = { + configuration, + framework, + sources, + sourceStatus, + ...domainLibs, + }; + + return libs; +} diff --git a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts b/x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts new file mode 100644 index 0000000000000..2b3f73a9eccf7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/capabilities_domain/capabilities_domain.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraCapabilitiesAdapter } from '../../adapters/capabilities'; +import { InfraCapabilityAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework'; +import { InfraSources } from '../../sources'; + +export class InfraCapabilitiesDomain { + constructor( + private readonly adapter: InfraCapabilitiesAdapter, + private readonly libs: { sources: InfraSources } + ) {} + + public async getCapabilities( + req: InfraFrameworkRequest, + sourceId: string, + nodeName: string, + nodeType: string + ) { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const metricsPromise = this.adapter.getMetricCapabilities( + req, + sourceConfiguration, + nodeName, + nodeType + ); + const logsPromise = this.adapter.getLogCapabilities( + req, + sourceConfiguration, + nodeName, + nodeType + ); + + const metrics = await metricsPromise; + const logs = await logsPromise; + + const metricCapabilities = pickCapabilities(metrics).map(metricCapability => { + return { name: metricCapability, source: 'metrics' }; + }); + + const logCapabilities = pickCapabilities(logs).map(logCapability => { + return { name: logCapability, source: 'logs' }; + }); + + return metricCapabilities.concat(logCapabilities); + } +} + +const pickCapabilities = (buckets: InfraCapabilityAggregationBucket[]): string[] => { + if (buckets) { + const capabilities = buckets + .map(module => { + if (module.names) { + return module.names.buckets.map(name => { + return `${module.key}.${name.key}`; + }); + } else { + return []; + } + }) + .reduce((a: string[], b: string[]) => a.concat(b), []); + return capabilities; + } else { + return []; + } +}; diff --git a/x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts b/x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts new file mode 100644 index 0000000000000..525e60a00f786 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/capabilities_domain/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './capabilities_domain'; diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts new file mode 100644 index 0000000000000..2b6f29e0d6e20 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraIndexField, InfraIndexType } from '../../../common/graphql/types'; +import { FieldsAdapter } from '../adapters/fields'; +import { InfraFrameworkRequest } from '../adapters/framework'; +import { InfraSources } from '../sources'; + +export class InfraFieldsDomain { + constructor( + private readonly adapter: FieldsAdapter, + private readonly libs: { sources: InfraSources } + ) {} + + public async getFields( + request: InfraFrameworkRequest, + sourceId: string, + indexType: InfraIndexType + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); + const includeLogIndices = [InfraIndexType.ANY, InfraIndexType.LOGS].includes(indexType); + + const fields = await this.adapter.getIndexFields(request, [ + ...(includeMetricIndices ? [sourceConfiguration.metricAlias] : []), + ...(includeLogIndices ? [sourceConfiguration.logAlias] : []), + ]); + + return fields; + } +} diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts new file mode 100644 index 0000000000000..cdb2ad32267ca --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_apache2.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const filebeatApache2Rules = [ + { + when: { + exists: ['apache2.access'], + }, + format: [ + { + constant: 'apache2', + }, + { + constant: ' ', + }, + { + field: 'apache2.access.remote_ip', + }, + { + constant: ' ', + }, + { + field: 'apache2.access.user_name', + }, + { + constant: ' "', + }, + { + field: 'apache2.access.method', + }, + { + constant: ' ', + }, + { + field: 'apache2.access.url', + }, + { + constant: ' HTTP/', + }, + { + field: 'apache2.access.http_version', + }, + { + constant: '" ', + }, + { + field: 'apache2.access.response_code', + }, + { + constant: ' ', + }, + { + field: 'apache2.access.body_sent.bytes', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.ts new file mode 100644 index 0000000000000..d44fe4924490b --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_nginx.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const filebeatNginxRules = [ + { + when: { + exists: ['nginx.access'], + }, + format: [ + { + constant: 'nginx', + }, + { + constant: ' ', + }, + { + field: 'nginx.access.remote_ip', + }, + { + constant: ' ', + }, + { + field: 'nginx.access.user_name', + }, + { + constant: ' "', + }, + { + field: 'nginx.access.method', + }, + { + constant: ' ', + }, + { + field: 'nginx.access.url', + }, + { + constant: ' HTTP/', + }, + { + field: 'nginx.access.http_version', + }, + { + constant: '" ', + }, + { + field: 'nginx.access.response_code', + }, + { + constant: ' ', + }, + { + field: 'nginx.access.body_sent.bytes', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_redis.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_redis.ts new file mode 100644 index 0000000000000..e842a54457769 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_redis.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const filebeatRedisRules = [ + { + when: { + exists: ['redis.log.message'], + }, + format: [ + { + constant: 'redis', + }, + { + constant: ' ', + }, + { + field: 'redis.log.message', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_system.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_system.ts new file mode 100644 index 0000000000000..0a84720fc8cfb --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/filebeat_system.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const filebeatSystemRules = [ + { + when: { + exists: ['system.syslog.message'], + }, + format: [ + { + field: 'system.syslog.message', + }, + ], + }, + { + when: { + exists: ['system.auth.message'], + }, + format: [ + { + field: 'system.auth.message', + }, + ], + }, + { + when: { + exists: ['system.auth.ssh.event'], + }, + format: [ + { + constant: 'ssh', + }, + { + constant: ' ', + }, + { + field: 'system.auth.ssh.event', + }, + { + constant: ' user ', + }, + { + field: 'system.auth.user', + }, + { + constant: ' from ', + }, + { + field: 'system.auth.ssh.ip', + }, + ], + }, + { + when: { + exists: ['system.auth.ssh.dropped_ip'], + }, + format: [ + { + constant: 'ssh', + }, + { + constant: ' Dropped connection from ', + }, + { + field: 'system.auth.ssh.dropped_ip', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.ts new file mode 100644 index 0000000000000..bd0e51d5c8d30 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/generic.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const genericRules = [ + { + when: { + exists: ['message'], + }, + format: [ + { + field: 'message', + }, + ], + }, + { + when: { + exists: ['@message'], + }, + format: [ + { + field: '@message', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/index.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/index.ts new file mode 100644 index 0000000000000..2e0c5ac1a9ac6 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/builtin_rules/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { filebeatApache2Rules } from './filebeat_apache2'; +import { filebeatNginxRules } from './filebeat_nginx'; +import { filebeatRedisRules } from './filebeat_redis'; +import { filebeatSystemRules } from './filebeat_system'; +import { genericRules } from './generic'; + +export const builtinRules = [ + ...filebeatApache2Rules, + ...filebeatNginxRules, + ...filebeatRedisRules, + ...filebeatSystemRules, + ...genericRules, + { + when: { + exists: ['source'], + }, + format: [ + { + constant: 'failed to format message from ', + }, + { + field: 'source', + }, + ], + }, + { + when: { + exists: [], + }, + format: [ + { + constant: 'failed to find message', + }, + ], + }, +]; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/index.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/index.ts new file mode 100644 index 0000000000000..2cb8140febdcd --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './log_entries_domain'; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts new file mode 100644 index 0000000000000..27663abdecada --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraLogEntry, + InfraLogMessageSegment, + InfraLogSummaryBucket, +} from '../../../../common/graphql/types'; +import { TimeKey } from '../../../../common/time'; +import { JsonObject } from '../../../../common/typed_json'; +import { InfraDateRangeAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework'; +import { InfraSourceConfiguration, InfraSources } from '../../sources'; +import { builtinRules } from './builtin_rules'; +import { compileFormattingRules } from './message'; + +export class InfraLogEntriesDomain { + constructor( + private readonly adapter: LogEntriesAdapter, + private readonly libs: { sources: InfraSources } + ) {} + + public async getLogEntriesAround( + request: InfraFrameworkRequest, + sourceId: string, + key: TimeKey, + maxCountBefore: number, + maxCountAfter: number, + filterQuery?: LogEntryQuery, + highlightQuery?: string + ): Promise<{ entriesBefore: InfraLogEntry[]; entriesAfter: InfraLogEntry[] }> { + if (maxCountBefore <= 0 && maxCountAfter <= 0) { + return { + entriesBefore: [], + entriesAfter: [], + }; + } + + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const formattingRules = compileFormattingRules(builtinRules); + + const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments( + request, + sourceConfiguration, + formattingRules.requiredFields, + key, + 'desc', + Math.max(maxCountBefore, 1), + filterQuery, + highlightQuery + ); + const lastKeyBefore = + documentsBefore.length > 0 + ? documentsBefore[documentsBefore.length - 1].key + : { + time: key.time - 1, + tiebreaker: 0, + }; + + const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments( + request, + sourceConfiguration, + formattingRules.requiredFields, + lastKeyBefore, + 'asc', + maxCountAfter, + filterQuery, + highlightQuery + ); + + return { + entriesBefore: (maxCountBefore > 0 ? documentsBefore : []).map( + convertLogDocumentToEntry(sourceId, formattingRules.format) + ), + entriesAfter: documentsAfter.map(convertLogDocumentToEntry(sourceId, formattingRules.format)), + }; + } + + public async getLogEntriesBetween( + request: InfraFrameworkRequest, + sourceId: string, + startKey: TimeKey, + endKey: TimeKey, + filterQuery?: LogEntryQuery, + highlightQuery?: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const formattingRules = compileFormattingRules(builtinRules); + const documents = await this.adapter.getContainedLogEntryDocuments( + request, + sourceConfiguration, + formattingRules.requiredFields, + startKey, + endKey, + filterQuery, + highlightQuery + ); + const entries = documents.map(convertLogDocumentToEntry(sourceId, formattingRules.format)); + return entries; + } + + public async getLogSummaryBucketsBetween( + request: InfraFrameworkRequest, + sourceId: string, + start: number, + end: number, + bucketSize: number, + filterQuery?: LogEntryQuery + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( + request, + sourceConfiguration, + start, + end, + bucketSize, + filterQuery + ); + const buckets = dateRangeBuckets.map(convertDateRangeBucketToSummaryBucket); + return buckets; + } +} + +export interface LogEntriesAdapter { + getAdjacentLogEntryDocuments( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + fields: string[], + start: TimeKey, + direction: 'asc' | 'desc', + maxCount: number, + filterQuery?: LogEntryQuery, + highlightQuery?: string + ): Promise; + + getContainedLogEntryDocuments( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + fields: string[], + start: TimeKey, + end: TimeKey, + filterQuery?: LogEntryQuery, + highlightQuery?: string + ): Promise; + + getContainedLogSummaryBuckets( + request: InfraFrameworkRequest, + sourceConfiguration: InfraSourceConfiguration, + start: number, + end: number, + bucketSize: number, + filterQuery?: LogEntryQuery + ): Promise; +} + +export type LogEntryQuery = JsonObject; + +export interface LogEntryDocument { + fields: LogEntryDocumentFields; + gid: string; + key: TimeKey; +} + +export interface LogEntryDocumentFields { + [fieldName: string]: string | number | null; +} + +const convertLogDocumentToEntry = ( + sourceId: string, + formatMessage: (fields: LogEntryDocumentFields) => InfraLogMessageSegment[] +) => (document: LogEntryDocument): InfraLogEntry => ({ + key: document.key, + gid: document.gid, + source: sourceId, + message: formatMessage(document.fields), +}); + +const convertDateRangeBucketToSummaryBucket = ( + bucket: InfraDateRangeAggregationBucket +): InfraLogSummaryBucket => ({ + entriesCount: bucket.doc_count, + start: bucket.from || 0, + end: bucket.to || 0, +}); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts new file mode 100644 index 0000000000000..b97c2704cdfba --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraLogMessageSegment } from '../../../../common/graphql/types'; + +export function compileFormattingRules(rules: LogMessageFormattingRule[]) { + const compiledRules = rules.map(compileRule); + + return { + requiredFields: Array.from( + new Set( + compiledRules.reduce( + (combinedRequiredFields, { requiredFields }) => [ + ...combinedRequiredFields, + ...requiredFields, + ], + [] as string[] + ) + ) + ), + format: (fields: Fields): InfraLogMessageSegment[] => { + for (const compiledRule of compiledRules) { + if (compiledRule.fulfillsCondition(fields)) { + return compiledRule.format(fields); + } + } + + return []; + }, + }; +} + +const compileRule = (rule: LogMessageFormattingRule): CompiledLogMessageFormattingRule => { + const { conditionFields, fulfillsCondition } = compileCondition(rule.when); + const { formattingFields, format } = compileFormattingInstructions(rule.format); + + return { + requiredFields: [...conditionFields, ...formattingFields], + fulfillsCondition, + format, + }; +}; + +const compileCondition = ( + condition: LogMessageFormattingCondition +): CompiledLogMessageFormattingCondition => + [compileExistsCondition, compileFieldValueCondition].reduce( + (compiledCondition, compile) => compile(condition) || compiledCondition, + catchAllCondition + ); + +const catchAllCondition: CompiledLogMessageFormattingCondition = { + conditionFields: [] as string[], + fulfillsCondition: (fields: Fields) => false, +}; + +const compileExistsCondition = (condition: LogMessageFormattingCondition) => + 'exists' in condition + ? { + conditionFields: condition.exists, + fulfillsCondition: (fields: Fields) => + condition.exists.every(fieldName => fieldName in fields), + } + : null; + +const compileFieldValueCondition = (condition: LogMessageFormattingCondition) => + 'values' in condition + ? { + conditionFields: Object.keys(condition.values), + fulfillsCondition: (fields: Fields) => + Object.entries(condition.values).every( + ([fieldName, expectedValue]) => fields[fieldName] === expectedValue + ), + } + : null; + +const compileFormattingInstructions = ( + formattingInstructions: LogMessageFormattingInstruction[] +): CompiledLogMessageFormattingInstruction => + formattingInstructions.reduce( + (combinedFormattingInstructions, formattingInstruction) => { + const compiledFormattingInstruction = compileFormattingInstruction(formattingInstruction); + + return { + formattingFields: [ + ...combinedFormattingInstructions.formattingFields, + ...compiledFormattingInstruction.formattingFields, + ], + format: (fields: Fields) => [ + ...combinedFormattingInstructions.format(fields), + ...compiledFormattingInstruction.format(fields), + ], + }; + }, + { + formattingFields: [], + format: (fields: Fields) => [], + } as CompiledLogMessageFormattingInstruction + ); + +const compileFormattingInstruction = ( + formattingInstruction: LogMessageFormattingInstruction +): CompiledLogMessageFormattingInstruction => + [compileFieldReferenceFormattingInstruction, compileConstantFormattingInstruction].reduce( + (compiledFormattingInstruction, compile) => + compile(formattingInstruction) || compiledFormattingInstruction, + catchAllFormattingInstruction + ); + +const catchAllFormattingInstruction: CompiledLogMessageFormattingInstruction = { + formattingFields: [], + format: (fields: Fields) => [ + { + constant: 'invalid format', + }, + ], +}; + +const compileFieldReferenceFormattingInstruction = ( + formattingInstruction: LogMessageFormattingInstruction +): CompiledLogMessageFormattingInstruction | null => + 'field' in formattingInstruction + ? { + formattingFields: [formattingInstruction.field], + format: (fields: Fields) => [ + { + field: formattingInstruction.field, + value: `${fields[formattingInstruction.field]}`, + highlights: [], + }, + ], + } + : null; + +const compileConstantFormattingInstruction = ( + formattingInstruction: LogMessageFormattingInstruction +): CompiledLogMessageFormattingInstruction | null => + 'constant' in formattingInstruction + ? { + formattingFields: [] as string[], + format: (fields: Fields) => [ + { + constant: formattingInstruction.constant, + }, + ], + } + : null; + +interface Fields { + [fieldName: string]: string | number | boolean | null; +} + +interface LogMessageFormattingRule { + when: LogMessageFormattingCondition; + format: LogMessageFormattingInstruction[]; +} + +type LogMessageFormattingCondition = + | LogMessageFormattingExistsCondition + | LogMessageFormattingFieldValueCondition; + +interface LogMessageFormattingExistsCondition { + exists: string[]; +} + +interface LogMessageFormattingFieldValueCondition { + values: { + [fieldName: string]: string | number | boolean | null; + }; +} + +type LogMessageFormattingInstruction = + | LogMessageFormattingFieldReference + | LogMessageFormattingConstant; + +interface LogMessageFormattingFieldReference { + field: string; +} + +interface LogMessageFormattingConstant { + constant: string; +} + +interface CompiledLogMessageFormattingRule { + requiredFields: string[]; + fulfillsCondition(fields: Fields): boolean; + format(fields: Fields): InfraLogMessageSegment[]; +} + +interface CompiledLogMessageFormattingCondition { + conditionFields: string[]; + fulfillsCondition(fields: Fields): boolean; +} + +interface CompiledLogMessageFormattingInstruction { + formattingFields: string[]; + format(fields: Fields): InfraLogMessageSegment[]; +} diff --git a/x-pack/plugins/infra/server/lib/domains/metrics_domain.ts b/x-pack/plugins/infra/server/lib/domains/metrics_domain.ts new file mode 100644 index 0000000000000..522298579a5c4 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/metrics_domain.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraMetricData } from '../../../common/graphql/types'; +import { InfraFrameworkRequest } from '../adapters/framework/adapter_types'; +import { InfraMetricsAdapter, InfraMetricsRequestOptions } from '../adapters/metrics/adapter_types'; + +export class InfraMetricsDomain { + private adapter: InfraMetricsAdapter; + + constructor(adapter: InfraMetricsAdapter) { + this.adapter = adapter; + } + + public async getMetrics( + req: InfraFrameworkRequest, + options: InfraMetricsRequestOptions + ): Promise { + return await this.adapter.getMetrics(req, options); + } +} diff --git a/x-pack/plugins/infra/server/lib/domains/nodes_domain.ts b/x-pack/plugins/infra/server/lib/domains/nodes_domain.ts new file mode 100644 index 0000000000000..4c6d990dbeb95 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/nodes_domain.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNode } from '../../../common/graphql/types'; +import { InfraFrameworkRequest } from '../adapters/framework'; +import { InfraNodeRequestOptions, InfraNodesAdapter } from '../adapters/nodes'; + +export class InfraNodesDomain { + private adapter: InfraNodesAdapter; + + constructor(adapter: InfraNodesAdapter) { + this.adapter = adapter; + } + + public async getNodes( + req: InfraFrameworkRequest, + options: InfraNodeRequestOptions + ): Promise { + return await this.adapter.getNodes(req, options); + } +} diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts new file mode 100644 index 0000000000000..2a24a65daf8d6 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraConfigurationAdapter } from './adapters/configuration'; +import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; +import { InfraCapabilitiesDomain } from './domains/capabilities_domain'; +import { InfraFieldsDomain } from './domains/fields_domain'; +import { InfraLogEntriesDomain } from './domains/log_entries_domain'; +import { InfraMetricsDomain } from './domains/metrics_domain'; +import { InfraNodesDomain } from './domains/nodes_domain'; +import { InfraSourceStatus } from './source_status'; +import { InfraSourceConfigurations, InfraSources } from './sources'; + +export interface InfraDomainLibs { + capabilities: InfraCapabilitiesDomain; + fields: InfraFieldsDomain; + logEntries: InfraLogEntriesDomain; + nodes: InfraNodesDomain; + metrics: InfraMetricsDomain; +} + +export interface InfraBackendLibs extends InfraDomainLibs { + configuration: InfraConfigurationAdapter; + framework: InfraBackendFrameworkAdapter; + sources: InfraSources; + sourceStatus: InfraSourceStatus; +} + +export interface InfraConfiguration { + enabled: boolean; + query: { + partitionSize: number; + partitionFactor: number; + }; + sources: InfraSourceConfigurations; +} + +export interface InfraContext { + req: InfraFrameworkRequest; +} diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts new file mode 100644 index 0000000000000..1d161135b1ed8 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraFrameworkRequest } from './adapters/framework'; +import { InfraSources } from './sources'; + +export class InfraSourceStatus { + constructor( + private readonly adapter: InfraSourceStatusAdapter, + private readonly libs: { sources: InfraSources } + ) {} + + public async getLogIndexNames( + request: InfraFrameworkRequest, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const indexNames = await this.adapter.getIndexNames(request, sourceConfiguration.logAlias); + return indexNames; + } + public async getMetricIndexNames( + request: InfraFrameworkRequest, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const indexNames = await this.adapter.getIndexNames(request, sourceConfiguration.metricAlias); + return indexNames; + } + public async hasLogAlias(request: InfraFrameworkRequest, sourceId: string): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const hasAlias = await this.adapter.hasAlias(request, sourceConfiguration.logAlias); + return hasAlias; + } + public async hasMetricAlias(request: InfraFrameworkRequest, sourceId: string): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const hasAlias = await this.adapter.hasAlias(request, sourceConfiguration.metricAlias); + return hasAlias; + } + public async hasLogIndices(request: InfraFrameworkRequest, sourceId: string): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const hasIndices = await this.adapter.hasIndices(request, sourceConfiguration.logAlias); + return hasIndices; + } + public async hasMetricIndices( + request: InfraFrameworkRequest, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getConfiguration(sourceId); + const hasIndices = await this.adapter.hasIndices(request, sourceConfiguration.metricAlias); + return hasIndices; + } +} + +export interface InfraSourceStatusAdapter { + getIndexNames(request: InfraFrameworkRequest, aliasName: string): Promise; + hasAlias(request: InfraFrameworkRequest, aliasName: string): Promise; + hasIndices(request: InfraFrameworkRequest, indexNames: string): Promise; +} diff --git a/x-pack/plugins/infra/server/lib/sources.ts b/x-pack/plugins/infra/server/lib/sources.ts new file mode 100644 index 0000000000000..4bf9ce6c61f0e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class InfraSources { + constructor(private readonly adapter: InfraSourcesAdapter) {} + + public async getConfiguration(sourceId: string) { + const sourceConfigurations = await this.getAllConfigurations(); + const requestedSourceConfiguration = sourceConfigurations[sourceId]; + + if (!requestedSourceConfiguration) { + throw new Error(`Failed to find source '${sourceId}'`); + } + + return requestedSourceConfiguration; + } + + public getAllConfigurations() { + return this.adapter.getAll(); + } +} + +export interface InfraSourcesAdapter { + getAll(): Promise; +} + +export interface InfraSourceConfigurations { + [sourceId: string]: InfraSourceConfiguration; +} + +export interface InfraSourceConfiguration { + metricAlias: string; + logAlias: string; + fields: { + container: string; + host: string; + message: string[]; + pod: string; + tiebreaker: string; + timestamp: string; + }; +} diff --git a/x-pack/plugins/infra/server/logging_legacy/adjacent_search_results.ts b/x-pack/plugins/infra/server/logging_legacy/adjacent_search_results.ts new file mode 100644 index 0000000000000..58ad2743fa5dd --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/adjacent_search_results.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Boom from 'boom'; +import { SearchParams } from 'elasticsearch'; +import * as Joi from 'joi'; + +import { + AdjacentSearchResultsApiPostPayload, + AdjacentSearchResultsApiPostResponse, +} from '../../common/http_api'; +import { LogEntryFieldsMapping, LogEntryTime } from '../../common/log_entry'; +import { SearchResult } from '../../common/log_search_result'; +import { + InfraBackendFrameworkAdapter, + InfraDatabaseSearchResponse, + InfraWrappableRequest, +} from '../lib/adapters/framework'; +import { convertHitToSearchResult } from './converters'; +import { isHighlightedHit, SortedHit } from './elasticsearch'; +import { fetchLatestTime } from './latest_log_entries'; +import { indicesSchema, logEntryFieldsMappingSchema, logEntryTimeSchema } from './schemas'; + +const INITIAL_HORIZON_OFFSET = 1000 * 60 * 60 * 24; +const MAX_HORIZON = 9999999999999; + +export const initAdjacentSearchResultsRoutes = (framework: InfraBackendFrameworkAdapter) => { + const callWithRequest = framework.callWithRequest; + + framework.registerRoute< + InfraWrappableRequest, + AdjacentSearchResultsApiPostResponse + >({ + config: { + validate: { + payload: Joi.object().keys({ + after: Joi.number() + .min(0) + .default(0), + before: Joi.number() + .min(0) + .default(0), + fields: logEntryFieldsMappingSchema.required(), + indices: indicesSchema.required(), + query: Joi.string().required(), + target: logEntryTimeSchema.required(), + }), + }, + }, + handler: async (request, reply) => { + const timings = { + esRequestSent: Date.now(), + esResponseProcessed: 0, + }; + + try { + const search = (params: SearchParams) => + callWithRequest(request, 'search', params); + + const latestTime = await fetchLatestTime( + search, + request.payload.indices, + request.payload.fields.time + ); + const searchResultsAfterTarget = await fetchSearchResults( + search, + request.payload.indices, + request.payload.fields, + { + tiebreaker: request.payload.target.tiebreaker - 1, + time: request.payload.target.time, + }, + request.payload.after, + 'asc', + request.payload.query, + request.payload.target.time + INITIAL_HORIZON_OFFSET, + latestTime + ); + const searchResultsBeforeTarget = (await fetchSearchResults( + search, + request.payload.indices, + request.payload.fields, + request.payload.target, + request.payload.before, + 'desc', + request.payload.query, + request.payload.target.time - INITIAL_HORIZON_OFFSET + )).reverse(); + + timings.esResponseProcessed = Date.now(); + + return reply({ + results: { + after: searchResultsAfterTarget, + before: searchResultsBeforeTarget, + }, + timings, + }); + } catch (requestError) { + return reply(Boom.wrap(requestError)); + } + }, + method: 'POST', + path: '/api/logging/adjacent-search-results', + }); +}; + +export async function fetchSearchResults( + search: (params: SearchParams) => Promise>, + indices: string[], + fields: LogEntryFieldsMapping, + target: LogEntryTime, + size: number, + direction: 'asc' | 'desc', + query: string, + horizon: number, + maxHorizon: number = MAX_HORIZON +): Promise { + if (size <= 0) { + return []; + } + + const request = { + allowNoIndices: true, + body: { + _source: false, + highlight: { + boundary_scanner: 'word', + fields: { + [fields.message]: {}, + }, + fragment_size: 1, + number_of_fragments: 100, + post_tags: [''], + pre_tags: [''], + }, + query: { + bool: { + filter: [ + { + query_string: { + default_field: fields.message, + default_operator: 'AND', + query, + }, + }, + { + range: { + [fields.time]: { + [direction === 'asc' ? 'gte' : 'lte']: target.time, + [direction === 'asc' ? 'lte' : 'gte']: horizon, + }, + }, + }, + ], + }, + }, + search_after: [target.time, target.tiebreaker], + size, + sort: [{ [fields.time]: direction }, { [fields.tiebreaker]: direction }], + }, + ignoreUnavailable: true, + index: indices, + }; + const response = await search(request); + + const hits = response.hits.hits as SortedHit[]; + const nextHorizon = horizon + (horizon - target.time); + + if (hits.length >= size || nextHorizon < 0 || nextHorizon > maxHorizon) { + const filteredHits = hits.filter(isHighlightedHit); + return filteredHits.map(convertHitToSearchResult(fields)); + } else { + return fetchSearchResults( + search, + indices, + fields, + target, + size, + direction, + query, + nextHorizon, + maxHorizon + ); + } +} diff --git a/x-pack/plugins/infra/server/logging_legacy/contained_search_results.ts b/x-pack/plugins/infra/server/logging_legacy/contained_search_results.ts new file mode 100644 index 0000000000000..d53d731cd293f --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/contained_search_results.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Boom from 'boom'; +import { SearchParams } from 'elasticsearch'; +import * as Joi from 'joi'; + +import { + ContainedSearchResultsApiPostPayload, + ContainedSearchResultsApiPostResponse, +} from '../../common/http_api'; +import { isLessOrEqual, LogEntryFieldsMapping, LogEntryTime } from '../../common/log_entry'; +import { SearchResult } from '../../common/log_search_result'; +import { + InfraBackendFrameworkAdapter, + InfraDatabaseSearchResponse, + InfraWrappableRequest, +} from '../lib/adapters/framework'; +import { convertHitToSearchResult } from './converters'; +import { isHighlightedHit, SortedHit } from './elasticsearch'; +import { indicesSchema, logEntryFieldsMappingSchema, logEntryTimeSchema } from './schemas'; + +export const initContainedSearchResultsRoutes = (framework: InfraBackendFrameworkAdapter) => { + const callWithRequest = framework.callWithRequest; + + framework.registerRoute< + InfraWrappableRequest, + ContainedSearchResultsApiPostResponse + >({ + config: { + validate: { + payload: Joi.object().keys({ + end: logEntryTimeSchema.required(), + fields: logEntryFieldsMappingSchema.required(), + indices: indicesSchema.required(), + query: Joi.string().required(), + start: logEntryTimeSchema.required(), + }), + }, + }, + handler: async (request, reply) => { + const timings = { + esRequestSent: Date.now(), + esResponseProcessed: 0, + }; + + try { + const search = (params: SearchParams) => + callWithRequest(request, 'search', params); + + const searchResults = await fetchSearchResultsBetween( + search, + request.payload.indices, + request.payload.fields, + request.payload.start, + request.payload.end, + request.payload.query + ); + + timings.esResponseProcessed = Date.now(); + + return reply({ + results: searchResults, + timings, + }); + } catch (requestError) { + return reply(Boom.wrap(requestError)); + } + }, + method: 'POST', + path: '/api/logging/contained-search-results', + }); +}; + +export async function fetchSearchResultsBetween( + search: (params: SearchParams) => Promise>, + indices: string[], + fields: LogEntryFieldsMapping, + start: LogEntryTime, + end: LogEntryTime, + query: string +): Promise { + const request = { + allowNoIndices: true, + body: { + _source: false, + highlight: { + boundary_scanner: 'word', + fields: { + [fields.message]: {}, + }, + fragment_size: 1, + number_of_fragments: 100, + post_tags: [''], + pre_tags: [''], + }, + query: { + bool: { + filter: [ + { + query_string: { + default_field: fields.message, + default_operator: 'AND', + query, + }, + }, + { + range: { + [fields.time]: { + gte: start.time, + lte: end.time, + }, + }, + }, + ], + }, + }, + search_after: [start.time, start.tiebreaker - 1], + size: 10000, + sort: [{ [fields.time]: 'asc' }, { [fields.tiebreaker]: 'asc' }], + }, + ignoreUnavailable: true, + index: indices, + }; + const response = await search(request); + + const hits = response.hits.hits as SortedHit[]; + const filteredHits = hits + .filter(hit => isLessOrEqual({ time: hit.sort[0], tiebreaker: hit.sort[1] }, end)) + .filter(isHighlightedHit); + return filteredHits.map(convertHitToSearchResult(fields)); +} diff --git a/x-pack/plugins/infra/server/logging_legacy/converters.ts b/x-pack/plugins/infra/server/logging_legacy/converters.ts new file mode 100644 index 0000000000000..164981c2dbe18 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/converters.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import invert from 'lodash/fp/invert'; +import mapKeys from 'lodash/fp/mapKeys'; + +import { LogEntryFieldsMapping } from '../../common/log_entry'; +import { SearchResult } from '../../common/log_search_result'; +import { SearchSummaryBucket } from '../../common/log_search_summary'; +import { + DateHistogramResponse, + HighlightedHit, + Hit, + HitsBucket, + isBucketWithAggregation, +} from './elasticsearch'; + +export const convertHitToSearchResult = (fields: LogEntryFieldsMapping) => { + const invertedFields = invert(fields); + return (hit: HighlightedHit): SearchResult => { + const matches = mapKeys(key => invertedFields[key], hit.highlight || {}); + return { + fields: { + tiebreaker: hit.sort[1], // use the sort property to get the normalized values + time: hit.sort[0], + }, + gid: getHitGid(hit), + matches, + }; + }; +}; + +export const convertDateHistogramToSearchSummaryBuckets = ( + fields: LogEntryFieldsMapping, + end: number +) => (buckets: DateHistogramResponse['buckets']): SearchSummaryBucket[] => + buckets.reduceRight( + ({ previousStart, aggregatedBuckets }, bucket) => { + const representative = + isBucketWithAggregation(bucket, 'top_entries') && + bucket.top_entries.hits.hits.length > 0 + ? convertHitToSearchResult(fields)(bucket.top_entries.hits.hits[0]) + : null; + return { + aggregatedBuckets: [ + ...(representative + ? [ + { + count: bucket.doc_count, + end: previousStart, + representative, + start: bucket.key, + }, + ] + : []), + ...aggregatedBuckets, + ], + previousStart: bucket.key, + }; + }, + { previousStart: end, aggregatedBuckets: [] } as { + previousStart: number; + aggregatedBuckets: SearchSummaryBucket[]; + } + ).aggregatedBuckets; + +const getHitGid = (hit: Hit): string => `${hit._index}:${hit._type}:${hit._id}`; diff --git a/x-pack/plugins/infra/server/logging_legacy/elasticsearch.ts b/x-pack/plugins/infra/server/logging_legacy/elasticsearch.ts new file mode 100644 index 0000000000000..020b9ae7ba2c7 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/elasticsearch.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MSearchParams, MSearchResponse, SearchParams, SearchResponse } from 'elasticsearch'; + +export interface ESCluster { + callWithRequest( + request: any, + endpoint: 'msearch', + clientOptions: MSearchParams, + options?: object + ): Promise>; + callWithRequest( + request: any, + endpoint: 'search', + clientOptions: SearchParams, + options?: object + ): Promise>; + callWithRequest( + request: any, + endpoint: string, + clientOptions?: object, + options?: object + ): Promise; +} + +export type Hit = SearchResponse['hits']['hits'][0]; + +export interface SortedHit extends Hit { + sort: any[]; + _source: { + [field: string]: any; + }; +} + +export interface HighlightedHit extends SortedHit { + highlight?: { + [field: string]: string[]; + }; +} + +export const isHighlightedHit = (hit: Hit): hit is HighlightedHit => !!hit.highlight; + +export interface DateHistogramBucket { + key: number; + key_as_string: string; + doc_count: number; +} + +export interface HitsBucket { + hits: { + total: number; + max_score: number | null; + hits: SortedHit[]; + }; +} + +export interface DateHistogramResponse { + buckets: DateHistogramBucket[]; +} + +export type WithSubAggregation< + SubAggregationType, + SubAggregationName extends string, + BucketType +> = BucketType & { [subAggregationName in SubAggregationName]: SubAggregationType }; + +export const isBucketWithAggregation = < + SubAggregationType extends object, + SubAggregationName extends string = any, + BucketType extends object = {} +>( + bucket: BucketType, + aggregationName: SubAggregationName +): bucket is WithSubAggregation => + aggregationName in bucket; diff --git a/x-pack/plugins/infra/server/logging_legacy/index.ts b/x-pack/plugins/infra/server/logging_legacy/index.ts new file mode 100644 index 0000000000000..e193655246016 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraBackendFrameworkAdapter } from '../lib/adapters/framework'; +import { initAdjacentSearchResultsRoutes } from './adjacent_search_results'; +import { initContainedSearchResultsRoutes } from './contained_search_results'; +import { initSearchSummaryRoutes } from './search_summary'; + +export const initLegacyLoggingRoutes = (framework: InfraBackendFrameworkAdapter) => { + initAdjacentSearchResultsRoutes(framework); + initContainedSearchResultsRoutes(framework); + initSearchSummaryRoutes(framework); +}; diff --git a/x-pack/plugins/infra/server/logging_legacy/latest_log_entries.ts b/x-pack/plugins/infra/server/logging_legacy/latest_log_entries.ts new file mode 100644 index 0000000000000..2b40309b510d2 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/latest_log_entries.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchParams } from 'elasticsearch'; + +import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; + +export async function fetchLatestTime( + search: ( + params: SearchParams + ) => Promise>, + indices: string[], + timeField: string +): Promise { + const response = await search({ + allowNoIndices: true, + body: { + aggregations: { + max_time: { + max: { + field: timeField, + }, + }, + }, + query: { + match_all: {}, + }, + size: 0, + }, + ignoreUnavailable: true, + index: indices, + }); + + if (response.aggregations && response.aggregations.max_time) { + return response.aggregations.max_time.value; + } else { + return 0; + } +} diff --git a/x-pack/plugins/infra/server/logging_legacy/schemas.ts b/x-pack/plugins/infra/server/logging_legacy/schemas.ts new file mode 100644 index 0000000000000..12dc60d369bd6 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/schemas.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Joi from 'joi'; + +export const timestampSchema = Joi.number() + .integer() + .min(0); + +export const logEntryFieldsMappingSchema = Joi.object().keys({ + message: Joi.string().required(), + tiebreaker: Joi.string().required(), + time: Joi.string().required(), +}); + +export const logEntryTimeSchema = Joi.object().keys({ + tiebreaker: Joi.number().integer(), + time: timestampSchema, +}); + +export const indicesSchema = Joi.array().items(Joi.string()); + +export const summaryBucketSizeSchema = Joi.object().keys({ + unit: Joi.string() + .valid(['y', 'M', 'w', 'd', 'h', 'm', 's']) + .required(), + value: Joi.number() + .integer() + .min(0) + .required(), +}); diff --git a/x-pack/plugins/infra/server/logging_legacy/search_summary.ts b/x-pack/plugins/infra/server/logging_legacy/search_summary.ts new file mode 100644 index 0000000000000..161dc7ea90a75 --- /dev/null +++ b/x-pack/plugins/infra/server/logging_legacy/search_summary.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Boom from 'boom'; +import { SearchParams } from 'elasticsearch'; +import * as Joi from 'joi'; + +import { SearchSummaryApiPostPayload, SearchSummaryApiPostResponse } from '../../common/http_api'; +import { LogEntryFieldsMapping } from '../../common/log_entry'; +import { SearchSummaryBucket } from '../../common/log_search_summary'; +import { SummaryBucketSize } from '../../common/log_summary'; +import { + InfraBackendFrameworkAdapter, + InfraDatabaseSearchResponse, + InfraWrappableRequest, +} from '../lib/adapters/framework'; +import { convertDateHistogramToSearchSummaryBuckets } from './converters'; +import { DateHistogramResponse } from './elasticsearch'; +import { + indicesSchema, + logEntryFieldsMappingSchema, + summaryBucketSizeSchema, + timestampSchema, +} from './schemas'; + +export const initSearchSummaryRoutes = (framework: InfraBackendFrameworkAdapter) => { + const callWithRequest = framework.callWithRequest; + + framework.registerRoute< + InfraWrappableRequest, + SearchSummaryApiPostResponse + >({ + config: { + validate: { + payload: Joi.object().keys({ + bucketSize: summaryBucketSizeSchema.required(), + end: timestampSchema.required(), + fields: logEntryFieldsMappingSchema.required(), + indices: indicesSchema.required(), + query: Joi.string().required(), + start: timestampSchema.required(), + }), + }, + }, + handler: async (request, reply) => { + const timings = { + esRequestSent: Date.now(), + esResponseProcessed: 0, + }; + + try { + const search = (params: SearchParams) => + callWithRequest(request, 'search', params); + const summaryBuckets = await fetchSummaryBuckets( + search, + request.payload.indices, + request.payload.fields, + request.payload.start, + request.payload.end, + request.payload.bucketSize, + request.payload.query + ); + + timings.esResponseProcessed = Date.now(); + + return reply({ + buckets: summaryBuckets, + timings, + }); + } catch (requestError) { + return reply(Boom.wrap(requestError)); + } + }, + method: 'POST', + path: '/api/logging/search-summary', + }); +}; + +async function fetchSummaryBuckets( + search: ( + params: SearchParams + ) => Promise>, + indices: string[], + fields: LogEntryFieldsMapping, + start: number, + end: number, + bucketSize: { + unit: SummaryBucketSize; + value: number; + }, + query: string +): Promise { + const response = await search({ + allowNoIndices: true, + body: { + aggregations: { + count_by_date: { + aggregations: { + top_entries: { + top_hits: { + _source: [fields.message], + size: 1, + sort: [{ [fields.time]: 'desc' }, { [fields.tiebreaker]: 'desc' }], + }, + }, + }, + date_histogram: { + extended_bounds: { + max: end, + min: start, + }, + field: fields.time, + interval: `${bucketSize.value}${bucketSize.unit}`, + min_doc_count: 0, + }, + }, + }, + query: { + bool: { + filter: [ + { + query_string: { + default_field: fields.message, + default_operator: 'AND', + query, + }, + }, + { + range: { + [fields.time]: { + format: 'epoch_millis', + gte: start, + lt: end, + }, + }, + }, + ], + }, + }, + size: 0, + }, + ignoreUnavailable: true, + index: indices, + }); + + if (response.aggregations && response.aggregations.count_by_date) { + return convertDateHistogramToSearchSummaryBuckets(fields, end)( + response.aggregations.count_by_date.buckets + ); + } else { + return []; + } +} diff --git a/x-pack/plugins/infra/server/usage/usage_collector.ts b/x-pack/plugins/infra/server/usage/usage_collector.ts new file mode 100644 index 0000000000000..72e8dc251da8b --- /dev/null +++ b/x-pack/plugins/infra/server/usage/usage_collector.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InfraNodeType } from '../../common/graphql/types'; +import { KbnServer } from '../kibana.index'; + +const KIBANA_REPORTING_TYPE = 'infraops'; + +interface InfraopsSum { + infraopsHosts: number; + infraopsDocker: number; + infraopsKubernetes: number; + logs: number; +} + +export class UsageCollector { + public static getUsageCollector(server: KbnServer) { + const { collectorSet } = server.usage; + + return collectorSet.makeUsageCollector({ + type: KIBANA_REPORTING_TYPE, + fetch: async () => { + return this.getReport(); + }, + }); + } + + public static countNode(nodeType: InfraNodeType) { + const bucket = this.getBucket(); + this.maybeInitializeBucket(bucket); + + switch (nodeType) { + case InfraNodeType.pod: + this.counters[bucket].infraopsKubernetes += 1; + break; + case InfraNodeType.container: + this.counters[bucket].infraopsDocker += 1; + break; + default: + this.counters[bucket].infraopsHosts += 1; + } + } + + public static countLogs() { + const bucket = this.getBucket(); + this.maybeInitializeBucket(bucket); + this.counters[bucket].logs += 1; + } + + private static counters: any = {}; + private static BUCKET_SIZE = 3600; // seconds in an hour + private static BUCKET_NUMBER = 24; // report the last 24 hours + + private static getBucket() { + const now = Math.floor(Date.now() / 1000); + return now - (now % this.BUCKET_SIZE); + } + + private static maybeInitializeBucket(bucket: any) { + if (!this.counters[bucket]) { + this.counters[bucket] = { + infraopsHosts: 0, + infraopsDocker: 0, + infraopsKubernetes: 0, + logs: 0, + }; + } + } + + private static getReport() { + const keys = Object.keys(this.counters); + + // only keep the newest BUCKET_NUMBER buckets + const cutoff = this.getBucket() - this.BUCKET_SIZE * (this.BUCKET_NUMBER - 1); + keys.forEach(key => { + if (parseInt(key, 10) < cutoff) { + delete this.counters[key]; + } + }); + + // all remaining buckets are current + const sums = Object.keys(this.counters).reduce( + (a: InfraopsSum, b: any) => { + const key = parseInt(b, 10); + return { + infraopsHosts: a.infraopsHosts + this.counters[key].infraopsHosts, + infraopsDocker: a.infraopsDocker + this.counters[key].infraopsDocker, + infraopsKubernetes: a.infraopsKubernetes + this.counters[key].infraopsKubernetes, + logs: a.logs + this.counters[key].logs, + }; + }, + { + infraopsHosts: 0, + infraopsDocker: 0, + infraopsKubernetes: 0, + logs: 0, + } + ); + + return { + last_24_hours: { + hits: { + infraops_hosts: sums.infraopsHosts, + infraops_docker: sums.infraopsDocker, + infraops_kubernetes: sums.infraopsKubernetes, + logs: sums.logs, + }, + }, + }; + } +} diff --git a/x-pack/plugins/infra/server/utils/README.md b/x-pack/plugins/infra/server/utils/README.md new file mode 100644 index 0000000000000..8a6a27aa29867 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/README.md @@ -0,0 +1 @@ +Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/plugins/infra/server/utils/serialized_query.ts b/x-pack/plugins/infra/server/utils/serialized_query.ts new file mode 100644 index 0000000000000..932df847e65d0 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/serialized_query.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UserInputError } from 'apollo-server-errors'; + +import { JsonObject } from '../../common/typed_json'; + +export const parseFilterQuery = ( + filterQuery: string | null | undefined +): JsonObject | undefined => { + try { + if (filterQuery) { + const parsedFilterQuery = JSON.parse(filterQuery); + if ( + !parsedFilterQuery || + ['string', 'number', 'boolean'].includes(typeof parsedFilterQuery) || + Array.isArray(parsedFilterQuery) + ) { + throw new Error('expected value to be an object'); + } + return parsedFilterQuery; + } else { + return undefined; + } + } catch (err) { + throw new UserInputError(`Failed to parse query: ${err}`, { + query: filterQuery, + originalError: err, + }); + } +}; diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts new file mode 100644 index 0000000000000..ea4c9eefdc6f0 --- /dev/null +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * /!\ These type definitions are temporary until the upstream @elastic/eui + * package includes them. + */ + +import { CommonProps, EuiToolTipPosition } from '@elastic/eui'; +import moment from 'moment'; +import { MouseEventHandler, ReactType, Ref } from 'react'; +import { ReactDatePickerProps } from 'react-datepicker'; +import { JsonObject } from '../common/typed_json'; + +declare module '@elastic/eui' { + export interface EuiBreadcrumbDefinition { + text: React.ReactNode; + href?: string; + onClick?: React.MouseEventHandler; + } + type EuiBreadcrumbsProps = CommonProps & { + responsive?: boolean; + truncate?: boolean; + max?: number; + breadcrumbs: EuiBreadcrumbDefinition[]; + }; + + type EuiHeaderProps = CommonProps; + export const EuiHeader: React.SFC; + + export type EuiHeaderSectionSide = 'left' | 'right'; + type EuiHeaderSectionProps = CommonProps & { + side?: EuiHeaderSectionSide; + }; + export const EuiHeaderSection: React.SFC; + + type EuiHeaderBreadcrumbsProps = EuiBreadcrumbsProps; + export const EuiHeaderBreadcrumbs: React.SFC; + + type EuiDatePickerProps = CommonProps & + Pick< + ReactDatePickerProps, + Exclude< + keyof ReactDatePickerProps, + | 'monthsShown' + | 'showWeekNumbers' + | 'fixedHeight' + | 'dropdownMode' + | 'useShortMonthInDropdown' + | 'todayButton' + | 'timeCaption' + | 'disabledKeyboardNavigation' + | 'isClearable' + | 'withPortal' + | 'ref' + | 'placeholderText' + | 'selected' + > + > & { + fullWidth?: boolean; + inputRef?: Ref; + injectTimes?: moment.Moment[]; + isInvalid?: boolean; + isLoading?: boolean; + selected?: moment.Moment | null | undefined; + placeholder?: string; + shadow?: boolean; + calendarContainer?: React.ReactNode; + onChange?: (date: moment.Moment | null) => void; + startDate?: moment.Moment | undefined; + endDate?: moment.Moment | undefined; + }; + export const EuiDatePicker: React.SFC; + + type EuiFilterGroupProps = CommonProps; + export const EuiFilterGroup: React.SFC; + + type EuiFilterButtonProps = CommonProps & { + color?: ButtonColor; + href?: string; + iconSide?: ButtonIconSide; + iconType?: IconType; + isDisabled?: boolean; + isSelected?: boolean; + onClick: MouseEventHandler; + rel?: string; + target?: string; + type?: string; + }; + export const EuiFilterButton: React.SFC; + + interface EuiOutsideClickDetectorProps { + children: React.ReactNode; + isDisabled?: boolean; + onOutsideClick: React.MouseEventHandler; + } + export const EuiOutsideClickDetector: React.SFC; + + interface EuiFormControlLayoutIconProps { + type: IconType; + side?: 'left' | 'right'; + onClick?: React.MouseEventHandler; + } + + interface EuiFormControlLayoutClearIconProps { + onClick?: React.MouseEventHandler; + } + + type EuiFormControlLayoutProps = CommonProps & { + icon?: string | EuiFormControlLayoutIconProps; + clear?: EuiFormControlLayoutClearIconProps; + fullWidth?: boolean; + isLoading?: boolean; + compressed?: boolean; + prepend?: React.ReactNode; + append?: React.ReactNode; + }; + export const EuiFormControlLayout: React.SFC; + + type EuiSideNavProps = CommonProps & { + style?: any; + items: Array<{ + id: string | number; + name: string; + items: Array<{ + id: string; + name: string; + onClick: () => void; + }>; + }>; + mobileTitle?: React.ReactNode; + toggleOpenOnMobile?: () => void; + isOpenOnMobile?: boolean; + }; + export const EuiSideNav: React.SFC; + + type EuiErrorBoundaryProps = CommonProps & { + children: React.ReactNode; + }; + + export const EuiErrorBoundary: React.SFC; + + type EuiSelectProps = CommonProps & { + compressed?: boolean; + disabled?: boolean; + fullWidth?: boolean; + hasNoInitialSelection?: boolean; + inputRef?: Ref; + isInvalid?: boolean; + isLoading?: boolean; + onChange?: (arg: any) => void; + options?: any[]; + value: any; + }; + export const EuiSelect: React.SFC; + + type EuiSizesResponsive = 'xs' | 's' | 'm' | 'l' | 'xl'; + type EuiResponsiveProps = CommonProps & { + children: React.ReactNode; + sizes: EuiSizesResponsive[]; + }; + + export const EuiHideFor: React.SFC; + + export const EuiShowFor: React.SFC; + + type EuiDatePickerRangeProps = CommonProps & { + startDateControl: React.ReactNode; + endDateControl: React.ReactNode; + iconType?: IconType | boolean; + fullWidth?: boolean; + disabled?: boolean; + isLoading?: boolean; + dateFormat?: string; + }; + + export const EuiDatePickerRange: React.SFC; + + type EuiFieldNumberProps = CommonProps & { + defaultValue: string; + value?: number; + onChange?: (arg: any) => void; + step?: number; + }; + + export const EuiFieldNumber: React.SFC; +} diff --git a/x-pack/plugins/infra/types/eui_experimental.d.ts b/x-pack/plugins/infra/types/eui_experimental.d.ts new file mode 100644 index 0000000000000..3e016f491e555 --- /dev/null +++ b/x-pack/plugins/infra/types/eui_experimental.d.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare module '@elastic/eui/lib/experimental' { + import { CommonProps } from '@elastic/eui'; + export type EuiSeriesChartProps = CommonProps & { + xType?: string; + stackBy?: string; + statusText?: string; + yDomain?: number[]; + showCrosshair?: boolean; + showDefaultAxis?: boolean; + enableSelectionBrush?: boolean; + crosshairValue?: number; + onSelectionBrushEnd?: (args: any) => void; + onCrosshairUpdate?: (crosshairValue: number) => void; + }; + export const EuiSeriesChart: React.SFC; + + type EuiSeriesProps = CommonProps & { + data: Array<{ x: number; y: number; y0?: number }>; + lineSize?: number; + name: string; + color?: string; + marginLeft?: number; + }; + export const EuiLineSeries: React.SFC; + export const EuiAreaSeries: React.SFC; + export const EuiBarSeries: React.SFC; + + type EuiYAxisProps = CommonProps & { + tickFormat: (value: number) => string; + marginLeft?: number; + }; + export const EuiYAxis: React.SFC; + + type EuiXAxisProps = CommonProps & { + tickFormat?: (value: number) => string; + marginLeft?: number; + }; + export const EuiXAxis: React.SFC; + + export interface EuiDataPoint { + seriesIndex: number; + x: number; + y: number; + originalValues: { + x: number; + y: number; + x0?: number; + }; + } + + export interface EuiFormattedValue { + title: any; + value: any; + } + type EuiCrosshairXProps = CommonProps & { + seriesNames: string[]; + titleFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue | undefined; + itemsFormat?: (dataPoints: EuiDataPoint[]) => EuiFormattedValue[]; + }; + export const EuiCrosshairX: React.SFC; +} diff --git a/x-pack/plugins/infra/types/graphql_fields.d.ts b/x-pack/plugins/infra/types/graphql_fields.d.ts new file mode 100644 index 0000000000000..11d26e913015c --- /dev/null +++ b/x-pack/plugins/infra/types/graphql_fields.d.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare module 'graphql-fields' { + function graphqlFields(info: any, obj?: any): any; + export default graphqlFields; +} diff --git a/x-pack/plugins/infra/types/redux_observable.d.ts b/x-pack/plugins/infra/types/redux_observable.d.ts new file mode 100644 index 0000000000000..6813c4bf6a054 --- /dev/null +++ b/x-pack/plugins/infra/types/redux_observable.d.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action, MiddlewareAPI } from 'redux'; +import { ActionsObservable, Epic } from 'redux-observable'; +import { Observable } from 'rxjs'; + +declare module 'redux-observable' { + function combineEpics< + T1 extends Action, + T2 extends Action, + O1 extends T1, + O2 extends T2, + S, + D1, + D2 + >(epic1: Epic, epic2: Epic): Epic; + function combineEpics< + T1 extends Action, + T2 extends Action, + T3 extends Action, + O1 extends T1, + O2 extends T2, + O3 extends T3, + S, + D1, + D2, + D3 + >( + epic1: Epic, + epic2: Epic, + epic3: Epic + ): Epic; + function combineEpics< + T1 extends Action, + T2 extends Action, + T3 extends Action, + T4 extends Action, + O1 extends T1, + O2 extends T2, + O3 extends T3, + O4 extends T4, + S, + D1, + D2, + D3, + D4 + >( + epic1: Epic, + epic2: Epic, + epic3: Epic, + epic4: Epic + ): Epic; + function combineEpics< + T1 extends Action, + T2 extends Action, + T3 extends Action, + T4 extends Action, + T5 extends Action, + O1 extends T1, + O2 extends T2, + O3 extends T3, + O4 extends T4, + O5 extends T5, + S, + D1, + D2, + D3, + D4, + D5 + >( + epic1: Epic, + epic2: Epic, + epic3: Epic, + epic4: Epic, + epic5: Epic + ): Epic; + + type EpicWithState = E extends Epic + ? Epic + : E; +} diff --git a/x-pack/plugins/infra/types/rison_node.d.ts b/x-pack/plugins/infra/types/rison_node.d.ts new file mode 100644 index 0000000000000..5448c4bcd74a9 --- /dev/null +++ b/x-pack/plugins/infra/types/rison_node.d.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// tslint:disable:variable-name + +declare module 'rison-node' { + export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; + + export interface RisonArray extends Array {} + + export interface RisonObject { + [key: string]: RisonValue; + } + + export const decode: (input: string) => RisonValue; + + export const decode_object: (input: string) => RisonObject; + + export const encode: (input: Input) => string; + + export const encode_object: (input: Input) => string; +} diff --git a/x-pack/plugins/infra/yarn.lock b/x-pack/plugins/infra/yarn.lock new file mode 100644 index 0000000000000..b054aeb5441b4 --- /dev/null +++ b/x-pack/plugins/infra/yarn.lock @@ -0,0 +1,56 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/boom@3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@types/boom/-/boom-3.2.2.tgz#6773bb1bbfec111f5ea683874b8d932157d58885" + integrity sha512-Wbgg2JXCnlEMWB2faZgT8x1MPPgzqqLBWx1zXXWGPDQDo9CcvQkIW89QqY+yekoXIKI+qW/eZsg/gv6+WNFwEg== + dependencies: + "@types/node" "*" + +"@types/color-convert@*": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d" + integrity sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.0.tgz#926f76f7e66f49cc59ad880bb15b030abbf0b66d" + integrity sha512-gZ/Rb+MFXF0pXSEQxdRoPMm5jeO3TycjOdvbpbcpHX/B+n9AqaHFe5q6Ga9CsZ7ir/UgIWPfrBzUzn3F19VH/w== + +"@types/color@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" + integrity sha512-5qqtNia+m2I0/85+pd2YzAXaTyKO8j+svirO5aN+XaQJ5+eZ8nx0jPtEWZLxCi50xwYsX10xUHetFzfb1WEs4Q== + dependencies: + "@types/color-convert" "*" + +"@types/lodash@^4.14.110": + version "4.14.116" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9" + integrity sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg== + +"@types/node@*": + version "10.11.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.11.4.tgz#e8bd933c3f78795d580ae41d86590bfc1f4f389d" + integrity sha512-ojnbBiKkZFYRfQpmtnnWTMw+rzGp/JiystjluW9jgN3VzRwilXddJ6aGQ9V/7iuDG06SBgn7ozW9k3zcAnYjYQ== + +boom@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-3.1.1.tgz#b6424f01ed8d492b2b12ae86047c24e8b6a7c937" + integrity sha1-tkJPAe2NSSsrEq6GBHwk6LanyTc= + dependencies: + hoek "3.x.x" + +hoek@3.x.x: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-3.0.4.tgz#268adff66bb6695c69b4789a88b1e0847c3f3123" + integrity sha1-Jorf9mu2aVxptHiaiLHghHw/MSM= + +lodash@^4.17.10: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== diff --git a/x-pack/plugins/reporting/public/lib/reporting_client.ts b/x-pack/plugins/reporting/public/lib/reporting_client.ts index 2e10a1333de49..8481d2993ffb3 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_client.ts @@ -14,8 +14,12 @@ import { jobCompletionNotifications } from './job_completion_notifications'; const API_BASE_URL = '/api/reporting/generate'; +interface JobParams { + [paramName: string]: any; +} + class ReportingClient { - public getReportingJobPath = (exportType: string, jobParams: object) => { + public getReportingJobPath = (exportType: string, jobParams: JobParams) => { return `${chrome.addBasePath(API_BASE_URL)}/${exportType}?${QueryString.param( 'jobParams', rison.encode(jobParams) diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_selector.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_selector.tsx index 5f7d902cb7459..641417fb9276f 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_selector.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_selector.tsx @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiSelect, -} from '@elastic/eui'; +import { EuiSelect } from '@elastic/eui'; import React, { ChangeEvent, Component } from 'react'; import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege'; import { NO_PRIVILEGE_VALUE } from '../../../lib/constants'; diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index 20579915c2393..db30e9fd111ca 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { Server } from 'hapi'; import { SpacesClient } from '../../../lib/spaces_client'; import { createSpaces } from './create_spaces'; +interface KibanaServer extends Server { + savedObjects: any; +} + export interface TestConfig { [configKey: string]: any; } @@ -67,7 +70,7 @@ export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: pre = pre.mockImplementation(preCheckLicenseImpl); } - const server = new Server(); + const server = new Server() as KibanaServer; const config = { ...baseConfig, diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 17871271ebd38..724d77e1b868f 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -155,16 +155,77 @@ url-join "^4.0.0" ws "^4.1.0" +"@types/angular@^1.6.50": + version "1.6.51" + resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.51.tgz#a67515b0ba6a2ff68894a39405c1343cbf9c36d4" + integrity sha512-wYU+/zlJWih7ZmonWVjGQ18tG7GboI9asMNjRBM5fpIFJWXSioQttCTw9qGL44cP82ghM8sCV9apEqm1zBDq2w== + +"@types/async@2.0.49": + version "2.0.49" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0" + integrity sha512-Benr3i5odUkvpFkOpzGqrltGdbSs+EVCkEBGXbuR7uT0VzhXKIkhem6PDzHdx5EonA+rfbB3QvP6aDOw5+zp5Q== + +"@types/babel-types@*": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.4.tgz#bfd5b0d0d1ba13e351dff65b6e52783b816826c8" + integrity sha512-WiZhq3SVJHFRgRYLXvpf65XnV6ipVHhnNaNvE8yCimejrGglkg38kEj0JcizqwSHxmPSjcTlig/6JouxLGEhGw== + +"@types/babylon@6.16.3": + version "6.16.3" + resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.3.tgz#c2937813a89fcb5e79a00062fc4a8b143e7237bb" + integrity sha512-lyJ8sW1PbY3uwuvpOBZ9zMYKshMnQpXmeDHh8dj9j2nJm/xrW0FgB5gLSYOArj5X0IfaXnmhFoJnhS4KbqIMug== + dependencies: + "@types/babel-types" "*" + "@types/cookiejar@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" integrity sha512-EIjmpvnHj+T4nMcKwHwxZKUfDmphIKJc2qnEMhSoOvr1lYEQpuRKRz8orWr//krYIIArS/KGGLfL2YGVUYXmIA== +"@types/d3-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.1.tgz#e489605208d46a1c9d980d2e5772fa9c75d9ec65" + integrity sha512-YBaAfimGdWE4nDuoGVKsH89/dkz2hWZ0i8qC+xxqmqi+XJ/aXiRF0jPtzXmN7VdkpVjy1xuDmM5/m1FNuB6VWA== + +"@types/d3-path@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.7.tgz#a0736fceed688a695f48265a82ff7a3369414b81" + integrity sha512-U8dFRG+8WhkLJr2sxZ9Cw/5WeRgBnNqMxGdA1+Z0+ZG6tK0s75OQ4OXnxeyfKuh6E4wQPY8OAKr1+iNDx01BEQ== + +"@types/d3-scale@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.0.1.tgz#f94cd991c50422b2e68d8f43be3f9fffdb1ae7be" + integrity sha512-D5ZWv8ToLvqacE7XkdMNHMiiVDULdDxT7FMMGU0YJC3/nVzBmApjyTyxracUWOQyY3KK7YhZ05on8pOcNi0dfQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^1.2.2": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.3.tgz#cadc9f93a626db9190f306048a650df4ffa4e500" + integrity sha512-iP9TcX0EVi+LlX+jK9ceS+yhEz5abTitF+JaO2ugpRE/J+bccaYLe/0/3LETMmdaEkYarIyboZW8OF67Mpnj1w== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22" + integrity sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA== + +"@types/d3-time@*", "@types/d3-time@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.8.tgz#6c083127b330b3c2fc65cd0f3a6e9cbd9607b28c" + integrity sha512-/UCphyyw97YAq4zKsuXH33R3UNB4jDSza0fLvMubWr/ONh9IePi1NbgFP222blhiCe724ebJs8U87+aDuAq/jA== + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" integrity sha512-D1/YuYOcdOIdaQnaiUJ77VcilVvESkynw79CtGqpjkXyv4OUezEVZtdXnSOwXL8Zcelu66QbyC8QQcVQ/ZPdig== +"@types/elasticsearch@^5.0.22": + version "5.0.24" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.24.tgz#b09082d2ba3d8ae1627ea771bd2fbd2851e4a035" + integrity sha512-QRpGleGwKv70hEcdklBh3HiLZ3OHPp40nRiVfhLk9wlQ4+V//SX+n90uIHN/mfKz828bjSSAxSG/kDUEp4Yp8Q== + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -189,6 +250,33 @@ dependencies: "@types/node" "*" +"@types/graphql@0.12.6": + version "0.12.6" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.12.6.tgz#3d619198585fcabe5f4e1adfb5cf5f3388c66c13" + integrity sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ== + +"@types/graphql@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.13.1.tgz#7d39750355c9ecb921816d6f76c080405b5f6bea" + integrity sha512-a6vRcP4M6+7Lqev1JeF3hGFmC3FNBIJ5cRnaSN/z1LtGVr/CaY6nqSlQbnFr2j8ucDlUAQU43LCnJrz6uLUyHg== + +"@types/handlebars@4.0.38": + version "4.0.38" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.38.tgz#d0ebec1934c0bba97f99a0cb703fe2ef8c4a662f" + integrity sha512-oMzU0D7jDp+H2go/i0XqBHfr+HEhYD/e1TvkhHi3yrhQm/7JFR8FJMdvoH76X8G1FBpgc6Pwi+QslCJBeJ1N9g== + +"@types/hapi@15.0.1": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-15.0.1.tgz#919e1d3a9160a080c9fdefaccc892239772e1258" + integrity sha512-5nxjmOY+irp5h0S7T87jtJqp2/cJAKWczC4fQ85Xt1SEQmeBYgzHn+LKrHOEY7pcZkg2+rJLFUDw4ohuorTbRg== + dependencies: + "@types/node" "*" + +"@types/history@*", "@types/history@^4.6.2": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" + integrity sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw== + "@types/is-stream@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" @@ -206,6 +294,11 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.4.tgz#0989d69e792a7db13e951852e6949df6787f113f" integrity sha512-eS6EeSGueXvS16CsHa7OKkRK1xBb6L+rXuXlzbWSWvb4v7zgNFPmY8l6aWWgEkHFeITVBadeQHQhVUpx0sd1tw== +"@types/lodash@^3.10.1": + version "3.10.2" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" + integrity sha512-TmlYodTNhMzVzv3CK/9sXGzh31jWsRKHE3faczhVgYFCdXIRQRCOPD+0NDlR+SvJlCj914yP3q3aAupt53p2Ug== + "@types/loglevel@^1.5.3": version "1.5.3" resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8" @@ -228,6 +321,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" integrity sha512-wNBfvNjzsJl4tswIZKXCFQY0lss9nKUyJnG6T94X/eqjRgI2jHZ4evdjhQYBSan/vGtF6XVXPApOmNH2rf0KKw== +"@types/node@^9.4.6": + version "9.6.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.22.tgz#05b55093faaadedea7a4b3f76e9a61346a6dd209" + integrity sha512-RIg9EkxzVMkNH0M4sLRngK23f5QiigJC0iODQmu4nopzstt8AjegYund3r82iMrd2BNCjcZVnklaItvKHaGfBA== + "@types/node@^9.4.7": version "9.6.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d" @@ -257,11 +355,82 @@ dependencies: "@types/node" "*" +"@types/prettier@1.13.1": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.13.1.tgz#5dd359398de96863a0629156f382913a06ebe013" + integrity sha512-OlcCdqLtMvl+Hq4UkAxxppKX252NXsBm6RyJZVuBZtkduu3Dl8pdx78XS4K7oPGPOxpD6T+KzK0DV11G8ykTkw== + +"@types/prop-types@^15.5.3": + version "15.5.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.3.tgz#bef071852dca2a2dbb65fecdb7bfb30cedae2de2" + integrity sha512-sfjHrNF4zWRv3fJUGyZW46wVxhYJ/GeWIPdKxbnLIhY3bWR0Ncl2kIhZI7rpjY9KtUQAkDP8jWEmaGQGFFvruA== + +"@types/react-datepicker@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-1.1.5.tgz#2070d5ab86eacf69f5e9573b1d69af9982841c02" + integrity sha512-ngVoBPI60NIzqCl8PeJ292MjPkIvHNrRW0krjsW9IEC4TANaAeFjW+ql9bnmPQW8MdhxB6OGU5P0EcNceEL91g== + dependencies: + "@types/react" "*" + moment ">=2.14.0" + popper.js "^1.14.1" + +"@types/react-dom@^16.0.5": + version "16.0.6" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c" + integrity sha512-M+1zmwa5KxUpkCuxA4whlDJKYTGNvNQW4pIoCLH16xGbClicD9CzPry4y94kTjCCk/bJZCZ/GVqUsP7eKcO/mQ== + dependencies: + "@types/node" "*" + "@types/react" "*" + +"@types/react-redux@^6.0.6": + version "6.0.6" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.6.tgz#87f1d0a6ea901b93fcaf95fa57641ff64079d277" + integrity sha512-sD/QEn45h+CH0OAhCn6/9COlihZ94bzpP58QzYYCL3tOFta/WBhuvMoyLP8khJLfwQBx1PT70HP/1GnDws9YXQ== + dependencies: + "@types/react" "*" + redux "^4.0.0" + +"@types/react-router-dom@^4.2.6": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c" + integrity sha512-6sIP3dIj6xquvcAuYDaxpbeLjr9954OuhCXnniMhnDgykAw2tVji9b0jKHofPJGUoHEMBsWzO83tjnk7vfzozA== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "4.0.27" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.27.tgz#553f54df7c4b09d6046b0201ce9b91c46b2940e3" + integrity sha512-EqGMptbgv4IkwJdU/ozonsFiL1iESUXk57rA6myayd/bIgYP4/pD0cZJUpOWCSvYT7QLDBuDkrwyEgCqfMZfNg== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react@*", "@types/react@^16.3.14": + version "16.3.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.14.tgz#f90ac6834de172e13ecca430dcb6814744225d36" + integrity sha512-wNUGm49fPl7eE2fnYdF0v5vSOrUMdKMQD/4NwtQRnb6mnPwtkhabmuFz37eq90+hhyfz0pWd38jkZHOcaZ6LGw== + dependencies: + csstype "^2.2.0" + +"@types/reduce-reducers@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@types/reduce-reducers/-/reduce-reducers-0.1.3.tgz#69f252207622ced7e063c7526ad46ec60b69f0c0" + integrity sha512-7U5EJ8PJs9x4RkVrJzq4cyq6ejK+/7rUQEs4CuNxmP4pkYWR7aPher9DRbLQ429Szw0TU6Twq8j3Fk6NsoTJbg== + dependencies: + redux "^3.6.0" + "@types/retry@*", "@types/retry@^0.10.2": version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" integrity sha512-LqJkY4VQ7S09XhI7kA3ON71AxauROhSv74639VsNXC9ish4IWHnIi98if+nP1MxQV3RMPqXSCYgpPsDHjlg9UQ== +"@types/sinon@^5.0.1": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.3.tgz#2b1840122f372350c563e3ceda2f447b55f3a927" + integrity sha512-JvnfqYfBapg1Ktjjvb79myQ2A848yCdB+1g/okS9OyNqPrT/qWUIt71G0eyi7msZf0gC4fBd40Yu15Btw6BMfQ== + "@types/superagent@*": version "3.8.4" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.4.tgz#24a5973c7d1a9c024b4bbda742a79267c33fb86a" @@ -282,6 +451,18 @@ resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" integrity sha1-EYHsvh2XtwNODqHjXmLobMJrQi0= +"@types/uuid@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5" + integrity sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw== + dependencies: + "@types/node" "*" + +"@types/valid-url@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45" + integrity sha1-YPpDXOJL/VuhB7jSqAeWrq86j0U= + "@types/ws@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-4.0.2.tgz#b29037627dd7ba31ec49a4f1584840422efb856f" @@ -290,6 +471,11 @@ "@types/events" "*" "@types/node" "*" +"@types/zen-observable@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" + integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + abab@^1.0.3, abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -533,6 +719,148 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" +apollo-cache-control@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" + integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA== + dependencies: + graphql-extensions "^0.0.x" + +apollo-cache-inmemory@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.2.7.tgz#80517c4b5e910022ab8d318f47d9364f99db8541" + integrity sha512-ikL3hWsd1DejiZSAuiGnX6TG3cKAZmkMTZZfNZggp9vcTa47kfPqms/pX0F4iajCJP/p7/AllhbpsQ3zVMOZGg== + dependencies: + apollo-cache "^1.1.14" + apollo-utilities "^1.0.18" + graphql-anywhere "^4.1.16" + +apollo-cache@^1.1.14: + version "1.1.14" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.14.tgz#c7d54cdbc7f544161f78fa5e4bae56650e22f7ad" + integrity sha512-Zmo9nVqpWFogki2QyulX6Xx6KYXMyYWX74grwgsYYUOukl4pIAdtYyK8e874o0QDgzSOq5AYPXjtfkoVpqhCRw== + dependencies: + apollo-utilities "^1.0.18" + +apollo-client@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.3.8.tgz#0384a7210eb601ab88b1c13750da076fc9255b95" + integrity sha512-X5wsBD1be1P/mScGsH5H+2hIE8d78WAfqOvFvBpP+C+jzJ9387uHLyFmYYMLRRqDQ3ihjI4iSID7KEOW2gyCcQ== + dependencies: + "@types/zen-observable" "^0.8.0" + apollo-cache "^1.1.14" + apollo-link "^1.0.0" + apollo-link-dedup "^1.0.0" + apollo-utilities "^1.0.18" + symbol-observable "^1.0.2" + zen-observable "^0.8.0" + optionalDependencies: + "@types/async" "2.0.49" + +apollo-link-dedup@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.9.tgz#3c4e4af88ef027cbddfdb857c043fd0574051dad" + integrity sha512-RbuEKpmSHVMtoREMPh2wUFTeh65q+0XPVeqgaOP/rGEAfvLyOMvX0vT2nVaejMohoMxuUnfZwpldXaDFWnlVbg== + dependencies: + apollo-link "^1.2.2" + +apollo-link-http-common@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.4.tgz#877603f7904dc8f70242cac61808b1f8d034b2c3" + integrity sha512-4j6o6WoXuSPen9xh4NBaX8/vL98X1xY2cYzUEK1F8SzvHe2oFONfxJBTekwU8hnvapcuq8Qh9Uct+gelu8T10g== + dependencies: + apollo-link "^1.2.2" + +apollo-link-http@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.4.tgz#b80b7b4b342c655b6a5614624b076a36be368f43" + integrity sha512-e9Ng3HfnW00Mh3TI6DhNRfozmzQOtKgdi+qUAsHBOEcTP0PTAmb+9XpeyEEOueLyO0GXhB92HUCIhzrWMXgwyg== + dependencies: + apollo-link "^1.2.2" + apollo-link-http-common "^0.2.4" + +apollo-link-schema@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/apollo-link-schema/-/apollo-link-schema-1.1.0.tgz#033fda26ffdbfc809d04892de554867f50e2af8e" + integrity sha512-sqWjse5RfrMAhrXecv0WdSLLdF1R5lI4YpbfkioIeJAkB7VB2o+mgA/+onATYKp214MSjloCDWzkvnVpRPFoBw== + dependencies: + apollo-link "^1.2.2" + +apollo-link-state@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.1.tgz#65e9e0e12c67936b8c4b12b8438434f393104579" + integrity sha512-69/til4ENfl/Fvf7br2xSsLSBcxcXPbOHVNkzLLejvUZickl93HLO4/fO+uvoBi4dCYRgN17Zr8FwI41ueRx0g== + dependencies: + apollo-utilities "^1.0.8" + graphql-anywhere "^4.1.0-alpha.0" + +apollo-link@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.1.tgz#c120b16059f9bd93401b9f72b94d2f80f3f305d2" + integrity sha512-6Ghf+j3cQLCIvjXd2dJrLw+16HZbWbwmB1qlTc41BviB2hv+rK1nJr17Y9dWK0UD4p3i9Hfddx3tthpMKrueHg== + dependencies: + "@types/node" "^9.4.6" + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.6" + +apollo-link@^1.0.0, apollo-link@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.2.tgz#54c84199b18ac1af8d63553a68ca389c05217a03" + integrity sha512-Uk/BC09dm61DZRDSu52nGq0nFhq7mcBPTjy5EEH1eunJndtCaNXQhQz/BjkI2NdrfGI+B+i5he6YSoRBhYizdw== + dependencies: + "@types/graphql" "0.12.6" + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.9" + +apollo-server-core@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.6.tgz#08636243c2de56fa8c267d68dd602cb1fbd323e3" + integrity sha1-CGNiQ8LeVvqMJn1o3WAssfvTI+M= + dependencies: + apollo-cache-control "^0.1.0" + apollo-tracing "^0.1.0" + graphql-extensions "^0.0.x" + +apollo-server-errors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.0.2.tgz#e9cbb1b74d2cd78aed23cd886ca2d0c186323b2b" + integrity sha512-zyWDqAVDCkj9espVsoUpZr9PwDznM8UW6fBfhV+i1br//s2AQb07N6ektZ9pRIEvkhykDZW+8tQbDwAO0vUROg== + +apollo-server-hapi@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-server-hapi/-/apollo-server-hapi-1.3.6.tgz#44dea128b64c1c10fdd35ac8307896a57ba1f4a8" + integrity sha1-RN6hKLZMHBD901rIMHiWpXuh9Kg= + dependencies: + apollo-server-core "^1.3.6" + apollo-server-module-graphiql "^1.3.4" + boom "^7.1.0" + +apollo-server-module-graphiql@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.3.4.tgz#50399b7c51b7267d0c841529f5173e5fc7304de4" + integrity sha1-UDmbfFG3Jn0MhBUp9Rc+X8cwTeQ= + +apollo-tracing@^0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" + integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ== + dependencies: + graphql-extensions "~0.0.9" + +apollo-utilities@^1.0.0, apollo-utilities@^1.0.1, apollo-utilities@^1.0.16, apollo-utilities@^1.0.8: + version "1.0.16" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.16.tgz#787310df4c3900a68c0beb3d351c59725a588cdb" + integrity sha512-5oKnElKqkV920KRbitiyISLeG63tUGAyNdotg58bQSX9Omr+smoNDTIRMRLbyIdKOYLaw3LpDaRepOPqljj0NQ== + dependencies: + fast-json-stable-stringify "^2.0.0" + +apollo-utilities@^1.0.18: + version "1.0.18" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.18.tgz#e4ee91534283fde2b744a26caaea120fe6a94f67" + integrity sha512-hHrmsoMYzzzfUlTOPpxr0qRpTLotMkBIQ93Ub7ki2SWdLfYYKrp6/KB8YOUkbCwXxSFvYSV24ccuwUEqZIaHIA== + dependencies: + fast-json-stable-stringify "^2.0.0" + append-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" @@ -806,6 +1134,11 @@ async@^2.5.0: dependencies: lodash "^4.17.10" +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + integrity sha1-+PwEyjoTeErenhZBr5hXjPvWR6k= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1369,6 +1702,15 @@ babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-tra invariant "^2.2.2" lodash "^4.17.4" +babel-types@7.0.0-beta.3: + version "7.0.0-beta.3" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.3.tgz#cd927ca70e0ae8ab05f4aab83778cfb3e6eb20b4" + integrity sha512-36k8J+byAe181OmCMawGhw+DtKO7AwexPVtsPXoMfAkjtZgoCX3bEuHWfdE5sYxRM8dojvtG/+O08M0Z/YDC6w== + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" @@ -1379,6 +1721,11 @@ babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24. lodash "^4.17.4" to-fast-properties "^1.0.3" +babylon@7.0.0-beta.47: + version "7.0.0-beta.47" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80" + integrity sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ== + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1530,6 +1877,13 @@ boom@5.x.x: dependencies: hoek "4.x.x" +boom@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-7.2.0.tgz#2bff24a55565767fde869ec808317eb10c48e966" + integrity sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY= + dependencies: + hoek "5.x.x" + brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -1733,6 +2087,14 @@ callsites@^2.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= +camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -1829,6 +2191,30 @@ chance@1.0.10: resolved "https://registry.yarnpkg.com/chance/-/chance-1.0.10.tgz#03500b04ad94e778dd2891b09ec73a6ad87b1996" integrity sha1-A1ALBK2U53jdKJGwnsc6ath7GZY= +change-case@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.2.tgz#fd48746cce02f03f0a672577d1d3a8dc2eceb037" + integrity sha512-Mww+SLF6MZ0U6kdg11algyKd5BARbyM4TbFBepwowYSR5ClfQGCGtxNXgykpN0uF/bstWeaGDT4JWaDh8zWAHA== + dependencies: + camel-case "^3.0.0" + constant-case "^2.0.0" + dot-case "^2.1.0" + header-case "^1.0.0" + is-lower-case "^1.1.0" + is-upper-case "^1.1.0" + lower-case "^1.1.1" + lower-case-first "^1.0.0" + no-case "^2.3.2" + param-case "^2.1.0" + pascal-case "^2.0.0" + path-case "^2.1.0" + sentence-case "^2.1.0" + snake-case "^2.1.0" + swap-case "^1.1.0" + title-case "^2.1.0" + upper-case "^1.1.1" + upper-case-first "^1.1.0" + change-emitter@^0.1.2: version "0.1.6" resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" @@ -2047,6 +2433,11 @@ colors@0.5.x: resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774" integrity sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q= +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= + colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2081,6 +2472,11 @@ commander@2.12.2, commander@^2.9.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" integrity sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA== +commander@2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" + integrity sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew== + commander@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" @@ -2098,6 +2494,11 @@ commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== +common-tags@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2170,6 +2571,14 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +constant-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + integrity sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY= + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + content-type-parser@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" @@ -2245,7 +2654,7 @@ core-js@^2.4.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" integrity sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4= -core-js@^2.5.1, core-js@^2.5.7: +core-js@^2.5.1, core-js@^2.5.3, core-js@^2.5.7: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== @@ -2377,6 +2786,11 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.5.tgz#4125484a3d42189a863943f23b9e4b80fedfa106" + integrity sha512-EGMjeoiN3aqEX5u/cyH5mSdGBDGdLcCQvcEcBWNGFSPXKd9uOTIeVG91YQ22OxI44DKpvI+4C7VUSmEpsHWJaA== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -2384,6 +2798,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI= + cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2525,6 +2944,11 @@ data-urls@^1.0.1: whatwg-mimetype "^2.1.0" whatwg-url "^7.0.0" +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + dateformat@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" @@ -2690,6 +3114,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +deprecated-decorator@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" + integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= + deprecated@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" @@ -2820,6 +3249,13 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +dot-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" + integrity sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4= + dependencies: + no-case "^2.2.0" + dotenv@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-2.0.0.tgz#bd759c357aaa70365e01c96b7b0bec08a6e0d949" @@ -3353,6 +3789,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= + falafel@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c" @@ -3964,17 +4405,7 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^4.3.1: - version "4.5.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" - integrity sha1-xstz0yJsHv7wTePFbQEvAzd+4V8= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "^2.0.1" - once "^1.3.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== @@ -3986,6 +4417,16 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + integrity sha1-xstz0yJsHv7wTePFbQEvAzd+4V8= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + glob@~3.1.21: version "3.1.21" resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" @@ -4140,6 +4581,106 @@ graceful-fs@~1.2.0: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +graphql-anywhere@^4.1.0-alpha.0: + version "4.1.14" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.14.tgz#89664cb885faaec1cbc66905351fadae8cc85a04" + integrity sha512-Wy4h9FWwxgrDRP16wXApP/VmNKXXTvY8jZLDtgxwDuC8Z6LSVkeCDjQSCyNKdgktky104UCbIQ3x+et9bQupDA== + dependencies: + apollo-utilities "^1.0.16" + +graphql-anywhere@^4.1.16: + version "4.1.16" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.16.tgz#82bb59643e30183cfb7b485ed4262a7b39d8a6c1" + integrity sha512-DNQGxrh2p8w4vQwHIW1Sw65ZDbOr6ktQCeol6itH3LeWy1a3IoZ67jxrhgrHM+Upg8oiazvteSr64VRxJ8n5+g== + dependencies: + apollo-utilities "^1.0.18" + +graphql-code-generator@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/graphql-code-generator/-/graphql-code-generator-0.10.1.tgz#b1c05b726ad4c9078a609233cb364eeeb512b5ab" + integrity sha1-scBbcmrUyQeKYJIzyzZO7rUStas= + dependencies: + "@types/babylon" "6.16.3" + "@types/prettier" "1.13.1" + "@types/valid-url" "1.0.2" + babel-types "7.0.0-beta.3" + babylon "7.0.0-beta.47" + commander "2.16.0" + glob "7.1.2" + graphql-codegen-compiler "0.10.1" + graphql-codegen-core "0.10.1" + mkdirp "0.5.1" + prettier "1.13.7" + request "2.87.0" + valid-url "1.0.9" + +graphql-codegen-compiler@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/graphql-codegen-compiler/-/graphql-codegen-compiler-0.10.1.tgz#de9a6dd037a665a1ad276b3366987e8a5f58bb9f" + integrity sha1-3ppt0DemZaGtJ2szZph+il9Yu58= + dependencies: + "@types/handlebars" "4.0.38" + change-case "3.0.2" + common-tags "1.8.0" + graphql-codegen-core "0.10.1" + handlebars "4.0.11" + moment "2.22.2" + +graphql-codegen-core@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/graphql-codegen-core/-/graphql-codegen-core-0.10.1.tgz#f7ab790b050f14b133c5a28f2260cdd8d1e7792d" + integrity sha1-96t5CwUPFLEzxaKPImDN2NHneS0= + dependencies: + graphql-tag "2.9.2" + graphql-tools "3.0.4" + winston "2.4.3" + +graphql-codegen-introspection-template@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/graphql-codegen-introspection-template/-/graphql-codegen-introspection-template-0.10.5.tgz#c8c0f647a9771c4e3c6ffbe26aa8da15af9af661" + integrity sha1-yMD2R6l3HE48b/viaqjaFa+a9mE= + +graphql-codegen-typescript-template@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/graphql-codegen-typescript-template/-/graphql-codegen-typescript-template-0.10.1.tgz#bb631d2ebfcd8f15117ca642a9988bd767183e02" + integrity sha1-u2MdLr/NjxURfKZCqZiL12cYPgI= + +graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: + version "0.0.10" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" + integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA== + dependencies: + core-js "^2.5.3" + source-map-support "^0.5.1" + +graphql-fields@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/graphql-fields/-/graphql-fields-1.0.2.tgz#099ee1d4445b42d0f47e06d622acebb33abc6cce" + integrity sha1-CZ7h1ERbQtD0fgbWIqzrszq8bM4= + +graphql-tag@2.9.2, graphql-tag@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.9.2.tgz#2f60a5a981375f430bf1e6e95992427dc18af686" + integrity sha512-qnNmof9pAqj/LUzs3lStP0Gw1qhdVCUS7Ab7+SUB6KD5aX1uqxWQRwMnOGTkhKuLvLNIs1TvNz+iS9kUGl1MhA== + +graphql-tools@3.0.4, graphql-tools@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-3.0.4.tgz#d08aa75db111d704cba05d92afd67ec5d1dc6b24" + integrity sha512-doQeqej/1D7kowXjYaAKk9H04KZN6+Vm6/KqXk3iqq8kfeI5edHOCg+eDc4LZXb0EEue0vy76JW7TLOwf9ZMZQ== + dependencies: + apollo-link "1.2.1" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + +graphql@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" + integrity sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog== + dependencies: + iterall "^1.2.1" + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" @@ -4245,6 +4786,17 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" +handlebars@4.0.11, handlebars@^4.0.3: + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + integrity sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw= + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + handlebars@^4.0.10: version "4.0.12" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" @@ -4256,17 +4808,6 @@ handlebars@^4.0.10: optionalDependencies: uglify-js "^3.1.4" -handlebars@^4.0.3: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" - integrity sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw= - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" - hapi-auth-cookie@6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/hapi-auth-cookie/-/hapi-auth-cookie-6.1.1.tgz#927db39e434916d81ab870d4181d70d53e745572" @@ -4473,6 +5014,14 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" +header-case@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" + integrity sha1-lTWXMZfBRLCWE81l0xfvGZY70C0= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.3" + heavy@4.x.x: version "4.0.4" resolved "https://registry.yarnpkg.com/heavy/-/heavy-4.0.4.tgz#36c91336c00ccfe852caa4d153086335cd2f00e9" @@ -4518,6 +5067,11 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" integrity sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ== +hoek@5.x.x: + version "5.0.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac" + integrity sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw== + hoist-non-react-statics@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" @@ -4980,6 +5534,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-lower-case@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" + integrity sha1-fhR75HaNxGbbO/shzGCzHmrWk5M= + dependencies: + lower-case "^1.1.0" + is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" @@ -5124,6 +5685,13 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-upper-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" + integrity sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8= + dependencies: + upper-case "^1.1.0" + is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -5184,7 +5752,7 @@ isomorphic-fetch@2.2.1, isomorphic-fetch@^2.1.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" -isstream@~0.1.2: +isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= @@ -5272,6 +5840,11 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" integrity sha1-i9FtnIOxlSneWuoyGsqtp4NkoZg= +iterall@^1.1.3, iterall@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + jade@0.26.3: version "0.26.3" resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" @@ -6013,7 +6586,7 @@ lodash-es@^4.17.4: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" integrity sha1-3MHXVS4VCgZABzupyzHXDwMpUOc= -lodash-es@^4.17.5: +lodash-es@^4.17.5, lodash-es@^4.2.1: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" integrity sha512-iesFYPmxYYGTcmQK0sL8bX3TGHyM6b2qREaB4kamHfQyfPJP0xgoGxp19nsH16nsfquLdiyKyX3mQkfiSGV8Rg== @@ -6331,7 +6904,7 @@ lodash@3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -lodash@^4.0.0, lodash@^4.17.5, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@~4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== @@ -6398,6 +6971,18 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" +lower-case-first@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" + integrity sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E= + dependencies: + lower-case "^1.1.2" + +lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + lowercase-keys@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -6803,16 +7388,16 @@ moment-timezone@^0.5.14: dependencies: moment ">= 2.9.0" +moment@2.22.2, moment@>=2.14.0: + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= + moment@2.x.x, "moment@>= 2.9.0", moment@^2.13.0, moment@^2.20.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" integrity sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg== -moment@>=2.14.0: - version "2.22.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= - move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -6952,6 +7537,13 @@ nise@^1.2.0: path-to-regexp "^1.7.0" text-encoding "^0.6.4" +no-case@^2.2.0, no-case@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + node-ensure@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" @@ -7505,6 +8097,13 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" +param-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= + dependencies: + no-case "^2.2.0" + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -7569,11 +8168,26 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" +pascal-case@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" + integrity sha1-LVeNNFX2YNpl7KGO+VtODekSdh4= + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +path-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" + integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU= + dependencies: + no-case "^2.2.0" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -7862,6 +8476,11 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier@1.13.7: + version "1.13.7" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" + integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== + pretty-format@^23.5.0: version "23.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.5.0.tgz#0f9601ad9da70fe690a269cd3efca732c210687c" @@ -8194,6 +8813,17 @@ react-addons-shallow-compare@^15.0.1: fbjs "^0.8.4" object-assign "^4.1.0" +react-apollo@^2.1.4: + version "2.1.8" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.1.8.tgz#ebac0d9bee0f0906df3ce29207f94df337009887" + integrity sha512-HBz9WDhvaqNxahKvBvW915a9MYSbarJ2Nrwh2pCeDctFiZ/bhixX1xJE/Ea0aU6gU5tGDEl+aWjxzx852FXHoA== + dependencies: + fbjs "^0.8.16" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.2" + lodash "^4.17.10" + prop-types "^15.6.0" + react-beautiful-dnd@^8.0.7: version "8.0.7" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" @@ -8647,10 +9277,10 @@ reduce-reducers@^0.1.0: resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" integrity sha1-+htHGLxSkqcd3R5dg5yb6pdw8Us= -reduce-reducers@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.5.tgz#ff77ca8068ff41007319b8b4b91533c7e0e54576" - integrity sha512-uoVmQnZQ+BtKKDKpBdbBri5SLNyIK9ULZGOA504++VbHcwouWE+fJDIo8AuESPF9/EYSkI0v05LDEQK6stCbTA== +reduce-reducers@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c" + integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw== redux-actions@2.2.1: version "2.2.1" @@ -8662,6 +9292,11 @@ redux-actions@2.2.1: lodash-es "^4.17.4" reduce-reducers "^0.1.0" +redux-observable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.0.0.tgz#780ff2455493eedcef806616fe286b454fd15d91" + integrity sha512-6bXnpqWTBeLaLQjXHyN1giXq4nLxCmv+SUkdmiwBgvmVxvDbdmydvL1Z7DGo0WItyzI/kqXQKiucUuTxnrPRkA== + redux-test-utils@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/redux-test-utils/-/redux-test-utils-0.2.2.tgz#593213f30173c5908f72315f08b705e1606094fe" @@ -8685,6 +9320,16 @@ redux@4.0.0, redux@^4.0.0: loose-envify "^1.1.0" symbol-observable "^1.2.0" +redux@^3.6.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" @@ -8845,6 +9490,32 @@ request@2.81.0, "request@>=2.9.0 <2.82.0": tunnel-agent "^0.6.0" uuid "^3.0.0" +request@2.87.0: + version "2.87.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e" + integrity sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + request@^2.83.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" @@ -9266,6 +9937,14 @@ semver@~5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= +sentence-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4" + integrity sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ= + dependencies: + no-case "^2.2.0" + upper-case-first "^1.1.2" + sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" @@ -9402,6 +10081,13 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + integrity sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8= + dependencies: + no-case "^2.2.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -9529,6 +10215,14 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" +source-map-support@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" + integrity sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" @@ -9640,6 +10334,11 @@ ssri@^5.2.4: dependencies: safe-buffer "^5.1.1" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" @@ -9956,7 +10655,15 @@ svgo@^0.7.2: sax "~1.2.1" whet.extend "~0.9.9" -symbol-observable@^1.0.4, symbol-observable@^1.2.0: +swap-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" + integrity sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM= + dependencies: + lower-case "^1.1.1" + upper-case "^1.1.1" + +symbol-observable@^1.0.2, symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -10174,6 +10881,14 @@ tinymath@^0.5.0: resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-0.5.0.tgz#4c8b788a40b5929c4aff36ecc7c128004202496a" integrity sha512-bC+dPDr5x8pm0n7ZCc28JMcwPCY3PJsxYOrtecBvKszq2x7p4HVqOl0wKI5r59vEZSoyRg+JGoaqvb4KmdHaKg== +title-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" + integrity sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o= + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + tmp@0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" @@ -10218,6 +10933,11 @@ to-fast-properties@^1.0.3: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + to-iso-string@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" @@ -10363,6 +11083,18 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-fsa-reducers@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/typescript-fsa-reducers/-/typescript-fsa-reducers-0.4.5.tgz#58fffb2f6eeca6817c2f656b7e7df2cb1c9d1f84" + integrity sha512-mBIpU4je365qpqp2XWKtNW3rGO/hA4OI+l8vkkXdHLUYukrp3wNeL+e3roUq1F6wa6Kcr0WaMblEQDsIdWHTEQ== + dependencies: + typescript-fsa "^2.0.0" + +typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" + integrity sha1-G67AG16PXzTDImedEycBbp4pT68= + typescript@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" @@ -10505,6 +11237,18 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +upper-case-first@^1.1.0, upper-case-first@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + integrity sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU= + dependencies: + upper-case "^1.1.1" + +upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -10588,6 +11332,11 @@ v8flags@^2.0.2: dependencies: user-home "^1.1.1" +valid-url@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" @@ -10858,6 +11607,18 @@ window-size@^0.2.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= +winston@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.3.tgz#7a9fdab371b6d3d9b63a592947846d856948c517" + integrity sha512-GYKuysPz2pxYAVJD2NPsDLP5Z79SDEzPm9/j4tCjkF/n89iBNGBMJcR+dMUqxgPNgoSs6fVygPi+Vl2oxIpBuw== + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -11151,6 +11912,18 @@ yeast@0.1.2: resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= +zen-observable-ts@^0.8.6, zen-observable-ts@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.9.tgz#d3c97af08c0afdca37ebcadf7cc3ee96bda9bab1" + integrity sha512-KJz2O8FxbAdAU5CSc8qZ1K2WYEJb1HxS6XDRF+hOJ1rOYcg6eTMmS9xYHCXzqZZzKw6BbXWyF4UpwSsBQnHJeA== + dependencies: + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.8" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.8.tgz#1ea93995bf098754a58215a1e0a7309e5749ec42" + integrity sha512-HnhhyNnwTFzS48nihkCZIJGsWGFcYUz+XPDlPK5W84Ifji8SksC6m7sQWOf8zdCGhzQ4tDYuMYGu5B0N1dXTtg== + zlib@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" diff --git a/yarn.lock b/yarn.lock index fa01cd54e62ab..24b70fd445a05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -335,6 +335,11 @@ resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.50.tgz#8b6599088d80f68ef0cad7d3a2062248ebe72b3d" integrity sha512-D3KB0PdaxdwtA44yOpK+NtptTscKWgUzXmf8fiLaaVxnX+b7QQ+dUMsyeVDweCQ6VX4PMwkd6x2hJ0X+ISIsoQ== +"@types/async@2.0.49": + version "2.0.49" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0" + integrity sha512-Benr3i5odUkvpFkOpzGqrltGdbSs+EVCkEBGXbuR7uT0VzhXKIkhem6PDzHdx5EonA+rfbB3QvP6aDOw5+zp5Q== + "@types/babel-core@^6.25.5": version "6.25.5" resolved "https://registry.yarnpkg.com/@types/babel-core/-/babel-core-6.25.5.tgz#7598b1287c2cb5a8e9150d60e4d4a8f2dbe29982" @@ -491,6 +496,11 @@ dependencies: "@types/node" "*" +"@types/graphql@0.12.6": + version "0.12.6" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.12.6.tgz#3d619198585fcabe5f4e1adfb5cf5f3388c66c13" + integrity sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ== + "@types/hapi-latest@npm:@types/hapi@17.0.12": version "17.0.12" resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-17.0.12.tgz#5751f4d8db4decb4eae6671a4efbeae671278ceb" @@ -605,6 +615,11 @@ dependencies: moment ">=2.14.0" +"@types/mustache@^0.8.31": + version "0.8.31" + resolved "https://registry.yarnpkg.com/@types/mustache/-/mustache-0.8.31.tgz#7c86cbf74f7733f9e3bdc28817623927eb386616" + integrity sha512-72flCZJkEJHPwhmpHgg4a0ZBLssMhg5NB0yltRblRlZMo4py3B/u/d7icevc4EeN9MPQUo/dPtuVOoVy9ih6cQ== + "@types/node@*": version "9.4.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" @@ -615,6 +630,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285" integrity sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w== +"@types/node@^9.4.6": + version "9.6.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.22.tgz#05b55093faaadedea7a4b3f76e9a61346a6dd209" + integrity sha512-RIg9EkxzVMkNH0M4sLRngK23f5QiigJC0iODQmu4nopzstt8AjegYund3r82iMrd2BNCjcZVnklaItvKHaGfBA== + "@types/node@^9.4.7": version "9.6.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.18.tgz#092e13ef64c47e986802c9c45a61c1454813b31d" @@ -770,6 +790,19 @@ "@types/events" "*" "@types/node" "*" +"@types/zen-observable@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" + integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + +JSONStream@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.1.1.tgz#c98bfd88c8f1e1e8694e53c5baa6c8691553e59a" + integrity sha1-yYv9iMjx4ehpTlPFuqbIaRVT5Zo= + dependencies: + jsonparse "^1.1.0" + through ">=2.2.7 <3" + abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1126,6 +1159,148 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apollo-cache-control@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz#173d14ceb3eb9e7cb53de7eb8b61bee6159d4171" + integrity sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA== + dependencies: + graphql-extensions "^0.0.x" + +apollo-cache-inmemory@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.2.7.tgz#80517c4b5e910022ab8d318f47d9364f99db8541" + integrity sha512-ikL3hWsd1DejiZSAuiGnX6TG3cKAZmkMTZZfNZggp9vcTa47kfPqms/pX0F4iajCJP/p7/AllhbpsQ3zVMOZGg== + dependencies: + apollo-cache "^1.1.14" + apollo-utilities "^1.0.18" + graphql-anywhere "^4.1.16" + +apollo-cache@^1.1.14: + version "1.1.14" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.14.tgz#c7d54cdbc7f544161f78fa5e4bae56650e22f7ad" + integrity sha512-Zmo9nVqpWFogki2QyulX6Xx6KYXMyYWX74grwgsYYUOukl4pIAdtYyK8e874o0QDgzSOq5AYPXjtfkoVpqhCRw== + dependencies: + apollo-utilities "^1.0.18" + +apollo-client@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.3.8.tgz#0384a7210eb601ab88b1c13750da076fc9255b95" + integrity sha512-X5wsBD1be1P/mScGsH5H+2hIE8d78WAfqOvFvBpP+C+jzJ9387uHLyFmYYMLRRqDQ3ihjI4iSID7KEOW2gyCcQ== + dependencies: + "@types/zen-observable" "^0.8.0" + apollo-cache "^1.1.14" + apollo-link "^1.0.0" + apollo-link-dedup "^1.0.0" + apollo-utilities "^1.0.18" + symbol-observable "^1.0.2" + zen-observable "^0.8.0" + optionalDependencies: + "@types/async" "2.0.49" + +apollo-link-dedup@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.9.tgz#3c4e4af88ef027cbddfdb857c043fd0574051dad" + integrity sha512-RbuEKpmSHVMtoREMPh2wUFTeh65q+0XPVeqgaOP/rGEAfvLyOMvX0vT2nVaejMohoMxuUnfZwpldXaDFWnlVbg== + dependencies: + apollo-link "^1.2.2" + +apollo-link-http-common@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.4.tgz#877603f7904dc8f70242cac61808b1f8d034b2c3" + integrity sha512-4j6o6WoXuSPen9xh4NBaX8/vL98X1xY2cYzUEK1F8SzvHe2oFONfxJBTekwU8hnvapcuq8Qh9Uct+gelu8T10g== + dependencies: + apollo-link "^1.2.2" + +apollo-link-http@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.4.tgz#b80b7b4b342c655b6a5614624b076a36be368f43" + integrity sha512-e9Ng3HfnW00Mh3TI6DhNRfozmzQOtKgdi+qUAsHBOEcTP0PTAmb+9XpeyEEOueLyO0GXhB92HUCIhzrWMXgwyg== + dependencies: + apollo-link "^1.2.2" + apollo-link-http-common "^0.2.4" + +apollo-link-schema@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/apollo-link-schema/-/apollo-link-schema-1.1.0.tgz#033fda26ffdbfc809d04892de554867f50e2af8e" + integrity sha512-sqWjse5RfrMAhrXecv0WdSLLdF1R5lI4YpbfkioIeJAkB7VB2o+mgA/+onATYKp214MSjloCDWzkvnVpRPFoBw== + dependencies: + apollo-link "^1.2.2" + +apollo-link-state@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.1.tgz#65e9e0e12c67936b8c4b12b8438434f393104579" + integrity sha512-69/til4ENfl/Fvf7br2xSsLSBcxcXPbOHVNkzLLejvUZickl93HLO4/fO+uvoBi4dCYRgN17Zr8FwI41ueRx0g== + dependencies: + apollo-utilities "^1.0.8" + graphql-anywhere "^4.1.0-alpha.0" + +apollo-link@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.1.tgz#c120b16059f9bd93401b9f72b94d2f80f3f305d2" + integrity sha512-6Ghf+j3cQLCIvjXd2dJrLw+16HZbWbwmB1qlTc41BviB2hv+rK1nJr17Y9dWK0UD4p3i9Hfddx3tthpMKrueHg== + dependencies: + "@types/node" "^9.4.6" + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.6" + +apollo-link@^1.0.0, apollo-link@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.2.tgz#54c84199b18ac1af8d63553a68ca389c05217a03" + integrity sha512-Uk/BC09dm61DZRDSu52nGq0nFhq7mcBPTjy5EEH1eunJndtCaNXQhQz/BjkI2NdrfGI+B+i5he6YSoRBhYizdw== + dependencies: + "@types/graphql" "0.12.6" + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.9" + +apollo-server-core@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.6.tgz#08636243c2de56fa8c267d68dd602cb1fbd323e3" + integrity sha1-CGNiQ8LeVvqMJn1o3WAssfvTI+M= + dependencies: + apollo-cache-control "^0.1.0" + apollo-tracing "^0.1.0" + graphql-extensions "^0.0.x" + +apollo-server-errors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.0.2.tgz#e9cbb1b74d2cd78aed23cd886ca2d0c186323b2b" + integrity sha512-zyWDqAVDCkj9espVsoUpZr9PwDznM8UW6fBfhV+i1br//s2AQb07N6ektZ9pRIEvkhykDZW+8tQbDwAO0vUROg== + +apollo-server-hapi@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/apollo-server-hapi/-/apollo-server-hapi-1.3.6.tgz#44dea128b64c1c10fdd35ac8307896a57ba1f4a8" + integrity sha1-RN6hKLZMHBD901rIMHiWpXuh9Kg= + dependencies: + apollo-server-core "^1.3.6" + apollo-server-module-graphiql "^1.3.4" + boom "^7.1.0" + +apollo-server-module-graphiql@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.3.4.tgz#50399b7c51b7267d0c841529f5173e5fc7304de4" + integrity sha1-UDmbfFG3Jn0MhBUp9Rc+X8cwTeQ= + +apollo-tracing@^0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.1.4.tgz#5b8ae1b01526b160ee6e552a7f131923a9aedcc7" + integrity sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ== + dependencies: + graphql-extensions "~0.0.9" + +apollo-utilities@^1.0.0, apollo-utilities@^1.0.1, apollo-utilities@^1.0.16, apollo-utilities@^1.0.8: + version "1.0.16" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.16.tgz#787310df4c3900a68c0beb3d351c59725a588cdb" + integrity sha512-5oKnElKqkV920KRbitiyISLeG63tUGAyNdotg58bQSX9Omr+smoNDTIRMRLbyIdKOYLaw3LpDaRepOPqljj0NQ== + dependencies: + fast-json-stable-stringify "^2.0.0" + +apollo-utilities@^1.0.18: + version "1.0.18" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.18.tgz#e4ee91534283fde2b744a26caaea120fe6a94f67" + integrity sha512-hHrmsoMYzzzfUlTOPpxr0qRpTLotMkBIQ93Ub7ki2SWdLfYYKrp6/KB8YOUkbCwXxSFvYSV24ccuwUEqZIaHIA== + dependencies: + fast-json-stable-stringify "^2.0.0" + append-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" @@ -2499,7 +2674,7 @@ boom@5.2.0, boom@5.x.x: dependencies: hoek "4.x.x" -boom@7.x.x: +boom@7.x.x, boom@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/boom/-/boom-7.2.0.tgz#2bff24a55565767fde869ec808317eb10c48e966" integrity sha1-K/8kpVVldn/ehp7ICDF+sQxI6WY= @@ -3843,7 +4018,7 @@ core-js@^2.2.0, core-js@^2.5.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" integrity sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4= -core-js@^2.4.0, core-js@^2.5.1, core-js@^2.5.7: +core-js@^2.4.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.5.7: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== @@ -4407,6 +4582,11 @@ dashify@^0.1.0: resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + date-fns@^1.27.2: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" @@ -4682,6 +4862,11 @@ depd@~1.1.0, depd@~1.1.1: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +deprecated-decorator@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" + integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -6908,6 +7093,56 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, g resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +graphql-anywhere@^4.1.0-alpha.0: + version "4.1.14" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.14.tgz#89664cb885faaec1cbc66905351fadae8cc85a04" + integrity sha512-Wy4h9FWwxgrDRP16wXApP/VmNKXXTvY8jZLDtgxwDuC8Z6LSVkeCDjQSCyNKdgktky104UCbIQ3x+et9bQupDA== + dependencies: + apollo-utilities "^1.0.16" + +graphql-anywhere@^4.1.16: + version "4.1.16" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.16.tgz#82bb59643e30183cfb7b485ed4262a7b39d8a6c1" + integrity sha512-DNQGxrh2p8w4vQwHIW1Sw65ZDbOr6ktQCeol6itH3LeWy1a3IoZ67jxrhgrHM+Upg8oiazvteSr64VRxJ8n5+g== + dependencies: + apollo-utilities "^1.0.18" + +graphql-extensions@^0.0.x, graphql-extensions@~0.0.9: + version "0.0.10" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.0.10.tgz#34bdb2546d43f6a5bc89ab23c295ec0466c6843d" + integrity sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA== + dependencies: + core-js "^2.5.3" + source-map-support "^0.5.1" + +graphql-fields@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/graphql-fields/-/graphql-fields-1.0.2.tgz#099ee1d4445b42d0f47e06d622acebb33abc6cce" + integrity sha1-CZ7h1ERbQtD0fgbWIqzrszq8bM4= + +graphql-tag@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.9.2.tgz#2f60a5a981375f430bf1e6e95992427dc18af686" + integrity sha512-qnNmof9pAqj/LUzs3lStP0Gw1qhdVCUS7Ab7+SUB6KD5aX1uqxWQRwMnOGTkhKuLvLNIs1TvNz+iS9kUGl1MhA== + +graphql-tools@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-3.0.4.tgz#d08aa75db111d704cba05d92afd67ec5d1dc6b24" + integrity sha512-doQeqej/1D7kowXjYaAKk9H04KZN6+Vm6/KqXk3iqq8kfeI5edHOCg+eDc4LZXb0EEue0vy76JW7TLOwf9ZMZQ== + dependencies: + apollo-link "1.2.1" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + +graphql@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" + integrity sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog== + dependencies: + iterall "^1.2.1" + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" @@ -8586,6 +8821,11 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" integrity sha1-i9FtnIOxlSneWuoyGsqtp4NkoZg= +iterall@^1.1.3, iterall@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + jclass@^1.0.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jclass/-/jclass-1.2.1.tgz#eaafeec0dd6a5bf8b3ea43c04e010c637638768b" @@ -9246,6 +9486,11 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonparse@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -12861,6 +13106,17 @@ react-anything-sortable@^1.7.4: create-react-class "^15.5.2" prop-types "^15.5.8" +react-apollo@^2.1.4: + version "2.1.8" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.1.8.tgz#ebac0d9bee0f0906df3ce29207f94df337009887" + integrity sha512-HBz9WDhvaqNxahKvBvW915a9MYSbarJ2Nrwh2pCeDctFiZ/bhixX1xJE/Ea0aU6gU5tGDEl+aWjxzx852FXHoA== + dependencies: + fbjs "^0.8.16" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.2" + lodash "^4.17.10" + prop-types "^15.6.0" + react-beautiful-dnd@^8.0.7: version "8.0.7" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" @@ -13464,10 +13720,10 @@ reduce-reducers@^0.1.0: resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.2.tgz#fa1b4718bc5292a71ddd1e5d839c9bea9770f14b" integrity sha1-+htHGLxSkqcd3R5dg5yb6pdw8Us= -reduce-reducers@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.5.tgz#ff77ca8068ff41007319b8b4b91533c7e0e54576" - integrity sha512-uoVmQnZQ+BtKKDKpBdbBri5SLNyIK9ULZGOA504++VbHcwouWE+fJDIo8AuESPF9/EYSkI0v05LDEQK6stCbTA== +reduce-reducers@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.4.3.tgz#8e052618801cd8fc2714b4915adaa8937eb6d66c" + integrity sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw== redux-actions@2.2.1: version "2.2.1" @@ -13479,6 +13735,11 @@ redux-actions@2.2.1: lodash-es "^4.17.4" reduce-reducers "^0.1.0" +redux-observable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.0.0.tgz#780ff2455493eedcef806616fe286b454fd15d91" + integrity sha512-6bXnpqWTBeLaLQjXHyN1giXq4nLxCmv+SUkdmiwBgvmVxvDbdmydvL1Z7DGo0WItyzI/kqXQKiucUuTxnrPRkA== + redux-thunk@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -14569,6 +14830,14 @@ source-map-support@^0.4.15, source-map-support@^0.4.2: dependencies: source-map "^0.5.6" +source-map-support@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" + integrity sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" @@ -15200,7 +15469,7 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: +symbol-observable@^1.0.2, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -15383,7 +15652,7 @@ through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@~2.0.0, through2@~2.0.3 readable-stream "^2.1.5" xtend "~4.0.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -15836,6 +16105,18 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript-fsa-reducers@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/typescript-fsa-reducers/-/typescript-fsa-reducers-0.4.5.tgz#58fffb2f6eeca6817c2f656b7e7df2cb1c9d1f84" + integrity sha512-mBIpU4je365qpqp2XWKtNW3rGO/hA4OI+l8vkkXdHLUYukrp3wNeL+e3roUq1F6wa6Kcr0WaMblEQDsIdWHTEQ== + dependencies: + typescript-fsa "^2.0.0" + +typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" + integrity sha1-G67AG16PXzTDImedEycBbp4pT68= + typescript@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" @@ -17341,6 +17622,18 @@ yn@^2.0.0: resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= +zen-observable-ts@^0.8.6, zen-observable-ts@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.9.tgz#d3c97af08c0afdca37ebcadf7cc3ee96bda9bab1" + integrity sha512-KJz2O8FxbAdAU5CSc8qZ1K2WYEJb1HxS6XDRF+hOJ1rOYcg6eTMmS9xYHCXzqZZzKw6BbXWyF4UpwSsBQnHJeA== + dependencies: + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.8" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.8.tgz#1ea93995bf098754a58215a1e0a7309e5749ec42" + integrity sha512-HnhhyNnwTFzS48nihkCZIJGsWGFcYUz+XPDlPK5W84Ifji8SksC6m7sQWOf8zdCGhzQ4tDYuMYGu5B0N1dXTtg== + zlib@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0"