diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json
index bb40d65d311e8..b8796ad7a358e 100644
--- a/x-pack/plugins/infra/kibana.json
+++ b/x-pack/plugins/infra/kibana.json
@@ -11,7 +11,8 @@
"data",
"dataEnhanced",
"metrics",
- "alerting"
+ "alerting",
+ "triggers_actions_ui"
],
"server": true,
"ui": true,
diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx
index a797e4c9d4ba7..a986ee6ece352 100644
--- a/x-pack/plugins/infra/public/apps/start_app.tsx
+++ b/x-pack/plugins/infra/public/apps/start_app.tsx
@@ -15,7 +15,8 @@ import { CoreStart, AppMountParameters } from 'kibana/public';
// TODO use theme provided from parentApp when kibana supports it
import { EuiErrorBoundary } from '@elastic/eui';
-import { EuiThemeProvider } from '../../../observability/public';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { EuiThemeProvider } from '../../../observability/public/typings/eui_styled_components';
import { InfraFrontendLibs } from '../lib/lib';
import { createStore } from '../store';
import { ApolloClientContext } from '../utils/apollo_context';
@@ -26,6 +27,8 @@ import {
KibanaContextProvider,
} from '../../../../../src/plugins/kibana_react/public';
import { AppRouter } from '../routers';
+import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
+import { TriggersActionsProvider } from '../utils/triggers_actions_context';
import '../index.scss';
export const CONTAINER_CLASSNAME = 'infra-container-element';
@@ -35,7 +38,8 @@ export async function startApp(
core: CoreStart,
plugins: object,
params: AppMountParameters,
- Router: AppRouter
+ Router: AppRouter,
+ triggersActionsUI: TriggersAndActionsUIPublicPluginSetup
) {
const { element, appBasePath } = params;
const history = createBrowserHistory({ basename: appBasePath });
@@ -51,19 +55,21 @@ export async function startApp(
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx
new file mode 100644
index 0000000000000..0a464d91fbe06
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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, { useState, useCallback, useMemo } from 'react';
+import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { AlertFlyout } from './alert_flyout';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+
+export const AlertDropdown = () => {
+ const [popoverOpen, setPopoverOpen] = useState(false);
+ const [flyoutVisible, setFlyoutVisible] = useState(false);
+ const kibana = useKibana();
+
+ const closePopover = useCallback(() => {
+ setPopoverOpen(false);
+ }, [setPopoverOpen]);
+
+ const openPopover = useCallback(() => {
+ setPopoverOpen(true);
+ }, [setPopoverOpen]);
+
+ const menuItems = useMemo(() => {
+ return [
+ setFlyoutVisible(true)}>
+
+ ,
+
+
+ ,
+ ];
+ }, [kibana.services]);
+
+ return (
+ <>
+
+
+
+ }
+ isOpen={popoverOpen}
+ closePopover={closePopover}
+ >
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx
new file mode 100644
index 0000000000000..a00d63af8aac2
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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, { useContext } from 'react';
+import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
+import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
+import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
+import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options';
+import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
+
+interface Props {
+ visible?: boolean;
+ options?: Partial;
+ series?: MetricsExplorerSeries;
+ setVisible: React.Dispatch>;
+}
+
+export const AlertFlyout = (props: Props) => {
+ const { triggersActionsUI } = useContext(TriggerActionsContext);
+ const { services } = useKibana();
+
+ return (
+ <>
+ {triggersActionsUI && (
+
+
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx
new file mode 100644
index 0000000000000..ea8dd1484a670
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx
@@ -0,0 +1,473 @@
+/*
+ * 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, useMemo, useEffect, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiSpacer,
+ EuiText,
+ EuiFormRow,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { IFieldType } from 'src/plugins/data/public';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+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 { 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 { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options';
+import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar';
+import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer';
+import { useSource } from '../../../containers/source';
+import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by';
+
+export interface MetricExpression {
+ aggType?: string;
+ metric?: string;
+ comparator?: Comparator;
+ threshold?: number[];
+ timeSize?: number;
+ timeUnit?: TimeUnit;
+ indexPattern?: string;
+}
+
+interface AlertContextMeta {
+ currentOptions?: Partial;
+ series?: MetricsExplorerSeries;
+}
+
+interface Props {
+ errors: IErrorObject[];
+ alertParams: {
+ criteria: MetricExpression[];
+ groupBy?: string;
+ filterQuery?: string;
+ };
+ alertsContext: AlertsContextValue;
+ setAlertParams(key: string, value: any): void;
+ setAlertProperty(key: string, value: any): void;
+}
+
+type Comparator = '>' | '>=' | 'between' | '<' | '<=';
+type TimeUnit = 's' | 'm' | 'h' | 'd';
+
+export const Expressions: React.FC = props => {
+ const { setAlertParams, alertParams, errors, alertsContext } = props;
+ const { source, createDerivedIndexPattern } = useSource({ sourceId: 'default' });
+ const [timeSize, setTimeSize] = useState(1);
+ const [timeUnit, setTimeUnit] = useState('s');
+
+ const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
+ createDerivedIndexPattern,
+ ]);
+
+ const options = useMemo(() => {
+ if (alertsContext.metadata?.currentOptions?.metrics) {
+ return alertsContext.metadata.currentOptions as MetricsExplorerOptions;
+ } else {
+ return {
+ metrics: [],
+ aggregation: 'avg',
+ };
+ }
+ }, [alertsContext.metadata]);
+
+ const defaultExpression = useMemo(
+ () => ({
+ aggType: AGGREGATION_TYPES.MAX,
+ comparator: '>',
+ threshold: [],
+ timeSize: 1,
+ timeUnit: 's',
+ indexPattern: source?.configuration.metricAlias,
+ }),
+ [source]
+ );
+
+ const updateParams = useCallback(
+ (id, e: MetricExpression) => {
+ const exp = alertParams.criteria ? alertParams.criteria.slice() : [];
+ exp[id] = { ...exp[id], ...e };
+ setAlertParams('criteria', exp);
+ },
+ [setAlertParams, alertParams.criteria]
+ );
+
+ const addExpression = useCallback(() => {
+ const exp = alertParams.criteria.slice();
+ exp.push(defaultExpression);
+ setAlertParams('criteria', exp);
+ }, [setAlertParams, alertParams.criteria, defaultExpression]);
+
+ const removeExpression = useCallback(
+ (id: number) => {
+ const exp = alertParams.criteria.slice();
+ if (exp.length > 1) {
+ exp.splice(id, 1);
+ setAlertParams('criteria', exp);
+ }
+ },
+ [setAlertParams, alertParams.criteria]
+ );
+
+ const onFilterQuerySubmit = useCallback(
+ (filter: any) => {
+ setAlertParams('filterQuery', filter);
+ },
+ [setAlertParams]
+ );
+
+ const onGroupByChange = useCallback(
+ (group: string | null) => {
+ setAlertParams('groupBy', group || undefined);
+ },
+ [setAlertParams]
+ );
+
+ const emptyError = useMemo(() => {
+ return {
+ aggField: [],
+ timeSizeUnit: [],
+ timeWindowSize: [],
+ };
+ }, []);
+
+ const updateTimeSize = useCallback(
+ (ts: number | undefined) => {
+ const criteria = alertParams.criteria.map(c => ({
+ ...c,
+ timeSize: ts,
+ }));
+ setTimeSize(ts || undefined);
+ setAlertParams('criteria', criteria);
+ },
+ [alertParams.criteria, setAlertParams]
+ );
+
+ const updateTimeUnit = useCallback(
+ (tu: string) => {
+ const criteria = alertParams.criteria.map(c => ({
+ ...c,
+ timeUnit: tu,
+ }));
+ setTimeUnit(tu as TimeUnit);
+ setAlertParams('criteria', criteria);
+ },
+ [alertParams.criteria, setAlertParams]
+ );
+
+ useEffect(() => {
+ const md = alertsContext.metadata;
+ if (md) {
+ if (md.currentOptions?.metrics) {
+ setAlertParams(
+ 'criteria',
+ md.currentOptions.metrics.map(metric => ({
+ metric: metric.field,
+ comparator: '>',
+ threshold: [],
+ timeSize,
+ timeUnit,
+ indexPattern: source?.configuration.metricAlias,
+ aggType: metric.aggregation,
+ }))
+ );
+ } else {
+ setAlertParams('criteria', [defaultExpression]);
+ }
+
+ if (md.currentOptions) {
+ if (md.currentOptions.filterQuery) {
+ setAlertParams('filterQuery', md.currentOptions.filterQuery);
+ } else if (md.currentOptions.groupBy && md.series) {
+ const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`;
+ setAlertParams('filterQuery', filter);
+ }
+
+ setAlertParams('groupBy', md.currentOptions.groupBy);
+ }
+ }
+ }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {alertParams.criteria &&
+ alertParams.criteria.map((e, idx) => {
+ return (
+ 1}
+ fields={derivedIndexPattern.fields}
+ remove={removeExpression}
+ addExpression={addExpression}
+ key={idx} // idx's don't usually make good key's but here the index has semantic meaning
+ expressionId={idx}
+ setAlertParams={updateParams}
+ errors={errors[idx] || emptyError}
+ expression={e || {}}
+ />
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {alertsContext.metadata && (
+
+
+
+ )}
+ >
+ );
+};
+
+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 -${props => props.theme.eui.euiSizeXS};
+`;
+
+const StyledExpression = euiStyled.div`
+ padding: 0 ${props => props.theme.eui.euiSizeXS};
+`;
+
+export const ExpressionRow: React.FC = props => {
+ const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props;
+ const { aggType = AGGREGATION_TYPES.MAX, metric, comparator = '>', threshold = [] } = expression;
+
+ const updateAggType = useCallback(
+ (at: string) => {
+ setAlertParams(expressionId, { ...expression, aggType: at });
+ },
+ [expressionId, expression, setAlertParams]
+ );
+
+ const updateMetric = useCallback(
+ (m?: string) => {
+ 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 => {
+ setAlertParams(expressionId, { ...expression, threshold: t });
+ },
+ [expressionId, expression, setAlertParams]
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+ {aggType !== 'count' && (
+
+ ({
+ normalizedType: f.type,
+ name: f.name,
+ }))}
+ aggType={aggType}
+ errors={errors}
+ onChangeSelectedAggField={updateMetric}
+ />
+
+ )}
+
+ '}
+ threshold={threshold}
+ onChangeSelectedThresholdComparator={updateComparator}
+ onChangeSelectedThreshold={updateThreshold}
+ errors={errors}
+ />
+
+
+
+ {canDelete && (
+
+ remove(expressionId)}
+ />
+
+ )}
+
+
+ >
+ );
+};
+
+enum AGGREGATION_TYPES {
+ COUNT = 'count',
+ AVERAGE = 'avg',
+ SUM = 'sum',
+ MIN = 'min',
+ MAX = 'max',
+ RATE = 'rate',
+ CARDINALITY = 'cardinality',
+}
+
+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'],
+ },
+};
diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts
new file mode 100644
index 0000000000000..d3b5aaa7c8796
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { 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';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types';
+
+export function getAlertType(): AlertTypeModel {
+ return {
+ id: METRIC_THRESHOLD_ALERT_TYPE_ID,
+ name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', {
+ defaultMessage: 'Alert Trigger',
+ }),
+ iconClass: 'bell',
+ alertParamsExpression: Expressions,
+ validate: validateMetricThreshold,
+ };
+}
diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx
new file mode 100644
index 0000000000000..0f5b07f8c0e13
--- /dev/null
+++ b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+
+import { MetricExpression } from './expression';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
+
+export function validateMetricThreshold({
+ criteria,
+}: {
+ criteria: MetricExpression[];
+}): ValidationResult {
+ const validationResult = { errors: {} };
+ const errors: {
+ [id: string]: {
+ aggField: string[];
+ timeSizeUnit: string[];
+ timeWindowSize: string[];
+ threshold0: string[];
+ threshold1: string[];
+ };
+ } = {};
+ validationResult.errors = errors;
+
+ if (!criteria || !criteria.length) {
+ return validationResult;
+ }
+
+ criteria.forEach((c, idx) => {
+ // Create an id for each criteria, so we can map errors to specific criteria.
+ const id = idx.toString();
+
+ errors[id] = errors[id] || {
+ aggField: [],
+ timeSizeUnit: [],
+ timeWindowSize: [],
+ threshold0: [],
+ threshold1: [],
+ };
+ if (!c.aggType) {
+ errors[id].aggField.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', {
+ defaultMessage: 'Aggreation is required.',
+ })
+ );
+ }
+
+ if (!c.threshold || !c.threshold.length) {
+ errors[id].threshold0.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
+ defaultMessage: 'Threshold is required.',
+ })
+ );
+ }
+
+ if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) {
+ errors[id].threshold1.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', {
+ defaultMessage: 'Threshold is required.',
+ })
+ );
+ }
+
+ if (!c.timeSize) {
+ errors[id].timeWindowSize.push(
+ i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', {
+ defaultMessage: 'Time size is Required.',
+ })
+ );
+ }
+ });
+
+ return validationResult;
+}
diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx
index a23a2739a8e23..8ffef269a42ea 100644
--- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx
+++ b/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx
@@ -143,7 +143,7 @@ describe('MetricsExplorerChartContextMenu', () => {
uiCapabilities: customUICapabilities,
chartOptions,
});
- expect(component.find('button').length).toBe(0);
+ expect(component.find('button').length).toBe(1);
});
});
diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx
index c50550f1de56f..75a04cbe9799e 100644
--- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx
+++ b/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx
@@ -24,6 +24,7 @@ import { createTSVBLink } from './helpers/create_tsvb_link';
import { getNodeDetailUrl } from '../../pages/link_to/redirect_to_node_detail';
import { SourceConfiguration } from '../../utils/source_configuration';
import { InventoryItemType } from '../../../common/inventory_models/types';
+import { AlertFlyout } from '../alerting/metrics/alert_flyout';
import { useLinkProps } from '../../hooks/use_link_props';
export interface Props {
@@ -81,6 +82,7 @@ export const MetricsExplorerChartContextMenu: React.FC = ({
chartOptions,
}: Props) => {
const [isPopoverOpen, setPopoverState] = useState(false);
+ const [flyoutVisible, setFlyoutVisible] = useState(false);
const supportFiltering = options.groupBy != null && onFilter != null;
const handleFilter = useCallback(() => {
// onFilter needs check for Typescript even though it's
@@ -141,7 +143,20 @@ export const MetricsExplorerChartContextMenu: React.FC = ({
]
: [];
- const itemPanels = [...filterByItem, ...openInVisualize, ...viewNodeDetail];
+ const itemPanels = [
+ ...filterByItem,
+ ...openInVisualize,
+ ...viewNodeDetail,
+ {
+ name: i18n.translate('xpack.infra.metricsExplorer.alerts.createAlertButton', {
+ defaultMessage: 'Create alert',
+ }),
+ icon: 'bell',
+ onClick() {
+ setFlyoutVisible(true);
+ },
+ },
+ ];
// If there are no itemPanels then there is no reason to show the actions button.
if (itemPanels.length === 0) return null;
@@ -174,15 +189,24 @@ export const MetricsExplorerChartContextMenu: React.FC = ({
{actionLabel}
);
+
return (
-
-
-
+ <>
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
index 0e18deedd404c..dcc160d05b6ad 100644
--- a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
+++ b/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx
@@ -16,6 +16,7 @@ interface Props {
derivedIndexPattern: IIndexPattern;
onSubmit: (query: string) => void;
value?: string | null;
+ placeholder?: string;
}
function validateQuery(query: string) {
@@ -27,7 +28,12 @@ function validateQuery(query: string) {
return true;
}
-export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, value }: Props) => {
+export const MetricsExplorerKueryBar = ({
+ derivedIndexPattern,
+ onSubmit,
+ value,
+ placeholder,
+}: Props) => {
const [draftQuery, setDraftQuery] = useState(value || '');
const [isValid, setValidation] = useState(true);
@@ -48,9 +54,12 @@ export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, value }
fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)),
};
- const placeholder = i18n.translate('xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', {
- defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
- });
+ const defaultPlaceholder = i18n.translate(
+ 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder',
+ {
+ defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)',
+ }
+ );
return (
@@ -62,7 +71,7 @@ export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, value }
loadSuggestions={loadSuggestions}
onChange={handleChange}
onSubmit={onSubmit}
- placeholder={placeholder}
+ placeholder={placeholder || defaultPlaceholder}
suggestions={suggestions}
value={draftQuery}
/>
diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx
index 9e96819a36cac..0fbb0b6acad17 100644
--- a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx
+++ b/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx
@@ -63,6 +63,7 @@ export const MetricsExplorerToolbar = ({
const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0;
const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges');
const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges);
+
return (
diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx
index cc6a94c8a41a2..5f05cebd8f616 100644
--- a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx
+++ b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx
@@ -8,7 +8,7 @@ import { EuiPopoverProps, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import React, { useMemo } from 'react';
+import React, { useMemo, useState } from 'react';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
import { createUptimeLink } from './lib/create_uptime_link';
@@ -25,6 +25,7 @@ import {
SectionLink,
} from '../../../../observability/public';
import { useLinkProps } from '../../hooks/use_link_props';
+import { AlertFlyout } from '../alerting/metrics/alert_flyout';
interface Props {
options: InfraWaffleMapOptions;
@@ -46,6 +47,7 @@ export const NodeContextMenu: React.FC = ({
nodeType,
popoverPosition,
}) => {
+ const [flyoutVisible, setFlyoutVisible] = useState(false);
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const uiCapabilities = useKibana().services.application?.capabilities;
@@ -144,41 +146,48 @@ export const NodeContextMenu: React.FC = ({
};
return (
-
-
-
-
-
-
- {inventoryId.label && (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+ {inventoryId.label && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx
index b4ff7aeff696c..730f67ab2bdca 100644
--- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx
+++ b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
+import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { DocumentTitle } from '../../components/document_title';
import { HelpCenterContent } from '../../components/help_center_content';
import { RoutedTabs } from '../../components/navigation/routed_tabs';
@@ -24,9 +25,11 @@ import { MetricsSettingsPage } from './settings';
import { AppNavigation } from '../../components/navigation/app_navigation';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
+import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown';
export const InfrastructurePage = ({ match }: RouteComponentProps) => {
const uiCapabilities = useKibana().services.application?.capabilities;
+
return (
@@ -59,31 +62,38 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
defaultMessage: 'Metrics',
})}
>
-
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index d576331662a08..15796f35856bd 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -29,6 +29,8 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/pl
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public';
import { LogsRouter, MetricsRouter } from './routers';
+import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public';
+import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type';
export type ClientSetup = void;
export type ClientStart = void;
@@ -38,6 +40,7 @@ export interface ClientPluginsSetup {
data: DataPublicPluginSetup;
usageCollection: UsageCollectionSetup;
dataEnhanced: DataEnhancedSetup;
+ triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
}
export interface ClientPluginsStart {
@@ -58,6 +61,8 @@ export class Plugin
setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) {
registerFeatures(pluginsSetup.home);
+ pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getAlertType());
+
core.application.register({
id: 'logs',
title: i18n.translate('xpack.infra.logs.pluginTitle', {
@@ -76,7 +81,8 @@ export class Plugin
coreStart,
plugins,
params,
- LogsRouter
+ LogsRouter,
+ pluginsSetup.triggers_actions_ui
);
},
});
@@ -99,7 +105,8 @@ export class Plugin
coreStart,
plugins,
params,
- MetricsRouter
+ MetricsRouter,
+ pluginsSetup.triggers_actions_ui
);
},
});
diff --git a/x-pack/plugins/infra/public/utils/triggers_actions_context.tsx b/x-pack/plugins/infra/public/utils/triggers_actions_context.tsx
new file mode 100644
index 0000000000000..4ca4aedb4a08b
--- /dev/null
+++ b/x-pack/plugins/infra/public/utils/triggers_actions_context.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 * as React from 'react';
+import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
+
+interface ContextProps {
+ triggersActionsUI: TriggersAndActionsUIPublicPluginSetup | null;
+}
+
+export const TriggerActionsContext = React.createContext({
+ triggersActionsUI: null,
+});
+
+interface Props {
+ triggersActionsUI: TriggersAndActionsUIPublicPluginSetup;
+}
+
+export const TriggersActionsProvider: React.FC = props => {
+ return (
+
+ {props.children}
+
+ );
+};