[Metrics UI] Add Charts to Alert Conditions (#64384)
* [Metrics UI] Add Charts to Alert Conditions
- Reorganize files under public/alerting
- Change Metrics Explorer API to force interval
- Add charts to expression rows
- Allow expression rows to be collapsable
- Adding sum aggregation to Metrics Explorer for parity
* Adding interval information to Metrics Eexplorer API
* Moving data hook into the expression charts component
* Revert "Adding interval information to Metrics Eexplorer API"
This reverts commit f6e2fc11be
.
* Reducing the opacity for the threshold areas
* Changing darkMode to use alertsContext.uiSettings
This commit is contained in:
parent
c8b9bdd0ec
commit
0399f70050
|
@ -13,6 +13,7 @@ export const METRIC_EXPLORER_AGGREGATIONS = [
|
|||
'cardinality',
|
||||
'rate',
|
||||
'count',
|
||||
'sum',
|
||||
] as const;
|
||||
|
||||
type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number];
|
||||
|
@ -54,6 +55,7 @@ export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
|
|||
afterKey: rt.union([rt.string, rt.null, rt.undefined]),
|
||||
limit: rt.union([rt.number, rt.null, rt.undefined]),
|
||||
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
|
||||
forceInterval: rt.boolean,
|
||||
});
|
||||
|
||||
export const metricsExplorerRequestBodyRT = rt.intersection([
|
|
@ -35,6 +35,9 @@ export const metricsExplorerViewSavedObjectMappings: {
|
|||
},
|
||||
options: {
|
||||
properties: {
|
||||
forceInterval: {
|
||||
type: 'boolean',
|
||||
},
|
||||
metrics: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
|
|
|
@ -6,9 +6,6 @@
|
|||
|
||||
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiFormRow,
|
||||
|
@ -18,40 +15,28 @@ import {
|
|||
EuiIcon,
|
||||
EuiFieldSearch,
|
||||
} from '@elastic/eui';
|
||||
import { IFieldType } from 'src/plugins/data/public';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
MetricExpressionParams,
|
||||
Comparator,
|
||||
Aggregators,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import {
|
||||
WhenExpression,
|
||||
OfExpression,
|
||||
ThresholdExpression,
|
||||
ForLastExpression,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../triggers_actions_ui/public/common';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
|
||||
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
|
||||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by';
|
||||
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
|
||||
|
||||
interface AlertContextMeta {
|
||||
currentOptions?: Partial<MetricsExplorerOptions>;
|
||||
series?: MetricsExplorerSeries;
|
||||
}
|
||||
import { ExpressionRow } from './expression_row';
|
||||
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
|
||||
import { ExpressionChart } from './expression_chart';
|
||||
|
||||
interface Props {
|
||||
errors: IErrorObject[];
|
||||
|
@ -67,11 +52,6 @@ interface Props {
|
|||
setAlertProperty(key: string, value: any): void;
|
||||
}
|
||||
|
||||
type TimeUnit = 's' | 'm' | 'h' | 'd';
|
||||
type MetricExpression = Omit<MetricExpressionParams, 'metric'> & {
|
||||
metric?: string;
|
||||
};
|
||||
|
||||
const defaultExpression = {
|
||||
aggType: Aggregators.AVERAGE,
|
||||
comparator: Comparator.GT,
|
||||
|
@ -80,17 +60,6 @@ const defaultExpression = {
|
|||
timeUnit: 'm',
|
||||
} as MetricExpression;
|
||||
|
||||
const customComparators = {
|
||||
...builtInComparators,
|
||||
[Comparator.OUTSIDE_RANGE]: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', {
|
||||
defaultMessage: 'Is not between',
|
||||
}),
|
||||
value: Comparator.OUTSIDE_RANGE,
|
||||
requiredValues: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const Expressions: React.FC<Props> = props => {
|
||||
const { setAlertParams, alertParams, errors, alertsContext } = props;
|
||||
const { source, createDerivedIndexPattern } = useSourceViaHttp({
|
||||
|
@ -101,7 +70,6 @@ export const Expressions: React.FC<Props> = props => {
|
|||
});
|
||||
const [timeSize, setTimeSize] = useState<number | undefined>(1);
|
||||
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
|
||||
|
||||
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
|
||||
createDerivedIndexPattern,
|
||||
]);
|
||||
|
@ -127,14 +95,14 @@ export const Expressions: React.FC<Props> = props => {
|
|||
);
|
||||
|
||||
const addExpression = useCallback(() => {
|
||||
const exp = alertParams.criteria.slice();
|
||||
const exp = alertParams.criteria?.slice() || [];
|
||||
exp.push(defaultExpression);
|
||||
setAlertParams('criteria', exp);
|
||||
}, [setAlertParams, alertParams.criteria]);
|
||||
|
||||
const removeExpression = useCallback(
|
||||
(id: number) => {
|
||||
const exp = alertParams.criteria.slice();
|
||||
const exp = alertParams.criteria?.slice() || [];
|
||||
if (exp.length > 1) {
|
||||
exp.splice(id, 1);
|
||||
setAlertParams('criteria', exp);
|
||||
|
@ -167,10 +135,11 @@ export const Expressions: React.FC<Props> = props => {
|
|||
|
||||
const updateTimeSize = useCallback(
|
||||
(ts: number | undefined) => {
|
||||
const criteria = alertParams.criteria.map(c => ({
|
||||
...c,
|
||||
timeSize: ts,
|
||||
}));
|
||||
const criteria =
|
||||
alertParams.criteria?.map(c => ({
|
||||
...c,
|
||||
timeSize: ts,
|
||||
})) || [];
|
||||
setTimeSize(ts || undefined);
|
||||
setAlertParams('criteria', criteria);
|
||||
},
|
||||
|
@ -179,10 +148,11 @@ export const Expressions: React.FC<Props> = props => {
|
|||
|
||||
const updateTimeUnit = useCallback(
|
||||
(tu: string) => {
|
||||
const criteria = alertParams.criteria.map(c => ({
|
||||
...c,
|
||||
timeUnit: tu,
|
||||
}));
|
||||
const criteria =
|
||||
alertParams.criteria?.map(c => ({
|
||||
...c,
|
||||
timeUnit: tu,
|
||||
})) || [];
|
||||
setTimeUnit(tu as TimeUnit);
|
||||
setAlertParams('criteria', criteria);
|
||||
},
|
||||
|
@ -250,7 +220,7 @@ export const Expressions: React.FC<Props> = props => {
|
|||
alertParams.criteria.map((e, idx) => {
|
||||
return (
|
||||
<ExpressionRow
|
||||
canDelete={alertParams.criteria.length > 1}
|
||||
canDelete={(alertParams.criteria && alertParams.criteria.length > 1) || false}
|
||||
fields={derivedIndexPattern.fields}
|
||||
remove={removeExpression}
|
||||
addExpression={addExpression}
|
||||
|
@ -259,17 +229,28 @@ export const Expressions: React.FC<Props> = props => {
|
|||
setAlertParams={updateParams}
|
||||
errors={errors[idx] || emptyError}
|
||||
expression={e || {}}
|
||||
/>
|
||||
>
|
||||
<ExpressionChart
|
||||
expression={e}
|
||||
context={alertsContext}
|
||||
derivedIndexPattern={derivedIndexPattern}
|
||||
source={source}
|
||||
filterQuery={alertParams.filterQuery}
|
||||
groupBy={alertParams.groupBy}
|
||||
/>
|
||||
</ExpressionRow>
|
||||
);
|
||||
})}
|
||||
|
||||
<ForLastExpression
|
||||
timeWindowSize={timeSize}
|
||||
timeWindowUnit={timeUnit}
|
||||
errors={emptyError}
|
||||
onChangeWindowSize={updateTimeSize}
|
||||
onChangeWindowUnit={updateTimeUnit}
|
||||
/>
|
||||
<div style={{ marginLeft: 28 }}>
|
||||
<ForLastExpression
|
||||
timeWindowSize={timeSize}
|
||||
timeWindowUnit={timeUnit}
|
||||
errors={emptyError}
|
||||
onChangeWindowSize={updateTimeSize}
|
||||
onChangeWindowUnit={updateTimeUnit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EuiButtonEmpty
|
||||
|
@ -359,175 +340,3 @@ export const Expressions: React.FC<Props> = props => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExpressionRowProps {
|
||||
fields: IFieldType[];
|
||||
expressionId: number;
|
||||
expression: MetricExpression;
|
||||
errors: IErrorObject;
|
||||
canDelete: boolean;
|
||||
addExpression(): void;
|
||||
remove(id: number): void;
|
||||
setAlertParams(id: number, params: MetricExpression): void;
|
||||
}
|
||||
|
||||
const StyledExpressionRow = euiStyled(EuiFlexGroup)`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px;
|
||||
`;
|
||||
|
||||
const StyledExpression = euiStyled.div`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
|
||||
const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props;
|
||||
const {
|
||||
aggType = Aggregators.MAX,
|
||||
metric,
|
||||
comparator = Comparator.GT,
|
||||
threshold = [],
|
||||
} = expression;
|
||||
|
||||
const updateAggType = useCallback(
|
||||
(at: string) => {
|
||||
setAlertParams(expressionId, {
|
||||
...expression,
|
||||
aggType: at as MetricExpression['aggType'],
|
||||
metric: at === 'count' ? undefined : expression.metric,
|
||||
});
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateMetric = useCallback(
|
||||
(m?: MetricExpression['metric']) => {
|
||||
setAlertParams(expressionId, { ...expression, metric: m });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateComparator = useCallback(
|
||||
(c?: string) => {
|
||||
setAlertParams(expressionId, { ...expression, comparator: c as Comparator });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateThreshold = useCallback(
|
||||
t => {
|
||||
if (t.join() !== expression.threshold.join()) {
|
||||
setAlertParams(expressionId, { ...expression, threshold: t });
|
||||
}
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow>
|
||||
<StyledExpressionRow>
|
||||
<StyledExpression>
|
||||
<WhenExpression
|
||||
customAggTypesOptions={aggregationType}
|
||||
aggType={aggType}
|
||||
onChangeSelectedAggType={updateAggType}
|
||||
/>
|
||||
</StyledExpression>
|
||||
{aggType !== 'count' && (
|
||||
<StyledExpression>
|
||||
<OfExpression
|
||||
customAggTypesOptions={aggregationType}
|
||||
aggField={metric}
|
||||
fields={fields.map(f => ({
|
||||
normalizedType: f.type,
|
||||
name: f.name,
|
||||
}))}
|
||||
aggType={aggType}
|
||||
errors={errors}
|
||||
onChangeSelectedAggField={updateMetric}
|
||||
/>
|
||||
</StyledExpression>
|
||||
)}
|
||||
<StyledExpression>
|
||||
<ThresholdExpression
|
||||
thresholdComparator={comparator || Comparator.GT}
|
||||
threshold={threshold}
|
||||
customComparators={customComparators}
|
||||
onChangeSelectedThresholdComparator={updateComparator}
|
||||
onChangeSelectedThreshold={updateThreshold}
|
||||
errors={errors}
|
||||
/>
|
||||
</StyledExpression>
|
||||
</StyledExpressionRow>
|
||||
</EuiFlexItem>
|
||||
{canDelete && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', {
|
||||
defaultMessage: 'Remove condition',
|
||||
})}
|
||||
color={'danger'}
|
||||
iconType={'trash'}
|
||||
onClick={() => remove(expressionId)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'s'} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const aggregationType: { [key: string]: any } = {
|
||||
avg: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {
|
||||
defaultMessage: 'Average',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number'],
|
||||
value: Aggregators.AVERAGE,
|
||||
},
|
||||
max: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', {
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number', 'date'],
|
||||
value: Aggregators.MAX,
|
||||
},
|
||||
min: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', {
|
||||
defaultMessage: 'Min',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number', 'date'],
|
||||
value: Aggregators.MIN,
|
||||
},
|
||||
cardinality: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', {
|
||||
defaultMessage: 'Cardinality',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: Aggregators.CARDINALITY,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
rate: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', {
|
||||
defaultMessage: 'Rate',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: Aggregators.RATE,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
count: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', {
|
||||
defaultMessage: 'Document count',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: Aggregators.COUNT,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import {
|
||||
Axis,
|
||||
Chart,
|
||||
niceTimeFormatter,
|
||||
Position,
|
||||
Settings,
|
||||
TooltipValue,
|
||||
RectAnnotation,
|
||||
} 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';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
||||
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 { MetricsExplorerColor, colorTransformer } from '../../../../common/color_palette';
|
||||
import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api';
|
||||
import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart';
|
||||
import { MetricExpression, AlertContextMeta } 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';
|
||||
|
||||
interface Props {
|
||||
context: AlertsContextValue<AlertContextMeta>;
|
||||
expression: MetricExpression;
|
||||
derivedIndexPattern: IIndexPattern;
|
||||
source: InfraSource | null;
|
||||
filterQuery?: string;
|
||||
groupBy?: 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,
|
||||
context,
|
||||
derivedIndexPattern,
|
||||
source,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
}) => {
|
||||
const { loading, data } = useMetricsExplorerChartData(
|
||||
expression,
|
||||
context,
|
||||
derivedIndexPattern,
|
||||
source,
|
||||
filterQuery,
|
||||
groupBy
|
||||
);
|
||||
|
||||
const metric = {
|
||||
field: expression.metric,
|
||||
aggregation: expression.aggType as MetricsExplorerAggregation,
|
||||
color: MetricsExplorerColor.color0,
|
||||
};
|
||||
const isDarkMode = context.uiSettings?.get('theme:darkMode') || false;
|
||||
const dateFormatter = useMemo(() => {
|
||||
const firstSeries = data ? first(data.series) : null;
|
||||
return firstSeries && firstSeries.rows.length > 0
|
||||
? niceTimeFormatter([first(firstSeries.rows).timestamp, last(firstSeries.rows).timestamp])
|
||||
: (value: number) => `${value}`;
|
||||
}, [data]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const thresholds = expression.threshold.slice().sort();
|
||||
|
||||
// Creating a custom series where the ID is changed to 0
|
||||
// so that we can get a proper domian
|
||||
const firstSeries = first(data.series);
|
||||
if (!firstSeries) {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<EuiText color="subdued">Oops, no chart data available</EuiText>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const series = {
|
||||
...firstSeries,
|
||||
rows: firstSeries.rows.map(row => {
|
||||
const newRow: MetricsExplorerRow = {
|
||||
timestamp: row.timestamp,
|
||||
metric_0: row.metric_0 || null,
|
||||
};
|
||||
thresholds.forEach((thresholdValue, index) => {
|
||||
newRow[`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),
|
||||
};
|
||||
|
||||
if (domain.min === first(expression.threshold)) {
|
||||
domain.min = domain.min * 0.9;
|
||||
}
|
||||
|
||||
const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator);
|
||||
const opacity = 0.3;
|
||||
const timeLabel = TIME_LABELS[expression.timeUnit];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartContainer>
|
||||
<Chart>
|
||||
<MetricExplorerSeriesChart
|
||||
type={MetricsExplorerChartType.area}
|
||||
metric={metric}
|
||||
id="0"
|
||||
series={series}
|
||||
stack={false}
|
||||
/>
|
||||
{thresholds.length ? (
|
||||
<MetricExplorerSeriesChart
|
||||
type={isAbove ? MetricsExplorerChartType.line : MetricsExplorerChartType.area}
|
||||
metric={{
|
||||
...metric,
|
||||
color: MetricsExplorerColor.color1,
|
||||
label: i18n.translate('xpack.infra.metrics.alerts.thresholdLabel', {
|
||||
defaultMessage: 'Threshold',
|
||||
}),
|
||||
}}
|
||||
id={thresholds.map((t, i) => `threshold_${i}`)}
|
||||
series={series}
|
||||
stack={false}
|
||||
opacity={opacity}
|
||||
/>
|
||||
) : null}
|
||||
{thresholds.length && expression.comparator === Comparator.OUTSIDE_RANGE ? (
|
||||
<>
|
||||
<MetricExplorerSeriesChart
|
||||
type={MetricsExplorerChartType.line}
|
||||
metric={{
|
||||
...metric,
|
||||
color: MetricsExplorerColor.color1,
|
||||
label: i18n.translate('xpack.infra.metrics.alerts.thresholdLabel', {
|
||||
defaultMessage: 'Threshold',
|
||||
}),
|
||||
}}
|
||||
id={thresholds.map((t, i) => `threshold_${i}`)}
|
||||
series={series}
|
||||
stack={false}
|
||||
opacity={opacity}
|
||||
/>
|
||||
<RectAnnotation
|
||||
id="lower-threshold"
|
||||
style={{
|
||||
fill: colorTransformer(MetricsExplorerColor.color1),
|
||||
opacity,
|
||||
}}
|
||||
dataValues={[
|
||||
{
|
||||
coordinates: {
|
||||
x0: firstTimestamp,
|
||||
x1: lastTimestamp,
|
||||
y0: domain.min,
|
||||
y1: first(expression.threshold),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<RectAnnotation
|
||||
id="upper-threshold"
|
||||
style={{
|
||||
fill: colorTransformer(MetricsExplorerColor.color1),
|
||||
opacity,
|
||||
}}
|
||||
dataValues={[
|
||||
{
|
||||
coordinates: {
|
||||
x0: firstTimestamp,
|
||||
x1: lastTimestamp,
|
||||
y0: last(expression.threshold),
|
||||
y1: domain.max,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{isAbove ? (
|
||||
<RectAnnotation
|
||||
id="upper-threshold"
|
||||
style={{
|
||||
fill: colorTransformer(MetricsExplorerColor.color1),
|
||||
opacity,
|
||||
}}
|
||||
dataValues={[
|
||||
{
|
||||
coordinates: {
|
||||
x0: firstTimestamp,
|
||||
x1: lastTimestamp,
|
||||
y0: first(expression.threshold),
|
||||
y1: domain.max,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
<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 20 {timeLabel} of data for {id}"
|
||||
values={{ id: series.id, timeLabel }}
|
||||
/>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.infra.metrics.alerts.dataTimeRangeLabel"
|
||||
defaultMessage="Last 20 {timeLabel}"
|
||||
values={{ timeLabel }}
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer } from '@elastic/eui';
|
||||
import { IFieldType } from 'src/plugins/data/public';
|
||||
import {
|
||||
WhenExpression,
|
||||
OfExpression,
|
||||
ThresholdExpression,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../../triggers_actions_ui/public/common';
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { MetricExpression, AGGREGATION_TYPES } from '../types';
|
||||
import {
|
||||
Comparator,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants';
|
||||
|
||||
const customComparators = {
|
||||
...builtInComparators,
|
||||
[Comparator.OUTSIDE_RANGE]: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', {
|
||||
defaultMessage: 'Is not between',
|
||||
}),
|
||||
value: Comparator.OUTSIDE_RANGE,
|
||||
requiredValues: 2,
|
||||
},
|
||||
};
|
||||
|
||||
interface ExpressionRowProps {
|
||||
fields: IFieldType[];
|
||||
expressionId: number;
|
||||
expression: MetricExpression;
|
||||
errors: IErrorObject;
|
||||
canDelete: boolean;
|
||||
addExpression(): void;
|
||||
remove(id: number): void;
|
||||
setAlertParams(id: number, params: MetricExpression): void;
|
||||
}
|
||||
|
||||
const StyledExpressionRow = euiStyled(EuiFlexGroup)`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -4px;
|
||||
`;
|
||||
|
||||
const StyledExpression = euiStyled.div`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
export const ExpressionRow: React.FC<ExpressionRowProps> = props => {
|
||||
const [isExpanded, setRowState] = useState(true);
|
||||
const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]);
|
||||
const {
|
||||
children,
|
||||
setAlertParams,
|
||||
expression,
|
||||
errors,
|
||||
expressionId,
|
||||
remove,
|
||||
fields,
|
||||
canDelete,
|
||||
} = props;
|
||||
const {
|
||||
aggType = AGGREGATION_TYPES.MAX,
|
||||
metric,
|
||||
comparator = Comparator.GT,
|
||||
threshold = [],
|
||||
} = expression;
|
||||
|
||||
const updateAggType = useCallback(
|
||||
(at: string) => {
|
||||
setAlertParams(expressionId, {
|
||||
...expression,
|
||||
aggType: at as MetricExpression['aggType'],
|
||||
metric: at === 'count' ? undefined : expression.metric,
|
||||
});
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateMetric = useCallback(
|
||||
(m?: MetricExpression['metric']) => {
|
||||
setAlertParams(expressionId, { ...expression, metric: m });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateComparator = useCallback(
|
||||
(c?: string) => {
|
||||
setAlertParams(expressionId, { ...expression, comparator: c as Comparator });
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
const updateThreshold = useCallback(
|
||||
t => {
|
||||
if (t.join() !== expression.threshold.join()) {
|
||||
setAlertParams(expressionId, { ...expression, threshold: t });
|
||||
}
|
||||
},
|
||||
[expressionId, expression, setAlertParams]
|
||||
);
|
||||
|
||||
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>
|
||||
<WhenExpression
|
||||
customAggTypesOptions={aggregationType}
|
||||
aggType={aggType}
|
||||
onChangeSelectedAggType={updateAggType}
|
||||
/>
|
||||
</StyledExpression>
|
||||
{aggType !== 'count' && (
|
||||
<StyledExpression>
|
||||
<OfExpression
|
||||
customAggTypesOptions={aggregationType}
|
||||
aggField={metric}
|
||||
fields={fields.map(f => ({
|
||||
normalizedType: f.type,
|
||||
name: f.name,
|
||||
}))}
|
||||
aggType={aggType}
|
||||
errors={errors}
|
||||
onChangeSelectedAggField={updateMetric}
|
||||
/>
|
||||
</StyledExpression>
|
||||
)}
|
||||
<StyledExpression>
|
||||
<ThresholdExpression
|
||||
thresholdComparator={comparator || Comparator.GT}
|
||||
threshold={threshold}
|
||||
customComparators={customComparators}
|
||||
onChangeSelectedThresholdComparator={updateComparator}
|
||||
onChangeSelectedThreshold={updateThreshold}
|
||||
errors={errors}
|
||||
/>
|
||||
</StyledExpression>
|
||||
</StyledExpressionRow>
|
||||
</EuiFlexItem>
|
||||
{canDelete && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', {
|
||||
defaultMessage: 'Remove condition',
|
||||
})}
|
||||
color={'danger'}
|
||||
iconType={'trash'}
|
||||
onClick={() => remove(expressionId)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
{isExpanded ? <div style={{ padding: '0 0 0 28px' }}>{children}</div> : null}
|
||||
<EuiSpacer size={'s'} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const aggregationType: { [key: string]: any } = {
|
||||
avg: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', {
|
||||
defaultMessage: 'Average',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number'],
|
||||
value: AGGREGATION_TYPES.AVERAGE,
|
||||
},
|
||||
max: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', {
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number', 'date'],
|
||||
value: AGGREGATION_TYPES.MAX,
|
||||
},
|
||||
min: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', {
|
||||
defaultMessage: 'Min',
|
||||
}),
|
||||
fieldRequired: true,
|
||||
validNormalizedTypes: ['number', 'date'],
|
||||
value: AGGREGATION_TYPES.MIN,
|
||||
},
|
||||
cardinality: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', {
|
||||
defaultMessage: 'Cardinality',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: AGGREGATION_TYPES.CARDINALITY,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
rate: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', {
|
||||
defaultMessage: 'Rate',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: AGGREGATION_TYPES.RATE,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
count: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', {
|
||||
defaultMessage: 'Document count',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: AGGREGATION_TYPES.COUNT,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
sum: {
|
||||
text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.sum', {
|
||||
defaultMessage: 'Sum',
|
||||
}),
|
||||
fieldRequired: false,
|
||||
value: AGGREGATION_TYPES.SUM,
|
||||
validNormalizedTypes: ['number'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
import { useMemo } from 'react';
|
||||
import { InfraSource } from '../../../../common/http_api/source_api';
|
||||
import { AlertContextMeta, MetricExpression } from '../types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
||||
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data';
|
||||
|
||||
export const useMetricsExplorerChartData = (
|
||||
expression: MetricExpression,
|
||||
context: AlertsContextValue<AlertContextMeta>,
|
||||
derivedIndexPattern: IIndexPattern,
|
||||
source: InfraSource | null,
|
||||
filterQuery?: string,
|
||||
groupBy?: string
|
||||
) => {
|
||||
const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' };
|
||||
const options: MetricsExplorerOptions = useMemo(
|
||||
() => ({
|
||||
limit: 1,
|
||||
forceInterval: true,
|
||||
groupBy,
|
||||
filterQuery,
|
||||
metrics: [
|
||||
{
|
||||
field: expression.metric,
|
||||
aggregation: expression.aggType,
|
||||
},
|
||||
],
|
||||
aggregation: expression.aggType || 'avg',
|
||||
}),
|
||||
[expression.aggType, expression.metric, filterQuery, groupBy]
|
||||
);
|
||||
const timerange = useMemo(
|
||||
() => ({
|
||||
interval: `>=${timeSize || 1}${timeUnit}`,
|
||||
from: `now-${(timeSize || 1) * 20}${timeUnit}`,
|
||||
to: 'now',
|
||||
}),
|
||||
[timeSize, timeUnit]
|
||||
);
|
||||
|
||||
return useMetricsExplorerData(
|
||||
options,
|
||||
source?.configuration,
|
||||
derivedIndexPattern,
|
||||
timerange,
|
||||
null,
|
||||
null,
|
||||
context.http.fetch
|
||||
);
|
||||
};
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { Expressions } from './expression';
|
||||
import { validateMetricThreshold } from './validation';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
|
||||
import { Expressions } from './components/expression';
|
||||
import { validateMetricThreshold } from './components/validation';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
|
||||
import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types';
|
||||
|
||||
export function getAlertType(): AlertTypeModel {
|
||||
export function createMetricThresholdAlertType(): AlertTypeModel {
|
||||
return {
|
||||
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', {
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { first } from 'lodash';
|
||||
import { MetricsExplorerResponse } from '../../../../common/http_api/metrics_explorer';
|
||||
import { MetricThresholdAlertParams, ExpressionChartSeries } from '../types';
|
||||
|
||||
export const transformMetricsExplorerData = (
|
||||
params: MetricThresholdAlertParams,
|
||||
data: MetricsExplorerResponse | null
|
||||
) => {
|
||||
const { criteria } = params;
|
||||
if (criteria && data) {
|
||||
const firstSeries = first(data.series);
|
||||
const series = firstSeries.rows.reduce((acc, row) => {
|
||||
const { timestamp } = row;
|
||||
criteria.forEach((item, index) => {
|
||||
if (!acc[index]) {
|
||||
acc[index] = [];
|
||||
}
|
||||
const value = (row[`metric_${index}`] as number) || 0;
|
||||
acc[index].push({ timestamp, value });
|
||||
});
|
||||
return acc;
|
||||
}, [] as ExpressionChartSeries);
|
||||
return { id: firstSeries.id, series };
|
||||
}
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
MetricExpressionParams,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../server/lib/alerting/metric_threshold/types';
|
||||
import { MetricsExplorerOptions } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer';
|
||||
|
||||
export interface AlertContextMeta {
|
||||
currentOptions?: Partial<MetricsExplorerOptions>;
|
||||
series?: MetricsExplorerSeries;
|
||||
}
|
||||
|
||||
export type TimeUnit = 's' | 'm' | 'h' | 'd';
|
||||
export type MetricExpression = Omit<MetricExpressionParams, 'metric'> & {
|
||||
metric?: string;
|
||||
};
|
||||
|
||||
export enum AGGREGATION_TYPES {
|
||||
COUNT = 'count',
|
||||
AVERAGE = 'avg',
|
||||
SUM = 'sum',
|
||||
MIN = 'min',
|
||||
MAX = 'max',
|
||||
RATE = 'rate',
|
||||
CARDINALITY = 'cardinality',
|
||||
}
|
||||
|
||||
export interface MetricThresholdAlertParams {
|
||||
criteria?: MetricExpression[];
|
||||
groupBy?: string;
|
||||
filterQuery?: string;
|
||||
sourceId?: string;
|
||||
}
|
||||
|
||||
export interface ExpressionChartRow {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export type ExpressionChartSeries = ExpressionChartRow[][];
|
||||
|
||||
export interface ExpressionChartData {
|
||||
id: string;
|
||||
series: ExpressionChartSeries;
|
||||
}
|
|
@ -28,7 +28,7 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
|||
import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options';
|
||||
import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time';
|
||||
import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters';
|
||||
import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown';
|
||||
import { AlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown';
|
||||
|
||||
export const InfrastructurePage = ({ match }: RouteComponentProps) => {
|
||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { AlertFlyout } from '../../../../../components/alerting/metrics/alert_flyout';
|
||||
import { AlertFlyout } from '../../../../../alerting/metric_threshold/components/alert_flyout';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
|
||||
import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to';
|
||||
import { createUptimeLink } from '../../lib/create_uptime_link';
|
||||
|
|
|
@ -26,6 +26,9 @@ export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) =
|
|||
['avg']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.avg', {
|
||||
defaultMessage: 'Average',
|
||||
}),
|
||||
['sum']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.sum', {
|
||||
defaultMessage: 'Sum',
|
||||
}),
|
||||
['max']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.max', {
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import DateMath from '@elastic/datemath';
|
||||
import { Capabilities } from 'src/core/public';
|
||||
import { AlertFlyout } from '../../../../components/alerting/metrics/alert_flyout';
|
||||
import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout';
|
||||
import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer';
|
||||
import {
|
||||
MetricsExplorerOptions,
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer';
|
||||
import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options';
|
||||
|
||||
export const createMetricLabel = (metric: MetricsExplorerMetric) => {
|
||||
export const createMetricLabel = (metric: MetricsExplorerOptionsMetric) => {
|
||||
if (metric.label) {
|
||||
return metric.label;
|
||||
}
|
||||
return `${metric.aggregation}(${metric.field || ''})`;
|
||||
};
|
||||
|
|
|
@ -21,12 +21,15 @@ import {
|
|||
MetricsExplorerChartType,
|
||||
} from '../hooks/use_metrics_explorer_options';
|
||||
|
||||
type NumberOrString = string | number;
|
||||
|
||||
interface Props {
|
||||
metric: MetricsExplorerOptionsMetric;
|
||||
id: string | number;
|
||||
id: NumberOrString | NumberOrString[];
|
||||
series: MetricsExplorerSeries;
|
||||
type: MetricsExplorerChartType;
|
||||
stack: boolean;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export const MetricExplorerSeriesChart = (props: Props) => {
|
||||
|
@ -36,13 +39,17 @@ export const MetricExplorerSeriesChart = (props: Props) => {
|
|||
return <MetricsExplorerAreaChart {...props} />;
|
||||
};
|
||||
|
||||
export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => {
|
||||
export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => {
|
||||
const color =
|
||||
(metric.color && colorTransformer(metric.color)) ||
|
||||
colorTransformer(MetricsExplorerColor.color0);
|
||||
|
||||
const yAccessor = `metric_${id}`;
|
||||
const chartId = `series-${series.id}-${yAccessor}`;
|
||||
const yAccessors = Array.isArray(id)
|
||||
? id.map(i => `metric_${i}`).slice(id.length - 1, id.length)
|
||||
: [`metric_${id}`];
|
||||
const y0Accessors =
|
||||
Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined;
|
||||
const chartId = `series-${series.id}-${yAccessors.join('-')}`;
|
||||
|
||||
const seriesAreaStyle: RecursivePartial<AreaSeriesStyle> = {
|
||||
line: {
|
||||
|
@ -50,19 +57,21 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Pr
|
|||
visible: true,
|
||||
},
|
||||
area: {
|
||||
opacity: 0.5,
|
||||
opacity: opacity || 0.5,
|
||||
visible: type === MetricsExplorerChartType.area,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AreaSeries
|
||||
id={yAccessor}
|
||||
id={chartId}
|
||||
key={chartId}
|
||||
name={createMetricLabel(metric)}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="timestamp"
|
||||
yAccessors={[yAccessor]}
|
||||
yAccessors={yAccessors}
|
||||
y0Accessors={y0Accessors}
|
||||
data={series.rows}
|
||||
stackAccessors={stack ? ['timestamp'] : void 0}
|
||||
areaSeriesStyle={seriesAreaStyle}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import DateMath from '@elastic/datemath';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { HttpHandler } from 'target/types/core/public/http';
|
||||
import { IIndexPattern } from 'src/plugins/data/public';
|
||||
import { SourceQuery } from '../../../../../common/graphql/types';
|
||||
import {
|
||||
|
@ -24,13 +25,15 @@ function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOpt
|
|||
|
||||
export function useMetricsExplorerData(
|
||||
options: MetricsExplorerOptions,
|
||||
source: SourceQuery.Query['source']['configuration'],
|
||||
source: SourceQuery.Query['source']['configuration'] | undefined,
|
||||
derivedIndexPattern: IIndexPattern,
|
||||
timerange: MetricsExplorerTimeOptions,
|
||||
afterKey: string | null,
|
||||
signal: any
|
||||
signal: any,
|
||||
fetch?: HttpHandler
|
||||
) {
|
||||
const fetch = useKibana().services.http?.fetch;
|
||||
const kibana = useKibana();
|
||||
const fetchFn = fetch ? fetch : kibana.services.http?.fetch;
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<MetricsExplorerResponse | null>(null);
|
||||
|
@ -46,13 +49,17 @@ export function useMetricsExplorerData(
|
|||
if (!from || !to) {
|
||||
throw new Error('Unalble to parse timerange');
|
||||
}
|
||||
if (!fetch) {
|
||||
if (!fetchFn) {
|
||||
throw new Error('HTTP service is unavailable');
|
||||
}
|
||||
if (!source) {
|
||||
throw new Error('Source is unavailable');
|
||||
}
|
||||
const response = decodeOrThrow(metricsExplorerResponseRT)(
|
||||
await fetch('/api/infra/metrics_explorer', {
|
||||
await fetchFn('/api/infra/metrics_explorer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
forceInterval: options.forceInterval,
|
||||
metrics:
|
||||
options.aggregation === 'count'
|
||||
? [{ aggregation: 'count' }]
|
||||
|
|
|
@ -40,6 +40,7 @@ export interface MetricsExplorerOptions {
|
|||
groupBy?: string;
|
||||
filterQuery?: string;
|
||||
aggregation: MetricsExplorerAggregation;
|
||||
forceInterval?: boolean;
|
||||
}
|
||||
|
||||
export interface MetricsExplorerTimeOptions {
|
||||
|
|
|
@ -21,8 +21,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p
|
|||
import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public';
|
||||
|
||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
|
||||
import { getAlertType as getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type';
|
||||
import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type';
|
||||
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
||||
|
||||
export type ClientSetup = void;
|
||||
export type ClientStart = void;
|
||||
|
@ -53,8 +53,8 @@ export class Plugin
|
|||
setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) {
|
||||
registerFeatures(pluginsSetup.home);
|
||||
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getMetricsAlertType());
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType());
|
||||
pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType());
|
||||
|
||||
core.application.register({
|
||||
id: 'logs',
|
||||
|
|
|
@ -72,7 +72,9 @@ export const populateSeriesWithTSVBData = (
|
|||
);
|
||||
|
||||
if (calculatedInterval) {
|
||||
model.interval = `>=${calculatedInterval}s`;
|
||||
model.interval = options.forceInterval
|
||||
? options.timerange.interval
|
||||
: `>=${calculatedInterval}s`;
|
||||
}
|
||||
|
||||
// Get TSVB results using the model, timerange and filters
|
||||
|
|
Loading…
Reference in a new issue