{
+ deleteAnomalyAlert();
+ setIsConfirmAlertDeleteOpen(false);
+ }}
+ onCancel={() => {
+ setIsConfirmAlertDeleteOpen(false);
+ }}
+ />
+ )}
+ >
);
};
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx
index e4bb3d0ac9e17..84634f328621f 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx
@@ -13,59 +13,61 @@ import {
isMLJobCreatingSelector,
selectDynamicSettings,
} from '../../../state/selectors';
-import { createMLJobAction, getExistingMLJobAction } from '../../../state/actions';
+import {
+ createMLJobAction,
+ getExistingMLJobAction,
+ setAlertFlyoutType,
+ setAlertFlyoutVisible,
+} from '../../../state/actions';
import { MLJobLink } from './ml_job_link';
import * as labels from './translations';
-import {
- useKibana,
- KibanaReactNotifications,
-} from '../../../../../../../src/plugins/kibana_react/public';
import { MLFlyoutView } from './ml_flyout';
-import { ML_JOB_ID } from '../../../../common/constants';
+import { CLIENT_ALERT_TYPES, ML_JOB_ID } from '../../../../common/constants';
import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts';
import { useGetUrlParams } from '../../../hooks';
import { getDynamicSettings } from '../../../state/actions/dynamic_settings';
import { useMonitorId } from '../../../hooks';
+import { kibanaService } from '../../../state/kibana_service';
+import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
interface Props {
onClose: () => void;
}
const showMLJobNotification = (
- notifications: KibanaReactNotifications,
monitorId: string,
basePath: string,
range: { to: string; from: string },
success: boolean,
- message = ''
+ error?: Error
) => {
if (success) {
- notifications.toasts.success({
- title: (
- {labels.JOB_CREATED_SUCCESS_TITLE}
- ),
- body: (
-
- {labels.JOB_CREATED_SUCCESS_MESSAGE}
-
- {labels.VIEW_JOB}
-
-
- ),
- toastLifeTimeMs: 10000,
- });
+ kibanaService.toasts.addSuccess(
+ {
+ title: toMountPoint(
+ {labels.JOB_CREATED_SUCCESS_TITLE}
+ ),
+ text: toMountPoint(
+
+ {labels.JOB_CREATED_SUCCESS_MESSAGE}
+
+ {labels.VIEW_JOB}
+
+
+ ),
+ },
+ { toastLifeTimeMs: 10000 }
+ );
} else {
- notifications.toasts.danger({
- title: {labels.JOB_CREATION_FAILED}
,
- body: message ?? {labels.JOB_CREATION_FAILED_MESSAGE}
,
+ kibanaService.toasts.addError(error!, {
+ title: labels.JOB_CREATION_FAILED,
+ toastMessage: labels.JOB_CREATION_FAILED_MESSAGE,
toastLifeTimeMs: 10000,
});
}
};
export const MachineLearningFlyout: React.FC = ({ onClose }) => {
- const { notifications } = useKibana();
-
const dispatch = useDispatch();
const { data: hasMLJob, error } = useSelector(hasNewMLJobSelector);
const isMLJobCreating = useSelector(isMLJobCreatingSelector);
@@ -100,7 +102,6 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
if (isCreatingJob && !isMLJobCreating) {
if (hasMLJob) {
showMLJobNotification(
- notifications,
monitorId as string,
basePath,
{ to: dateRangeEnd, from: dateRangeStart },
@@ -112,31 +113,22 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
loadMLJob(ML_JOB_ID);
refreshApp();
+ dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY));
+ dispatch(setAlertFlyoutVisible(true));
} else {
showMLJobNotification(
- notifications,
monitorId as string,
basePath,
{ to: dateRangeEnd, from: dateRangeStart },
false,
- error?.message || error?.body?.message
+ error as Error
);
}
setIsCreatingJob(false);
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- hasMLJob,
- notifications,
- onClose,
- isCreatingJob,
- error,
- isMLJobCreating,
- monitorId,
- dispatch,
- basePath,
- ]);
+ }, [hasMLJob, onClose, isCreatingJob, error, isMLJobCreating, monitorId, dispatch, basePath]);
useEffect(() => {
if (hasExistingMLJob && !isMLJobCreating && !hasMLJob && heartbeatIndices) {
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx
index 1de19dda3b88f..aa67c7ba1c2f9 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx
@@ -16,12 +16,12 @@ import {
import { deleteMLJobAction, getExistingMLJobAction, resetMLState } from '../../../state/actions';
import { ConfirmJobDeletion } from './confirm_delete';
import { UptimeRefreshContext } from '../../../contexts';
-import { getMLJobId } from '../../../state/api/ml_anomaly';
import * as labels from './translations';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ManageMLJobComponent } from './manage_ml_job';
import { JobStat } from '../../../../../../plugins/ml/public';
import { useMonitorId } from '../../../hooks';
+import { getMLJobId } from '../../../../common/lib';
export const MLIntegrationComponent = () => {
const [isMlFlyoutOpen, setIsMlFlyoutOpen] = useState(false);
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx
index 4b6f7e3ba061d..adc05695b4379 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx
@@ -8,7 +8,7 @@ import React from 'react';
import url from 'url';
import { EuiButtonEmpty } from '@elastic/eui';
import rison, { RisonValue } from 'rison-node';
-import { getMLJobId } from '../../../state/api/ml_anomaly';
+import { getMLJobId } from '../../../../common/lib';
interface Props {
monitorId: string;
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
index bcc3fca770652..90ebdf10a73f5 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx
@@ -89,6 +89,20 @@ export const DISABLE_ANOMALY_DETECTION = i18n.translate(
}
);
+export const ENABLE_ANOMALY_ALERT = i18n.translate(
+ 'xpack.uptime.ml.enableAnomalyDetectionPanel.enableAnomalyAlert',
+ {
+ defaultMessage: 'Enable anomaly alert',
+ }
+);
+
+export const DISABLE_ANOMALY_ALERT = i18n.translate(
+ 'xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert',
+ {
+ defaultMessage: 'Disable anomaly alert',
+ }
+);
+
export const MANAGE_ANOMALY_DETECTION = i18n.translate(
'xpack.uptime.ml.enableAnomalyDetectionPanel.manageAnomalyDetectionTitle',
{
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts
new file mode 100644
index 0000000000000..d204cdf10012a
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/use_anomaly_alert.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useContext, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { getExistingAlertAction } from '../../../state/actions/alerts';
+import { alertSelector, selectAlertFlyoutVisibility } from '../../../state/selectors';
+import { UptimeRefreshContext } from '../../../contexts';
+import { useMonitorId } from '../../../hooks';
+
+export const useAnomalyAlert = () => {
+ const { lastRefresh } = useContext(UptimeRefreshContext);
+
+ const dispatch = useDispatch();
+
+ const monitorId = useMonitorId();
+
+ const { data: anomalyAlert } = useSelector(alertSelector);
+
+ const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
+
+ useEffect(() => {
+ dispatch(getExistingAlertAction.get({ monitorId }));
+ }, [monitorId, lastRefresh, dispatch, alertFlyoutVisible]);
+
+ return anomalyAlert;
+};
diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
index df8ceed76b796..29edb69f4674b 100644
--- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx
@@ -19,10 +19,10 @@ import {
selectDurationLines,
} from '../../../state/selectors';
import { UptimeRefreshContext } from '../../../contexts';
-import { getMLJobId } from '../../../state/api/ml_anomaly';
import { JobStat } from '../../../../../ml/public';
import { MonitorDurationComponent } from './monitor_duration';
import { MonitorIdParam } from '../../../../common/types';
+import { getMLJobId } from '../../../../common/lib';
export const MonitorDuration: React.FC = ({ monitorId }) => {
const {
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx
index 0ae8c3a93da94..b5ef240e67dbf 100644
--- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx
@@ -14,8 +14,8 @@ interface AlertExpressionPopoverProps {
'data-test-subj': string;
isEnabled?: boolean;
id: string;
+ value: string | JSX.Element;
isInvalid?: boolean;
- value: string;
}
const getColor = (
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx
new file mode 100644
index 0000000000000..4b84012575ae9
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/anomaly_alert.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 {
+ EuiExpression,
+ EuiFlexItem,
+ EuiFlexGroup,
+ EuiSpacer,
+ EuiHealth,
+ EuiText,
+} from '@elastic/eui';
+import { useSelector } from 'react-redux';
+import React, { useEffect, useState } from 'react';
+import { AnomalyTranslations } from './translations';
+import { AlertExpressionPopover } from '../alert_expression_popover';
+import { DEFAULT_SEVERITY, SelectSeverity } from './select_severity';
+import { monitorIdSelector } from '../../../../state/selectors';
+import { getSeverityColor, getSeverityType } from '../../../../../../ml/public';
+
+interface Props {
+ alertParams: { [key: string]: any };
+ setAlertParams: (key: string, value: any) => void;
+}
+
+// eslint-disable-next-line import/no-default-export
+export default function AnomalyAlertComponent({ setAlertParams, alertParams }: Props) {
+ const [severity, setSeverity] = useState(DEFAULT_SEVERITY);
+
+ const monitorIdStore = useSelector(monitorIdSelector);
+
+ const monitorId = monitorIdStore || alertParams?.monitorId;
+
+ useEffect(() => {
+ setAlertParams('monitorId', monitorId);
+ }, [monitorId, setAlertParams]);
+
+ useEffect(() => {
+ setAlertParams('severity', severity.val);
+ }, [severity, setAlertParams]);
+
+ return (
+ <>
+
+
+
+
+ {monitorId}
+
+ }
+ />
+
+
+
+ }
+ data-test-subj={'uptimeAnomalySeverity'}
+ description={AnomalyTranslations.hasAnomalyWithSeverity}
+ id="severity"
+ value={
+
+ {getSeverityType(severity.val)}
+
+ }
+ isEnabled={true}
+ />
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx
new file mode 100644
index 0000000000000..0932d0c6eca8d
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/select_severity.tsx
@@ -0,0 +1,135 @@
+/*
+ * 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, { Fragment, FC, useState, useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';
+import { getSeverityColor } from '../../../../../../ml/public';
+
+const warningLabel = i18n.translate('xpack.uptime.controls.selectSeverity.warningLabel', {
+ defaultMessage: 'warning',
+});
+const minorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.minorLabel', {
+ defaultMessage: 'minor',
+});
+const majorLabel = i18n.translate('xpack.uptime.controls.selectSeverity.majorLabel', {
+ defaultMessage: 'major',
+});
+const criticalLabel = i18n.translate('xpack.uptime.controls.selectSeverity.criticalLabel', {
+ defaultMessage: 'critical',
+});
+
+const optionsMap = {
+ [warningLabel]: 0,
+ [minorLabel]: 25,
+ [majorLabel]: 50,
+ [criticalLabel]: 75,
+};
+
+interface TableSeverity {
+ val: number;
+ display: string;
+ color: string;
+}
+
+export const SEVERITY_OPTIONS: TableSeverity[] = [
+ {
+ val: 0,
+ display: warningLabel,
+ color: getSeverityColor(0),
+ },
+ {
+ val: 25,
+ display: minorLabel,
+ color: getSeverityColor(25),
+ },
+ {
+ val: 50,
+ display: majorLabel,
+ color: getSeverityColor(50),
+ },
+ {
+ val: 75,
+ display: criticalLabel,
+ color: getSeverityColor(75),
+ },
+];
+
+function optionValueToThreshold(value: number) {
+ // Get corresponding threshold object with required display and val properties from the specified value.
+ let threshold = SEVERITY_OPTIONS.find((opt) => opt.val === value);
+
+ // Default to warning if supplied value doesn't map to one of the options.
+ if (threshold === undefined) {
+ threshold = SEVERITY_OPTIONS[0];
+ }
+
+ return threshold;
+}
+
+export const DEFAULT_SEVERITY = SEVERITY_OPTIONS[3];
+
+const getSeverityOptions = () =>
+ SEVERITY_OPTIONS.map(({ color, display, val }) => ({
+ 'data-test-subj': `alertAnomaly${display}`,
+ value: display,
+ inputDisplay: (
+
+
+ {display}
+
+
+ ),
+ dropdownDisplay: (
+
+
+ {display}
+
+
+
+
+
+
+
+
+ ),
+ }));
+
+interface Props {
+ onChange: (sev: TableSeverity) => void;
+ value: TableSeverity;
+}
+
+export const SelectSeverity: FC = ({ onChange, value }) => {
+ const [severity, setSeverity] = useState(DEFAULT_SEVERITY);
+
+ const onSeverityChange = (valueDisplay: string) => {
+ const option = optionValueToThreshold(optionsMap[valueDisplay]);
+ setSeverity(option);
+ onChange(option);
+ };
+
+ useEffect(() => {
+ setSeverity(value);
+ }, [value]);
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts
new file mode 100644
index 0000000000000..5fd37609f86bf
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/anomaly_alert/translations.ts
@@ -0,0 +1,26 @@
+/*
+ * 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';
+
+export const AnomalyTranslations = {
+ criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.ariaLabel', {
+ defaultMessage: 'An expression displaying the criteria for a selected monitor.',
+ }),
+ whenMonitor: i18n.translate('xpack.uptime.alerts.anomaly.criteriaExpression.description', {
+ defaultMessage: 'When monitor',
+ }),
+ scoreAriaLabel: i18n.translate('xpack.uptime.alerts.anomaly.scoreExpression.ariaLabel', {
+ defaultMessage: 'An expression displaying the criteria for an anomaly alert threshold.',
+ }),
+ hasAnomalyWithSeverity: i18n.translate(
+ 'xpack.uptime.alerts.anomaly.scoreExpression.description',
+ {
+ defaultMessage: 'has anomaly with severity',
+ description: 'An expression displaying the criteria for an anomaly alert threshold.',
+ }
+ ),
+};
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx
new file mode 100644
index 0000000000000..f0eb305461582
--- /dev/null
+++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 from 'react';
+import { Provider as ReduxProvider } from 'react-redux';
+import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
+import { CLIENT_ALERT_TYPES } from '../../../common/constants';
+import { DurationAnomalyTranslations } from './translations';
+import { AlertTypeInitializer } from '.';
+import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
+import { store } from '../../state';
+
+const { name, defaultActionMessage } = DurationAnomalyTranslations;
+const AnomalyAlertExpression = React.lazy(() =>
+ import('../../components/overview/alerts/anomaly_alert/anomaly_alert')
+);
+export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
+ core,
+ plugins,
+}): AlertTypeModel => ({
+ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY,
+ iconClass: 'uptimeApp',
+ alertParamsExpression: (params: any) => (
+
+
+
+
+
+ ),
+ name,
+ validate: () => ({ errors: {} }),
+ defaultActionMessage,
+ requiresAppContext: false,
+});
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts
index f2f72311d2262..5eb693c6bd5c3 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts
+++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts
@@ -9,6 +9,7 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
import { ClientPluginsStart } from '../../apps/plugin';
+import { initDurationAnomalyAlertType } from './duration_anomaly';
export type AlertTypeInitializer = (dependenies: {
core: CoreStart;
@@ -18,4 +19,5 @@ export type AlertTypeInitializer = (dependenies: {
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,
initTlsAlertType,
+ initDurationAnomalyAlertType,
];
diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts
index 11fa70bc56f4a..9232dd590ad5e 100644
--- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts
+++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts
@@ -26,7 +26,7 @@ export const TlsTranslations = {
{expiringConditionalOpen}
Expiring cert count: {expiringCount}
Expiring Certificates: {expiringCommonNameAndDate}
-{expiringConditionalClose}
+{expiringConditionalClose}
{agingConditionalOpen}
Aging cert count: {agingCount}
@@ -49,3 +49,23 @@ Aging Certificates: {agingCommonNameAndDate}
defaultMessage: 'Uptime TLS',
}),
};
+
+export const DurationAnomalyTranslations = {
+ defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
+ defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
+Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
+ values: {
+ severity: '{{state.severity}}',
+ anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
+ monitor: '{{state.monitor}}',
+ monitorUrl: '{{{state.monitorUrl}}}',
+ slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
+ expectedResponseTime: '{{state.expectedResponseTime}}',
+ severityScore: '{{state.severityScore}}',
+ observerLocation: '{{state.observerLocation}}',
+ },
+ }),
+ name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
+ defaultMessage: 'Uptime Duration Anomaly',
+ }),
+};
diff --git a/x-pack/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx
index ab7cf5b2cb3e2..f7012fc5119e9 100644
--- a/x-pack/plugins/uptime/public/pages/monitor.tsx
+++ b/x-pack/plugins/uptime/public/pages/monitor.tsx
@@ -16,6 +16,7 @@ import { MonitorCharts } from '../components/monitor';
import { MonitorStatusDetails, PingList } from '../components/monitor';
import { getDynamicSettings } from '../state/actions/dynamic_settings';
import { Ping } from '../../common/runtime_types/ping';
+import { setSelectedMonitorId } from '../state/actions';
const isAutogeneratedId = (id: string) => {
const autoGeneratedId = /^auto-(icmp|http|tcp)-OX[A-F0-9]{16}-[a-f0-9]{16}/;
@@ -43,6 +44,10 @@ export const MonitorPage: React.FC = () => {
const monitorId = useMonitorId();
+ useEffect(() => {
+ dispatch(setSelectedMonitorId(monitorId));
+ }, [monitorId, dispatch]);
+
const selectedMonitor = useSelector(monitorStatusSelector);
useTrackPageview({ app: 'uptime', path: 'monitor' });
diff --git a/x-pack/plugins/uptime/public/state/actions/alerts.ts b/x-pack/plugins/uptime/public/state/actions/alerts.ts
new file mode 100644
index 0000000000000..a650a9ba8d08b
--- /dev/null
+++ b/x-pack/plugins/uptime/public/state/actions/alerts.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 { createAsyncAction } from './utils';
+import { MonitorIdParam } from './types';
+import { Alert } from '../../../../triggers_actions_ui/public';
+
+export const getExistingAlertAction = createAsyncAction(
+ 'GET EXISTING ALERTS'
+);
+
+export const deleteAlertAction = createAsyncAction<{ alertId: string }, any>('DELETE ALERTS');
diff --git a/x-pack/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts
index 04ad6c2fa0bf3..9387506e4e7b5 100644
--- a/x-pack/plugins/uptime/public/state/actions/ui.ts
+++ b/x-pack/plugins/uptime/public/state/actions/ui.ts
@@ -25,3 +25,5 @@ export const setSearchTextAction = createAction('SET SEARCH');
export const toggleIntegrationsPopover = createAction(
'TOGGLE INTEGRATION POPOVER STATE'
);
+
+export const setSelectedMonitorId = createAction('SET MONITOR ID');
diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts
new file mode 100644
index 0000000000000..526abd6b303e5
--- /dev/null
+++ b/x-pack/plugins/uptime/public/state/api/alerts.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { apiService } from './utils';
+import { API_URLS } from '../../../common/constants';
+import { MonitorIdParam } from '../actions/types';
+import { Alert } from '../../../../triggers_actions_ui/public';
+
+export const fetchAlertRecords = async ({ monitorId }: MonitorIdParam): Promise => {
+ const data = {
+ page: 1,
+ per_page: 500,
+ filter: 'alert.attributes.alertTypeId:(xpack.uptime.alerts.durationAnomaly)',
+ default_search_operator: 'AND',
+ sort_field: 'name.keyword',
+ sort_order: 'asc',
+ };
+ const alerts = await apiService.get(API_URLS.ALERTS_FIND, data);
+ return alerts.data.find((alert: Alert) => alert.params.monitorId === monitorId);
+};
+
+export const disableAnomalyAlert = async ({ alertId }: { alertId: string }) => {
+ return await apiService.delete(API_URLS.ALERT + alertId);
+};
diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
index 5ec7a6262db66..1d25f35e8f38a 100644
--- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
+++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
@@ -7,38 +7,19 @@
import moment from 'moment';
import { apiService } from './utils';
import { AnomalyRecords, AnomalyRecordsParams } from '../actions';
-import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants';
+import { API_URLS, ML_MODULE_ID } from '../../../common/constants';
import {
- MlCapabilitiesResponse,
DataRecognizerConfigResponse,
JobExistResult,
+ MlCapabilitiesResponse,
} from '../../../../../plugins/ml/public';
import {
CreateMLJobSuccess,
DeleteJobResults,
- MonitorIdParam,
HeartbeatIndicesParam,
+ MonitorIdParam,
} from '../actions/types';
-
-const getJobPrefix = (monitorId: string) => {
- // ML App doesn't support upper case characters in job name
- // Also Spaces and the characters / ? , " < > | * are not allowed
- // so we will replace all special chars with _
-
- const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
-
- // ML Job ID can't be greater than 64 length, so will be substring it, and hope
- // At such big length, there is minimum chance of having duplicate monitor id
- // Subtracting ML_JOB_ID constant as well
- const postfix = '_' + ML_JOB_ID;
-
- if ((prefix + postfix).length > 64) {
- return prefix.substring(0, 64 - postfix.length) + '_';
- }
- return prefix + '_';
-};
-
-export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`;
+import { getJobPrefix, getMLJobId } from '../../../common/lib/ml';
export const getMLCapabilities = async (): Promise => {
return await apiService.get(API_URLS.ML_CAPABILITIES);
diff --git a/x-pack/plugins/uptime/public/state/effects/alerts.ts b/x-pack/plugins/uptime/public/state/effects/alerts.ts
new file mode 100644
index 0000000000000..5f71b0bea7b2c
--- /dev/null
+++ b/x-pack/plugins/uptime/public/state/effects/alerts.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { Action } from 'redux-actions';
+import { call, put, takeLatest, select } from 'redux-saga/effects';
+import { fetchEffectFactory } from './fetch_effect';
+import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
+import { disableAnomalyAlert, fetchAlertRecords } from '../api/alerts';
+import { kibanaService } from '../kibana_service';
+import { monitorIdSelector } from '../selectors';
+
+export function* fetchAlertsEffect() {
+ yield takeLatest(
+ getExistingAlertAction.get,
+ fetchEffectFactory(
+ fetchAlertRecords,
+ getExistingAlertAction.success,
+ getExistingAlertAction.fail
+ )
+ );
+
+ yield takeLatest(String(deleteAlertAction.get), function* (action: Action<{ alertId: string }>) {
+ try {
+ const response = yield call(disableAnomalyAlert, action.payload);
+ yield put(deleteAlertAction.success(response));
+ kibanaService.core.notifications.toasts.addSuccess('Alert successfully deleted!');
+ const monitorId = yield select(monitorIdSelector);
+ yield put(getExistingAlertAction.get({ monitorId }));
+ } catch (err) {
+ kibanaService.core.notifications.toasts.addError(err, {
+ title: 'Alert cannot be deleted',
+ });
+ yield put(deleteAlertAction.fail(err));
+ }
+ });
+}
diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts
index 211067c840d54..b13ba7f1a9107 100644
--- a/x-pack/plugins/uptime/public/state/effects/index.ts
+++ b/x-pack/plugins/uptime/public/state/effects/index.ts
@@ -17,6 +17,7 @@ import { fetchMonitorDurationEffect } from './monitor_duration';
import { fetchMLJobEffect } from './ml_anomaly';
import { fetchIndexStatusEffect } from './index_status';
import { fetchCertificatesEffect } from '../certificates/certificates';
+import { fetchAlertsEffect } from './alerts';
export function* rootEffect() {
yield fork(fetchMonitorDetailsEffect);
@@ -33,4 +34,5 @@ export function* rootEffect() {
yield fork(fetchMonitorDurationEffect);
yield fork(fetchIndexStatusEffect);
yield fork(fetchCertificatesEffect);
+ yield fork(fetchAlertsEffect);
}
diff --git a/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts
index a6a376b546ab8..00f8a388c689f 100644
--- a/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts
+++ b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { takeLatest } from 'redux-saga/effects';
+import { Action } from 'redux-actions';
+import { call, put, select, takeLatest } from 'redux-saga/effects';
import {
getMLCapabilitiesAction,
getExistingMLJobAction,
@@ -20,6 +21,9 @@ import {
deleteMLJob,
getMLCapabilities,
} from '../api/ml_anomaly';
+import { deleteAlertAction } from '../actions/alerts';
+import { alertSelector } from '../selectors';
+import { MonitorIdParam } from '../actions/types';
export function* fetchMLJobEffect() {
yield takeLatest(
@@ -38,10 +42,22 @@ export function* fetchMLJobEffect() {
getAnomalyRecordsAction.fail
)
);
- yield takeLatest(
- deleteMLJobAction.get,
- fetchEffectFactory(deleteMLJob, deleteMLJobAction.success, deleteMLJobAction.fail)
- );
+
+ yield takeLatest(String(deleteMLJobAction.get), function* (action: Action) {
+ try {
+ const response = yield call(deleteMLJob, action.payload);
+ yield put(deleteMLJobAction.success(response));
+
+ // let's delete alert as well if it's there
+ const { data: anomalyAlert } = yield select(alertSelector);
+ if (anomalyAlert) {
+ yield put(deleteAlertAction.get({ alertId: anomalyAlert.id as string }));
+ }
+ } catch (err) {
+ yield put(deleteMLJobAction.fail(err));
+ }
+ });
+
yield takeLatest(
getMLCapabilitiesAction.get,
fetchEffectFactory(
diff --git a/x-pack/plugins/uptime/public/state/kibana_service.ts b/x-pack/plugins/uptime/public/state/kibana_service.ts
index 4fd2d446daa17..f1eb3af9da667 100644
--- a/x-pack/plugins/uptime/public/state/kibana_service.ts
+++ b/x-pack/plugins/uptime/public/state/kibana_service.ts
@@ -20,6 +20,10 @@ class KibanaService {
apiService.http = this._core.http;
}
+ public get toasts() {
+ return this._core.notifications.toasts;
+ }
+
private constructor() {}
static getInstance(): KibanaService {
diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap
index c11b146101d35..040fbf7f4fe0a 100644
--- a/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap
+++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap
@@ -9,6 +9,7 @@ Object {
"id": "popover-2",
"open": true,
},
+ "monitorId": "test",
"searchText": "",
}
`;
@@ -19,6 +20,7 @@ Object {
"basePath": "yyz",
"esKuery": "",
"integrationsPopoverOpen": null,
+ "monitorId": "test",
"searchText": "",
}
`;
diff --git a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts
index 4683c654270db..c265cd9fc7ecd 100644
--- a/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts
+++ b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts
@@ -24,6 +24,7 @@ describe('ui reducer', () => {
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
+ monitorId: 'test',
},
action
)
@@ -43,6 +44,7 @@ describe('ui reducer', () => {
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
+ monitorId: 'test',
},
action
)
@@ -59,6 +61,7 @@ describe('ui reducer', () => {
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
+ monitorId: 'test',
},
action
)
@@ -68,6 +71,7 @@ describe('ui reducer', () => {
"basePath": "",
"esKuery": "",
"integrationsPopoverOpen": null,
+ "monitorId": "test",
"searchText": "",
}
`);
@@ -83,6 +87,7 @@ describe('ui reducer', () => {
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
+ monitorId: 'test',
},
action
)
@@ -92,6 +97,7 @@ describe('ui reducer', () => {
"basePath": "",
"esKuery": "",
"integrationsPopoverOpen": null,
+ "monitorId": "test",
"searchText": "lorem ipsum",
}
`);
diff --git a/x-pack/plugins/uptime/public/state/reducers/alerts.ts b/x-pack/plugins/uptime/public/state/reducers/alerts.ts
new file mode 100644
index 0000000000000..a2cd844e24964
--- /dev/null
+++ b/x-pack/plugins/uptime/public/state/reducers/alerts.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { handleActions } from 'redux-actions';
+import { getAsyncInitialState, handleAsyncAction } from './utils';
+import { AsyncInitialState } from './types';
+import { deleteAlertAction, getExistingAlertAction } from '../actions/alerts';
+import { Alert } from '../../../../triggers_actions_ui/public';
+
+export interface AlertsState {
+ alert: AsyncInitialState;
+ alertDeletion: AsyncInitialState;
+}
+
+const initialState: AlertsState = {
+ alert: getAsyncInitialState(),
+ alertDeletion: getAsyncInitialState(),
+};
+
+export const alertsReducer = handleActions(
+ {
+ ...handleAsyncAction('alert', getExistingAlertAction),
+ ...handleAsyncAction('alertDeletion', deleteAlertAction),
+ },
+ initialState
+);
diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts
index c05c740ab8ebf..01baf7cf07c92 100644
--- a/x-pack/plugins/uptime/public/state/reducers/index.ts
+++ b/x-pack/plugins/uptime/public/state/reducers/index.ts
@@ -20,6 +20,7 @@ import { indexStatusReducer } from './index_status';
import { mlJobsReducer } from './ml_anomaly';
import { certificatesReducer } from '../certificates/certificates';
import { selectedFiltersReducer } from './selected_filters';
+import { alertsReducer } from './alerts';
export const rootReducer = combineReducers({
monitor: monitorReducer,
@@ -37,4 +38,5 @@ export const rootReducer = combineReducers({
indexStatus: indexStatusReducer,
certificates: certificatesReducer,
selectedFilters: selectedFiltersReducer,
+ alerts: alertsReducer,
});
diff --git a/x-pack/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts
index 3cf4ae9c0bbf2..568234a3a83cd 100644
--- a/x-pack/plugins/uptime/public/state/reducers/ui.ts
+++ b/x-pack/plugins/uptime/public/state/reducers/ui.ts
@@ -14,6 +14,7 @@ import {
setAlertFlyoutType,
setAlertFlyoutVisible,
setSearchTextAction,
+ setSelectedMonitorId,
} from '../actions';
export interface UiState {
@@ -23,6 +24,7 @@ export interface UiState {
esKuery: string;
searchText: string;
integrationsPopoverOpen: PopoverState | null;
+ monitorId: string;
}
const initialState: UiState = {
@@ -31,6 +33,7 @@ const initialState: UiState = {
esKuery: '',
searchText: '',
integrationsPopoverOpen: null,
+ monitorId: '',
};
export const uiReducer = handleActions(
@@ -64,6 +67,10 @@ export const uiReducer = handleActions(
...state,
searchText: action.payload,
}),
+ [String(setSelectedMonitorId)]: (state, action: Action) => ({
+ ...state,
+ monitorId: action.payload,
+ }),
},
initialState
);
diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts
index b1885ddeeba3f..de8615c7016a7 100644
--- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts
+++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts
@@ -45,6 +45,7 @@ describe('state selectors', () => {
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
+ monitorId: '',
},
monitorStatus: {
status: null,
@@ -108,6 +109,10 @@ describe('state selectors', () => {
},
},
selectedFilters: null,
+ alerts: {
+ alertDeletion: { data: null, loading: false },
+ alert: { data: null, loading: false },
+ },
};
it('selects base path from state', () => {
diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts
index 4c2b671203f0a..bf6c9b3666a6a 100644
--- a/x-pack/plugins/uptime/public/state/selectors/index.ts
+++ b/x-pack/plugins/uptime/public/state/selectors/index.ts
@@ -59,6 +59,8 @@ export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob;
export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading;
export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading;
+export const isAnomalyAlertDeletingSelector = ({ alerts }: AppState) =>
+ alerts.alertDeletion.loading;
export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob;
@@ -88,3 +90,7 @@ export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery;
export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;
+
+export const monitorIdSelector = ({ ui: { monitorId } }: AppState) => monitorId;
+
+export const alertSelector = ({ alerts }: AppState) => alerts.alert;
diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
index 2e732f59e4f30..75d9c8aa959b1 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts
@@ -14,6 +14,7 @@ import {
import { UMKibanaRoute } from '../../../rest_api';
import { PluginSetupContract } from '../../../../../features/server';
import { DynamicSettings } from '../../../../common/runtime_types';
+import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
export type APICaller = (
endpoint: string,
@@ -39,6 +40,7 @@ export interface UptimeCorePlugins {
alerts: any;
elasticsearch: any;
usageCollection: UsageCollectionSetup;
+ ml: MlSetup;
}
export interface UMBackendFrameworkAdapter {
diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
index d85752768b47b..a38132d0f7a83 100644
--- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
+++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts
@@ -17,7 +17,7 @@ import { GetMonitorStatusResult } from '../../requests';
import { AlertType } from '../../../../../alerts/server';
import { IRouter } from 'kibana/server';
import { UMServerLibs } from '../../lib';
-import { UptimeCoreSetup } from '../../adapters';
+import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks';
@@ -33,9 +33,10 @@ const bootstrapDependencies = (customRequests?: any) => {
// these server/libs parameters don't have any functionality, which is fine
// because we aren't testing them here
const server: UptimeCoreSetup = { router };
+ const plugins: UptimeCorePlugins = {} as any;
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
libs.requests = { ...libs.requests, ...customRequests };
- return { server, libs };
+ return { server, libs, plugins };
};
/**
@@ -82,8 +83,8 @@ describe('status check alert', () => {
expect.assertions(4);
const mockGetter = jest.fn();
mockGetter.mockReturnValue([]);
- const { server, libs } = bootstrapDependencies({ getMonitorStatus: mockGetter });
- const alert = statusCheckAlertFactory(server, libs);
+ const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter });
+ const alert = statusCheckAlertFactory(server, libs, plugins);
// @ts-ignore the executor can return `void`, but ours never does
const state: Record