diff --git a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx index 448b802e..992a43c3 100644 --- a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx +++ b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx @@ -34,9 +34,9 @@ import { useDispatch } from 'react-redux'; import { Datum } from '@elastic/charts/dist/utils/commons'; import React from 'react'; import { TIME_RANGE_OPTIONS } from '../../Dashboard/utils/constants'; -import { get } from 'lodash'; +import { get, isEmpty } from 'lodash'; import { searchES } from '../../../redux/reducers/elasticsearch'; -import { MAX_DETECTORS } from '../../../utils/constants'; +import { MAX_DETECTORS, MAX_ANOMALIES } from '../../../utils/constants'; import { AD_DOC_FIELDS } from '../../../../server/utils/constants'; export interface AnomaliesDistributionChartProps { allDetectorsSelected: boolean; @@ -64,17 +64,23 @@ export const AnomaliesDistributionChart = ( const [timeRange, setTimeRange] = useState(TIME_RANGE_OPTIONS[0].value); const getAnomalyResult = async (currentDetectors: DetectorListItem[]) => { - const finalAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange( + const latestAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange( searchES, props.selectedDetectors, timeRange, - MAX_DETECTORS, - dispatch + dispatch, + 0, + MAX_ANOMALIES, + MAX_DETECTORS ); - setAnomalyResults(finalAnomalyResult); + + const nonZeroAnomalyResult = latestAnomalyResult.filter( + anomalyData => get(anomalyData, AD_DOC_FIELDS.ANOMALY_GRADE, 0) > 0 + ); + setAnomalyResults(nonZeroAnomalyResult); const resultDetectors = getFinalDetectors( - finalAnomalyResult, + nonZeroAnomalyResult, props.selectedDetectors ); setIndicesNumber(getFinalIndices(resultDetectors).size); @@ -123,9 +129,8 @@ export const AnomaliesDistributionChart = (

- { - 'The inner circle shows the anomaly distribution by your indices. The outer circle shows the anomaly distribution by your detectors.' - } + {'The inner circle shows the anomaly distribution by your indices. ' + + 'The outer circle shows the anomaly distribution by your detectors.'}

@@ -141,13 +146,14 @@ export const AnomaliesDistributionChart = ( /> } > - + @@ -156,17 +162,12 @@ export const AnomaliesDistributionChart = ( title={finalDetectors.length} isLoading={anomalyResultsLoading} titleSize="s" + style={{ color: '#000' }} /> {anomalyResultsLoading ? ( - - - - - - @@ -174,67 +175,69 @@ export const AnomaliesDistributionChart = ( ) : ( - - d.count as number} - valueFormatter={(d: number) => d.toString()} - layers={[ - { - groupByRollup: (d: Datum) => d.indices, - nodeLabel: (d: Datum) => { - return d; - }, - fillLabel: { - textInvertible: true, - }, - shape: { - fillColor: d => { - return fillOutColors( - d, - (d.x0 + d.x1) / 2 / (2 * Math.PI), - [] - ); + {isEmpty(anomalyResults) ? null : ( + + d.count as number} + valueFormatter={(d: number) => d.toString()} + layers={[ + { + groupByRollup: (d: Datum) => d.indices, + nodeLabel: (d: Datum) => { + return d; + }, + fillLabel: { + textInvertible: true, + }, + shape: { + fillColor: d => { + return fillOutColors( + d, + (d.x0 + d.x1) / 2 / (2 * Math.PI), + [] + ); + }, }, }, - }, - { - groupByRollup: (d: Datum) => d.name, - nodeLabel: (d: Datum) => { - return d; + { + groupByRollup: (d: Datum) => d.name, + nodeLabel: (d: Datum) => { + return d; + }, + fillLabel: { + textInvertible: true, + }, + shape: { + fillColor: d => { + return fillOutColors( + d, + (d.x0 + d.x1) / 2 / (2 * Math.PI), + [] + ); + }, + }, }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + fontFamily: 'Arial', + outerSizeRatio: 1, fillLabel: { textInvertible: true, }, - shape: { - fillColor: d => { - return fillOutColors( - d, - (d.x0 + d.x1) / 2 / (2 * Math.PI), - [] - ); - }, - }, - }, - ]} - config={{ - partitionLayout: PartitionLayout.sunburst, - fontFamily: 'Arial', - outerSizeRatio: 1, - fillLabel: { - textInvertible: true, - }, - // TODO: Given only 1 detector exists, the inside Index circle will have issue in following scenarios: - // 1: if Linked Label is configured for identifying index, label of Index circle will be invisible; - // 2: if Fill Label is configured for identifying index, label of it will be overlapped with outer Detector circle - // Issue link: https://github.com/opendistro-for-elasticsearch/anomaly-detection-kibana-plugin/issues/24 - }} - /> - + // TODO: Given only 1 detector exists, the inside Index circle will have issue in following scenarios: + // 1: if Linked Label is configured for identifying index, label of Index circle will be invisible; + // 2: if Fill Label is configured for identifying index, label of it will be overlapped with outer Detector circle + // Issue link: https://github.com/opendistro-for-elasticsearch/anomaly-detection-kibana-plugin/issues/24 + }} + /> + + )} )} diff --git a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx index 8c4fa443..f2e35fb9 100644 --- a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx +++ b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx @@ -21,6 +21,8 @@ import { } from '../../../../server/utils/constants'; import { EuiBadge, + EuiButton, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLoadingChart, @@ -52,6 +54,7 @@ import { getLatestAnomalyResultsForDetectorsByTimeRange, } from '../utils/utils'; import { AppState } from '../../../redux/reducers'; +import { MAX_ANOMALIES } from '../../../utils/constants'; export interface AnomaliesLiveChartProps { allDetectorsSelected: boolean; @@ -81,20 +84,41 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const [liveAnomalyData, setLiveAnomalyData] = useState([] as object[]); + const [anomalousDetectorCount, setAnomalousDetectorCount] = useState(0); + + const [hasLatestAnomalyData, setHasLatestAnomalyData] = useState(false); + + const [isFullScreen, setIsFullScreen] = useState(false); + const getLiveAnomalyResults = async () => { - const finalLiveAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange( + const latestLiveAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange( searchES, props.selectedDetectors, '30m', - MAX_LIVE_DETECTORS, - dispatch + dispatch, + -1, + MAX_ANOMALIES, + MAX_LIVE_DETECTORS + ); + + setHasLatestAnomalyData(!isEmpty(latestLiveAnomalyResult)); + + const nonZeroAnomalyResult = latestLiveAnomalyResult.filter( + anomalyData => get(anomalyData, AD_DOC_FIELDS.ANOMALY_GRADE, 0) > 0 ); + setLiveAnomalyData(nonZeroAnomalyResult); - setLiveAnomalyData(finalLiveAnomalyResult); - if (!isEmpty(finalLiveAnomalyResult)) { - setLastAnomalyResult(finalLiveAnomalyResult[0]); + if (!isEmpty(nonZeroAnomalyResult)) { + setLastAnomalyResult(nonZeroAnomalyResult[0]); + const uniqueIds = new Set( + nonZeroAnomalyResult.map(anomalyData => + get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') + ) + ); + setAnomalousDetectorCount(uniqueIds.size); } else { setLastAnomalyResult(undefined); + setAnomalousDetectorCount(0); } setLiveTimeRange({ startDateTime: moment().subtract(30, 'minutes'), @@ -137,11 +161,12 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { continue; } result.push({ - [AD_DOC_FIELDS.DETECTOR_NAME]: '', + [AD_DOC_FIELDS.DETECTOR_NAME]: null, [AD_DOC_FIELDS.PLOT_TIME]: currentTime, [AD_DOC_FIELDS.ANOMALY_GRADE]: null, }); } + return result; }; @@ -153,14 +178,25 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const annotations = [timeNowAnnotation]; - // Add View full screen button - // Issue link: https://github.com/opendistro-for-elasticsearch/anomaly-detection-kibana-plugin/issues/26 + const fullScreenButton = () => ( + setIsFullScreen(isFullScreen => !isFullScreen)} + iconType={isFullScreen ? 'exit' : 'fullScreen'} + aria-label="View full screen" + > + {isFullScreen ? 'Exit full screen' : 'View full screen'} + + ); + return (

- Live anomalies Live + Live anomalies{' '} + + Live +

} @@ -168,101 +204,160 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => {

- {'Live anomaly results across detectors for the last 30 minutes'} + {'Live anomaly results across detectors for the last 30 minutes. ' + + 'The results refresh every 1 minute. ' + + 'For each detector, if an anomaly occurence 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} > - - - - - - - - - - - -
- {elasticsearchState.requesting ? ( - - - - - - - - - - - - ) : ( - [ - -

10 detectors with the most recent anomaly occurrence

-
, - - - - + + + + + ) : !hasLatestAnomalyData ? ( + +

+ 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 + anomalousDetectorCount >= MAX_LIVE_DETECTORS ? ( + +

+ 10 detectors with the most recent anomalies are shown on the + chart. Adjust filters if there are specific detectors you + would like to monitor. +

+
+ ) : anomalousDetectorCount === 0 ? ( + // all the data points have anomaly grade as 0 + + ) : null, +
+ + + + + + 0 + // we make `id` to blank string to hide the legend of placeholder data point + id={!isEmpty(liveAnomalyData) ? '' : ' '} + xScaleType={ScaleType.Time} + timeZone="local" + yScaleType="linear" + xAccessor={AD_DOC_FIELDS.PLOT_TIME} + yAccessors={[AD_DOC_FIELDS.ANOMALY_GRADE]} + splitSeriesAccessors={[AD_DOC_FIELDS.DETECTOR_NAME]} + data={prepareVisualizedAnomalies(visualizedAnomalies)} + /> + +
, + ]} +
, + ] + )} ); }; diff --git a/public/pages/Dashboard/Components/AnomalousDetectorsList.tsx b/public/pages/Dashboard/Components/AnomalousDetectorsList.tsx index 4197086e..f1dfb309 100644 --- a/public/pages/Dashboard/Components/AnomalousDetectorsList.tsx +++ b/public/pages/Dashboard/Components/AnomalousDetectorsList.tsx @@ -76,22 +76,24 @@ export const AnomalousDetectorsList = (props: AnomalousDetectorsListProps) => { }; return ( - - - +
+ + + +
); }; diff --git a/public/pages/Dashboard/Container/Dashboard.tsx b/public/pages/Dashboard/Container/Dashboard.tsx index b70b29be..ab19bb84 100644 --- a/public/pages/Dashboard/Container/Dashboard.tsx +++ b/public/pages/Dashboard/Container/Dashboard.tsx @@ -22,34 +22,28 @@ import { EmptyDashboard } from '../Components/EmptyDashboard/EmptyDashboard'; import { EuiLoadingSpinner } from '@elastic/eui'; import { DashboardHeader } from '../Components/utils/DashboardHeader'; import { DashboardOverview } from './DashboardOverview'; -//@ts-ignore -import chrome from 'ui/chrome'; -import { BREADCRUMBS } from '../../../utils/constants'; export const Dashboard = () => { - // Set breadcrumbs on page initialization - useEffect(() => { - chrome.breadcrumbs.set([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DASHBOARD, - ]); - }, []); - const dispatch = useDispatch(); const [isLoading, setIsLoading] = useState(true); const onRefreshPage = async () => { - await dispatch( - getDetectorList({ - from: 0, - size: 1, - search: '', - sortDirection: SORT_DIRECTION.DESC, - sortField: 'name', - }) - ); - setIsLoading(false); + try { + await dispatch( + getDetectorList({ + from: 0, + size: 1, + search: '', + sortDirection: SORT_DIRECTION.DESC, + sortField: 'name', + }) + ); + } catch (error) { + console.log('Error is found during getting detector list', error); + } finally { + setIsLoading(false); + } }; const totalDetectors = useSelector( diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 952e2190..a165ee1d 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -187,8 +187,8 @@ export function DashboardOverview() { return ( - - + + - + - + diff --git a/public/pages/Dashboard/index.scss b/public/pages/Dashboard/index.scss index ad1192b1..492cbd13 100644 --- a/public/pages/Dashboard/index.scss +++ b/public/pages/Dashboard/index.scss @@ -14,23 +14,29 @@ */ .live-anomaly-results-subtile { - height: 14px; - width: 376px; color: #879196; - font-family: 'Helvetica Neue'; font-size: 12px; letter-spacing: 0; - line-height: 14px; + line-height: 16px; } .anomaly-distribution-subtitle { color: #879196; - font-family: 'Helvetica Neue'; font-size: 12px; letter-spacing: 0; + line-height: 16px; } .anomalies-distribution-sunburst { - height: 500px; - width: 500px; + height: 400px; + width: 400px; +} + +.full-screen { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; } diff --git a/public/pages/Dashboard/utils/utils.tsx b/public/pages/Dashboard/utils/utils.tsx index b83adb85..bffbd16c 100644 --- a/public/pages/Dashboard/utils/utils.tsx +++ b/public/pages/Dashboard/utils/utils.tsx @@ -34,6 +34,7 @@ import { import { get, orderBy } from 'lodash'; import { APIAction } from 'public/redux/middleware/types'; import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; /** * Get the recent anomaly result query for the last timeRange period(Date-Math) @@ -344,7 +345,7 @@ export const anomalousDetectorsStaticColumn = [ textOnly: true, render: (name: string, detector: Detector) => ( {name} @@ -434,7 +435,8 @@ export const getFloorPlotTime = (plotTime: number): number => { export const buildGetAnomalyResultQueryByRange = ( timeRange: string, from: number, - size: number + size: number, + threadhold: number ) => { return { index: `${ANOMALY_RESULT_INDEX}*`, @@ -446,7 +448,7 @@ export const buildGetAnomalyResultQueryByRange = ( { range: { [AD_DOC_FIELDS.ANOMALY_GRADE]: { - gt: 0.0, + gt: threadhold, }, }, }, @@ -473,13 +475,14 @@ export const buildGetAnomalyResultQueryByRange = ( }; }; -//TODO: clean unused code like server/routes/ad.ts#getAnomalyResults.range export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( func: (request: any) => APIAction, selectedDetectors: DetectorListItem[], timeRange: string, - detectorNum = MAX_DETECTORS, - dispatch = useDispatch() + dispatch: Dispatch, + threadhold: number, + anomalySize: number, + detectorNum: number ): Promise => { const detectorAndIdMap = buildDetectorAndIdMap(selectedDetectors); let from = 0; @@ -487,7 +490,14 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( let anomalyResults = [] as object[]; do { const searchResponse = await dispatch( - func(buildGetAnomalyResultQueryByRange(timeRange, from, MAX_ANOMALIES)) + func( + buildGetAnomalyResultQueryByRange( + timeRange, + from, + anomalySize, + threadhold + ) + ) ); const searchAnomalyResponse = searchResponse.data.response; @@ -535,7 +545,7 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( ); const finalLiveAnomalyResult = orderedLiveAnomalyData.filter(anomalyData => - latestDetetorIds.includes(get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '')) + latestDetetorIds.has(get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '')) ); return finalLiveAnomalyResult; }; @@ -554,27 +564,21 @@ const buildDetectorAndIdMap = ( const selectLatestDetetorIds = ( orderedAnomalyData: object[], - needeDetectorNum: number -): string[] => { - const uniqueIds = [ - ...new Set( - orderedAnomalyData.map(anomalyData => - get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') - ) - ), - ]; - if (uniqueIds.length <= needeDetectorNum) { + neededDetectorNum: number +): Set => { + const uniqueIds = new Set( + orderedAnomalyData.map(anomalyData => + get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') + ) + ); + if (uniqueIds.size <= neededDetectorNum) { return uniqueIds; } - const latestDetectorIds = [] as string[]; + const latestDetectorIds = new Set(); for (let anomalyData of orderedAnomalyData) { - if ( - !latestDetectorIds.includes( - get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') - ) - ) { - } - if (latestDetectorIds.length === needeDetectorNum) { + const detectorId = get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, ''); + latestDetectorIds.add(detectorId); + if (latestDetectorIds.size === neededDetectorNum) { return latestDetectorIds; } }