[Infra UI] Smooth out spikey charts (#163608)

## Summary

This PR smooths out spikey charts.

- Dotted line caused by period set in metricbeat to be greater than the
date histogram interval determined by lens
<img width="1464" alt="image"
src="0f24b647-f000-4a09-a9a2-025efc56307a">


- Gap caused by a host that stopped shipping metrics
<img width="1460" alt="image"
src="6bb1c1bd-891a-42b4-bf3b-67d4abea7bb8">

- Before this change
<img width="1448" alt="image"
src="b4d2f0e2-698f-4339-bcea-e32451398a5b">

_The spikes are a result of the `period` set in metricbeat/integration
being greater than the date histogram interval that Lens automatically
sets according to the date range passed to the charts._

### How to test this PR

- Setup a local Kibana instance
- Configure `metricbeat.yml`, enabling the `system` module and setting
the `period` with `1m`. Start a local metricbeat instance.
- Navigate to `Infrastructure` > `Hosts`
- Verify if when date picker is set to < 1h hour interval the graphs
will show the dotted lines
- With 1h+ interval, there shouldn't be dotted lines, *unless*
metricbeat is stopped and restarted after a few minutes
- Configure `metricbeat.yml`, enabling the `system` module and setting
the `period` with `10s`. Restart the metricbeat instance.
  - Verify if the charts maintain the existing behaviour

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-08-11 16:25:52 +02:00 committed by GitHub
parent 40a666b04e
commit 07ad32ff9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 383 additions and 225 deletions

View file

@ -42,3 +42,4 @@ export const hostLensFormulas = {
};
export const HOST_METRICS_DOC_HREF = 'https://ela.st/docs-infra-host-metrics';
export const HOST_METRICS_DOTTED_LINES_DOC_HREF = 'https://ela.st/docs-infra-why-dotted';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { XYChart } from './xy_chart';
export { XYChart, type XYVisualOptions } from './xy_chart';
export { MetricChart } from './metric_chart';
export * from './layers';

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import type { FormBasedPersistedState, XYLayerConfig, XYState } from '@kbn/lens-plugin/public';
import type {
FormBasedPersistedState,
XYArgs,
XYLayerConfig,
XYState,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedObjectReference } from '@kbn/core/server';
import { DEFAULT_LAYER_ID } from '../utils';
@ -13,8 +18,20 @@ import type { Chart, ChartConfig, ChartLayer } from '../../types';
const ACCESSOR = 'formula_accessor';
// This needs be more specialized by `preferredSeriesType`
export interface XYVisualOptions {
lineInterpolation?: XYArgs['curveType'];
missingValues?: XYArgs['fittingFunction'];
endValues?: XYArgs['endValue'];
showDottedLine?: boolean;
}
export class XYChart implements Chart<XYState> {
constructor(private chartConfig: ChartConfig<Array<ChartLayer<XYLayerConfig>>>) {}
constructor(
private chartConfig: ChartConfig<Array<ChartLayer<XYLayerConfig>>> & {
visualOptions?: XYVisualOptions;
}
) {}
getVisualizationType(): string {
return 'lnsXY';
@ -32,15 +49,21 @@ export class XYChart implements Chart<XYState> {
}
getVisualizationState(): XYState {
return getXYVisualizationState({
layers: [
...this.chartConfig.layers.map((layerItem, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return layerItem.getLayerConfig(layerId, accessorId);
}),
],
});
return {
...getXYVisualizationState({
layers: [
...this.chartConfig.layers.map((layerItem, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return layerItem.getLayerConfig(layerId, accessorId);
}),
],
}),
fittingFunction: this.chartConfig.visualOptions?.missingValues ?? 'Zero',
endValue: this.chartConfig.visualOptions?.endValues,
curveType: this.chartConfig.visualOptions?.lineInterpolation ?? 'LINEAR',
emphasizeFitting: !this.chartConfig.visualOptions?.showDottedLine,
};
}
getReferences(): SavedObjectReference[] {
@ -68,8 +91,6 @@ export const getXYVisualizationState = (
showSingleSeries: false,
},
valueLabels: 'show',
fittingFunction: 'Zero',
curveType: 'LINEAR',
yLeftScale: 'linear',
axisTitlesVisibilitySettings: {
x: false,
@ -93,7 +114,6 @@ export const getXYVisualizationState = (
},
preferredSeriesType: 'line',
valuesInLegend: false,
emphasizeFitting: true,
hideEndzones: true,
...custom,
});

View file

@ -0,0 +1,45 @@
/*
* 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 { EuiPopover, EuiIcon, IconType } from '@elastic/eui';
import { css } from '@emotion/react';
import React from 'react';
import { useBoolean } from '../../../../hooks/use_boolean';
export const Popover = ({
children,
icon,
...props
}: {
children: React.ReactNode;
icon: IconType;
'data-test-subj'?: string;
}) => {
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
return (
<EuiPopover
panelPaddingSize="s"
button={
<EuiIcon
data-test-subj={props['data-test-subj']}
type={icon}
onClick={togglePopover}
css={css`
cursor: pointer;
`}
/>
}
isOpen={isPopoverOpen}
offset={10}
closePopover={closePopover}
repositionOnScroll
anchorPosition="upCenter"
>
{children}
</EuiPopover>
);
};

View file

@ -6,7 +6,7 @@
*/
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPopover, EuiIcon, EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useSummaryTimeRange } from '@kbn/observability-plugin/public';
import type { TimeRange } from '@kbn/es-query';
@ -16,13 +16,13 @@ import type { InventoryItemType } from '../../../../../common/inventory_models/t
import { findInventoryFields } from '../../../../../common/inventory_models';
import { createAlertsEsQuery } from '../../../../common/alerts/create_alerts_es_query';
import { infraAlertFeatureIds } from '../../../../pages/metrics/hosts/components/tabs/config';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { LinkToAlertsRule } from '../../links/link_to_alerts';
import { LinkToAlertsPage } from '../../links/link_to_alerts_page';
import { AlertFlyout } from '../../../../alerting/inventory/components/alert_flyout';
import { useBoolean } from '../../../../hooks/use_boolean';
import { ALERT_STATUS_ALL } from '../../../../common/alerts/constants';
import { Popover } from '../common/popover';
export const AlertsSummaryContent = ({
assetName,
@ -107,10 +107,8 @@ const MemoAlertSummaryWidget = React.memo(
);
const AlertsSectionTitle = () => {
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
return (
<EuiFlexGroup gutterSize="xs">
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle data-test-subj="infraAssetDetailsAlertsTitle" size="xxs">
<h5>
@ -122,21 +120,9 @@ const AlertsSectionTitle = () => {
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiIcon
data-test-subj="infraAssetDetailsAlertsPopoverButton"
type="iInCircle"
onClick={togglePopover}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
repositionOnScroll
anchorPosition="upCenter"
>
<Popover icon="iInCircle" data-test-subj="infraAssetDetailsAlertsPopoverButton">
<AlertsTooltipContent />
</EuiPopover>
</Popover>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -5,20 +5,14 @@
* 2.0.
*/
import {
EuiDescriptionListTitle,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPopover,
} from '@elastic/eui';
import { EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useBoolean } from '../../../../../hooks/use_boolean';
import { EuiText } from '@elastic/eui';
import type { MetadataData } from './metadata_summary_list';
import { Popover } from '../../common/popover';
const columnTitles = {
hostIp: i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.metadataHostIpHeading', {
@ -51,52 +45,43 @@ interface MetadataSummaryProps {
}
export const MetadataHeader = ({ metadataValue }: MetadataSummaryProps) => {
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
return (
<EuiDescriptionListTitle
css={css`
white-space: nowrap;
`}
>
<EuiFlexGroup gutterSize="xs">
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
{columnTitles[metadataValue.field as MetadataFields]}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiIcon
data-test-subj="infraAssetDetailsMetadataSummaryPopoverButton"
type="questionInCircle"
onClick={togglePopover}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
repositionOnScroll
anchorPosition="upCenter"
<Popover
icon="questionInCircle"
data-test-subj="infraAssetDetailsMetadataSummaryPopoverButton"
>
{metadataValue.tooltipLink ? (
<FormattedMessage
id="xpack.infra.assetDetails.overviewMetadata.tooltip.documentationLabel"
defaultMessage="See {documentation} for more details."
values={{
documentation: (
<EuiLink
data-test-subj="infraAssetDetailsTooltipMetadataDocumentationLink"
href={metadataValue.tooltipLink}
target="_blank"
>
<code>{metadataValue.tooltipFieldLabel}</code>
</EuiLink>
),
}}
/>
) : (
<code>{metadataValue.tooltipFieldLabel}</code>
)}
</EuiPopover>
<EuiText size="xs">
{metadataValue.tooltipLink ? (
<FormattedMessage
id="xpack.infra.assetDetails.overviewMetadata.tooltip.documentationLabel"
defaultMessage="See {documentation} for more details."
values={{
documentation: (
<EuiLink
data-test-subj="infraAssetDetailsTooltipMetadataDocumentationLink"
href={metadataValue.tooltipLink}
target="_blank"
>
<code>{metadataValue.tooltipFieldLabel}</code>
</EuiLink>
),
}}
/>
) : (
<code>{metadataValue.tooltipFieldLabel}</code>
)}
</EuiText>
</Popover>
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListTitle>

View file

@ -11,15 +11,18 @@ import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { HostMetricsExplanationContent } from '../../../../lens/metric_explanation/host_metrics_explanation_content';
import { buildCombinedHostsFilter } from '../../../../../utils/filters/build';
import type { Layer } from '../../../../../hooks/use_lens_attributes';
import { HostMetricsDocsLink, LensChart, type LensChartProps } from '../../../../lens';
import { LensChart, type LensChartProps } from '../../../../lens';
import {
type FormulaConfig,
hostLensFormulas,
type XYLayerOptions,
type XYVisualOptions,
} from '../../../../../common/visualizations';
import { METRIC_CHART_HEIGHT } from '../../../constants';
import { Popover } from '../../common/popover';
type DataViewOrigin = 'logs' | 'metrics';
interface MetricChartConfig extends Pick<LensChartProps, 'id' | 'title' | 'overrides'> {
@ -44,6 +47,11 @@ const LEGEND_SETTINGS: Pick<MetricChartConfig, 'overrides'>['overrides'] = {
},
};
const XY_VISUAL_OPTIONS: XYVisualOptions = {
showDottedLine: true,
missingValues: 'Linear',
};
const CHARTS_IN_ORDER: Array<
Pick<MetricChartConfig, 'id' | 'title' | 'layers' | 'overrides'> & {
dataViewOrigin: DataViewOrigin;
@ -303,17 +311,9 @@ export const MetricsGrid = React.memo(
return (
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.infra.assetDetails.overview.metricsSectionTitle"
defaultMessage="Metrics"
/>
</h5>
</EuiTitle>
<MetricsSectionTitle />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<HostMetricsDocsLink />
<EuiSpacer size="s" />
<EuiFlexGrid
columns={2}
@ -328,6 +328,7 @@ export const MetricsGrid = React.memo(
dataView={getDataView(dataViewOrigin)}
dateRange={timeRange}
height={METRIC_CHART_HEIGHT}
visualOptions={XY_VISUAL_OPTIONS}
layers={layers}
filters={getFilters(dataViewOrigin)}
title={title}
@ -343,3 +344,25 @@ export const MetricsGrid = React.memo(
);
}
);
const MetricsSectionTitle = () => {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.infra.assetDetails.overview.metricsSectionTitle"
defaultMessage="Metrics"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Popover icon="questionInCircle" data-test-subj="infraAssetDetailsMetricsPopoverButton">
<HostMetricsExplanationContent />
</Popover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -9,3 +9,4 @@ export { LensChart, type LensChartProps } from './lens_chart';
export { ChartPlaceholder } from './chart_placeholder';
export { TooltipContent } from './metric_explanation/tooltip_content';
export { HostMetricsDocsLink } from './metric_explanation/host_metrics_docs_link';
export { HostMetricsExplanationContent } from './metric_explanation/host_metrics_explanation_content';

View file

@ -7,21 +7,40 @@
import React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { HOST_METRICS_DOC_HREF } from '../../../common/visualizations/constants';
import { i18n } from '@kbn/i18n';
import {
HOST_METRICS_DOC_HREF,
HOST_METRICS_DOTTED_LINES_DOC_HREF,
} from '../../../common/visualizations/constants';
export const HostMetricsDocsLink = () => {
const DocLinks = {
metrics: {
href: HOST_METRICS_DOC_HREF,
label: i18n.translate('xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink', {
defaultMessage: 'What are these metrics?',
}),
},
dottedLines: {
href: HOST_METRICS_DOTTED_LINES_DOC_HREF,
label: i18n.translate('xpack.infra.hostsViewPage.tooltip.whyAmISeeingDottedLines', {
defaultMessage: 'Why am I seeing dotted lines?',
}),
},
};
interface Props {
type: keyof typeof DocLinks;
}
export const HostMetricsDocsLink = ({ type }: Props) => {
return (
<EuiText size="xs">
<EuiLink
data-test-subj="hostsViewMetricsDocumentationLink"
href={HOST_METRICS_DOC_HREF}
href={DocLinks[type].href}
target="_blank"
>
<FormattedMessage
id="xpack.infra.hostsViewPage.tooltip.whatAreTheseMetricsLink"
defaultMessage="What are these metrics?"
/>
{DocLinks[type].label}
</EuiLink>
</EuiText>
);

View file

@ -0,0 +1,30 @@
/*
* 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 } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { HostMetricsDocsLink } from './host_metrics_docs_link';
export const HostMetricsExplanationContent = () => {
return (
<EuiText size="xs">
<p>
<FormattedMessage
id="xpack.infra.hostsViewPage.metricsExplanation"
defaultMessage="Showing metrics for your host(s)"
/>
</p>
<p>
<HostMetricsDocsLink type="metrics" />
</p>
<p>
<HostMetricsDocsLink type="dottedLines" />
</p>
</EuiText>
);
};

View file

@ -19,32 +19,35 @@ import {
type MetricLayerOptions,
type FormulaConfig,
type LensAttributes,
type XYVisualOptions,
type Chart,
LensAttributesBuilder,
XYDataLayer,
MetricLayer,
XYChart,
MetricChart,
XYReferenceLinesLayer,
Chart,
LensVisualizationState,
} from '../common/visualizations';
import { useLazyRef } from './use_lazy_ref';
type Options = XYLayerOptions | MetricLayerOptions;
type LayerOptions = XYLayerOptions | MetricLayerOptions;
type ChartType = 'lnsXY' | 'lnsMetric';
type VisualOptions = XYVisualOptions;
export type LayerType = Exclude<LensLayerType, 'annotations' | 'metricTrendline'>;
export interface Layer<
TOptions extends Options,
TLayerOptions extends LayerOptions,
TFormulaConfig extends FormulaConfig | FormulaConfig[],
TLayerType extends LayerType = LayerType
> {
layerType: TLayerType;
data: TFormulaConfig;
options?: TOptions;
options?: TLayerOptions;
}
interface UseLensAttributesBaseParams<
TOptions extends Options,
TOptions extends LayerOptions,
TLayers extends Array<Layer<TOptions, FormulaConfig[]>> | Layer<TOptions, FormulaConfig>
> {
dataView?: DataView;
@ -58,6 +61,7 @@ interface UseLensAttributesXYChartParams
Array<Layer<XYLayerOptions, FormulaConfig[], 'data' | 'referenceLine'>>
> {
visualizationType: 'lnsXY';
visualOptions?: XYVisualOptions;
}
interface UseLensAttributesMetricChartParams
@ -77,6 +81,7 @@ export const useLensAttributes = ({
layers,
title,
visualizationType,
...extraParams
}: UseLensAttributesParams) => {
const {
services: { lens },
@ -97,6 +102,7 @@ export const useLensAttributes = ({
layers,
title,
visualizationType,
...extraParams,
}),
});
@ -184,12 +190,14 @@ const chartFactory = <
layers,
title,
visualizationType,
visualOptions,
}: {
dataView: DataView;
formulaAPI: FormulaPublicApi;
visualizationType: ChartType;
layers: TLayers;
title?: string;
visualOptions?: VisualOptions;
}): Chart<LensVisualizationState> => {
switch (visualizationType) {
case 'lnsXY':
@ -221,6 +229,7 @@ const chartFactory = <
});
}),
title,
visualOptions,
});
case 'lnsMetric':

View file

@ -20,7 +20,7 @@ import {
export const KPIGrid = () => {
return (
<HostCountProvider>
<HostMetricsDocsLink />
<HostMetricsDocsLink type="metrics" />
<EuiSpacer size="s" />
<EuiFlexGroup direction="row" gutterSize="s" data-test-subj="hostsViewKPIGrid">
<EuiFlexItem>

View file

@ -4,115 +4,38 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState, useRef, useCallback, useLayoutEffect } from 'react';
import { EuiPopover, EuiIcon, EuiFlexGroup, useEuiTheme } from '@elastic/eui';
import React from 'react';
import { EuiFlexGroup } from '@elastic/eui';
import { css } from '@emotion/react';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { TooltipContent } from '../../../../../components/lens/metric_explanation/tooltip_content';
import { useBoolean } from '../../../../../hooks/use_boolean';
import { Popover } from './popover';
interface Props {
label: string;
toolTip?: string;
formula?: string;
popoverContainerRef?: React.RefObject<HTMLDivElement>;
}
const SEARCH_BAR_OFFSET = 250;
const ANCHOR_SPACING = 10;
export const ColumnHeader = React.memo(({ label, toolTip, formula }: Props) => {
return (
<EuiFlexGroup gutterSize="xs">
<div
css={css`
overflow-wrap: break-word !important;
word-break: break-word;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
`}
>
{label}
</div>
const findTableParentElement = (element: HTMLElement | null): HTMLElement | null => {
let currentElement = element;
while (currentElement && currentElement.className !== APP_WRAPPER_CLASS) {
currentElement = currentElement.parentElement;
}
return currentElement;
};
export const ColumnHeader = React.memo(
({ label, toolTip, formula, popoverContainerRef }: Props) => {
const buttonRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLElement | null>(null);
const [offset, setOffset] = useState(0);
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
const { euiTheme } = useEuiTheme();
useLayoutEffect(() => {
containerRef.current = findTableParentElement(buttonRef.current);
}, []);
const calculateHeaderOffset = () => {
const { top: containerTop = 0 } = containerRef.current?.getBoundingClientRect() ?? {};
const headerOffset = containerTop + window.scrollY;
return headerOffset;
};
const onButtonClick = useCallback(
(e: React.MouseEvent<SVGElement>) => {
e.preventDefault();
e.stopPropagation();
const { top: buttonTop = 0 } = buttonRef.current?.getBoundingClientRect() ?? {};
// gets the actual page position, discounting anything above the page content (e.g: header, dismissible banner)
const headerOffset = calculateHeaderOffset();
// determines if the scroll position is close to overlapping with the button
const scrollPosition = buttonTop - headerOffset - SEARCH_BAR_OFFSET;
const isAboveElement = scrollPosition <= 0;
// offset to be taken into account when positioning the popover
setOffset(headerOffset * (isAboveElement ? -1 : 1) + ANCHOR_SPACING);
togglePopover();
},
[togglePopover]
);
return (
<EuiFlexGroup gutterSize="xs">
<div
css={css`
overflow-wrap: break-word !important;
word-break: break-word;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
`}
>
{label}
</div>
{toolTip && (
<EuiPopover
panelPaddingSize="s"
buttonRef={(el) => (buttonRef.current = el)}
button={
<EuiIcon
data-test-subj="hostsViewTableColumnPopoverButton"
type="questionInCircle"
onClick={onButtonClick}
/>
}
insert={
popoverContainerRef && popoverContainerRef?.current
? {
sibling: popoverContainerRef.current,
position: 'after',
}
: undefined
}
offset={offset}
anchorPosition={offset <= 0 ? 'downCenter' : 'upCenter'}
isOpen={isPopoverOpen}
closePopover={closePopover}
zIndex={Number(euiTheme.levels.header) - 1}
panelStyle={{ maxWidth: 350 }}
>
<TooltipContent formula={formula} description={toolTip} showDocumentationLink />
</EuiPopover>
)}
</EuiFlexGroup>
);
}
);
{toolTip && (
<Popover>
<TooltipContent formula={formula} description={toolTip} showDocumentationLink />
</Popover>
)}
</EuiFlexGroup>
);
});

View file

@ -0,0 +1,101 @@
/*
* 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, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { EuiPopover, EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { useBoolean } from '../../../../../hooks/use_boolean';
import { useHostsTableContext } from '../../hooks/use_hosts_table';
const SEARCH_BAR_OFFSET = 250;
const ANCHOR_SPACING = 10;
const findTableParentElement = (element: HTMLElement | null): HTMLElement | null => {
let currentElement = element;
while (currentElement && currentElement.className !== APP_WRAPPER_CLASS) {
currentElement = currentElement.parentElement;
}
return currentElement;
};
export const Popover = ({ children }: { children: React.ReactNode }) => {
const buttonRef = useRef<HTMLDivElement | null>(null);
const containerRef = useRef<HTMLElement | null>(null);
const [offset, setOffset] = useState(0);
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);
const {
refs: { popoverContainerRef },
} = useHostsTableContext();
const { euiTheme } = useEuiTheme();
useLayoutEffect(() => {
containerRef.current = findTableParentElement(buttonRef.current);
}, []);
const calculateHeaderOffset = () => {
const { top: containerTop = 0 } = containerRef.current?.getBoundingClientRect() ?? {};
const headerOffset = containerTop + window.scrollY;
return headerOffset;
};
const onButtonClick = useCallback(
(e: React.MouseEvent<SVGElement>) => {
e.preventDefault();
e.stopPropagation();
const { top: buttonTop = 0 } = buttonRef.current?.getBoundingClientRect() ?? {};
// gets the actual page position, discounting anything above the page content (e.g: header, dismissible banner)
const headerOffset = calculateHeaderOffset();
// determines if the scroll position is close to overlapping with the button
const scrollPosition = buttonTop - headerOffset - SEARCH_BAR_OFFSET;
const isAboveElement = scrollPosition <= 0;
// offset to be taken into account when positioning the popover
setOffset(headerOffset * (isAboveElement ? -1 : 1) + ANCHOR_SPACING);
togglePopover();
},
[togglePopover]
);
return (
<EuiPopover
panelPaddingSize="s"
ownFocus={false}
buttonRef={(el) => (buttonRef.current = el)}
button={
<EuiIcon
data-test-subj="hostsViewTableColumnPopoverButton"
type="questionInCircle"
css={css`
cursor: pointer;
`}
onClick={onButtonClick}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
offset={offset}
anchorPosition={offset <= 0 ? 'downCenter' : 'upCenter'}
insert={
popoverContainerRef && popoverContainerRef?.current
? {
sibling: popoverContainerRef.current,
position: 'after',
}
: undefined
}
zIndex={Number(euiTheme.levels.header) - 1}
panelStyle={{ maxWidth: 350 }}
>
{children}
</EuiPopover>
);
};

View file

@ -10,7 +10,11 @@ import { LensChart } from '../../../../../../components/lens';
import type { Layer } from '../../../../../../hooks/use_lens_attributes';
import { useMetricsDataViewContext } from '../../../hooks/use_data_view';
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import type { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations';
import type {
FormulaConfig,
XYLayerOptions,
XYVisualOptions,
} from '../../../../../../common/visualizations';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
import { buildCombinedHostsFilter } from '../../../../../../utils/filters/build';
import { useHostsTableContext } from '../../../hooks/use_hosts_table';
@ -20,9 +24,10 @@ import { METRIC_CHART_HEIGHT } from '../../../constants';
export interface MetricChartProps extends Pick<TypedLensByValueInput, 'id' | 'overrides'> {
title: string;
layers: Array<Layer<XYLayerOptions, FormulaConfig[]>>;
visualOptions?: XYVisualOptions;
}
export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) => {
export const MetricChart = ({ id, title, layers, visualOptions, overrides }: MetricChartProps) => {
const { searchCriteria } = useUnifiedSearchContext();
const { dataView } = useMetricsDataViewContext();
const { requestTs, loading } = useHostsViewContext();
@ -58,6 +63,7 @@ export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps)
dateRange={afterLoadedState.dateRange}
height={METRIC_CHART_HEIGHT}
layers={layers}
visualOptions={visualOptions}
lastReloadRequestTime={afterLoadedState.lastReloadRequestTime}
loading={loading}
filters={filters}

View file

@ -6,12 +6,18 @@
*/
import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGrid, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
import { hostLensFormulas, type XYLayerOptions } from '../../../../../../common/visualizations';
import { HostMetricsDocsLink } from '../../../../../../components/lens';
import { EuiFlexGroup } from '@elastic/eui';
import {
hostLensFormulas,
type XYVisualOptions,
type XYLayerOptions,
} from '../../../../../../common/visualizations';
import { HostMetricsExplanationContent } from '../../../../../../components/lens';
import { MetricChart, MetricChartProps } from './metric_chart';
import { Popover } from '../../table/popover';
const DEFAULT_BREAKDOWN_SIZE = 20;
const XY_LAYER_OPTIONS: XYLayerOptions = {
@ -21,6 +27,11 @@ const XY_LAYER_OPTIONS: XYLayerOptions = {
},
};
const XY_VISUAL_OPTIONS: XYVisualOptions = {
showDottedLine: true,
missingValues: 'Linear',
};
const PERCENT_LEFT_AXIS: Pick<MetricChartProps, 'overrides'>['overrides'] = {
axisLeft: {
domain: {
@ -28,6 +39,7 @@ const PERCENT_LEFT_AXIS: Pick<MetricChartProps, 'overrides'>['overrides'] = {
max: 1,
},
},
settings: {},
};
const CHARTS_IN_ORDER: MetricChartProps[] = [
@ -210,12 +222,22 @@ const CHARTS_IN_ORDER: MetricChartProps[] = [
export const MetricsGrid = React.memo(() => {
return (
<>
<HostMetricsDocsLink />
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText size="xs">Learn more about metrics</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<Popover>
<HostMetricsExplanationContent />
</Popover>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="s" data-test-subj="hostsView-metricChart">
{CHARTS_IN_ORDER.map((chartProp, index) => (
<EuiFlexItem key={index} grow={false}>
<MetricChart {...chartProp} />
<MetricChart {...chartProp} visualOptions={XY_VISUAL_OPTIONS} />
</EuiFlexItem>
))}
</EuiFlexGrid>

View file

@ -256,7 +256,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.cpuUsage}
toolTip={TOOLTIP.cpuUsage}
formula={hostLensFormulas.cpuUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'cpu',
@ -271,7 +270,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.normalizedLoad1m}
toolTip={TOOLTIP.normalizedLoad1m}
formula={hostLensFormulas.normalizedLoad1m.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'normalizedLoad1m',
@ -286,7 +284,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.memoryUsage}
toolTip={TOOLTIP.memoryUsage}
formula={hostLensFormulas.memoryUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'memory',
@ -301,7 +298,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.memoryFree}
toolTip={TOOLTIP.memoryFree}
formula={hostLensFormulas.memoryFree.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'memoryFree',
@ -316,7 +312,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.diskSpaceUsage}
toolTip={TOOLTIP.diskSpaceUsage}
formula={hostLensFormulas.diskSpaceUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'diskSpaceUsage',
@ -331,7 +326,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.rx}
toolTip={TOOLTIP.rx}
formula={hostLensFormulas.rx.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'rx',
@ -347,7 +341,6 @@ export const useHostsTable = () => {
label={TABLE_COLUMN_LABEL.tx}
toolTip={TOOLTIP.tx}
formula={hostLensFormulas.tx.value}
popoverContainerRef={popoverContainerRef}
/>
),
field: 'tx',
@ -358,13 +351,7 @@ export const useHostsTable = () => {
width: '120px',
},
],
[
hostFlyoutState?.itemId,
reportHostEntryClick,
searchCriteria.dateRange,
setHostFlyoutState,
popoverContainerRef,
]
[hostFlyoutState?.itemId, reportHostEntryClick, searchCriteria.dateRange, setHostFlyoutState]
);
const selection: EuiTableSelectionType<HostNodeRow> = {