diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout.tsx new file mode 100644 index 0000000000000..c3d4f8283c4e5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState } from 'react'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; +import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; +import { MetadataTab } from './metadata/metadata'; +import type { HostNodeRow } from '../../hooks/use_hosts_table'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; + +interface Props { + node: HostNodeRow; + closeFlyout: () => void; +} + +const flyoutTabs = [MetadataTab]; + +export const Flyout = ({ node, closeFlyout }: Props) => { + const { getDateRangeAsTimestamp } = useUnifiedSearchContext(); + + const tabs = useMemo(() => { + const currentTimeRange = { + ...getDateRangeAsTimestamp(), + interval: '1m', + }; + + return flyoutTabs.map((m) => { + const TabContent = m.content; + return { + ...m, + content: , + }; + }); + }, [getDateRangeAsTimestamp, node]); + + const [selectedTab, setSelectedTab] = useState(0); + + return ( + + + +

{node.name}

+
+ + + {tabs.map((tab, i) => ( + setSelectedTab(i)}> + {tab.name} + + ))} + +
+ {tabs[selectedTab].content} +
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.tsx new file mode 100644 index 0000000000000..4a4b52d0b45cf --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.tsx @@ -0,0 +1,110 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiLoadingChart } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useSourceContext } from '../../../../../../containers/metrics_source'; +import { findInventoryModel } from '../../../../../../../common/inventory_models'; +import type { InventoryItemType } from '../../../../../../../common/inventory_models/types'; +import { useMetadata } from '../../../../metric_detail/hooks/use_metadata'; +import { Table } from './table'; +import { getAllFields } from './utils'; +import type { HostNodeRow } from '../../../hooks/use_hosts_table'; +import type { MetricsTimeInput } from '../../../../metric_detail/hooks/use_metrics_time'; + +const NODE_TYPE = 'host' as InventoryItemType; + +export interface TabProps { + currentTimeRange: MetricsTimeInput; + node: HostNodeRow; +} + +const Metadata = ({ node, currentTimeRange }: TabProps) => { + const nodeId = node.name; + const inventoryModel = findInventoryModel(NODE_TYPE); + const { sourceId } = useSourceContext(); + const { + loading: metadataLoading, + error, + metadata, + } = useMetadata(nodeId, NODE_TYPE, inventoryModel.requiredMetrics, sourceId, currentTimeRange); + + const fields = useMemo(() => getAllFields(metadata), [metadata]); + + if (metadataLoading) { + return ; + } + + if (error) { + return ( + + window.location.reload()} + > + {i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.errorAction', { + defaultMessage: 'reload the page', + })} + + ), + }} + /> + + ); + } + + return fields.length > 0 ? ( + + ) : ( + + ); +}; + +const LoadingPlaceholder = () => { + return ( +
+ +
+ ); +}; + +export const MetadataTab = { + id: 'metadata', + name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', { + defaultMessage: 'Metadata', + }), + content: Metadata, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx new file mode 100644 index 0000000000000..91b048b72d882 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiLink, EuiBasicTable } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Row { + name: string; + value: string | string[] | undefined; +} + +interface Props { + rows: Row[]; +} + +/** + * Columns translations + */ +const FIELD_LABEL = i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.field', { + defaultMessage: 'Field', +}); + +const VALUE_LABEL = i18n.translate('xpack.infra.hostsViewPage.hostDetail.metadata.value', { + defaultMessage: 'Value', +}); + +export const Table = (props: Props) => { + const { rows } = props; + const columns = useMemo( + () => [ + { + field: 'name', + name: FIELD_LABEL, + width: '35%', + sortable: false, + render: (name: string) => {name}, + }, + { + field: 'value', + name: VALUE_LABEL, + width: '65%', + sortable: false, + render: (_name: string, item: Row) => , + }, + ], + [] + ); + + return ; +}; + +interface ExpandableContentProps { + values: string | string[] | undefined; +} +const ExpandableContent = (props: ExpandableContentProps) => { + const { values } = props; + const [isExpanded, setIsExpanded] = useState(false); + const expand = useCallback(() => { + setIsExpanded(true); + }, []); + + const collapse = useCallback(() => { + setIsExpanded(false); + }, []); + + const list = Array.isArray(values) ? values : [values]; + const [first, ...others] = list; + const hasOthers = others.length > 0; + const shouldShowMore = hasOthers && !isExpanded; + + return ( + +
+ {first} + {shouldShowMore && ( + <> + {' ... '} + + + + + )} +
+ {isExpanded && others.map((item) => {item})} + {hasOthers && isExpanded && ( + + + {i18n.translate('xpack.infra.nodeDetails.tabs.metadata.seeLess', { + defaultMessage: 'Show less', + })} + + + )} +
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts new file mode 100644 index 0000000000000..06ca4e85cc5e6 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InfraMetadata } from '../../../../../../../common/http_api'; + +export const getAllFields = (metadata: InfraMetadata | null) => { + if (!metadata?.info) return []; + return prune([ + { + name: 'host.architecture', + value: metadata.info.host?.architecture, + }, + { + name: 'host.hostname', + value: metadata.info.host?.name, + }, + { + name: 'host.id', + value: metadata.info.host?.id, + }, + { + name: 'host.ip', + value: metadata.info.host?.ip, + }, + { + name: 'host.mac', + value: metadata.info.host?.mac, + }, + { + name: 'host.name', + value: metadata.info.host?.name, + }, + { + name: 'host.os.build', + value: metadata.info.host?.os?.build, + }, + { + name: 'host.os.family', + value: metadata.info.host?.os?.family, + }, + { + name: 'host.os.name', + value: metadata.info.host?.os?.name, + }, + { + name: 'host.os.kernel', + value: metadata.info.host?.os?.kernel, + }, + { + name: 'host.os.platform', + value: metadata.info.host?.os?.platform, + }, + { + name: 'host.os.version', + value: metadata.info.host?.os?.version, + }, + { + name: 'cloud.account.id', + value: metadata.info.cloud?.account?.id, + }, + { + name: 'cloud.account.name', + value: metadata.info.cloud?.account?.name, + }, + { + name: 'cloud.availability_zone', + value: metadata.info.cloud?.availability_zone, + }, + { + name: 'cloud.instance.id', + value: metadata.info.cloud?.instance?.id, + }, + { + name: 'cloud.instance.name', + value: metadata.info.cloud?.instance?.name, + }, + { + name: 'cloud.machine.type', + value: metadata.info.cloud?.machine?.type, + }, + { + name: 'cloud.provider', + value: metadata.info.cloud?.provider, + }, + { + name: 'cloud.region', + value: metadata.info.cloud?.region, + }, + { + name: 'agent.id', + value: metadata.info.agent?.id, + }, + { + name: 'agent.version', + value: metadata.info.agent?.version, + }, + { + name: 'agent.policy', + value: metadata.info.agent?.policy, + }, + ]); +}; + +const prune = (fields: Array<{ name: string; value: string | string[] | undefined }>) => + fields.filter((f) => !!f.value); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index 7b576ac9007e9..ce6a5d5aa9d04 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -15,13 +15,18 @@ import { useHostsTable } from '../hooks/use_hosts_table'; import { useTableProperties } from '../hooks/use_table_properties_url_state'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; +import { Flyout } from './host_details_flyout/flyout'; export const HostsTable = () => { const { hostNodes, loading } = useHostsViewContext(); const { onSubmit, searchCriteria } = useUnifiedSearchContext(); const [properties, setProperties] = useTableProperties(); - const { columns, items } = useHostsTable(hostNodes, { time: searchCriteria.dateRange }); + const { columns, items, isFlyoutOpen, closeFlyout, clickedItemUuid } = useHostsTable(hostNodes, { + time: searchCriteria.dateRange, + }); + + const clickedItem = items.find(({ uuid }) => uuid === clickedItemUuid); const noData = items.length === 0; @@ -74,18 +79,23 @@ export const HostsTable = () => { } return ( - + <> + + {isFlyoutOpen && clickedItem && } + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index 6d7b196bfed36..98aa5dfa14f9b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -9,6 +9,10 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; import { SnapshotNode } from '../../../../../common/http_api'; +jest.mock('uuid', () => ({ + v4: () => 'uuidv4', +})); + describe('useHostTable hook', () => { it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { const nodes: SnapshotNode[] = [ @@ -73,6 +77,7 @@ describe('useHostTable hook', () => { { name: 'host-0', os: '-', + uuid: 'uuidv4', title: { cloudProvider: 'aws', name: 'host-0', @@ -102,6 +107,7 @@ describe('useHostTable hook', () => { { name: 'host-1', os: 'macOS', + uuid: 'uuidv4', title: { cloudProvider: null, name: 'host-1', diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 2db824f9c01d7..7078f229e82f1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TimeRange } from '@kbn/es-query'; +import { v4 as uuidv4 } from 'uuid'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; @@ -33,10 +34,9 @@ export interface HostNodeRow extends HostMetrics { servicesOnHost?: number | null; title: { name: string; cloudProvider?: CloudProvider | null }; name: string; + uuid: string; } -// type MappedMetrics = Record; - interface HostTableParams { time: TimeRange; } @@ -50,6 +50,7 @@ const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefin const buildItemsList = (nodes: SnapshotNode[]) => { return nodes.map(({ metrics, path, name }) => ({ + uuid: uuidv4(), name, os: path.at(-1)?.os ?? '-', title: { @@ -107,6 +108,13 @@ const averageMemoryUsageLabel = i18n.translate( } ); +const toggleDialogActionLabel = i18n.translate( + 'xpack.infra.hostsViewPage.table.toggleDialogWithDetails', + { + defaultMessage: 'Toggle dialog with details', + } +); + /** * Build a table columns and items starting from the snapshot nodes. */ @@ -115,6 +123,11 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) services: { telemetry }, } = useKibanaContextForPlugin(); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const [clickedItemUuid, setClickedItemUuid] = useState(() => uuidv4()); + + const closeFlyout = () => setIsFlyoutOpen(false); + const reportHostEntryClick = useCallback( ({ name, cloudProvider }: HostNodeRow['title']) => { telemetry.reportHostEntryClicked({ @@ -129,6 +142,27 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) const columns: Array> = useMemo( () => [ + { + name: '', + width: '40px', + field: 'uuid', + actions: [ + { + name: toggleDialogActionLabel, + description: toggleDialogActionLabel, + icon: ({ uuid }) => (isFlyoutOpen && uuid === clickedItemUuid ? 'minimize' : 'expand'), + type: 'icon', + onClick: ({ uuid }) => { + setClickedItemUuid(uuid); + if (isFlyoutOpen && uuid === clickedItemUuid) { + setIsFlyoutOpen(false); + } else { + setIsFlyoutOpen(true); + } + }, + }, + ], + }, { name: titleLabel, field: 'title', @@ -191,8 +225,8 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) align: 'right', }, ], - [reportHostEntryClick, time] + [clickedItemUuid, isFlyoutOpen, reportHostEntryClick, time] ); - return { columns, items }; + return { columns, items, isFlyoutOpen, closeFlyout, clickedItemUuid }; };