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 };
};