/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ import { useState, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { DetectorListItem } from '../../../models/interfaces'; import { AD_DOC_FIELDS, MIN_IN_MILLI_SECS, } from '../../../../server/utils/constants'; import { EuiBadge, EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingChart, //@ts-ignore EuiStat, } from '@elastic/eui'; import { get, isEmpty } from 'lodash'; import moment, { Moment } from 'moment'; import ContentPanel from '../../../components/ContentPanel/ContentPanel'; import { Chart, Axis, Settings, Position, BarSeries, niceTimeFormatter, ScaleType, LineAnnotation, AnnotationDomainType, LineAnnotationDatum, } from '@elastic/charts'; import { EuiText, EuiTitle } from '@elastic/eui'; import React from 'react'; import { TIME_NOW_LINE_STYLE } from '../utils/constants'; import { SHOW_DECIMAL_NUMBER_THRESHOLD } from '../../../../server/utils/helpers'; import { visualizeAnomalyResultForXYChart, getFloorPlotTime, getLatestAnomalyResultsForDetectorsByTimeRange, getLatestAnomalyResultsByTimeRange, } from '../utils/utils'; import { MAX_ANOMALIES, SPACE_STR } from '../../../utils/constants'; import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../utils/constants'; import { searchResults } from '../../../redux/reducers/anomalyResults'; export interface AnomaliesLiveChartProps { selectedDetectors: DetectorListItem[]; } interface LiveTimeRangeState { startDateTime: Moment; endDateTime: Moment; } const MAX_LIVE_DETECTORS = 10; export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const dispatch = useDispatch(); const [liveTimeRange, setLiveTimeRange] = useState({ startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), }); const [lastAnomalyResult, setLastAnomalyResult] = useState(); const [liveAnomalyData, setLiveAnomalyData] = useState([] as object[]); const [isFullScreen, setIsFullScreen] = useState(false); const [isLoadingAnomalies, setIsLoadingAnomalies] = useState(true); const [hasLatestAnomalyResult, setHasLatestAnomalyResult] = useState(true); const [latestAnomalousDetectorsCount, setLatestLiveAnomalousDetectorsCount] = useState(0); const getLiveAnomalyResults = async () => { setIsLoadingAnomalies(true); // check if there is any anomaly result in last 30mins // need to initially check if there is an error when accessing anomaly results index // in the case that it doesn't exist upon cluster initialization let latestSingleLiveAnomalyResult = [] as any[]; try { latestSingleLiveAnomalyResult = await getLatestAnomalyResultsByTimeRange( searchResults, '30m', dispatch, -1, 1, true, ALL_CUSTOM_AD_RESULT_INDICES, false ); } catch (err) { console.log( 'Error getting latest anomaly results - index may not exist yet', err ); setIsLoadingAnomalies(false); } setHasLatestAnomalyResult(!isEmpty(latestSingleLiveAnomalyResult)); // get anomalies(anomaly_grade>0) in last 30mins const latestLiveAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange( searchResults, props.selectedDetectors, '30m', dispatch, 0, MAX_ANOMALIES, MAX_LIVE_DETECTORS, false, ALL_CUSTOM_AD_RESULT_INDICES, false ); setLiveAnomalyData(latestLiveAnomalyResult); setLatestLiveAnomalousDetectorsCount( new Set( latestLiveAnomalyResult.map((anomalyData) => get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') ) ).size ); if (!isEmpty(latestLiveAnomalyResult)) { setLastAnomalyResult(latestLiveAnomalyResult[0]); } else { setLastAnomalyResult(undefined); } setLiveTimeRange({ startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), }); setIsLoadingAnomalies(false); }; useEffect(() => { getLiveAnomalyResults(); const id = setInterval(getLiveAnomalyResults, MIN_IN_MILLI_SECS); return () => { clearInterval(id); }; }, [props.selectedDetectors]); const timeFormatter = niceTimeFormatter([ liveTimeRange.startDateTime.valueOf(), liveTimeRange.endDateTime.valueOf(), ]); const visualizedAnomalies = liveAnomalyData.flatMap((anomalyResult) => visualizeAnomalyResultForXYChart(anomalyResult) ); const prepareVisualizedAnomalies = ( liveVisualizedAnomalies: object[] ): object[] => { // add data point placeholder at every minute, // to ensure chart evenly distrubted const existingPlotTimes = liveVisualizedAnomalies.map((anomaly) => getFloorPlotTime(get(anomaly, AD_DOC_FIELDS.PLOT_TIME, 0)) ); const result = [...liveVisualizedAnomalies]; for ( let currentTime = getFloorPlotTime(liveTimeRange.startDateTime.valueOf()); currentTime <= liveTimeRange.endDateTime.valueOf(); currentTime += MIN_IN_MILLI_SECS ) { if (existingPlotTimes.includes(currentTime)) { continue; } result.push({ [AD_DOC_FIELDS.DETECTOR_NAME]: !isEmpty(liveAnomalyData) ? '' : SPACE_STR, [AD_DOC_FIELDS.PLOT_TIME]: currentTime, [AD_DOC_FIELDS.ANOMALY_GRADE]: null, }); } return result; }; const timeNowAnnotation = { dataValue: getFloorPlotTime(liveTimeRange.endDateTime.valueOf()), header: 'Now', details: liveTimeRange.endDateTime.format('MM/DD/YY h:mm A'), } as LineAnnotationDatum; const annotations = [timeNowAnnotation]; const fullScreenButton = () => ( setIsFullScreen((isFullScreen) => !isFullScreen)} iconType={isFullScreen ? 'exit' : 'fullScreen'} aria-label="View full screen" data-test-subj="dashboardFullScreenButton" > {isFullScreen ? 'Exit full screen' : 'View full screen'} ); return (

Live anomalies{' '} Live

} subTitle={`Live anomaly results across detectors for the last 30 minutes. 'The results refresh every 1 minute. 'For each detector, if an anomaly occurrence is detected at the end of the detector interval, 'you will see a bar representing its anomaly grade.`} actions={[fullScreenButton()]} contentPanelClassName={isFullScreen ? 'full-screen' : undefined} > {isLoadingAnomalies ? ( ) : !hasLatestAnomalyResult ? (

All matching detectors are under initialization or stopped for the last 30 minutes. Please adjust filters or come back later.

) : ( // show below content as long as there exists anomaly data, // regardless of whether anomaly grade is 0 or larger. [ ,
{[ // only show below message when anomalousDetectorCount >= MAX_LIVE_DETECTORS latestAnomalousDetectorsCount >= MAX_LIVE_DETECTORS ? (

{`${MAX_LIVE_DETECTORS} detectors with the most recent anomalies are shown on the chart. Adjust filters if there are specific detectors you would like to monitor.`}

) : latestAnomalousDetectorsCount === 0 ? ( // all the data points have anomaly grade as 0 ) : null,
, ]}
, ] )}
); };