mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
40a666b04e
commit
07ad32ff9e
17 changed files with 383 additions and 225 deletions
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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':
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue