Skip to content

Commit

Permalink
[Infrastructure UI] Replace Snapshot API with InfraMetrics API in Hos…
Browse files Browse the repository at this point in the history
…ts View (#155531)

closes [#154443](#154443)
## Summary

This PR replaces the usage of the Snapshot API in favor of the new
`metrics/infra` endpoint and also includes a new control in the Search
Bar to allow users to select how many hosts they want the API to return.


https://user-images.githubusercontent.com/2767137/233728658-bccc7258-6955-47fb-8f7b-85ef6ec5d0f9.mov

Because the KPIs now needs to show an "Average (of X hosts)", they will
only start fetching the data once the table has been loaded.

The hosts count KPI tile was not converted to Lens, because the page
needs to know the total number of hosts.

### Possible follow-up

Since now everything depends on the table to be loaded, I have been
experimenting with batched requests to the new API. The idea is to fetch
at least the host names as soon as possible.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
crespocarlos and kibanamachine authored Apr 24, 2023
1 parent 3e94b43 commit 0654527
Show file tree
Hide file tree
Showing 31 changed files with 673 additions and 396 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export const RangeRT = rt.type({
});

export const InfraAssetMetadataTypeRT = rt.keyof({
'host.os.name': null,
'cloud.provider': null,
'host.ip': null,
'host.os.name': null,
});

export const InfraAssetMetricsRT = rt.type({
Expand All @@ -35,7 +36,7 @@ export const InfraAssetMetricsRT = rt.type({
export const InfraAssetMetadataRT = rt.type({
// keep the actual field name from the index mappings
name: InfraAssetMetadataTypeRT,
value: rt.union([rt.string, rt.number, rt.null]),
value: rt.union([rt.string, rt.null]),
});

export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([
Expand Down Expand Up @@ -64,6 +65,7 @@ export const GetInfraMetricsResponsePayloadRT = rt.type({

export type InfraAssetMetrics = rt.TypeOf<typeof InfraAssetMetricsRT>;
export type InfraAssetMetadata = rt.TypeOf<typeof InfraAssetMetadataRT>;
export type InfraAssetMetadataType = rt.TypeOf<typeof InfraAssetMetadataTypeRT>;
export type InfraAssetMetricType = rt.TypeOf<typeof InfraMetricTypeRT>;
export type InfraAssetMetricsItem = rt.TypeOf<typeof InfraAssetMetricsItemRT>;

Expand Down
8 changes: 2 additions & 6 deletions x-pack/plugins/infra/public/hooks/use_lens_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ export const useLensAttributes = ({
return visualizationAttributes;
}, [dataView, formulaAPI, options, type, visualizationType]);

const injectFilters = (data: {
timeRange: TimeRange;
filters: Filter[];
query: Query;
}): LensAttributes | null => {
const injectFilters = (data: { filters: Filter[]; query: Query }): LensAttributes | null => {
if (!attributes) {
return null;
}
Expand Down Expand Up @@ -121,7 +117,7 @@ export const useLensAttributes = ({
return true;
},
async execute(_context: ActionExecutionContext): Promise<void> {
const injectedAttributes = injectFilters({ timeRange, filters, query });
const injectedAttributes = injectFilters({ filters, query });
if (injectedAttributes) {
navigateToPrefilledEditor(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once';
import { LensAttributes } from '../../../../../common/visualizations';
import { ChartLoader } from './chart_loader';

export interface Props {
export interface LensWrapperProps {
id: string;
attributes: LensAttributes | null;
dateRange: TimeRange;
Expand All @@ -42,11 +42,12 @@ export const LensWrapper = ({
lastReloadRequestTime,
loading = false,
hasTitle = false,
}: Props) => {
}: LensWrapperProps) => {
const intersectionRef = useRef(null);
const [loadedOnce, setLoadedOnce] = useState(false);

const [state, setState] = useState({
attributes,
lastReloadRequestTime,
query,
filters,
Expand All @@ -65,15 +66,23 @@ export const LensWrapper = ({
useEffect(() => {
if ((intersection?.intersectionRatio ?? 0) === 1) {
setState({
attributes,
lastReloadRequestTime,
query,
dateRange,
filters,
dateRange,
});
}
}, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]);
}, [
attributes,
dateRange,
filters,
intersection?.intersectionRatio,
lastReloadRequestTime,
query,
]);

const isReady = attributes && intersectedOnce;
const isReady = state.attributes && intersectedOnce;

return (
<div ref={intersectionRef}>
Expand All @@ -83,11 +92,11 @@ export const LensWrapper = ({
style={style}
hasTitle={hasTitle}
>
{isReady && (
{state.attributes && (
<EmbeddableComponent
id={id}
style={style}
attributes={attributes}
attributes={state.attributes}
viewMode={ViewMode.VIEW}
timeRange={state.dateRange}
query={state.query}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useMemo, useRef } from 'react';
import {
Chart,
Metric,
MetricTrendShape,
type MetricWNumber,
type MetricWTrend,
} from '@elastic/charts';
import { EuiPanel } from '@elastic/eui';
import React, { useEffect, useRef } from 'react';
import { Chart, Metric, type MetricWNumber, type MetricWTrend } from '@elastic/charts';
import { EuiPanel, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import { EuiToolTip } from '@elastic/eui';
import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../../common/http_api';
import { createInventoryMetricFormatter } from '../../../inventory_view/lib/create_inventory_metric_formatter';
import type { SnapshotMetricType } from '../../../../../../common/inventory_models/types';
import { ChartLoader } from './chart_loader';

type MetricType = keyof Pick<SnapshotNodeMetric, 'avg' | 'max' | 'value'>;

type AcceptedType = SnapshotMetricType | 'hostsCount';

export interface ChartBaseProps
extends Pick<
MetricWTrend,
'title' | 'color' | 'extra' | 'subtitle' | 'trendA11yDescription' | 'trendA11yTitle'
> {
type: AcceptedType;
toolTip: string;
metricType: MetricType;
['data-test-subj']?: string;
}

interface Props extends ChartBaseProps {
export interface Props extends Pick<MetricWTrend, 'title' | 'color' | 'extra' | 'subtitle'> {
id: string;
nodes: SnapshotNode[];
loading: boolean;
overrideValue?: number;
value: number;
toolTip: string;
['data-test-subj']?: string;
}

const MIN_HEIGHT = 150;
Expand All @@ -49,23 +25,13 @@ export const MetricChartWrapper = ({
extra,
id,
loading,
metricType,
nodes,
overrideValue,
value,
subtitle,
title,
toolTip,
trendA11yDescription,
trendA11yTitle,
type,
...props
}: Props) => {
const loadedOnce = useRef(false);
const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]);
const metricsTimeseries = useMemo(
() => (metrics ?? []).find((m) => m.name === type)?.timeseries,
[metrics, type]
);

useEffect(() => {
if (!loadedOnce.current && !loading) {
Expand All @@ -76,29 +42,13 @@ export const MetricChartWrapper = ({
};
}, [loading]);

const metricsValue = useMemo(() => {
if (overrideValue) {
return overrideValue;
}
return (metrics ?? []).find((m) => m.name === type)?.[metricType] ?? 0;
}, [metricType, metrics, overrideValue, type]);

const metricsData: MetricWNumber = {
title,
subtitle,
color,
extra,
value: metricsValue,
valueFormatter: (d: number) =>
type === 'hostsCount' ? d.toString() : createInventoryMetricFormatter({ type })(d),
...(!!metricsTimeseries
? {
trend: metricsTimeseries.rows.map((row) => ({ x: row.timestamp, y: row.metric_0 ?? 0 })),
trendShape: MetricTrendShape.Area,
trendA11yTitle,
trendA11yDescription,
}
: {}),
value,
valueFormatter: (d: number) => d.toString(),
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { InfraLoadingPanel } from '../../../../components/loading';
import { useMetricsDataViewContext } from '../hooks/use_data_view';
import { UnifiedSearchBar } from './unified_search_bar';
import { UnifiedSearchBar } from './search_bar/unified_search_bar';
import { HostsTable } from './hosts_table';
import { KPIGrid } from './kpis/kpi_grid';
import { Tabs } from './tabs/tabs';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,46 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useHostCountContext } from '../../hooks/use_host_count';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';

import { useHostsViewContext } from '../../hooks/use_hosts_view';
import { type ChartBaseProps, MetricChartWrapper } from '../chart/metric_chart_wrapper';
import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper';

export const HostsTile = ({ type, ...props }: ChartBaseProps) => {
const { hostNodes, loading } = useHostsViewContext();
const HOSTS_CHART: Omit<Props, 'loading' | 'value'> = {
id: `metric-hostCount`,
color: '#6DCCB1',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', {
defaultMessage: 'Hosts',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', {
defaultMessage: 'The number of hosts returned by your current search criteria.',
}),
['data-test-subj']: 'hostsView-metricsTrend-hosts',
};

export const HostsTile = () => {
const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext();
const { searchCriteria } = useUnifiedSearchContext();

const getSubtitle = () => {
return searchCriteria.limit < (hostCountData?.count.value ?? 0)
? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit', {
defaultMessage: 'Limited to {limit}',
values: {
limit: searchCriteria.limit,
},
})
: undefined;
};

return (
<MetricChartWrapper
id={`metric-${type}`}
type={type}
nodes={[]}
loading={loading}
overrideValue={hostNodes?.length}
{...props}
{...HOSTS_CHART}
value={hostCountData?.count.value ?? 0}
subtitle={getSubtitle()}
loading={hostCountLoading}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,17 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { KPIChartProps, Tile } from './tile';
import { HostCountProvider } from '../../hooks/use_host_count';
import { HostsTile } from './hosts_tile';
import { ChartBaseProps } from '../chart/metric_chart_wrapper';

const KPI_CHARTS: KPIChartProps[] = [
const KPI_CHARTS: Array<Omit<KPIChartProps, 'loading' | 'subtitle'>> = [
{
type: 'cpu',
trendLine: true,
backgroundColor: '#F1D86F',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.title', {
defaultMessage: 'CPU usage',
}),
subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.subtitle', {
defaultMessage: 'Average',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.tooltip', {
defaultMessage:
'Average of percentage of CPU time spent in states other than Idle and IOWait, normalized by the number of CPU cores. Includes both time spent on user space and kernel space. 100% means all CPUs of the host are busy.',
Expand All @@ -35,9 +32,6 @@ const KPI_CHARTS: KPIChartProps[] = [
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.title', {
defaultMessage: 'Memory usage',
}),
subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.subtitle', {
defaultMessage: 'Average',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.tooltip', {
defaultMessage:
"Average of percentage of main memory usage excluding page cache. This includes resident memory for all processes plus memory used by the kernel structures and code apart the page cache. A high level indicates a situation of memory saturation for a host. 100% means the main memory is entirely filled with memory that can't be reclaimed, except by swapping out.",
Expand All @@ -50,9 +44,6 @@ const KPI_CHARTS: KPIChartProps[] = [
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.title', {
defaultMessage: 'Network inbound (RX)',
}),
subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.subtitle', {
defaultMessage: 'Average',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.tooltip', {
defaultMessage:
'Number of bytes which have been received per second on the public interfaces of the hosts.',
Expand All @@ -65,48 +56,31 @@ const KPI_CHARTS: KPIChartProps[] = [
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.title', {
defaultMessage: 'Network outbound (TX)',
}),
subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.subtitle', {
defaultMessage: 'Average',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.tooltip', {
defaultMessage:
'Number of bytes which have been received per second on the public interfaces of the hosts.',
}),
},
];

const HOSTS_CHART: ChartBaseProps = {
type: 'hostsCount',
color: '#6DCCB1',
metricType: 'value',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', {
defaultMessage: 'Hosts',
}),
trendA11yTitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.a11y.title', {
defaultMessage: 'Hosts count.',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', {
defaultMessage: 'The number of hosts returned by your current search criteria.',
}),
['data-test-subj']: 'hostsView-metricsTrend-hosts',
};

export const KPIGrid = () => {
return (
<EuiFlexGroup
direction="row"
gutterSize="s"
style={{ flexGrow: 0 }}
data-test-subj="hostsView-metricsTrend"
>
<EuiFlexItem>
<HostsTile {...HOSTS_CHART} />
</EuiFlexItem>
{KPI_CHARTS.map(({ ...chartProp }) => (
<EuiFlexItem key={chartProp.type}>
<Tile {...chartProp} />
<HostCountProvider>
<EuiFlexGroup
direction="row"
gutterSize="s"
style={{ flexGrow: 0 }}
data-test-subj="hostsView-metricsTrend"
>
<EuiFlexItem>
<HostsTile />
</EuiFlexItem>
))}
</EuiFlexGroup>
{KPI_CHARTS.map(({ ...chartProp }) => (
<EuiFlexItem key={chartProp.type}>
<Tile {...chartProp} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</HostCountProvider>
);
};
Loading

0 comments on commit 0654527

Please sign in to comment.