mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
749a22989b
commit
978226f39a
7 changed files with 625 additions and 234 deletions
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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'} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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": "閉じる",
|
||||
|
|
|
@ -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": "关闭",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue