diff --git a/public/components/ContentPanel/ContentPanel.tsx b/public/components/ContentPanel/ContentPanel.tsx index cd9b02ca..9106f4ca 100644 --- a/public/components/ContentPanel/ContentPanel.tsx +++ b/public/components/ContentPanel/ContentPanel.tsx @@ -14,8 +14,9 @@ */ import React from 'react'; -//@ts-ignore + import { + //@ts-ignore EuiTitleSize, EuiFlexGroup, EuiFlexItem, @@ -38,14 +39,19 @@ type ContentPanelProps = { titleContainerStyles?: React.CSSProperties; actions?: React.ReactNode | React.ReactNode[]; children: React.ReactNode | React.ReactNode[]; + contentPanelClassName?: string; }; const ContentPanel = (props: ContentPanelProps) => ( @@ -53,7 +59,7 @@ const ContentPanel = (props: ContentPanelProps) => ( {typeof props.title === 'string' ? (

{props.title}

@@ -105,7 +111,14 @@ const ContentPanel = (props: ContentPanelProps) => ( className={props.horizontalRuleClassName} /> )} -
+
{props.children}
diff --git a/public/components/ContentPanel/__snapshots__/ContentPanel.test.tsx.snap b/public/components/ContentPanel/__snapshots__/ContentPanel.test.tsx.snap index fa070b98..5722660a 100644 --- a/public/components/ContentPanel/__snapshots__/ContentPanel.test.tsx.snap +++ b/public/components/ContentPanel/__snapshots__/ContentPanel.test.tsx.snap @@ -13,7 +13,7 @@ exports[` spec renders the component 1`] = ` class="euiFlexItem" >

Testing

diff --git a/public/components/ContentPanel/index.scss b/public/components/ContentPanel/index.scss index a4a3ab4a..8af7601c 100644 --- a/public/components/ContentPanel/index.scss +++ b/public/components/ContentPanel/index.scss @@ -13,13 +13,8 @@ * permissions and limitations under the License. */ -.content-panel-title { - color: #3f3f3f; -} - .content-panel-subTitle { color: #879196; - font-family: 'Helvetica Neue'; font-size: 12px; letter-spacing: 0; } diff --git a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx index 190f6ed4..5a0846d4 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 detector' - } + {'The inner circle shows the anomaly distribution by your indices. ' + + 'The outer circle shows the anomaly distribution by your detectors.'}

@@ -141,7 +146,7 @@ export const AnomaliesDistributionChart = ( /> } > - + {anomalyResultsLoading ? ( - - - - - - @@ -174,67 +173,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 640551c2..04263fd8 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; @@ -69,7 +72,7 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const dispatch = useDispatch(); const [liveTimeRange, setLiveTimeRange] = useState({ - startDateTime: moment().subtract(30, 'minutes'), + startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), }); @@ -81,23 +84,44 @@ 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'), + startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), }); }; @@ -137,30 +161,42 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { continue; } result.push({ - [AD_DOC_FIELDS.DETECTOR_NAME]: '', + [AD_DOC_FIELDS.DETECTOR_NAME]: !isEmpty(liveAnomalyData) ? '' : null, [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'), + details: liveTimeRange.endDateTime.format('MM/DD/YY h:mm A'), } as LineAnnotationDatum; 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,154 @@ 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 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} > - - - - - - - - - - - -
- {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 ? ( + +

+ {`${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.`} +

+
+ ) : anomalousDetectorCount === 0 ? ( + // all the data points have anomaly grade as 0 + + ) : null, +
+ + + + + + + +
, + ]} +
, + ] + )}
); }; 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 acdd07ad..ab19bb84 100644 --- a/public/pages/Dashboard/Container/Dashboard.tsx +++ b/public/pages/Dashboard/Container/Dashboard.tsx @@ -29,16 +29,21 @@ export const Dashboard = () => { 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..e5cbfb8f 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -23,7 +23,6 @@ import { DetectorListItem } from '../../../models/interfaces'; import { getIndices, getAliases } from '../../../redux/reducers/elasticsearch'; import { getDetectorList } from '../../../redux/reducers/ad'; import { - EuiButton, EuiFlexGroup, EuiFlexItem, EuiComboBox, @@ -40,11 +39,7 @@ import { import { AppState } from '../../../redux/reducers'; import { CatIndex, IndexAlias } from '../../../../server/models/types'; import { getVisibleOptions } from '../../utils/helpers'; -import { - DETECTOR_STATE, - PLUGIN_NAME, - APP_PATH, -} from '../../../utils/constants'; +import { DETECTOR_STATE } from '../../../utils/constants'; import { getDetectorStateOptions } from '../../DetectorsList/utils/helpers'; export function DashboardOverview() { @@ -187,8 +182,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 a19452ff..e72f8864 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, + threshold: number ) => { return { index: `${ANOMALY_RESULT_INDEX}*`, @@ -446,7 +448,7 @@ export const buildGetAnomalyResultQueryByRange = ( { range: { [AD_DOC_FIELDS.ANOMALY_GRADE]: { - gt: 0.0, + gt: threshold, }, }, }, @@ -477,8 +479,10 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( func: (request: any) => APIAction, selectedDetectors: DetectorListItem[], timeRange: string, - detectorNum = MAX_DETECTORS, - dispatch = useDispatch() + dispatch: Dispatch, + threshold: number, + anomalySize: number, + detectorNum: number ): Promise => { const detectorAndIdMap = buildDetectorAndIdMap(selectedDetectors); let from = 0; @@ -486,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, + threshold + ) + ) ); const searchAnomalyResponse = searchResponse.data.response; @@ -528,13 +539,13 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( SORT_DIRECTION.DESC ); - const latestDetetorIds = selectLatestDetetorIds( + const latestDetetorIds = selectLatestDetectorIds( orderedLiveAnomalyData, detectorNum ); 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; }; @@ -551,31 +562,15 @@ const buildDetectorAndIdMap = ( return detectorAndIdMap; }; -const selectLatestDetetorIds = ( +const selectLatestDetectorIds = ( orderedAnomalyData: object[], - needeDetectorNum: number -): string[] => { - const uniqueIds = [ - ...new Set( - orderedAnomalyData.map(anomalyData => - get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') - ) - ), - ]; - if (uniqueIds.length <= needeDetectorNum) { - return uniqueIds; - } - const latestDetectorIds = [] as string[]; - for (let anomalyData of orderedAnomalyData) { - if ( - !latestDetectorIds.includes( - get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') - ) - ) { - } - if (latestDetectorIds.length === needeDetectorNum) { - return latestDetectorIds; - } - } - return latestDetectorIds; + neededDetectorNum: number +): Set => { + const uniqueIds = new Set( + orderedAnomalyData.map(anomalyData => + get(anomalyData, AD_DOC_FIELDS.DETECTOR_ID, '') + ) + ); + + return new Set(Array.from(uniqueIds).slice(0, neededDetectorNum)); }; diff --git a/public/pages/createDetector/components/DetectorInfo/__tests__/__snapshots__/DetectorInfo.test.tsx.snap b/public/pages/createDetector/components/DetectorInfo/__tests__/__snapshots__/DetectorInfo.test.tsx.snap index c4a36374..078b1aad 100644 --- a/public/pages/createDetector/components/DetectorInfo/__tests__/__snapshots__/DetectorInfo.test.tsx.snap +++ b/public/pages/createDetector/components/DetectorInfo/__tests__/__snapshots__/DetectorInfo.test.tsx.snap @@ -14,7 +14,7 @@ exports[` spec renders the component 1`] = ` class="euiFlexItem" >

Name and description

diff --git a/public/pages/createDetector/containers/__tests__/__snapshots__/CreateDetector.test.tsx.snap b/public/pages/createDetector/containers/__tests__/__snapshots__/CreateDetector.test.tsx.snap index 3dba8584..a511cf53 100644 --- a/public/pages/createDetector/containers/__tests__/__snapshots__/CreateDetector.test.tsx.snap +++ b/public/pages/createDetector/containers/__tests__/__snapshots__/CreateDetector.test.tsx.snap @@ -33,7 +33,7 @@ exports[` spec create detector renders the component 1`] = ` class="euiFlexItem" >

Name and description

@@ -164,7 +164,7 @@ exports[` spec create detector renders the component 1`] = ` class="euiFlexItem" >

Data Source

@@ -525,7 +525,7 @@ exports[` spec create detector renders the component 1`] = ` class="euiFlexItem" >

Detector operation settings