Skip to content

Commit

Permalink
Add Dashboard page with following components;
Browse files Browse the repository at this point in the history
1. Anomaly Live Chart for 10 detectors with most recent anomaly occurrence in last 30 mins
2. Anomaly Distribution Chart for anomalies by index and detector
3. Detector/Feature list to redirect user to detector details page

PR: opendistro-for-elasticsearch#17
  • Loading branch information
yizheliu-amazon committed Apr 16, 2020
1 parent 93317a0 commit a54387a
Show file tree
Hide file tree
Showing 21 changed files with 1,804 additions and 83 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@babel/preset-typescript": "^7.3.3",
"@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
"@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana",
"@elastic/eui": "6.10.7",
"@kbn/config-schema": "link:../../packages/kbn-config-schema",
"@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers",
"@testing-library/jest-dom": "^4.0.0",
Expand Down Expand Up @@ -71,10 +70,11 @@
"typescript": "3.0.3"
},
"dependencies": {
"@elastic/charts": "^18.2.2",
"babel-polyfill": "^6.26.0",
"formik": "^1.5.8",
"query-string": "^6.8.2",
"react-redux": "^7.1.0",
"reselect": "^4.0.0",
"babel-polyfill": "^6.26.0"
"reselect": "^4.0.0"
}
}
2 changes: 2 additions & 0 deletions public/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@
* permissions and limitations under the License.
*/

@import 'components/ContentPanel/index.scss';
@import 'pages/createDetector/index.scss';
@import 'pages/PreviewDetector/index.scss';
@import 'pages/Dashboard/index.scss';
3 changes: 3 additions & 0 deletions public/components/ContentPanel/ContentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
} from '@elastic/eui';

type ContentPanelProps = {
// keep title string part for backwards compatibility
// might need to refactor code and
// deprecate support for 'string' in the near future
title: string | React.ReactNode | React.ReactNode[];
titleSize?: EuiTitleSize;
subTitle?: React.ReactNode | React.ReactNode[];
Expand Down
25 changes: 25 additions & 0 deletions public/components/ContentPanel/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* 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;
}
1 change: 1 addition & 0 deletions public/models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type DetectorListItem = {
name: string;
indices: string[];
curState: DETECTOR_STATE;
featureAttributes: FeatureAttributes[];
totalAnomalies: number;
lastActiveAnomaly: number;
lastUpdateTime: number;
Expand Down
244 changes: 244 additions & 0 deletions public/pages/Dashboard/Components/AnomaliesDistribution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import { DetectorListItem } from '../../../models/interfaces';
import { useState, useEffect } from 'react';
import {
fillOutColors,
visualizeAnomalyResultForSunburstChart,
getLatestAnomalyResultsForDetectorsByTimeRange,
} from '../utils/utils';
import ContentPanel from '../../../components/ContentPanel/ContentPanel';
import {
EuiSelect,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingChart,
//@ts-ignore
EuiStat,
} from '@elastic/eui';
import { Chart, Partition, PartitionLayout } from '@elastic/charts';
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 { searchES } from '../../../redux/reducers/elasticsearch';
import { MAX_DETECTORS } from '../../../utils/constants';
import { AD_DOC_FIELDS } from '../../../../server/utils/constants';
export interface AnomaliesDistributionChartProps {
allDetectorsSelected: boolean;
selectedDetectors: DetectorListItem[];
}

export const AnomaliesDistributionChart = (
props: AnomaliesDistributionChartProps
) => {
const dispatch = useDispatch();

const [anomalyResults, setAnomalyResults] = useState([] as object[]);

// TODO: try to find a better way of using redux,
// which can leverage redux, and also get rid of issue with multiple redux on same page,
// so that we don't need to manualy update loading status
// Issue link: https://github.com/opendistro-for-elasticsearch/anomaly-detection-kibana-plugin/issues/23
const [anomalyResultsLoading, setAnomalyResultsLoading] = useState(true);
const [finalDetectors, setFinalDetectors] = useState(
[] as DetectorListItem[]
);

const [indicesNumber, setIndicesNumber] = useState(0);

const [timeRange, setTimeRange] = useState(TIME_RANGE_OPTIONS[0].value);

const getAnomalyResult = async (currentDetectors: DetectorListItem[]) => {
const finalAnomalyResult = await getLatestAnomalyResultsForDetectorsByTimeRange(
searchES,
props.selectedDetectors,
timeRange,
MAX_DETECTORS,
dispatch
);
setAnomalyResults(finalAnomalyResult);

const resultDetectors = getFinalDetectors(
finalAnomalyResult,
props.selectedDetectors
);
setIndicesNumber(getFinalIndices(resultDetectors).size);
setFinalDetectors(resultDetectors);
setAnomalyResultsLoading(false);
};

const getFinalIndices = (detectorList: DetectorListItem[]) => {
const indicesSet = new Set();
detectorList.forEach(detectorItem => {
indicesSet.add(detectorItem.indices.toString());
});

return indicesSet;
};

const getFinalDetectors = (
finalLiveAnomalyResult: object[],
detectorList: DetectorListItem[]
): DetectorListItem[] => {
const detectorSet = new Set<string>();
finalLiveAnomalyResult.forEach(anomalyResult => {
detectorSet.add(get(anomalyResult, AD_DOC_FIELDS.DETECTOR_ID, ''));
});

const filteredDetectors = detectorList.filter(detector =>
detectorSet.has(detector.id)
);

return filteredDetectors;
};

const handleOnChange = (e: any) => {
setTimeRange(e.target.value);
};

useEffect(() => {
getAnomalyResult(props.selectedDetectors);
}, [timeRange, props.selectedDetectors]);

return (
<ContentPanel
title="Anomalies by index and detector"
titleSize="s"
subTitle={
<EuiFlexItem>
<EuiText className={'anomaly-distribution-subtitle'}>
<p>
{
'The inner circle shows the anomaly distribution by your indices. The outer circle shows the anomaly distribution by your detector'
}
</p>
</EuiText>
</EuiFlexItem>
}
actions={
<EuiSelect
style={{ width: 150 }}
id="timeRangeSelect"
options={TIME_RANGE_OPTIONS}
value={timeRange}
onChange={handleOnChange}
fullWidth
/>
}
>
<EuiFlexGroup style={{ padding: '10px' }}>
<EuiFlexItem>
<EuiStat
description={'Indices with anomalies'}
title={indicesNumber}
isLoading={anomalyResultsLoading}
titleSize="s"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
description={'Detectors with anomalies'}
title={finalDetectors.length}
isLoading={anomalyResultsLoading}
titleSize="s"
/>
</EuiFlexItem>
</EuiFlexGroup>
{anomalyResultsLoading ? (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingChart size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLoadingChart size="l" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLoadingChart size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<Chart className="anomalies-distribution-sunburst">
<Partition
id="Anomalies by index and detector"
data={visualizeAnomalyResultForSunburstChart(
anomalyResults,
finalDetectors
)}
valueAccessor={(d: Datum) => d.count as number}
valueFormatter={(d: number) => d.toString()}
layers={[
{
groupByRollup: (d: Datum) => d.indices,
nodeLabel: (d: Datum) => {
return d;
},
fillLabel: {
textInvertible: false,
},
shape: {
fillColor: d => {
return fillOutColors(
d,
(d.x0 + d.x1) / 3 / (2 * Math.PI),
[]
);
},
},
},
{
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: 0.6,
fillLabel: {
textInvertible: true,
fontStyle: 'italic',
},
// 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
}}
/>
</Chart>
</EuiFlexItem>
</EuiFlexGroup>
)}
</ContentPanel>
);
};
Loading

0 comments on commit a54387a

Please sign in to comment.