From 7a7939af3780622f1db9e2657292fb0e92b1aa7e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 24 Mar 2020 14:28:34 +0100 Subject: [PATCH] [Uptime] Ml detection of duration anomalies (#59785) (#61051) * add flyout * add state * update state * ad job * update * updat * add ml analyze button * update api * use differential colors for duration chart * remove duration chart gql * update type * type fix * fix tyoe * update translation * update test * update conflicts * update anomaly record * chart * added annotations * update error handling * update * update types * fixed types * fix types * update types * update * update * remove unnecessary change * remove unnecessary change * fix type * update * save * update pr * update tets * update job deletion * update * update tets * upadte tests * fix types * update title text * update types * fixed tests * update tests and types * updated types * fix PR feedback * unit test * update more types * update test and manage job * resolve conflicts * types * remove unnecessary change * revert ml code * revert ml code * fixed formatting issues pointed by pr feedback --- src/plugins/kibana_react/public/index.ts | 1 + .../uptime/common/constants/rest_api.ts | 6 + .../plugins/uptime/common/constants/ui.ts | 4 + .../connected/charts/monitor_duration.tsx | 63 ++++- .../connected/empty_state/empty_state.tsx | 4 +- .../monitor/status_bar_container.tsx | 25 +- .../duration_charts.test.tsx.snap | 2 + .../charts/__tests__/duration_charts.test.tsx | 4 +- .../functional/charts/annotation_tooltip.tsx | 54 ++++ .../functional/charts/duration_chart.tsx | 71 +++++- .../charts/duration_line_bar_list.tsx | 91 +++++++ .../__snapshots__/empty_state.test.tsx.snap | 30 +-- .../__tests__/empty_state.test.tsx | 17 +- .../functional/empty_state/empty_state.tsx | 3 +- .../empty_state/empty_state_error.tsx | 3 +- .../functional/monitor_list/translations.ts | 7 + .../{ => ping_list}/location_name.tsx | 0 .../functional/ping_list/ping_list.tsx | 2 +- .../confirm_delete.test.tsx.snap | 56 ++++ .../__snapshots__/license_info.test.tsx.snap | 73 ++++++ .../__snapshots__/ml_flyout.test.tsx.snap | 240 ++++++++++++++++++ .../ml_integerations.test.tsx.snap | 86 +++++++ .../__snapshots__/ml_job_link.test.tsx.snap | 82 ++++++ .../__snapshots__/ml_manage_job.test.tsx.snap | 90 +++++++ .../ml/__tests__/confirm_delete.test.tsx | 25 ++ .../ml/__tests__/license_info.test.tsx | 21 ++ .../ml/__tests__/ml_flyout.test.tsx | 113 +++++++++ .../ml/__tests__/ml_integerations.test.tsx | 30 +++ .../ml/__tests__/ml_job_link.test.tsx | 25 ++ .../ml/__tests__/ml_manage_job.test.tsx | 32 +++ .../monitor_details/ml/confirm_delete.tsx | 59 +++++ .../monitor_details/ml/license_info.tsx | 33 +++ .../monitor_details/ml/manage_ml_job.tsx | 80 ++++++ .../monitor_details/ml/ml_flyout.tsx | 86 +++++++ .../ml/ml_flyout_container.tsx | 154 +++++++++++ .../monitor_details/ml/ml_integeration.tsx | 111 ++++++++ .../monitor_details/ml/ml_job_link.tsx | 52 ++++ .../monitor_details/ml/translations.tsx | 150 +++++++++++ .../contexts/uptime_settings_context.tsx | 20 +- .../uptime/public/state/actions/index.ts | 1 + .../public/state/actions/index_status.ts | 2 +- .../uptime/public/state/actions/ml_anomaly.ts | 46 ++++ .../public/state/actions/monitor_duration.ts | 5 +- .../uptime/public/state/actions/types.ts | 30 ++- .../uptime/public/state/actions/utils.ts | 14 +- .../uptime/public/state/api/ml_anomaly.ts | 88 +++++++ ...th_effect.test.ts => fetch_effect.test.ts} | 36 ++- .../public/state/effects/fetch_effect.ts | 8 +- .../uptime/public/state/effects/index.ts | 2 + .../uptime/public/state/effects/ml_anomaly.ts | 53 ++++ .../uptime/public/state/reducers/index.ts | 2 + .../public/state/reducers/index_status.ts | 14 +- .../public/state/reducers/ml_anomaly.ts | 71 ++++++ .../public/state/reducers/monitor_duration.ts | 6 +- .../uptime/public/state/reducers/types.ts | 7 +- .../uptime/public/state/reducers/utils.ts | 33 ++- .../state/selectors/__tests__/index.test.ts | 22 +- .../uptime/public/state/selectors/index.ts | 41 ++- x-pack/package.json | 2 +- x-pack/plugins/ml/common/types/anomalies.ts | 17 ++ .../ml/common/types/data_recognizer.ts | 17 ++ .../models/data_recognizer/data_recognizer.ts | 13 +- .../modules/uptime_heartbeat/logo.json | 3 + .../modules/uptime_heartbeat/manifest.json | 26 ++ .../ml/datafeed_high_latency_by_geo.json | 13 + .../ml/high_latency_by_geo.json | 29 +++ .../build_anomaly_table_items.d.ts | 19 +- .../models/results_service/results_service.ts | 4 +- yarn.lock | 73 +++++- 69 files changed, 2522 insertions(+), 180 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_line_bar_list.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/location_name.tsx (100%) create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/confirm_delete.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/license_info.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_flyout.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_integerations.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_job_link.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/ml_manage_job.test.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/confirm_delete.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/license_info.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/manage_ml_job.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_integeration.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_job_link.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts rename x-pack/legacy/plugins/uptime/public/state/effects/__tests__/{fecth_effect.test.ts => fetch_effect.test.ts} (67%) create mode 100644 x-pack/legacy/plugins/uptime/public/state/effects/ml_anomaly.ts create mode 100644 x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts create mode 100644 x-pack/plugins/ml/common/types/data_recognizer.ts create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/uptime_heartbeat/logo.json create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/uptime_heartbeat/manifest.json create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/uptime_heartbeat/ml/datafeed_high_latency_by_geo.json create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/uptime_heartbeat/ml/high_latency_by_geo.json diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index e88ca7178cde3..e1689e38dbfe0 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -26,6 +26,7 @@ export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; export { ValidatedDualRange } from './validated_range'; +export * from './notifications'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts index 61197d6dc373d..a1a3e86e6a97e 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/rest_api.ts @@ -16,4 +16,10 @@ export enum API_URLS { PING_HISTOGRAM = `/api/uptime/ping/histogram`, SNAPSHOT_COUNT = `/api/uptime/snapshot/count`, FILTERS = `/api/uptime/filters`, + + ML_MODULE_JOBS = `/api/ml/modules/jobs_exist/`, + ML_SETUP_MODULE = '/api/ml/modules/setup/', + ML_DELETE_JOB = `/api/ml/jobs/delete_jobs`, + ML_CAPABILITIES = '/api/ml/ml_capabilities', + ML_ANOMALIES_RESULT = `/api/ml/results/anomalies_table_data`, } diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts index 8d223dbbba556..29e8dabf53f92 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/ui.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -15,6 +15,10 @@ export enum STATUS { DOWN = 'down', } +export const ML_JOB_ID = 'high_latency_by_geo'; + +export const ML_MODULE_ID = 'uptime_heartbeat'; + export const UNNAMED_LOCATION = 'Unnamed-location'; export const SHORT_TS_LOCALE = 'en-short-locale'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx index 8d2b8d2cd8e0d..7d1cb08cb8b1c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/monitor_duration.tsx @@ -7,10 +7,21 @@ import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useUrlParams } from '../../../hooks'; -import { getMonitorDurationAction } from '../../../state/actions'; +import { + getAnomalyRecordsAction, + getMLCapabilitiesAction, + getMonitorDurationAction, +} from '../../../state/actions'; import { DurationChartComponent } from '../../functional/charts'; -import { selectDurationLines } from '../../../state/selectors'; +import { + anomaliesSelector, + hasMLFeatureAvailable, + hasMLJobSelector, + selectDurationLines, +} from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; +import { getMLJobId } from '../../../state/api/ml_anomaly'; +import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; interface Props { monitorId: string; @@ -18,24 +29,58 @@ interface Props { export const DurationChart: React.FC = ({ monitorId }: Props) => { const [getUrlParams] = useUrlParams(); - const { dateRangeStart, dateRangeEnd } = getUrlParams(); + const { + dateRangeStart, + dateRangeEnd, + absoluteDateRangeStart, + absoluteDateRangeEnd, + } = getUrlParams(); - const { monitor_duration, loading } = useSelector(selectDurationLines); + const { durationLines, loading } = useSelector(selectDurationLines); + + const isMLAvailable = useSelector(hasMLFeatureAvailable); + + const { data: mlJobs, loading: jobsLoading } = useSelector(hasMLJobSelector); + + const hasMLJob = + !!mlJobs?.jobsExist && + !!mlJobs.jobs.find((job: JobStat) => job.id === getMLJobId(monitorId as string)); + + const anomalies = useSelector(anomaliesSelector); const dispatch = useDispatch(); const { lastRefresh } = useContext(UptimeRefreshContext); useEffect(() => { - dispatch( - getMonitorDurationAction({ monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd }) - ); + if (isMLAvailable) { + const anomalyParams = { + listOfMonitorIds: [monitorId], + dateStart: absoluteDateRangeStart, + dateEnd: absoluteDateRangeEnd, + }; + + dispatch(getAnomalyRecordsAction.get(anomalyParams)); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId, isMLAvailable]); + + useEffect(() => { + const params = { monitorId, dateStart: dateRangeStart, dateEnd: dateRangeEnd }; + dispatch(getMonitorDurationAction(params)); }, [dateRangeStart, dateRangeEnd, dispatch, lastRefresh, monitorId]); + useEffect(() => { + dispatch(getMLCapabilitiesAction.get()); + }, [dispatch]); + return ( ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx index cac7042ca5b5c..b383a696095a3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/empty_state/empty_state.tsx @@ -11,7 +11,7 @@ import { indexStatusSelector } from '../../../state/selectors'; import { EmptyStateComponent } from '../../functional/empty_state/empty_state'; export const EmptyState: React.FC = ({ children }) => { - const { data, loading, errors } = useSelector(indexStatusSelector); + const { data, loading, error } = useSelector(indexStatusSelector); const dispatch = useDispatch(); @@ -23,7 +23,7 @@ export const EmptyState: React.FC = ({ children }) => { ); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx index 456fa2b30bca8..9e7834ae6f242 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -22,7 +22,8 @@ interface StateProps { } interface DispatchProps { - loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void; + loadMonitorStatus: typeof getMonitorStatusAction; + loadSelectedMonitor: typeof getSelectedMonitorAction; } interface OwnProps { @@ -33,6 +34,7 @@ type Props = OwnProps & StateProps & DispatchProps; const Container: React.FC = ({ loadMonitorStatus, + loadSelectedMonitor, monitorId, monitorStatus, monitorLocations, @@ -43,8 +45,9 @@ const Container: React.FC = ({ const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); useEffect(() => { - loadMonitorStatus(dateStart, dateEnd, monitorId); - }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); + loadMonitorStatus({ dateStart, dateEnd, monitorId }); + loadSelectedMonitor({ monitorId }); + }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh, loadSelectedMonitor]); return ( ({ }); const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ - loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { - dispatch( - getMonitorStatusAction({ - monitorId, - dateStart, - dateEnd, - }) - ); - dispatch( - getSelectedMonitorAction({ - monitorId, - }) - ); - }, + loadSelectedMonitor: params => dispatch(getSelectedMonitorAction(params)), + loadMonitorStatus: params => dispatch(getMonitorStatusAction(params)), }); // @ts-ignore TODO: Investigate typescript issues here diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap index 1e2d2b9144416..6c38f3e338cfd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap @@ -52,6 +52,8 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` } > { it('renders the component without errors', () => { const component = shallowWithRouter( ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx new file mode 100644 index 0000000000000..ad2a6d02c5364 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/annotation_tooltip.tsx @@ -0,0 +1,54 @@ +/* + * 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 moment from 'moment'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; + +const Header = styled.div` + font-weight: bold; + padding-left: 4px; +`; + +const RecordSeverity = styled.div` + font-weight: bold; + border-left: 4px solid ${props => props.color}; + padding-left: 2px; +`; + +const TimeDiv = styled.div` + font-weight: 500; + border-bottom: 1px solid gray; + padding-bottom: 2px; +`; + +export const AnnotationTooltip = ({ details }: { details: string }) => { + const data = JSON.parse(details); + + function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + return ( + <> + {moment(data.time).format('lll')} +
+ +
+ + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx index 6bd4e7431f97a..d149e7a6deb5a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/duration_chart.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts'; -import { EuiPanel, EuiTitle } from '@elastic/eui'; -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; +import { Axis, Chart, Position, timeFormatter, Settings } from '@elastic/charts'; +import { SeriesIdentifier } from '@elastic/charts/dist/chart_types/xy_chart/utils/series'; import { getChartDateLabel } from '../../../lib/helper'; import { LocationDurationLine } from '../../../../common/types'; import { DurationLineSeriesList } from './duration_line_series_list'; @@ -17,6 +18,9 @@ import { ChartWrapper } from './chart_wrapper'; import { useUrlParams } from '../../../hooks'; import { getTickFormat } from './get_tick_format'; import { ChartEmptyState } from './chart_empty_state'; +import { DurationAnomaliesBar } from './duration_line_bar_list'; +import { MLIntegrationComponent } from '../../monitor_details/ml/ml_integeration'; +import { AnomalyRecords } from '../../../state/actions'; interface DurationChartProps { /** @@ -29,6 +33,10 @@ interface DurationChartProps { * To represent the loading spinner on chart */ loading: boolean; + + hasMLJob: boolean; + + anomalies: AnomalyRecords | null; } /** @@ -37,29 +45,64 @@ interface DurationChartProps { * milliseconds. * @param props The props required for this component to render properly */ -export const DurationChartComponent = ({ locationDurationLines, loading }: DurationChartProps) => { +export const DurationChartComponent = ({ + locationDurationLines, + anomalies, + loading, + hasMLJob, +}: DurationChartProps) => { const hasLines = locationDurationLines.length > 0; const [getUrlParams, updateUrlParams] = useUrlParams(); const { absoluteDateRangeStart: min, absoluteDateRangeEnd: max } = getUrlParams(); + const [hiddenLegends, setHiddenLegends] = useState([]); + const onBrushEnd = (minX: number, maxX: number) => { updateUrlParams({ dateRangeStart: moment(minX).toISOString(), dateRangeEnd: moment(maxX).toISOString(), }); }; + + const legendToggleVisibility = (legendItem: SeriesIdentifier | null) => { + if (legendItem) { + setHiddenLegends(prevState => { + if (prevState.includes(legendItem.specId)) { + return [...prevState.filter(item => item !== legendItem.specId)]; + } else { + return [...prevState, legendItem.specId]; + } + }); + } + }; + return ( <> - -

- -

-
+ + + +

+ {hasMLJob ? ( + + ) : ( + + )} +

+
+
+ + + +
+ {hasLines ? ( @@ -69,6 +112,7 @@ export const DurationChartComponent = ({ locationDurationLines, loading }: Durat showLegendExtra legendPosition={Position.Bottom} onBrushEnd={onBrushEnd} + onLegendItemClick={legendToggleVisibility} /> + ) : ( { + const anomalyAnnotations: Map = new Map(); + + Object.keys(ANOMALY_SEVERITY).forEach(severityLevel => { + anomalyAnnotations.set(severityLevel.toLowerCase(), { rect: [], color: '' }); + }); + + if (anomalies?.anomalies) { + const records = anomalies.anomalies; + records.forEach((record: any) => { + let recordObsvLoc = record.source['observer.geo.name']?.[0] ?? 'N/A'; + if (recordObsvLoc === '') { + recordObsvLoc = 'N/A'; + } + if (hiddenLegends.length && hiddenLegends.includes(`loc-avg-${recordObsvLoc}`)) { + return; + } + const severityLevel = getSeverityType(record.severity); + + const tooltipData = { + time: record.source.timestamp, + score: record.severity, + severity: severityLevel, + color: getSeverityColor(record.severity), + }; + + const anomalyRect = { + coordinates: { + x0: moment(record.source.timestamp).valueOf(), + x1: moment(record.source.timestamp) + .add(record.source.bucket_span, 's') + .valueOf(), + }, + details: JSON.stringify(tooltipData), + }; + anomalyAnnotations.get(severityLevel)!.rect.push(anomalyRect); + anomalyAnnotations.get(severityLevel)!.color = getSeverityColor(record.severity); + }); + } + + const getRectStyle = (color: string) => { + return { + fill: color, + opacity: 1, + strokeWidth: 2, + stroke: color, + }; + }; + + const tooltipFormatter: AnnotationTooltipFormatter = (details?: string) => { + return ; + }; + + return ( + <> + {Array.from(anomalyAnnotations).map(([keyIndex, rectAnnotation]) => { + return rectAnnotation.rect.length > 0 ? ( + + ) : null; + })} + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap index cf46c6944ac17..66100f717244d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap @@ -749,17 +749,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = ` @@ -904,7 +884,7 @@ exports[`EmptyState component renders error message when an error occurs 1`] = ` body={

- An error occurred + There was an error fetching your data.

} @@ -971,9 +951,9 @@ exports[`EmptyState component renders error message when an error occurs 1`] = ` className="euiText euiText--medium" >

- An error occurred + There was an error fetching your data.

diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx index 20113df3010f8..a74ad543c3318 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/empty_state.test.tsx @@ -7,8 +7,9 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { EmptyStateComponent } from '../empty_state'; -import { GraphQLError } from 'graphql'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; +import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; +import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; describe('EmptyState component', () => { let statesIndexStatus: StatesIndexStatus; @@ -41,18 +42,8 @@ describe('EmptyState component', () => { }); it(`renders error message when an error occurs`, () => { - const errors: GraphQLError[] = [ - { - message: 'An error occurred', - locations: undefined, - path: undefined, - nodes: undefined, - source: undefined, - positions: undefined, - originalError: undefined, - extensions: undefined, - name: 'foo', - }, + const errors: IHttpFetchError[] = [ + new HttpFetchError('There was an error fetching your data.', 'error', {} as any), ]; const component = mountWithIntl( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx index 80afc2894ea44..ae6a1b892bc99 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state.tsx @@ -10,12 +10,13 @@ import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; import { DataMissing } from './data_missing'; import { StatesIndexStatus } from '../../../../common/runtime_types'; +import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; statesIndexStatus: StatesIndexStatus | null; loading: boolean; - errors?: Error[]; + errors?: IHttpFetchError[]; } export const EmptyStateComponent = ({ diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx index c8e2bece1cb7f..1135b969018a1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/empty_state_error.tsx @@ -7,9 +7,10 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; +import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; interface EmptyStateErrorProps { - errors: Error[]; + errors: IHttpFetchError[]; } export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts index 5252d90215e95..7b9b2d07f2a76 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts @@ -64,3 +64,10 @@ export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel' export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { defaultMessage: 'Down', }); + +export const RESPONSE_ANOMALY_SCORE = i18n.translate( + 'xpack.uptime.monitorList.anomalyColumn.label', + { + defaultMessage: 'Response Anomaly Score', + } +); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_name.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/location_name.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/location_name.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/location_name.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx index e8825dacc0078..d245bc1456e6a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx @@ -29,7 +29,7 @@ import { Ping, PingResults } from '../../../../common/graphql/types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; import { pingsQuery } from '../../../queries'; -import { LocationName } from './../location_name'; +import { LocationName } from './location_name'; import { Pagination } from './../monitor_list'; import { PingListExpandedRowComponent } from './expanded_row'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap new file mode 100644 index 0000000000000..24ef7eda0d129 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ML Confirm Job Delete shallow renders without errors 1`] = ` + + +

+ +

+

+ +

+
+
+`; + +exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] = ` + + +

+ + ) +

+ +
+
+`; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap new file mode 100644 index 0000000000000..2457488c4facc --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/license_info.test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShowLicenseInfo renders without errors 1`] = ` +Array [ +
+
+ +
+

+ In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. +

+ + + + Start free 14-day trial + + + +
+
, +
, +] +`; + +exports[`ShowLicenseInfo shallow renders without errors 1`] = ` + + +

+ In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. +

+ + Start free 14-day trial + +
+ +
+`; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap new file mode 100644 index 0000000000000..354521e7c55b9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap @@ -0,0 +1,240 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ML Flyout component renders without errors 1`] = ` + + + +

+ Enable anomaly detection +

+
+ +
+ + + +

+ Here you can create a machine learning job to calculate anomaly scores on + response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page + will show the expected bounds and annotate the graph with anomalies. You can also potentially + identify periods of increased latency across geographical regions. +

+

+ + Machine Learning jobs management page + , + } + } + /> +

+

+ + Note: It might take a few minutes for the job to begin calculating results. + +

+
+ +
+ + + + + Create new job + + + + +
+`; + +exports[`ML Flyout component shows license info if no ml available 1`] = ` +
+
+
+
+