[Metrics UI] Add preview charts to Inventory alerts (#91658)

* add first version of preview chart for inventory alerts

* Make preview chart collapsible

* Add margin to expressions without charts

* Remove some duplication in metric alerts preview charts

* Add warning thresholds to inventory alerts preview chart

* Add threshold annotations component

* Clean imports and unused variables

* Add tests for threschold annotations component

* Remove unused translations

* Set correct id to inventory alerts preview chart

* Get accountId and region with useWaffleOptions for preview chart

* Save inventory alert thresholds in the same unit as ES

* minor fixes

* Revert "Save inventory alert thresholds in the same unit as ES"

This reverts commit 118d83efeb.

* Remove threshold formatter function and convert values inside expression chart component

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ester Martí Vilaseca 2021-03-08 17:54:10 +01:00 committed by GitHub
parent 749a22989b
commit 978226f39a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 625 additions and 234 deletions

View file

@ -0,0 +1,153 @@
/*
* 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 { ThresholdAnnotations } from './threshold_annotations';
import {
Comparator,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/lib/alerting/metric_threshold/types';
// import { Color } from 'x-pack/plugins/infra/common/color_palette';
import { Color } from '../../../../common/color_palette';
import { shallow } from 'enzyme';
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
const mockComponent = (props: {}) => {
return <div {...props} />;
};
return {
...original,
LineAnnotation: mockComponent,
RectAnnotation: mockComponent,
};
});
describe('ThresholdAnnotations', () => {
async function setup(props = {}) {
const defaultProps = {
threshold: [20, 30],
sortedThresholds: [20, 30],
comparator: Comparator.GT,
color: Color.color0,
id: 'testId',
firstTimestamp: 123456789,
lastTimestamp: 987654321,
domain: { min: 10, max: 20 },
};
const wrapper = shallow(<ThresholdAnnotations {...defaultProps} {...props} />);
return wrapper;
}
it('should render a line annotation for each threshold', async () => {
const wrapper = await setup();
const annotation = wrapper.find('[data-test-subj="threshold-line"]');
const expectedValues = [{ dataValue: 20 }, { dataValue: 30 }];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
expect(annotation.length).toBe(1);
});
it('should render a rectangular annotation for in between thresholds', async () => {
const wrapper = await setup({ comparator: Comparator.BETWEEN });
const annotation = wrapper.find('[data-test-subj="between-rect"]');
const expectedValues = [
{
coordinates: {
x0: 123456789,
x1: 987654321,
y0: 20,
y1: 30,
},
},
];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
});
it('should render an upper rectangular annotation for outside range thresholds', async () => {
const wrapper = await setup({ comparator: Comparator.OUTSIDE_RANGE });
const annotation = wrapper.find('[data-test-subj="outside-range-lower-rect"]');
const expectedValues = [
{
coordinates: {
x0: 123456789,
x1: 987654321,
y0: 10,
y1: 20,
},
},
];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
});
it('should render a lower rectangular annotation for outside range thresholds', async () => {
const wrapper = await setup({ comparator: Comparator.OUTSIDE_RANGE });
const annotation = wrapper.find('[data-test-subj="outside-range-upper-rect"]');
const expectedValues = [
{
coordinates: {
x0: 123456789,
x1: 987654321,
y0: 30,
y1: 20,
},
},
];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
});
it('should render a rectangular annotation for below thresholds', async () => {
const wrapper = await setup({ comparator: Comparator.LT });
const annotation = wrapper.find('[data-test-subj="below-rect"]');
const expectedValues = [
{
coordinates: {
x0: 123456789,
x1: 987654321,
y0: 10,
y1: 20,
},
},
];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
});
it('should render a rectangular annotation for above thresholds', async () => {
const wrapper = await setup({ comparator: Comparator.GT });
const annotation = wrapper.find('[data-test-subj="above-rect"]');
const expectedValues = [
{
coordinates: {
x0: 123456789,
x1: 987654321,
y0: 20,
y1: 20,
},
},
];
const values = annotation.prop('dataValues');
expect(values).toEqual(expectedValues);
});
});

View file

@ -0,0 +1,164 @@
/*
* 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 { first, last } from 'lodash';
import { RectAnnotation, AnnotationDomainTypes, LineAnnotation } from '@elastic/charts';
import {
Comparator,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/lib/alerting/metric_threshold/types';
import { Color, colorTransformer } from '../../../../common/color_palette';
interface ThresholdAnnotationsProps {
threshold: number[];
sortedThresholds: number[];
comparator: Comparator;
color: Color;
id: string;
firstTimestamp: number;
lastTimestamp: number;
domain: { min: number; max: number };
}
const opacity = 0.3;
export const ThresholdAnnotations = ({
threshold,
sortedThresholds,
comparator,
color,
id,
firstTimestamp,
lastTimestamp,
domain,
}: ThresholdAnnotationsProps) => {
if (!comparator || !threshold) return null;
const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(comparator);
const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(comparator);
return (
<>
<LineAnnotation
id={`${id}-thresholds`}
domainType={AnnotationDomainTypes.YDomain}
data-test-subj="threshold-line"
dataValues={sortedThresholds.map((t) => ({
dataValue: t,
}))}
style={{
line: {
strokeWidth: 2,
stroke: colorTransformer(color),
opacity: 1,
},
}}
/>
{sortedThresholds.length === 2 && comparator === Comparator.BETWEEN ? (
<>
<RectAnnotation
id={`${id}-lower-threshold`}
data-test-subj="between-rect"
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: first(threshold),
y1: last(threshold),
},
},
]}
/>
</>
) : null}
{sortedThresholds.length === 2 && comparator === Comparator.OUTSIDE_RANGE ? (
<>
<RectAnnotation
id={`${id}-lower-threshold`}
data-test-subj="outside-range-lower-rect"
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: domain.min,
y1: first(threshold),
},
},
]}
/>
<RectAnnotation
id={`${id}-upper-threshold`}
data-test-subj="outside-range-upper-rect"
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: last(threshold),
y1: domain.max,
},
},
]}
/>
</>
) : null}
{isBelow && first(threshold) != null ? (
<RectAnnotation
id={`${id}-upper-threshold`}
data-test-subj="below-rect"
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: domain.min,
y1: first(threshold),
},
},
]}
/>
) : null}
{isAbove && first(threshold) != null ? (
<RectAnnotation
id={`${id}-upper-threshold`}
data-test-subj="above-rect"
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: first(threshold),
y1: domain.max,
},
},
]}
/>
) : null}
</>
);
};

View file

@ -71,6 +71,7 @@ import {
import { validateMetricThreshold } from './validation';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { ExpressionChart } from './expression_chart';
const FILTER_TYPING_DEBOUNCE_MS = 500;
export interface AlertContextMeta {
@ -291,11 +292,13 @@ export const Expressions: React.FC<Props> = (props) => {
</EuiText>
<StyledExpression>
<StyledExpressionRow>
<NodeTypeExpression
options={nodeTypes}
value={alertParams.nodeType || 'host'}
onChange={updateNodeType}
/>
<NonCollapsibleExpression>
<NodeTypeExpression
options={nodeTypes}
value={alertParams.nodeType || 'host'}
onChange={updateNodeType}
/>
</NonCollapsibleExpression>
</StyledExpressionRow>
</StyledExpression>
<EuiSpacer size={'xs'} />
@ -313,17 +316,26 @@ export const Expressions: React.FC<Props> = (props) => {
errors={(errors[idx] as IErrorObject) || emptyError}
expression={e || {}}
fields={derivedIndexPattern.fields}
/>
>
<ExpressionChart
expression={e}
filterQuery={alertParams.filterQueryText}
nodeType={alertParams.nodeType}
sourceId={alertParams.sourceId}
/>
</ExpressionRow>
);
})}
<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
/>
<NonCollapsibleExpression>
<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
/>
</NonCollapsibleExpression>
<div>
<EuiButtonEmpty
@ -424,6 +436,10 @@ interface ExpressionRowProps {
fields: IFieldType[];
}
const NonCollapsibleExpression = euiStyled.div`
margin-left: 28px;
`;
const StyledExpressionRow = euiStyled(EuiFlexGroup)`
display: flex;
flex-wrap: wrap;
@ -439,7 +455,19 @@ const StyledHealth = euiStyled(EuiHealth)`
`;
export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
const { setAlertParams, expression, errors, expressionId, remove, canDelete, fields } = props;
const [isExpanded, setRowState] = useState(true);
const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]);
const {
children,
setAlertParams,
expression,
errors,
expressionId,
remove,
canDelete,
fields,
} = props;
const {
metric,
comparator = Comparator.GT,
@ -579,6 +607,16 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
return (
<>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
onClick={toggleRowState}
aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.expandRowLabel', {
defaultMessage: 'Expand row.',
})}
/>
</EuiFlexItem>
<EuiFlexItem grow>
<StyledExpressionRow>
<StyledExpression>
@ -670,6 +708,7 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
</EuiFlexItem>
)}
</EuiFlexGroup>
{isExpanded ? <div style={{ padding: '0 0 0 28px' }}>{children}</div> : null}
<EuiSpacer size={'s'} />
</>
);

View file

@ -0,0 +1,236 @@
/*
* 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, useCallback } from 'react';
import { Axis, Chart, niceTimeFormatter, Position, Settings } from '@elastic/charts';
import { first, last } from 'lodash';
import moment from 'moment';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Color } from '../../../../common/color_palette';
import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api';
import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart';
import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain';
import { getMetricId } from '../../../pages/metrics/metrics_explorer/components/helpers/get_metric_id';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types';
import { useSnapshot } from '../../../pages/metrics/inventory_view/hooks/use_snaphot';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
import { createInventoryMetricFormatter } from '../../../pages/metrics/inventory_view/lib/create_inventory_metric_formatter';
import {
ChartContainer,
LoadingState,
NoDataState,
TIME_LABELS,
tooltipProps,
getChartTheme,
} from '../../common/criterion_preview_chart/criterion_preview_chart';
import { ThresholdAnnotations } from '../../common/criterion_preview_chart/threshold_annotations';
import { useWaffleOptionsContext } from '../../../pages/metrics/inventory_view/hooks/use_waffle_options';
interface Props {
expression: InventoryMetricConditions;
filterQuery?: string;
nodeType: InventoryItemType;
sourceId: string;
}
export const ExpressionChart: React.FC<Props> = ({
expression,
filterQuery,
nodeType,
sourceId,
}) => {
const timerange = useMemo(
() => ({
interval: `${expression.timeSize || 1}${expression.timeUnit}`,
from: moment()
.subtract((expression.timeSize || 1) * 20, expression.timeUnit)
.valueOf(),
to: moment().valueOf(),
forceInterval: true,
ignoreLookback: true,
}),
[expression.timeSize, expression.timeUnit]
);
const buildCustomMetric = (metric: any) => ({
...metric,
type: 'custom' as SnapshotMetricType,
});
const options = useWaffleOptionsContext();
const { loading, nodes } = useSnapshot(
filterQuery,
expression.metric === 'custom'
? [buildCustomMetric(expression.customMetric)]
: [{ type: expression.metric }],
[],
nodeType,
sourceId,
0,
options.accountId,
options.region,
true,
timerange
);
const { uiSettings } = useKibanaContextForPlugin().services;
const metric = {
field: expression.metric,
aggregation: 'avg' as MetricsExplorerAggregation,
color: Color.color0,
};
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
const dateFormatter = useMemo(() => {
const firstSeries = nodes[0]?.metrics[0]?.timeseries;
const firstTimestamp = first(firstSeries?.rows)?.timestamp;
const lastTimestamp = last(firstSeries?.rows)?.timestamp;
if (firstTimestamp == null || lastTimestamp == null) {
return (value: number) => `${value}`;
}
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
}, [nodes]);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const yAxisFormater = useCallback(
createInventoryMetricFormatter(
expression.metric === 'custom'
? buildCustomMetric(expression.customMetric)
: { type: expression.metric }
),
[expression.metric]
);
if (loading || !nodes) {
return <LoadingState />;
}
const convertThreshold = (threshold: number) => convertMetricValue(expression.metric, threshold);
const convertedThresholds = expression.threshold.map(convertThreshold);
const convertedWarningThresholds = expression.warningThreshold?.map(convertThreshold) ?? [];
const criticalThresholds = convertedThresholds.slice().sort();
const warningThresholds = convertedWarningThresholds.slice().sort();
const thresholds = [...criticalThresholds, ...warningThresholds].sort();
// Creating a custom series where the ID is changed to 0
// so that we can get a proper domian
const firstSeries = nodes[0]?.metrics[0]?.timeseries;
if (!firstSeries || !firstSeries.rows || firstSeries.rows.length === 0) {
return <NoDataState />;
}
const series = {
...firstSeries,
id: nodes[0]?.name,
rows: firstSeries.rows.map((row) => {
const newRow: MetricsExplorerRow = { ...row };
thresholds.forEach((thresholdValue, index) => {
newRow[getMetricId(metric, `threshold_${index}`)] = thresholdValue;
});
return newRow;
}),
};
const firstTimestamp = first(firstSeries.rows)!.timestamp;
const lastTimestamp = last(firstSeries.rows)!.timestamp;
const dataDomain = calculateDomain(series, [metric], false);
const domain = {
max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom.
min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min) * 0.9, // add 10% floor
};
if (domain.min === first(convertedThresholds)) {
domain.min = domain.min * 0.9;
}
const { timeSize, timeUnit } = expression;
const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS];
return (
<>
<ChartContainer>
<Chart>
<MetricExplorerSeriesChart
type={MetricsExplorerChartType.bar}
metric={metric}
id="0"
series={series}
stack={false}
/>
<ThresholdAnnotations
comparator={expression.comparator}
threshold={convertedThresholds}
sortedThresholds={criticalThresholds}
color={Color.color1}
id="critical"
firstTimestamp={firstTimestamp}
lastTimestamp={lastTimestamp}
domain={domain}
/>
{expression.warningComparator && expression.warningThreshold && (
<ThresholdAnnotations
comparator={expression.warningComparator}
threshold={convertedWarningThresholds}
sortedThresholds={warningThresholds}
color={Color.color5}
id="warning"
firstTimestamp={firstTimestamp}
lastTimestamp={lastTimestamp}
domain={domain}
/>
)}
<Axis
id={'timestamp'}
position={Position.Bottom}
showOverlappingTicks={true}
tickFormat={dateFormatter}
/>
<Axis id={'values'} position={Position.Left} tickFormat={yAxisFormater} domain={domain} />
<Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} />
</Chart>
</ChartContainer>
<div style={{ textAlign: 'center' }}>
{series.id !== 'ALL' ? (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data for {id}"
values={{ id: series.id, timeLabel, lookback: timeSize * 20 }}
/>
</EuiText>
) : (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel}"
values={{ timeLabel, lookback: timeSize * 20 }}
/>
</EuiText>
)}
</div>
</>
);
};
const convertMetricValue = (metric: SnapshotMetricType, value: number) => {
if (converters[metric]) {
return converters[metric](value);
} else {
return value;
}
};
const converters: Record<string, (n: number) => number> = {
cpu: (n) => Number(n) / 100,
memory: (n) => Number(n) / 100,
};

View file

@ -6,39 +6,31 @@
*/
import React, { useMemo, useCallback } from 'react';
import {
Axis,
Chart,
niceTimeFormatter,
Position,
Settings,
TooltipValue,
RectAnnotation,
AnnotationDomainTypes,
LineAnnotation,
} from '@elastic/charts';
import { Axis, Chart, niceTimeFormatter, Position, Settings } from '@elastic/charts';
import { first, last } from 'lodash';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { IIndexPattern } from 'src/plugins/data/public';
import { InfraSource } from '../../../../common/http_api/source_api';
import {
Comparator,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/lib/alerting/metric_threshold/types';
import { Color, colorTransformer } from '../../../../common/color_palette';
import { Color } from '../../../../common/color_palette';
import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api';
import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart';
import { MetricExpression } from '../types';
import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme';
import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric';
import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain';
import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data';
import { getMetricId } from '../../../pages/metrics/metrics_explorer/components/helpers/get_metric_id';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import {
ChartContainer,
LoadingState,
NoDataState,
TIME_LABELS,
tooltipProps,
getChartTheme,
} from '../../common/criterion_preview_chart/criterion_preview_chart';
import { ThresholdAnnotations } from '../../common/criterion_preview_chart/threshold_annotations';
interface Props {
expression: MetricExpression;
@ -48,18 +40,6 @@ interface Props {
groupBy?: string | string[];
}
const tooltipProps = {
headerFormatter: (tooltipValue: TooltipValue) =>
moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'),
};
const TIME_LABELS = {
s: i18n.translate('xpack.infra.metrics.alerts.timeLabels.seconds', { defaultMessage: 'seconds' }),
m: i18n.translate('xpack.infra.metrics.alerts.timeLabels.minutes', { defaultMessage: 'minutes' }),
h: i18n.translate('xpack.infra.metrics.alerts.timeLabels.hours', { defaultMessage: 'hours' }),
d: i18n.translate('xpack.infra.metrics.alerts.timeLabels.days', { defaultMessage: 'days' }),
};
export const ExpressionChart: React.FC<Props> = ({
expression,
derivedIndexPattern,
@ -99,16 +79,7 @@ export const ExpressionChart: React.FC<Props> = ({
const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]);
if (loading || !data) {
return (
<EmptyContainer>
<EuiText color="subdued">
<FormattedMessage
id="xpack.infra.metrics.alerts.loadingMessage"
defaultMessage="Loading"
/>
</EuiText>
</EmptyContainer>
);
return <LoadingState />;
}
const criticalThresholds = expression.threshold.slice().sort();
@ -119,16 +90,7 @@ export const ExpressionChart: React.FC<Props> = ({
// so that we can get a proper domian
const firstSeries = first(data.series);
if (!firstSeries || !firstSeries.rows || firstSeries.rows.length === 0) {
return (
<EmptyContainer>
<EuiText color="subdued" data-test-subj="noChartData">
<FormattedMessage
id="xpack.infra.metrics.alerts.noDataMessage"
defaultMessage="Oops, no chart data available"
/>
</EuiText>
</EmptyContainer>
);
return <NoDataState />;
}
const series = {
@ -154,137 +116,9 @@ export const ExpressionChart: React.FC<Props> = ({
domain.min = domain.min * 0.9;
}
const opacity = 0.3;
const { timeSize, timeUnit } = expression;
const timeLabel = TIME_LABELS[timeUnit as keyof typeof TIME_LABELS];
const ThresholdAnnotations = ({
threshold,
sortedThresholds,
comparator,
color,
id,
}: Partial<MetricExpression> & { sortedThresholds: number[]; color: Color; id: string }) => {
if (!comparator || !threshold) return null;
const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(comparator);
const isBelow = [Comparator.LT, Comparator.LT_OR_EQ].includes(comparator);
return (
<>
<LineAnnotation
id={`${id}-thresholds`}
domainType={AnnotationDomainTypes.YDomain}
dataValues={sortedThresholds.map((t) => ({
dataValue: t,
}))}
style={{
line: {
strokeWidth: 2,
stroke: colorTransformer(color),
opacity: 1,
},
}}
/>
{sortedThresholds.length === 2 && comparator === Comparator.BETWEEN ? (
<>
<RectAnnotation
id={`${id}-lower-threshold`}
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: first(expression.threshold),
y1: last(expression.threshold),
},
},
]}
/>
</>
) : null}
{sortedThresholds.length === 2 && comparator === Comparator.OUTSIDE_RANGE ? (
<>
<RectAnnotation
id={`${id}-lower-threshold`}
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: domain.min,
y1: first(threshold),
},
},
]}
/>
<RectAnnotation
id={`${id}-upper-threshold`}
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: last(threshold),
y1: domain.max,
},
},
]}
/>
</>
) : null}
{isBelow && first(threshold) != null ? (
<RectAnnotation
id={`${id}-upper-threshold`}
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: domain.min,
y1: first(threshold),
},
},
]}
/>
) : null}
{isAbove && first(threshold) != null ? (
<RectAnnotation
id={`${id}-upper-threshold`}
style={{
fill: colorTransformer(color),
opacity,
}}
dataValues={[
{
coordinates: {
x0: firstTimestamp,
x1: lastTimestamp,
y0: first(threshold),
y1: domain.max,
},
},
]}
/>
) : null}
</>
);
};
return (
<>
<ChartContainer>
@ -302,6 +136,9 @@ export const ExpressionChart: React.FC<Props> = ({
sortedThresholds={criticalThresholds}
color={Color.color1}
id="critical"
firstTimestamp={firstTimestamp}
lastTimestamp={lastTimestamp}
domain={domain}
/>
{expression.warningComparator && expression.warningThreshold && (
<ThresholdAnnotations
@ -310,6 +147,9 @@ export const ExpressionChart: React.FC<Props> = ({
sortedThresholds={warningThresholds}
color={Color.color5}
id="warning"
firstTimestamp={firstTimestamp}
lastTimestamp={lastTimestamp}
domain={domain}
/>
)}
<Axis
@ -344,28 +184,3 @@ export const ExpressionChart: React.FC<Props> = ({
</>
);
};
const EmptyContainer: React.FC = ({ children }) => (
<div
style={{
width: '100%',
height: 150,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
{children}
</div>
);
const ChartContainer: React.FC = ({ children }) => (
<div
style={{
width: '100%',
height: 150,
}}
>
{children}
</div>
);

View file

@ -10959,14 +10959,6 @@
"xpack.infra.metrics.alertName": "メトリックしきい値",
"xpack.infra.metrics.alerts.dataTimeRangeLabel": "過去{lookback} {timeLabel}",
"xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping": "{id}のデータの過去{lookback} {timeLabel}",
"xpack.infra.metrics.alerts.loadingMessage": "読み込み中",
"xpack.infra.metrics.alerts.noDataMessage": "グラフデータがありません",
"xpack.infra.metrics.alerts.timeLabels.days": "日",
"xpack.infra.metrics.alerts.timeLabels.hours": "時間",
"xpack.infra.metrics.alerts.timeLabels.minutes": "分",
"xpack.infra.metrics.alerts.timeLabels.seconds": "秒",
"xpack.infra.metrics.anomaly.alertFlyout.alertDescription": "異常スコアが定義されたしきい値を超えたときにアラートを発行します。",
"xpack.infra.metrics.anomaly.alertName": "インフラストラクチャーの異常",
"xpack.infra.metrics.emptyViewDescription": "期間またはフィルターを調整してみてください。",
"xpack.infra.metrics.emptyViewTitle": "表示するデータがありません。",
"xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel": "閉じる",

View file

@ -11109,14 +11109,6 @@
"xpack.infra.metrics.alertName": "指标阈值",
"xpack.infra.metrics.alerts.dataTimeRangeLabel": "过去 {lookback} {timeLabel}",
"xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping": "{id} 过去 {lookback} {timeLabel}的数据",
"xpack.infra.metrics.alerts.loadingMessage": "正在加载",
"xpack.infra.metrics.alerts.noDataMessage": "糟糕,没有可用的图表数据",
"xpack.infra.metrics.alerts.timeLabels.days": "天",
"xpack.infra.metrics.alerts.timeLabels.hours": "小时",
"xpack.infra.metrics.alerts.timeLabels.minutes": "分钟",
"xpack.infra.metrics.alerts.timeLabels.seconds": "秒",
"xpack.infra.metrics.anomaly.alertFlyout.alertDescription": "当异常分数超过定义的阈值时告警。",
"xpack.infra.metrics.anomaly.alertName": "基础架构异常",
"xpack.infra.metrics.emptyViewDescription": "尝试调整您的时间或筛选。",
"xpack.infra.metrics.emptyViewTitle": "没有可显示的数据。",
"xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel": "关闭",