From a511426e839b2004acc6190906c27a0d2bef1d20 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Wed, 30 Aug 2023 20:26:42 +0200 Subject: [PATCH] [Infra UI] Add tooltips to Asset Details (#164858) Closes https://github.com/elastic/kibana/issues/164594 ## Summary This PR adds tooltips to explain the time range used to collect process data and metadata. The tooltips will be shown inside the overview (metadata summary section) tab, metadata tab, and processes tab (both flyout and full page views). ## Fixes I saw that the icons showing different explanation/ docs links inside the overveiew tab are not consistent ( sometimes we have `questionInCircle` and sometimes `iInCircles`) - the designs are using `iInCircles` so I changed it everywhere: icons ## Testing 1. Go to host view and open the flyout for any host. The new explanation and tooltips should be shown in: - Overview tab overview - Metadata tab metadata - Processed tab processes 2. Click Open as page and check the same on the asset details page Screenshot 2023-08-28 at 11 28 47 Screenshot 2023-08-28 at 11 28 32 Screenshot 2023-08-28 at 11 28 16 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/common/http_api/metadata_api.ts | 10 +- .../components/metadata_explanation.tsx | 102 +++++++ .../components/processes_explanation.tsx | 111 +++++++ .../asset_details/tabs/common/popover.tsx | 13 +- .../asset_details/tabs/metadata/metadata.tsx | 21 +- .../metadata_summary/metadata_header.tsx | 5 +- .../metadata_summary_list.tsx | 51 ++-- .../tabs/overview/metrics/metrics_grid.tsx | 2 +- .../tabs/processes/processes.tsx | 7 + .../infra/server/routes/metadata/index.ts | 5 +- .../routes/metadata/lib/get_node_info.ts | 2 +- .../apis/metrics_ui/metadata.ts | 270 ++++++++++-------- 12 files changed, 437 insertions(+), 162 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx create mode 100644 x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx diff --git a/x-pack/plugins/infra/common/http_api/metadata_api.ts b/x-pack/plugins/infra/common/http_api/metadata_api.ts index 66d1d01f8175d..360923e5307a1 100644 --- a/x-pack/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/plugins/infra/common/http_api/metadata_api.ts @@ -83,6 +83,14 @@ export const InfraMetadataInfoRT = rt.partial({ cloud: InfraMetadataCloudRT, host: InfraMetadataHostRT, agent: InfraMetadataAgentRT, + '@timestamp': rt.string, +}); + +export const InfraMetadataInfoResponseRT = rt.partial({ + cloud: InfraMetadataCloudRT, + host: InfraMetadataHostRT, + agent: InfraMetadataAgentRT, + timestamp: rt.string, }); const InfraMetadataRequiredRT = rt.type({ @@ -92,7 +100,7 @@ const InfraMetadataRequiredRT = rt.type({ }); const InfraMetadataOptionalRT = rt.partial({ - info: InfraMetadataInfoRT, + info: InfraMetadataInfoResponseRT, }); export const InfraMetadataRT = rt.intersection([InfraMetadataRequiredRT, InfraMetadataOptionalRT]); diff --git a/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx b/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx new file mode 100644 index 0000000000000..bdd77916db02f --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/components/metadata_explanation.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { Popover } from '../tabs/common/popover'; +import { useMetadataStateProviderContext } from '../hooks/use_metadata_state'; + +const HOSTNAME_DOCS_LINK = + 'https://www.elastic.co/guide/en/ecs/current/ecs-host.html#field-host-name'; + +const MetadataExplanationTooltipContent = React.memo(() => { + const onClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + + + + + ), + hostName: ( + + + + ), + }} + /> + + ); +}); + +export const MetadataExplanationMessage = () => { + const { metadata, loading } = useMetadataStateProviderContext(); + + return loading ? ( + + ) : metadata?.info?.timestamp ? ( + + + + + ), + time: ( + + ), + }} + /> + + + + + + + + + ) : null; +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx b/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx new file mode 100644 index 0000000000000..5f1af3f9f8a26 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/components/processes_explanation.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useDateRangeProviderContext } from '../hooks/use_date_range'; +import { Popover } from '../tabs/common/popover'; + +const DOCUMENTATION_LINK = + 'https://www.elastic.co/guide/en/observability/current/view-infrastructure-metrics.html'; +const SYSTEM_INTEGRATION_DOCS_LINK = 'https://docs.elastic.co/en/integrations/system'; + +const ProcessesExplanationTooltipContent = React.memo(() => { + const onClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + return ( + +

+ + + + ), + }} + /> +

+

+ + + + ), + }} + /> +

+
+ ); +}); + +export const ProcessesExplanationMessage = () => { + const { getDateRangeInTimestamp } = useDateRangeProviderContext(); + const dateFromRange = new Date(getDateRangeInTimestamp().to); + + return ( + + + + + ), + time: ( + + ), + }} + /> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx index 263c61d46230b..823f330e742a7 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/common/popover.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { EuiPopover, EuiIcon, IconType } from '@elastic/eui'; +import { PanelPaddingSize } from '@elastic/eui'; +import { EuiPopover, EuiIcon, type IconType, type IconColor, type IconSize } from '@elastic/eui'; import { css } from '@emotion/react'; import React from 'react'; import { useBoolean } from '../../../../hooks/use_boolean'; @@ -13,20 +14,28 @@ import { useBoolean } from '../../../../hooks/use_boolean'; export const Popover = ({ children, icon, + iconColor, + iconSize, + panelPaddingSize, ...props }: { children: React.ReactNode; icon: IconType; + iconColor?: IconColor; + iconSize?: IconSize; + panelPaddingSize?: PanelPaddingSize; 'data-test-subj'?: string; }) => { const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); return ( { } return ( - + <> + + +
+ ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx index e19d261094d8f..7132587588f24 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_header.tsx @@ -56,10 +56,7 @@ export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => { {columnTitles[metadataValue.field as MetadataFields]} - + {metadataValue.tooltipLink ? ( + <> + + + + + + + + + + + + + + + + {(isCompactView ? metadataData(metadata?.info) @@ -98,21 +130,6 @@ export const MetadataSummaryList = ({ ) )} - - - - - - + ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx index 9b5953f9292dd..9b4036c1cdd13 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx @@ -121,7 +121,7 @@ const MetricsSectionTitle = () => { - + diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx index f0568332328dc..2156ca8c7555d 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/processes/processes.tsx @@ -19,6 +19,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { parseSearchString } from './parse_search_string'; import { ProcessesTable } from './processes_table'; import { STATE_NAMES } from './states'; @@ -31,6 +32,7 @@ import { import { getFieldByType } from '../../../../../common/inventory_models'; import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; import { useDateRangeProviderContext } from '../../hooks/use_date_range'; +import { ProcessesExplanationMessage } from '../../components/processes_explanation'; import { useAssetDetailsUrlState } from '../../hooks/use_asset_details_url_state'; const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ @@ -130,6 +132,11 @@ export const Processes = () => { + {loading ? ( + + ) : ( + !error && (response?.processList ?? []).length > 0 && + )} { id, name, features: [...metricFeatures, ...cloudMetricsFeatures], - info, + info: { + ...info, + timestamp: info['@timestamp'], + }, }), }); } diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index bc93d1f539e2a..5470efcb1fb47 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -59,7 +59,7 @@ export const getNodeInfo = async ( index: sourceConfiguration.metricAlias, body: { size: 1, - _source: ['host.*', 'cloud.*', 'agent.*'], + _source: ['host.*', 'cloud.*', 'agent.*', TIMESTAMP_FIELD], sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { diff --git a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts index 5cdca5a369edf..9a7f41e1d2b67 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts @@ -99,38 +99,42 @@ export default function ({ getService }: FtrProviderContext) { if (metadata) { expect(metadata.features.length).to.be(58); expect(metadata.name).to.equal('gke-observability-8--observability-8--bc1afd95-f0zc'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'europe-west1-c', - instance: { - name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - id: '6200309808276807579', - }, - provider: 'gcp', - machine: { type: 'n1-standard-4' }, - project: { id: 'elastic-observability' }, - }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', - id: 'c91c0d2b-6483-46bb-9731-f06afd32bb59', - ephemeral_id: '7cb259b1-795c-4c76-beaf-2eb8f18f5b02', - type: 'metricbeat', - version: '8.0.0', - }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', - os: { - kernel: '4.14.127+', - codename: 'Core', - name: 'CentOS Linux', - family: 'redhat', - version: '7 (Core)', - platform: 'centos', - }, - containerized: false, + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + availability_zone: 'europe-west1-c', + instance: { name: 'gke-observability-8--observability-8--bc1afd95-f0zc', - architecture: 'x86_64', + id: '6200309808276807579', + }, + provider: 'gcp', + machine: { type: 'n1-standard-4' }, + project: { id: 'elastic-observability' }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + id: 'c91c0d2b-6483-46bb-9731-f06afd32bb59', + ephemeral_id: '7cb259b1-795c-4c76-beaf-2eb8f18f5b02', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + os: { + kernel: '4.14.127+', + codename: 'Core', + name: 'CentOS Linux', + family: 'redhat', + version: '7 (Core)', + platform: 'centos', }, + containerized: false, + name: 'gke-observability-8--observability-8--bc1afd95-f0zc', + architecture: 'x86_64', }); } else { throw new Error('Metadata should never be empty'); @@ -148,38 +152,42 @@ export default function ({ getService }: FtrProviderContext) { expect(metadata.features.length).to.be(19); expect(metadata.features.some((f) => f.name === 'aws.ec2')).to.be(true); expect(metadata.name).to.equal('ip-172-31-47-9.us-east-2.compute.internal'); - expect(metadata.info).to.eql({ - cloud: { - availability_zone: 'us-east-2c', - image: { id: 'ami-0d8f6eb4f641ef691' }, - instance: { id: 'i-011454f72559c510b' }, - provider: 'aws', - machine: { type: 't2.micro' }, - region: 'us-east-2', - account: { id: '015351775590' }, - }, - agent: { - hostname: 'ip-172-31-47-9.us-east-2.compute.internal', - id: 'd0943b36-d0d3-426d-892b-7d79c071b44b', - ephemeral_id: '64c94244-88b8-4a37-adc0-30428fefaf53', - type: 'metricbeat', - version: '8.0.0', - }, - host: { - hostname: 'ip-172-31-47-9.us-east-2.compute.internal', - os: { - kernel: '4.14.123-111.109.amzn2.x86_64', - codename: 'Karoo', - name: 'Amazon Linux', - family: 'redhat', - version: '2', - platform: 'amzn', - }, - containerized: false, - name: 'ip-172-31-47-9.us-east-2.compute.internal', - id: 'ded64cbff86f478990a3dfbb63a8d238', - architecture: 'x86_64', + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + availability_zone: 'us-east-2c', + image: { id: 'ami-0d8f6eb4f641ef691' }, + instance: { id: 'i-011454f72559c510b' }, + provider: 'aws', + machine: { type: 't2.micro' }, + region: 'us-east-2', + account: { id: '015351775590' }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'd0943b36-d0d3-426d-892b-7d79c071b44b', + ephemeral_id: '64c94244-88b8-4a37-adc0-30428fefaf53', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + os: { + kernel: '4.14.123-111.109.amzn2.x86_64', + codename: 'Karoo', + name: 'Amazon Linux', + family: 'redhat', + version: '2', + platform: 'amzn', }, + containerized: false, + name: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'ded64cbff86f478990a3dfbb63a8d238', + architecture: 'x86_64', }); } else { throw new Error('Metadata should never be empty'); @@ -197,43 +205,47 @@ export default function ({ getService }: FtrProviderContext) { expect(metadata.features.length).to.be(29); // With this data set the `kubernetes.pod.name` fields have been removed. expect(metadata.name).to.equal('fluentd-gcp-v3.2.0-np7vw'); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '6613144177892233360', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', - }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', - }, - project: { - id: 'elastic-observability', - }, + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); + expect(metadata.info?.cloud).to.eql({ + instance: { + id: '6613144177892233360', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', - id: '66dc19e6-da36-49d2-9471-2c9475503178', - ephemeral_id: 'a0c3a9ff-470a-41a0-bf43-d1af6b7a3b5b', - type: 'metricbeat', - version: '8.0.0', + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', - name: 'gke-observability-8--observability-8--bc1afd95-ngmh', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', - }, - architecture: 'x86_64', - containerized: false, + project: { + id: 'elastic-observability', }, }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + id: '66dc19e6-da36-49d2-9471-2c9475503178', + ephemeral_id: 'a0c3a9ff-470a-41a0-bf43-d1af6b7a3b5b', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + name: 'gke-observability-8--observability-8--bc1afd95-ngmh', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', + }, + architecture: 'x86_64', + containerized: false, + }); } else { throw new Error('Metadata should never be empty'); } @@ -248,45 +260,49 @@ export default function ({ getService }: FtrProviderContext) { }); if (metadata) { expect(metadata.features.length).to.be(26); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.above( + timeRange800withAws.from + ); + expect(new Date(metadata.info?.timestamp ?? '')?.getTime()).to.be.below( + timeRange800withAws.to + ); expect(metadata.name).to.equal( 'k8s_prometheus-to-sd-exporter_fluentd-gcp-v3.2.0-w68r5_kube-system_26950cde-9aed-11e9-9a96-42010a84004d_0' ); - expect(metadata.info).to.eql({ - cloud: { - instance: { - id: '4039094952262994102', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - }, - provider: 'gcp', - availability_zone: 'europe-west1-c', - machine: { - type: 'n1-standard-4', - }, - project: { - id: 'elastic-observability', - }, + expect(metadata.info?.cloud).to.eql({ + instance: { + id: '4039094952262994102', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', }, - agent: { - hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', - id: 'c58a514c-e971-4590-8206-385400e184dd', - ephemeral_id: 'e9d46cb0-2e89-469d-bd3b-6f32d7c96cc0', - type: 'metricbeat', - version: '8.0.0', + provider: 'gcp', + availability_zone: 'europe-west1-c', + machine: { + type: 'n1-standard-4', }, - host: { - hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', - name: 'gke-observability-8--observability-8--bc1afd95-nhhw', - os: { - codename: 'Core', - family: 'redhat', - kernel: '4.14.127+', - name: 'CentOS Linux', - platform: 'centos', - version: '7 (Core)', - }, - architecture: 'x86_64', - containerized: false, + project: { + id: 'elastic-observability', + }, + }); + expect(metadata.info?.agent).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + id: 'c58a514c-e971-4590-8206-385400e184dd', + ephemeral_id: 'e9d46cb0-2e89-469d-bd3b-6f32d7c96cc0', + type: 'metricbeat', + version: '8.0.0', + }); + expect(metadata.info?.host).to.eql({ + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + name: 'gke-observability-8--observability-8--bc1afd95-nhhw', + os: { + codename: 'Core', + family: 'redhat', + kernel: '4.14.127+', + name: 'CentOS Linux', + platform: 'centos', + version: '7 (Core)', }, + architecture: 'x86_64', + containerized: false, }); } else { throw new Error('Metadata should never be empty');