From b7f33b94a85f6e8405addb9b985221f60e682305 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 1 Jul 2020 09:45:21 -0700 Subject: [PATCH] [Metrics UI] Enhance Inventory View Tooltips (#69757) * [Metrics UI] Enhance Inventory View Tooltips * Fixing typos * Removing toMetricOpt Co-authored-by: Elastic Machine --- .../infra/common/http_api/snapshot_api.ts | 4 +- .../common/inventory_models/aws_ec2/index.ts | 1 + .../common/inventory_models/aws_rds/index.ts | 7 + .../common/inventory_models/aws_s3/index.ts | 7 + .../common/inventory_models/aws_sqs/index.ts | 7 + .../inventory_models/container/index.ts | 1 + .../common/inventory_models/host/index.ts | 1 + .../common/inventory_models/intl_strings.ts | 79 +++++++++ .../common/inventory_models/pod/index.ts | 1 + .../infra/common/inventory_models/types.ts | 1 + x-pack/plugins/infra/public/lib/lib.ts | 2 +- .../inventory_view/components/layout.tsx | 5 +- .../components/nodes_overview.tsx | 14 +- .../inventory_view/components/table_view.tsx | 9 +- .../conditional_tooltip.test.tsx.snap | 80 +++++++++ .../waffle/conditional_tooltip.test.tsx | 155 ++++++++++++++++++ .../components/waffle/conditional_tooltip.tsx | 119 ++++++++++++-- .../inventory_view/components/waffle/node.tsx | 11 +- .../inventory_view/hooks/use_snaphot.ts | 16 +- .../lib/calculate_bounds_from_nodes.test.ts | 55 +++++++ .../lib/calculate_bounds_from_nodes.ts | 31 ++++ .../lib/create_uptime_link.test.ts | 56 ++++--- .../inventory_view/lib/nodes_to_wafflemap.ts | 2 +- .../inventory_view/lib/sort_nodes.test.ts | 56 +++++++ .../metrics/inventory_view/lib/sort_nodes.ts | 7 +- .../evaluate_condition.ts | 11 +- .../server/lib/snapshot/query_helpers.ts | 39 +++-- .../server/lib/snapshot/response_helpers.ts | 63 ++++--- .../infra/server/lib/snapshot/snapshot.ts | 19 ++- .../infra/server/routes/snapshot/index.ts | 4 +- .../apis/metrics_ui/snapshot.ts | 126 +++++++------- 31 files changed, 815 insertions(+), 174 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index 1c7dfed82783..9ddbcb17089f 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -34,7 +34,7 @@ export const SnapshotNodeMetricRT = rt.intersection([ SnapshotNodeMetricOptionalRT, ]); export const SnapshotNodeRT = rt.type({ - metric: SnapshotNodeMetricRT, + metrics: rt.array(SnapshotNodeMetricRT), path: rt.array(SnapshotNodePathRT), }); @@ -97,7 +97,7 @@ export const SnapshotMetricInputRT = rt.union([ export const SnapshotRequestRT = rt.intersection([ rt.type({ timerange: InfraTimerangeInputRT, - metric: SnapshotMetricInputRT, + metrics: rt.array(SnapshotMetricInputRT), groupBy: SnapshotGroupByRT, nodeType: ItemTypeRT, sourceId: rt.string, diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts index 5f667beebd83..c12137f7810d 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts @@ -30,4 +30,5 @@ export const awsEC2: InventoryModel = { ip: 'aws.ec2.instance.public.ip', }, requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'], + tooltipMetrics: ['cpu', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts index 02cef192b59e..fa7dd62c0b8f 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/index.ts @@ -35,4 +35,11 @@ export const awsRDS: InventoryModel = { 'awsRDSActiveTransactions', 'awsRDSLatency', ], + tooltipMetrics: [ + 'cpu', + 'rdsLatency', + 'rdsConnections', + 'rdsQueriesExecuted', + 'rdsActiveTransactions', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts index a786283a100a..59c24eb733f9 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/index.ts @@ -35,4 +35,11 @@ export const awsS3: InventoryModel = { 'awsS3DownloadBytes', 'awsS3UploadBytes', ], + tooltipMetrics: [ + 's3BucketSize', + 's3NumberOfObjects', + 's3TotalRequests', + 's3UploadBytes', + 's3DownloadBytes', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts index 21379ebb1e60..2a9f2ad13d94 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/index.ts @@ -35,4 +35,11 @@ export const awsSQS: InventoryModel = { 'awsSQSMessagesEmpty', 'awsSQSOldestMessage', ], + tooltipMetrics: [ + 'sqsMessagesVisible', + 'sqsMessagesDelayed', + 'sqsMessagesEmpty', + 'sqsMessagesSent', + 'sqsOldestMessage', + ], }; diff --git a/x-pack/plugins/infra/common/inventory_models/container/index.ts b/x-pack/plugins/infra/common/inventory_models/container/index.ts index 8f2336d11e42..8c9d6f393b6d 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/container/index.ts @@ -37,4 +37,5 @@ export const container: InventoryModel = { 'containerDiskIOBytes', 'containerDiskIOOps', ], + tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/index.ts b/x-pack/plugins/infra/common/inventory_models/host/index.ts index 538af4f5119b..b0bfbd6693e5 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/index.ts @@ -47,4 +47,5 @@ export const host: InventoryModel = { ...awsRequiredMetrics, ...nginxRequireMetrics, ], + tooltipMetrics: ['cpu', 'memory', 'tx', 'rx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts index 08949ed53eb1..2a885136f4ee 100644 --- a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts +++ b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { SnapshotMetricType } from './types'; export const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { defaultMessage: 'CPU usage', }); @@ -68,3 +69,81 @@ export const fieldToName = (field: string) => { }; return LOOKUP[field] || field; }; + +export const SNAPSHOT_METRIC_TRANSLATIONS = { + cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { + defaultMessage: 'CPU usage', + }), + + memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { + defaultMessage: 'Memory usage', + }), + + rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { + defaultMessage: 'Inbound traffic', + }), + + tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { + defaultMessage: 'Outbound traffic', + }), + + logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { + defaultMessage: 'Log rate', + }), + + load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { + defaultMessage: 'Load', + }), + + count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { + defaultMessage: 'Count', + }), + diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { + defaultMessage: 'Disk Reads', + }), + diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { + defaultMessage: 'Disk Writes', + }), + s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { + defaultMessage: 'Bucket Size', + }), + s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { + defaultMessage: 'Total Requests', + }), + s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { + defaultMessage: 'Number of Objects', + }), + s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { + defaultMessage: 'Downloads (Bytes)', + }), + s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { + defaultMessage: 'Uploads (Bytes)', + }), + rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { + defaultMessage: 'Connections', + }), + rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { + defaultMessage: 'Queries Executed', + }), + rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { + defaultMessage: 'Active Transactions', + }), + rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { + defaultMessage: 'Latency', + }), + sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { + defaultMessage: 'Messages Available', + }), + sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { + defaultMessage: 'Messages Delayed', + }), + sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { + defaultMessage: 'Messages Added', + }), + sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { + defaultMessage: 'Messages Returned Empty', + }), + sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { + defaultMessage: 'Oldest Message', + }), +} as Record; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/index.ts b/x-pack/plugins/infra/common/inventory_models/pod/index.ts index 961e0248c79d..70623175f8c0 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/pod/index.ts @@ -37,4 +37,5 @@ export const pod: InventoryModel = { 'podNetworkTraffic', ...nginxRequiredMetrics, ], + tooltipMetrics: ['cpu', 'memory', 'rx', 'tx'], }; diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 35d83440812d..2c6432c3e528 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -351,4 +351,5 @@ export interface InventoryModel { }; metrics: InventoryMetrics; requiredMetrics: InventoryMetric[]; + tooltipMetrics: SnapshotMetricType[]; } diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 93f7ef644f79..782f6ce5e0eb 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -22,7 +22,7 @@ export interface InfraWaffleMapNode { name: string; ip?: string | null; path: SnapshotNodePath[]; - metric: SnapshotNodeMetric; + metrics: SnapshotNodeMetric[]; } export type InfraWaffleMapGroup = InfraWaffleMapGroupOfNodes | InfraWaffleMapGroupOfGroups; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 3884ee5b7279..fddd92128708 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -9,7 +9,8 @@ import { useInterval } from 'react-use'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; -import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; +import { NodesOverview } from './nodes_overview'; +import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; import { PageContent } from '../../../../components/page'; import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; @@ -48,7 +49,7 @@ export const Layout = () => { const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); const { loading, nodes, reload, interval } = useSnapshot( filterQueryAsJson, - metric, + [metric], groupBy, nodeType, sourceId, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index db5949f916ff..723e8e581cda 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { max, min } from 'lodash'; import React, { useCallback } from 'react'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; @@ -16,6 +15,7 @@ import { InfraLoadingPanel } from '../../../../components/loading'; import { Map } from './waffle/map'; import { TableView } from './table_view'; import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; +import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; export interface KueryFilterQuery { kind: 'kuery'; @@ -36,18 +36,6 @@ interface Props { formatter: InfraFormatter; } -export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { - const maxValues = nodes.map((node) => node.metric.max); - const minValues = nodes.map((node) => node.metric.value); - // if there is only one value then we need to set the bottom range to zero for min - // otherwise the legend will look silly since both values are the same for top and - // bottom. - if (minValues.length === 1) { - minValues.unshift(0); - } - return { min: min(minValues) || 0, max: max(maxValues) || 0 }; -}; - export const NodesOverview = ({ autoBounds, boundsOverride, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx index 764eeb154d34..1d94ab2f2f41 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { last } from 'lodash'; +import { last, first } from 'lodash'; import React, { useState, useCallback, useEffect } from 'react'; import { createWaffleMapNode } from '../lib/nodes_to_wafflemap'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; @@ -142,6 +142,7 @@ export const TableView = (props: Props) => { const items = nodes.map((node) => { const name = last(node.path); + const metric = first(node.metrics); return { name: (name && name.label) || 'unknown', ...getGroupPaths(node.path).reduce( @@ -151,9 +152,9 @@ export const TableView = (props: Props) => { }), {} ), - value: node.metric.value, - avg: node.metric.avg, - max: node.metric.max, + value: (metric && metric.value) || 0, + avg: (metric && metric.avg) || 0, + max: (metric && metric.max) || 0, node: createWaffleMapNode(node), }; }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap new file mode 100644 index 000000000000..b8cdc0acac1d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConditionalToolTip should just work 1`] = ` +
+
+ host-01 +
+ + + CPU usage + + + 10% + + + + + Memory usage + + + 80% + + + + + Outbound traffic + + + 8Mbit/s + + + + + Inbound traffic + + + 8Mbit/s + + +
+`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx new file mode 100644 index 000000000000..d2c30a4f38ee --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { mount } from 'enzyme'; +// import { act } from 'react-dom/test-utils'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; +import { EuiToolTip } from '@elastic/eui'; +import { ConditionalToolTip } from './conditional_tooltip'; +import { + InfraWaffleMapNode, + InfraWaffleMapOptions, + InfraFormatterType, +} from '../../../../../lib/lib'; + +jest.mock('../../../../../containers/source', () => ({ + useSourceContext: () => ({ sourceId: 'default' }), +})); + +jest.mock('../../hooks/use_snaphot'); +import { useSnapshot } from '../../hooks/use_snaphot'; +const mockedUseSnapshot = useSnapshot as jest.Mock>; + +const NODE: InfraWaffleMapNode = { + pathId: 'host-01', + id: 'host-01', + name: 'host-01', + path: [{ value: 'host-01', label: 'host-01' }], + metrics: [{ name: 'cpu' }], +}; + +const OPTIONS: InfraWaffleMapOptions = { + formatter: InfraFormatterType.percent, + formatTemplate: '{value}', + metric: { type: 'cpu' }, + groupBy: [], + legend: { + type: 'steppedGradient', + rules: [], + }, + sort: { by: 'value', direction: 'desc' }, +}; + +export const nextTick = () => new Promise((res) => process.nextTick(res)); +const ChildComponent = () =>
child
; + +describe('ConditionalToolTip', () => { + afterEach(() => { + mockedUseSnapshot.mockReset(); + }); + + function createWrapper(currentTime: number = Date.now(), hidden: boolean = false) { + return mount( + + + + ); + } + + it('should return children when hidden', () => { + mockedUseSnapshot.mockReturnValue({ + nodes: [], + error: null, + loading: true, + interval: '', + reload: jest.fn(() => Promise.resolve()), + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, true); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + }); + + it('should just work', () => { + jest.useFakeTimers(); + const reloadMock = jest.fn(() => Promise.resolve()); + mockedUseSnapshot.mockReturnValue({ + nodes: [ + { + path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }], + metrics: [ + { name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 }, + { name: 'memory', value: 0.8, avg: 0.8, max: 1 }, + { name: 'tx', value: 1000000, avg: 1000000, max: 1000000 }, + { name: 'rx', value: 1000000, avg: 1000000, max: 1000000 }, + ], + }, + ], + error: null, + loading: false, + interval: '60s', + reload: reloadMock, + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, false); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); + const expectedQuery = JSON.stringify({ + bool: { + filter: { + match_phrase: { 'host.name': 'host-01' }, + }, + }, + }); + const expectedMetrics = [{ type: 'cpu' }, { type: 'memory' }, { type: 'tx' }, { type: 'rx' }]; + expect(mockedUseSnapshot).toBeCalledWith( + expectedQuery, + expectedMetrics, + [], + 'host', + 'default', + currentTime, + '', + '', + false + ); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); + wrapper.find(EuiToolTip).simulate('mouseOver'); + jest.advanceTimersByTime(500); + expect(reloadMock).toHaveBeenCalled(); + expect(wrapper.find(EuiToolTip).props().content).toMatchSnapshot(); + }); + + it('should not load data if mouse out before 200 ms', () => { + jest.useFakeTimers(); + const reloadMock = jest.fn(() => Promise.resolve()); + mockedUseSnapshot.mockReturnValue({ + nodes: [], + error: null, + loading: true, + interval: '', + reload: reloadMock, + }); + const currentTime = Date.now(); + const wrapper = createWrapper(currentTime, false); + expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); + expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); + jest.advanceTimersByTime(100); + wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOut'); + jest.advanceTimersByTime(200); + expect(reloadMock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index eda74da708c8..11f27f6401a3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -3,18 +3,117 @@ * 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 { EuiToolTip, EuiToolTipProps } from '@elastic/eui'; -import { omit } from 'lodash'; +import React, { useCallback, useState, useEffect } from 'react'; +import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { first } from 'lodash'; +import { withTheme, EuiTheme } from '../../../../../../../observability/public'; +import { useSourceContext } from '../../../../../containers/source'; +import { findInventoryModel } from '../../../../../../common/inventory_models'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../../common/inventory_models/types'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { useSnapshot } from '../../hooks/use_snaphot'; +import { createInventoryMetricFormatter } from '../../lib/create_inventory_metric_formatter'; +import { SNAPSHOT_METRIC_TRANSLATIONS } from '../../../../../../common/inventory_models/intl_strings'; -interface Props extends EuiToolTipProps { +export interface Props { + currentTime: number; hidden: boolean; + node: InfraWaffleMapNode; + options: InfraWaffleMapOptions; + formatter: (val: number) => string; + children: React.ReactElement; + nodeType: InventoryItemType; + theme: EuiTheme | undefined; } -export const ConditionalToolTip = (props: Props) => { - if (props.hidden) { - return props.children; +export const ConditionalToolTip = withTheme( + ({ theme, hidden, node, children, nodeType, currentTime }: Props) => { + const { sourceId } = useSourceContext(); + const [timer, setTimer] = useState | null>(null); + const model = findInventoryModel(nodeType); + const requestMetrics = model.tooltipMetrics.map((type) => ({ type })) as Array<{ + type: SnapshotMetricType; + }>; + const query = JSON.stringify({ + bool: { + filter: { + match_phrase: { [model.fields.id]: node.id }, + }, + }, + }); + + const { nodes, reload } = useSnapshot( + query, + requestMetrics, + [], + nodeType, + sourceId, + currentTime, + '', + '', + false // Doesn't send request until reload() is called + ); + + const handleDataLoad = useCallback(() => { + const id = setTimeout(reload, 200); + setTimer(id); + }, [reload]); + + const cancelDataLoad = useCallback(() => { + return (timer && clearTimeout(timer)) || void 0; + }, [timer]); + + useEffect(() => { + return cancelDataLoad; + }, [timer, cancelDataLoad]); + + if (hidden) { + return children; + } + + const dataNode = first(nodes); + const metrics = (dataNode && dataNode.metrics) || []; + const content = ( +
+
+ {node.name} +
+ {metrics.map((metric) => { + const name = SNAPSHOT_METRIC_TRANSLATIONS[metric.name] || metric.name; + const formatter = createInventoryMetricFormatter({ type: metric.name }); + return ( + + {name} + + {(metric.value && formatter(metric.value)) || '-'} + + + ); + })} +
+ ); + + return ( + +
+ {children} +
+
+ ); } - const propsWithoutHidden = omit(props, 'hidden'); - return {props.children}; -}; +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index e7bee82a9f0f..cc177b895ca5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { first } from 'lodash'; import { ConditionalToolTip } from './conditional_tooltip'; import { euiStyled } from '../../../../../../../observability/public'; import { @@ -41,7 +42,7 @@ export const Node = class extends React.PureComponent { public render() { const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; const { isPopoverOpen } = this.state; - const { metric } = node; + const metric = first(node.metrics); const valueMode = squareSize > 70; const ellipsisMode = squareSize > 30; const rawValue = (metric && metric.value) || 0; @@ -62,10 +63,12 @@ export const Node = class extends React.PureComponent { popoverPosition="downCenter" >