Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Add sample detectors and indices #294

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cypress/integration/ad/dashboard/ad_dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ context('AD Dashboard', () => {
});

cy.mockSearchIndexOnAction('search_index_response.json', () => {
cy.get('a[data-test-subj="add_detector"]').click({
cy.get('a[data-test-subj="createDetectorButton"]').click({
force: true,
});
});
2 changes: 2 additions & 0 deletions public/components/ContentPanel/ContentPanel.tsx
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ type ContentPanelProps = {
title: string | React.ReactNode | React.ReactNode[];
titleSize?: EuiTitleSize;
subTitle?: React.ReactNode | React.ReactNode[];
badgeLabel?: string;
bodyStyles?: React.CSSProperties;
panelStyles?: React.CSSProperties;
horizontalRuleClassName?: string;
@@ -45,6 +46,7 @@ const ContentPanel = (props: ContentPanelProps) => (
<EuiPanel
style={{ padding: '20px', ...props.panelStyles }}
className={props.contentPanelClassName}
betaBadgeLabel={props.badgeLabel}
>
<EuiFlexGroup
style={{ padding: '0px', ...props.titleContainerStyles }}
44 changes: 44 additions & 0 deletions public/components/CreateDetectorButtons/CreateDetectorButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import React from 'react';
import { APP_PATH, PLUGIN_NAME } from '../../utils/constants';

export const CreateDetectorButtons = () => {
return (
<EuiFlexGroup direction="row" gutterSize="m" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
style={{ width: '200px' }}
href={`${PLUGIN_NAME}#${APP_PATH.SAMPLE_DETECTORS}`}
data-test-subj="sampleDetectorButton"
>
Try a sample detector
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
style={{ width: '200px' }}
fill
href={`${PLUGIN_NAME}#${APP_PATH.CREATE_DETECTOR}`}
data-test-subj="createDetectorButton"
>
Create detector
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -13,9 +13,9 @@
* permissions and limitations under the License.
*/

import { EuiButton, EuiEmptyPrompt, EuiLink, EuiIcon } from '@elastic/eui';
import { EuiEmptyPrompt, EuiLink, EuiIcon } from '@elastic/eui';
import React, { Component, Fragment } from 'react';
import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants';
import { CreateDetectorButtons } from '../../../../components/CreateDetectorButtons/CreateDetectorButtons';

export class EmptyDashboard extends Component<{}, {}> {
render() {
@@ -27,7 +27,7 @@ export class EmptyDashboard extends Component<{}, {}> {
<p>Create detector first to detect anomalies in your data.</p>
<p>
Dashboard will generate insights on the anomalies across all of
your detectors
your detectors.
</p>
<p>
Read about{' '}
@@ -41,15 +41,7 @@ export class EmptyDashboard extends Component<{}, {}> {
</p>
</Fragment>
}
actions={
<EuiButton
fill
href={`${PLUGIN_NAME}#${APP_PATH.CREATE_DETECTOR}`}
data-test-subj="add_detector"
>
Create detector
</EuiButton>
}
actions={<CreateDetectorButtons />}
/>
);
}
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ exports[`<EmptyDetector /> spec Empty results renders component with empty messa
Create detector first to detect anomalies in your data.
</p>
<p>
Dashboard will generate insights on the anomalies across all of your detectors
Dashboard will generate insights on the anomalies across all of your detectors.
</p>
<p>
Read about
@@ -55,21 +55,51 @@ exports[`<EmptyDetector /> spec Empty results renders component with empty messa
<div
class="euiSpacer euiSpacer--s"
/>
<a
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="add_detector"
href="opendistro-anomaly-detection-kibana#/create-ad/"
rel="noreferrer"
<div
class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<span
class="euiButton__content"
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
class="euiButton__text"
<a
class="euiButton euiButton--primary"
data-test-subj="sampleDetectorButton"
href="opendistro-anomaly-detection-kibana#/sample-detectors"
rel="noreferrer"
style="width: 200px;"
>
Create detector
</span>
</span>
</a>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Try a sample detector
</span>
</span>
</a>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="createDetectorButton"
href="opendistro-anomaly-detection-kibana#/create-ad/"
rel="noreferrer"
style="width: 200px;"
>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Create detector
</span>
</span>
</a>
</div>
</div>
</div>
`;
27 changes: 27 additions & 0 deletions public/pages/DetectorResults/containers/AnomalyResults.tsx
Original file line number Diff line number Diff line change
@@ -63,6 +63,11 @@ import {
FEATURE_DATA_CHECK_WINDOW_OFFSET,
} from '../../utils/anomalyResultUtils';
import { getDetectorResults } from '../../../redux/reducers/anomalyResults';
import {
detectorIsSample,
getAssociatedIndex,
} from '../../SampleData/utils/helpers';
import { SampleIndexDetailsCallout } from '../../SampleData/components/SampleIndexDetailsCallout/SampleIndexDetailsCallout';

interface AnomalyResultsProps extends RouteComponentProps {
detectorId: string;
@@ -112,13 +117,26 @@ export function AnomalyResults(props: AnomalyResultsProps) {
}
}, [detector]);

useEffect(() => {
if (detector && detectorIsSample(detector)) {
setIsSampleDetector(true);
setSampleIndexName(getAssociatedIndex(detector));
} else {
setIsSampleDetector(false);
}
}, [detector]);

const monitors = useSelector((state: AppState) => state.alerting.monitors);
const monitor = get(monitors, `${detectorId}.0`);

const [featureMissingSeverity, setFeatureMissingSeverity] = useState<
MISSING_FEATURE_DATA_SEVERITY
>();

const [isSampleDetector, setIsSampleDetector] = useState<boolean>(false);

const [sampleIndexName, setSampleIndexName] = useState<string>('');

const [featureNamesAtHighSev, setFeatureNamesAtHighSev] = useState(
[] as string[]
);
@@ -367,6 +385,15 @@ export function AnomalyResults(props: AnomalyResultsProps) {
isDetectorInitializing ||
isDetectorFailed ? (
<Fragment>
{isSampleDetector ? (
<Fragment>
{' '}
<SampleIndexDetailsCallout
indexName={sampleIndexName}
/>{' '}
<EuiSpacer size="l" />{' '}
</Fragment>
) : null}
{isDetectorUpdated ||
isDetectorMissingData ||
isInitializingNormally ||
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@

import { EuiButton, EuiEmptyPrompt, EuiText } from '@elastic/eui';
import React from 'react';
import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants';
import { CreateDetectorButtons } from '../../../../components/CreateDetectorButtons/CreateDetectorButtons';

const filterText =
'There are no detectors matching your applied filters. Reset your filters to view all detectors.';
@@ -45,13 +45,7 @@ export const EmptyDetectorMessage = (props: EmptyDetectorProps) => (
Reset filters
</EuiButton>
) : (
<EuiButton
fill
href={`${PLUGIN_NAME}#${APP_PATH.CREATE_DETECTOR}`}
data-test-subj="createDetectorButton"
>
Create detector
</EuiButton>
<CreateDetectorButtons />
)
}
/>
Original file line number Diff line number Diff line change
@@ -26,22 +26,52 @@ exports[`<EmptyDetectorMessage /> spec Empty results renders component with empt
<div
class="euiSpacer euiSpacer--s"
/>
<a
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="createDetectorButton"
href="opendistro-anomaly-detection-kibana#/create-ad/"
rel="noreferrer"
<div
class="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<span
class="euiButton__content"
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<span
class="euiButton__text"
<a
class="euiButton euiButton--primary"
data-test-subj="sampleDetectorButton"
href="opendistro-anomaly-detection-kibana#/sample-detectors"
rel="noreferrer"
style="width: 200px;"
>
Create detector
</span>
</span>
</a>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Try a sample detector
</span>
</span>
</a>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
class="euiButton euiButton--primary euiButton--fill"
data-test-subj="createDetectorButton"
href="opendistro-anomaly-detection-kibana#/create-ad/"
rel="noreferrer"
style="width: 200px;"
>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Create detector
</span>
</span>
</a>
</div>
</div>
</div>
`;

118 changes: 118 additions & 0 deletions public/pages/SampleData/components/SampleDataBox/SampleDataBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 React from 'react';
import ContentPanel from '../../../../components/ContentPanel/ContentPanel';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiLink,
} from '@elastic/eui';
import { PLUGIN_NAME } from '../../../../utils/constants';

interface SampleDataBoxProps {
title: string;
icon: any;
description: string;
loadDataButtonDescription: string;
onOpenFlyout(): void;
onLoadData(): void;
isLoadingData: boolean;
isDataLoaded: boolean;
detectorId: string;
}

export const SampleDataBox = (props: SampleDataBoxProps) => {
return (
<div style={{ height: 'auto' }}>
<ContentPanel
title={
<div
style={{
display: 'flex',
flexDirection: 'row',
alignContent: 'flexStart',
}}
>
{props.icon}
<EuiTitle size="s">
<h2 style={{ marginLeft: '12px', marginTop: '-3px' }}>
{props.title}
</h2>
</EuiTitle>
<EuiLink
style={{ marginLeft: '12px' }}
onClick={props.onOpenFlyout}
>
Info
</EuiLink>
</div>
}
titleSize="s"
badgeLabel={props.isDataLoaded ? 'INSTALLED' : undefined}
>
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem grow={false} style={{ height: '70px' }}>
<p
style={{
textAlign: 'left',
lineHeight: 1.4,
maxHeight: 4.2,
}}
>
{props.description}
</p>
</EuiFlexItem>
<EuiFlexGroup
style={{
height: '100px',
marginTop: '0px',
marginBottom: '0px',
}}
direction="column"
alignItems="center"
>
<EuiFlexItem grow={false}>
<EuiButton
style={{ width: '300px' }}
data-test-subj="loadDataButton"
disabled={props.isLoadingData || props.isDataLoaded}
isLoading={props.isLoadingData}
onClick={() => {
props.onLoadData();
}}
>
{props.isLoadingData
? 'Creating detector'
: props.isDataLoaded
? 'Detector created'
: props.loadDataButtonDescription}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{props.isDataLoaded ? (
<EuiLink href={`${PLUGIN_NAME}#/detectors/${props.detectorId}`}>
View detector and sample data
</EuiLink>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</ContentPanel>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { EuiIcon } from '@elastic/eui';
import { SampleDataBox } from '../SampleDataBox';

const defaultProps = {
title: 'Sample title',
icon: <EuiIcon type="alert" />,
description: 'Sample description',
loadDataButtonDescription: 'Sample button description',
onLoadData: jest.fn(),
isLoadingData: false,
isDataLoaded: false,
detectorId: 'sample-detector-id',
};

describe('<SampleDataBox /> spec', () => {
describe('Data not loaded', () => {
test('renders component', () => {
const { container, getByText } = render(
<SampleDataBox {...defaultProps} />
);
expect(container.firstChild).toMatchSnapshot();
getByText('Sample title');
getByText('Sample description');
getByText('Sample button description');
});
});
describe('Data is loading', () => {
test('renders component', () => {
const { container, getByText } = render(
<SampleDataBox {...defaultProps} isLoadingData={true} />
);
expect(container.firstChild).toMatchSnapshot();
getByText('Sample title');
getByText('Sample description');
getByText('Creating detector');
});
});
describe('Data is loaded', () => {
test('renders component', () => {
const { container, getByText } = render(
<SampleDataBox {...defaultProps} isDataLoaded={true} />
);
expect(container.firstChild).toMatchSnapshot();
getByText('Sample title');
getByText('Sample description');
getByText('Detector created');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<SampleDataBox /> spec Data is loaded renders component 1`] = `
<div
style="height: auto;"
>
<div
class="euiPanel euiPanel--paddingMedium euiPanel--hasBetaBadge"
style="padding: 20px;"
>
<span
class="euiPanel__betaBadgeWrapper"
>
<span
class="euiBetaBadge euiPanel__betaBadge"
title="INSTALLED"
>
INSTALLED
</span>
</span>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
style="padding: 0px;"
>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
>
<div
style="display: flex;"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<h2
class="euiTitle euiTitle--small"
style="margin-left: 12px; margin-top: -3px;"
>
Sample title
</h2>
<button
class="euiLink euiLink--primary"
style="margin-left: 12px;"
type="button"
>
Info
</button>
</div>
</div>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
</div>
<hr
class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall"
/>
<div
style="padding: 10px 0px;"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
style="height: 70px;"
>
<p
style="text-align: left; line-height: 1.4; max-height: 4.2px;"
>
Sample description
</p>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
style="height: 100px; margin-top: 0px; margin-bottom: 0px;"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
class="euiButton euiButton--primary"
data-test-subj="loadDataButton"
disabled=""
style="width: 300px;"
type="button"
>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Detector created
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
class="euiLink euiLink--primary"
href="opendistro-anomaly-detection-kibana#/detectors/sample-detector-id"
rel="noreferrer"
>
View detector and sample data
</a>
</div>
</div>
</div>
</div>
</div>
</div>
`;

exports[`<SampleDataBox /> spec Data is loading renders component 1`] = `
<div
style="height: auto;"
>
<div
class="euiPanel euiPanel--paddingMedium"
style="padding: 20px;"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
style="padding: 0px;"
>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
>
<div
style="display: flex;"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<h2
class="euiTitle euiTitle--small"
style="margin-left: 12px; margin-top: -3px;"
>
Sample title
</h2>
<button
class="euiLink euiLink--primary"
style="margin-left: 12px;"
type="button"
>
Info
</button>
</div>
</div>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
</div>
<hr
class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall"
/>
<div
style="padding: 10px 0px;"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
style="height: 70px;"
>
<p
style="text-align: left; line-height: 1.4; max-height: 4.2px;"
>
Sample description
</p>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
style="height: 100px; margin-top: 0px; margin-bottom: 0px;"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
class="euiButton euiButton--primary"
data-test-subj="loadDataButton"
disabled=""
style="width: 300px;"
type="button"
>
<span
class="euiButton__content"
>
<span
class="euiLoadingSpinner euiLoadingSpinner--medium euiButton__spinner"
/>
<span
class="euiButton__text"
>
Creating detector
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
/>
</div>
</div>
</div>
</div>
</div>
`;

exports[`<SampleDataBox /> spec Data not loaded renders component 1`] = `
<div
style="height: auto;"
>
<div
class="euiPanel euiPanel--paddingMedium"
style="padding: 20px;"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
style="padding: 0px;"
>
<div
class="euiFlexItem"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
>
<div
style="display: flex;"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<h2
class="euiTitle euiTitle--small"
style="margin-left: 12px; margin-top: -3px;"
>
Sample title
</h2>
<button
class="euiLink euiLink--primary"
style="margin-left: 12px;"
type="button"
>
Info
</button>
</div>
</div>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
/>
</div>
</div>
</div>
<hr
class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall"
/>
<div
style="padding: 10px 0px;"
>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
style="height: 70px;"
>
<p
style="text-align: left; line-height: 1.4; max-height: 4.2px;"
>
Sample description
</p>
</div>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
style="height: 100px; margin-top: 0px; margin-bottom: 0px;"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
class="euiButton euiButton--primary"
data-test-subj="loadDataButton"
style="width: 300px;"
type="button"
>
<span
class="euiButton__content"
>
<span
class="euiButton__text"
>
Sample button description
</span>
</span>
</button>
</div>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
/>
</div>
</div>
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 React from 'react';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants';

export const SampleDataCallout = () => {
return (
<EuiCallOut
title="Looking to get more familiar with anomaly detection?"
color="primary"
iconType="help"
>
<p>
Read the{' '}
<EuiLink
href="https://opendistro.github.io/for-elasticsearch-docs/docs/ad/"
target="_blank"
>
documentation
</EuiLink>{' '}
or create a{' '}
<EuiLink href={`${PLUGIN_NAME}#${APP_PATH.SAMPLE_DETECTORS}`}>
sample detector
</EuiLink>{' '}
to get started.
</p>
</EuiCallOut>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Amazon.com, Inc. or its /*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { SampleDataCallout } from '../SampleDataCallout';

describe('<SampleDataCallout /> spec', () => {
describe('Data not loaded', () => {
test('renders component', () => {
const { container, getByText } = render(<SampleDataCallout />);
expect(container.firstChild).toMatchSnapshot();
getByText('Looking to get more familiar with anomaly detection?');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<SampleDataCallout /> spec Data not loaded renders component 1`] = `
<div
class="euiCallOut euiCallOut--primary"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading euiCallOutHeader__icon"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<span
class="euiCallOutHeader__title"
>
Looking to get more familiar with anomaly detection?
</span>
</div>
<div
class="euiText euiText--small"
>
<p>
Read the
<a
class="euiLink euiLink--primary"
href="https://opendistro.github.io/for-elasticsearch-docs/docs/ad/"
rel="noopener noreferrer"
target="_blank"
>
documentation
</a>
or create a
<a
class="euiLink euiLink--primary"
href="opendistro-anomaly-detection-kibana#/sample-detectors"
rel="noreferrer"
>
sample detector
</a>
to get started.
</p>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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 React from 'react';
import { get } from 'lodash';
import {
EuiAccordion,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiText,
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import {
getFieldsAndTypesGrid,
getFeaturesAndAggsAndFieldsGrid,
} from '../../utils/helpers';
import { SAMPLE_DATA } from '../../utils/constants';
import { EuiHorizontalRule } from '@elastic/eui';

interface SampleDetailsFlyoutProps {
title: string;
sampleData: SAMPLE_DATA;
interval: number;
onClose(): void;
}

export const SampleDetailsFlyout = (props: SampleDetailsFlyoutProps) => {
const fieldValues = Object.keys(props.sampleData.fieldMappings);
const fieldTypes = fieldValues.map((field) =>
get(props.sampleData.fieldMappings, `${field}.type`)
);
const featureNames = Object.keys(
get(props.sampleData.detectorConfig, 'uiMetadata.features')
);
const featureAggs = featureNames.map((feature) =>
get(
props.sampleData.detectorConfig,
`uiMetadata.features.${feature}.aggregationBy`
)
);
const featureFields = featureNames.map((feature) =>
get(
props.sampleData.detectorConfig,
`uiMetadata.features.${feature}.aggregationOf`
)
);
const detectorInterval = get(
props.sampleData.detectorConfig,
'detection_interval.period.interval'
);

return (
<EuiFlyout
ownFocus={false}
onClose={props.onClose}
aria-labelledby="flyoutTitle"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="flyoutTitle">{props.title}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiAccordion
id="detectorDetailsAccordion"
buttonContent={
<EuiTitle size="s">
<h3>Detector details</h3>
</EuiTitle>
}
initialIsOpen={true}
paddingSize="m"
>
<EuiText style={{ lineHeight: 2.0 }}>
<b>Name: </b>
<i>{props.sampleData.detectorName}</i>
<br></br>
<b>Detection interval: </b>
Every {detectorInterval} minutes
<br></br>
<b>Feature details: </b>
</EuiText>
<EuiSpacer size="s" />
{getFeaturesAndAggsAndFieldsGrid(
featureNames,
featureAggs,
featureFields
)}
</EuiAccordion>
<EuiHorizontalRule margin="m" />
<EuiAccordion
id="indexDetailsAccordion"
buttonContent={
<EuiTitle size="s">
<h3>Index details</h3>
</EuiTitle>
}
initialIsOpen={false}
paddingSize="m"
>
<EuiText style={{ lineHeight: 2.0 }}>
<b>Name: </b>
<i>{props.sampleData.indexName}</i>
<br></br>
<b>Log frequency: </b>Every {props.interval} minute(s)
<br></br>
<b>Log duration: </b>3 weeks
<br></br>
<b>Field details: </b>
</EuiText>
<EuiSpacer size="s" />
{getFieldsAndTypesGrid(fieldValues, fieldTypes)}
</EuiAccordion>
</EuiFlyoutBody>
</EuiFlyout>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 React from 'react';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { KIBANA_NAME, KIBANA_PATH } from '../../../../utils/constants';

interface SampleIndexDetailsCalloutProps {
indexName: string;
}

export const SampleIndexDetailsCallout = (
props: SampleIndexDetailsCalloutProps
) => {
return (
<EuiCallOut
title="Want more details on the sample data?"
color="primary"
iconType="help"
>
<p>
Check out the{' '}
<EuiLink
href={`${KIBANA_NAME}#${KIBANA_PATH.DISCOVER}`}
target="_blank"
>
Kibana Discover app
</EuiLink>
{''} to view the raw data for sample index '{props.indexName}'.
</p>
</EuiCallOut>
);
};
306 changes: 306 additions & 0 deletions public/pages/SampleData/containers/SampleData/SampleData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
/*
* 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 {
EuiSpacer,
EuiPageHeader,
EuiTitle,
EuiText,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import React, { Fragment, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
//@ts-ignore
import chrome from 'ui/chrome';
//@ts-ignore
import { toastNotifications } from 'ui/notify';
import { BREADCRUMBS, SAMPLE_TYPE } from '../../../../utils/constants';
import {
GET_SAMPLE_DETECTORS_QUERY_PARAMS,
GET_SAMPLE_INDICES_QUERY,
} from '../../../utils/constants';
import { AppState } from '../../../../redux/reducers';
import { getDetectorList } from '../../../../redux/reducers/ad';
import { createSampleData } from '../../../../redux/reducers/sampleData';

import {
getIndices,
createIndex,
} from '../../../../redux/reducers/elasticsearch';
import { createDetector, startDetector } from '../../../../redux/reducers/ad';
import {
sampleHttpResponses,
sampleEcommerce,
sampleHostHealth,
} from '../../utils/constants';
import {
containsSampleIndex,
containsSampleDetector,
getDetectorId,
} from '../../utils/helpers';
import { SampleDataBox } from '../../components/SampleDataBox/SampleDataBox';
import { SampleDetailsFlyout } from '../../components/SampleDetailsFlyout/SampleDetailsFlyout';

export const SampleData = () => {
const dispatch = useDispatch();
const visibleIndices = useSelector(
(state: AppState) => state.elasticsearch.indices
);
const allDetectors = Object.values(
useSelector((state: AppState) => state.ad.detectorList)
);

const [isLoadingHttpData, setIsLoadingHttpData] = useState<boolean>(false);
const [isLoadingEcommerceData, setIsLoadingEcommerceData] = useState<boolean>(
false
);
const [isLoadingHostHealthData, setIsLoadingHostHealthData] = useState<
boolean
>(false);
const [
showHttpResponseDetailsFlyout,
setShowHttpResponseDetailsFlyout,
] = useState<boolean>(false);
const [showEcommerceDetailsFlyout, setShowEcommerceDetailsFlyout] = useState<
boolean
>(false);
const [
showHostHealthDetailsFlyout,
setShowHostHealthDetailsFlyout,
] = useState<boolean>(false);

const getAllSampleDetectors = async () => {
await dispatch(getDetectorList(GET_SAMPLE_DETECTORS_QUERY_PARAMS)).catch(
(error: any) => {
console.error('Error getting all detectors: ', error);
}
);
};

const getAllSampleIndices = async () => {
await dispatch(getIndices(GET_SAMPLE_INDICES_QUERY)).catch((error: any) => {
console.error('Error getting all indices: ', error);
});
};

// Set breadcrumbs on page initialization
useEffect(() => {
chrome.breadcrumbs.set([
BREADCRUMBS.ANOMALY_DETECTOR,
BREADCRUMBS.SAMPLE_DETECTORS,
]);
}, []);

// Getting all initial sample detectors & indices
useEffect(() => {
getAllSampleDetectors();
getAllSampleIndices();
}, []);

// Create and populate sample index, create and start sample detector
const handleLoadData = async (
sampleType: SAMPLE_TYPE,
indexConfig: any,
detectorConfig: any,
setLoadingState: (isLoading: boolean) => void
) => {
setLoadingState(true);
let errorDuringAction = false;
let errorMessage = '';

// Create the index (if it doesn't exist yet)
if (!containsSampleIndex(visibleIndices, sampleType)) {
await dispatch(createIndex(indexConfig)).catch((error: any) => {
errorDuringAction = true;
errorMessage = 'Error creating sample index.';
console.error('Error creating sample index: ', error);
});
}

// Get the sample data from the server and bulk insert
if (!errorDuringAction) {
await dispatch(createSampleData(sampleType)).catch((error: any) => {
errorDuringAction = true;
errorMessage = error;
console.error('Error bulk inserting data: ', error);
});
}

// Create the detector
if (!errorDuringAction) {
await dispatch(createDetector(detectorConfig))
.then(function (response: any) {
const detectorId = response.data.response.id;
// Start the detector
dispatch(startDetector(detectorId)).catch((error: any) => {
errorDuringAction = true;
errorMessage = error.data.message;
console.error('Error starting sample detector: ', error);
});
})
.catch((error: any) => {
errorDuringAction = true;
errorMessage = error;
console.error('Error creating sample detector: ', error);
});
}

getAllSampleDetectors();
getAllSampleIndices();
setLoadingState(false);
if (!errorDuringAction) {
toastNotifications.addSuccess('Successfully loaded sample detector');
} else {
toastNotifications.addDanger(
`Unable to load all sample data, please try again. ${errorMessage}`
);
}
};

return (
<Fragment>
<EuiPageHeader>
<EuiTitle size="l">
<h1>Sample detectors</h1>
</EuiTitle>
</EuiPageHeader>
<EuiText>
Create a detector with streaming sample data to get a deeper
understanding of how anomaly detection works. You can create and
initialize a detector with configured settings for your selected sample
index.
</EuiText>
<EuiSpacer size="xl" />
<EuiFlexGroup direction="row" gutterSize="l">
<EuiFlexItem>
<SampleDataBox
title="Monitor HTTP responses"
icon={sampleHttpResponses.icon}
description={sampleHttpResponses.description}
loadDataButtonDescription="Create HTTP response detector"
onOpenFlyout={() => {
setShowHttpResponseDetailsFlyout(true);
setShowEcommerceDetailsFlyout(false);
setShowHostHealthDetailsFlyout(false);
}}
onLoadData={() => {
handleLoadData(
SAMPLE_TYPE.HTTP_RESPONSES,
sampleHttpResponses.indexConfig,
sampleHttpResponses.detectorConfig,
setIsLoadingHttpData
);
}}
isLoadingData={isLoadingHttpData}
isDataLoaded={containsSampleDetector(
allDetectors,
SAMPLE_TYPE.HTTP_RESPONSES
)}
detectorId={getDetectorId(
allDetectors,
sampleHttpResponses.detectorName
)}
/>
</EuiFlexItem>
<EuiFlexItem>
<SampleDataBox
title="Monitor eCommerce orders"
icon={sampleEcommerce.icon}
description={sampleEcommerce.description}
loadDataButtonDescription="Create eCommerce orders detector"
onOpenFlyout={() => {
setShowHttpResponseDetailsFlyout(false);
setShowEcommerceDetailsFlyout(true);
setShowHostHealthDetailsFlyout(false);
}}
onLoadData={() => {
handleLoadData(
SAMPLE_TYPE.ECOMMERCE,
sampleEcommerce.indexConfig,
sampleEcommerce.detectorConfig,
setIsLoadingEcommerceData
);
}}
isLoadingData={isLoadingEcommerceData}
isDataLoaded={containsSampleDetector(
allDetectors,
SAMPLE_TYPE.ECOMMERCE
)}
detectorId={getDetectorId(
allDetectors,
sampleEcommerce.detectorName
)}
/>
</EuiFlexItem>
<EuiFlexItem>
<SampleDataBox
title="Monitor host health"
icon={sampleHostHealth.icon}
description={sampleHostHealth.description}
loadDataButtonDescription="Create health monitor detector"
onOpenFlyout={() => {
setShowHttpResponseDetailsFlyout(false);
setShowEcommerceDetailsFlyout(false);
setShowHostHealthDetailsFlyout(true);
}}
onLoadData={() => {
handleLoadData(
SAMPLE_TYPE.HOST_HEALTH,
sampleHostHealth.indexConfig,
sampleHostHealth.detectorConfig,
setIsLoadingHostHealthData
);
}}
isLoadingData={isLoadingHostHealthData}
isDataLoaded={containsSampleDetector(
allDetectors,
SAMPLE_TYPE.HOST_HEALTH
)}
detectorId={getDetectorId(
allDetectors,
sampleHostHealth.detectorName
)}
/>
</EuiFlexItem>
<EuiSpacer size="m" />
</EuiFlexGroup>
{showHttpResponseDetailsFlyout ? (
<SampleDetailsFlyout
title="Monitor HTTP responses"
sampleData={sampleHttpResponses}
interval={1}
onClose={() => setShowHttpResponseDetailsFlyout(false)}
/>
) : null}
{showEcommerceDetailsFlyout ? (
<SampleDetailsFlyout
title="Monitor eCommerce orders"
sampleData={sampleEcommerce}
interval={1}
onClose={() => setShowEcommerceDetailsFlyout(false)}
/>
) : null}
{showHostHealthDetailsFlyout ? (
<SampleDetailsFlyout
title="Monitor host health"
sampleData={sampleHostHealth}
interval={1}
onClose={() => setShowHostHealthDetailsFlyout(false)}
/>
) : null}
</Fragment>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 React from 'react';
import { render, wait } from '@testing-library/react';
import { SampleData } from '../SampleData';
import { Provider } from 'react-redux';
import {
MemoryRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
import { httpClientMock } from '../../../../../../test/mocks';
import configureStore from '../../../../../redux/configureStore';
import {
Detectors,
initialDetectorsState,
} from '../../../../../redux/reducers/ad';
import { sampleHttpResponses } from '../../../utils/constants';

const renderWithRouter = (
initialAdState: Detectors = initialDetectorsState
) => ({
...render(
<Provider store={configureStore(httpClientMock)}>
<Router>
<Switch>
<Route exact path="/sample-detectors" render={() => <SampleData />} />
<Redirect from="/" to="/sample-detectors" />
</Switch>
</Router>
</Provider>
),
});

describe('<SampleData /> spec', () => {
jest.clearAllMocks();
describe('No sample detectors created', () => {
test('renders component', async () => {
httpClientMock.get = jest.fn().mockResolvedValue({
data: { ok: true, response: { detectorList: [], totalDetectors: 0 } },
});
const { container, getByText, queryByText } = renderWithRouter();
expect(container).toMatchSnapshot();
getByText('Sample detectors');
getByText('Monitor HTTP responses');
getByText('Monitor eCommerce orders');
getByText('Monitor host health');
expect(queryByText('INSTALLED')).toBeNull();
expect(queryByText('Detector created')).toBeNull();
expect(queryByText('View detector and sample data')).toBeNull();
});
});

describe('Some detectors created', () => {
jest.clearAllMocks();
test('renders component with sample detector', async () => {
httpClientMock.get = jest.fn().mockResolvedValue({
data: {
ok: true,
response: {
detectorList: [
{
id: 'sample-detector-id',
name: sampleHttpResponses.detectorName,
indices: sampleHttpResponses.indexName,
totalAnomalies: 0,
lastActiveAnomaly: 0,
},
],
totalDetectors: 1,
},
},
});
const { container, getByText, getAllByText } = renderWithRouter();
await wait();
expect(container).toMatchSnapshot();
getByText('Sample detectors');
getByText('Monitor HTTP responses');
getByText('Monitor eCommerce orders');
getByText('Monitor host health');
expect(getAllByText('Detector created')).toHaveLength(1);
expect(getAllByText('View detector and sample data')).toHaveLength(1);
expect(getAllByText('INSTALLED')).toHaveLength(1);
});
test('renders component with non-sample detector', async () => {
httpClientMock.get = jest.fn().mockResolvedValue({
data: {
ok: true,
response: {
detectorList: [
{
id: 'non-sample-detector-id',
name: 'non-sample-detector',
indices: 'non-sample-index',
totalAnomalies: 0,
lastActiveAnomaly: 0,
},
],
totalDetectors: 1,
},
},
});
const { container, getByText, queryByText } = renderWithRouter();
await wait();
expect(container).toMatchSnapshot();
getByText('Sample detectors');
getByText('Monitor HTTP responses');
getByText('Monitor eCommerce orders');
getByText('Monitor host health');
expect(queryByText('INSTALLED')).toBeNull();
expect(queryByText('Detector created')).toBeNull();
});
});
});

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions public/pages/SampleData/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.
*/

export { SampleData } from './containers/SampleData/SampleData';
360 changes: 360 additions & 0 deletions public/pages/SampleData/utils/constants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
/*
* 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.
*/

//@ts-ignore
import moment from 'moment';
import React from 'react';
import { EuiIcon } from '@elastic/eui';

// same as default Kibana sample data
export const indexSettings = {
index: {
number_of_shards: 1,
auto_expand_replicas: '0-1',
},
};

export interface SAMPLE_DATA {
indexName: string;
detectorName: string;
description: string;
icon: any;
fieldMappings: {};
indexConfig: {};
detectorConfig: {};
}

/*
*** SAMPLE HTTP RESPONSES CONSTANTS ***
*/
const httpResponsesIndexName = 'opendistro-sample-http-responses';
const httpResponsesDetectorName = 'opendistro-sample-http-responses-detector';
const httpFieldMappings = {
timestamp: {
type: 'date',
},
status_code: {
type: 'integer',
},
http_1xx: {
type: 'integer',
},
http_2xx: {
type: 'integer',
},
http_3xx: {
type: 'integer',
},
http_4xx: {
type: 'integer',
},
http_5xx: {
type: 'integer',
},
};
export const sampleHttpResponses = {
indexName: httpResponsesIndexName,
detectorName: httpResponsesDetectorName,
description:
'Detect high numbers of error response codes in an index containing HTTP response data.',
icon: <EuiIcon type="visLine" size="l" />,
fieldMappings: httpFieldMappings,
indexConfig: {
index: httpResponsesIndexName,
body: {
settings: indexSettings,
mappings: {
properties: httpFieldMappings,
},
},
},
detectorConfig: {
name: httpResponsesDetectorName,
description:
'A sample detector to detect anomalies with HTTP response code logs.',
time_field: 'timestamp',
indices: [httpResponsesIndexName],
featureAttributes: [
{
feature_name: 'sum_http_4xx',
feature_enabled: true,
importance: 1,
aggregationQuery: { sum_http_4xx: { sum: { field: 'http_4xx' } } },
},
{
feature_name: 'sum_http_5xx',
feature_enabled: true,
importance: 2,
aggregationQuery: { sum_http_5xx: { sum: { field: 'http_5xx' } } },
},
],
uiMetadata: {
features: {
sum_http_4xx: {
featureType: 'simple_aggs',
aggregationBy: 'sum',
aggregationOf: 'http_4xx',
},
sum_http_5xx: {
featureType: 'simple_aggs',
aggregationBy: 'sum',
aggregationOf: 'http_5xx',
},
},
filters: [],
},
detection_interval: {
period: {
interval: 10,
unit: 'Minutes',
},
},
window_delay: {
period: {
interval: 1,
unit: 'Minutes',
},
},
},
} as SAMPLE_DATA;

/*
*** ECOMMERCE CONSTANTS ***
*/
const ecommerceIndexName = 'opendistro-sample-ecommerce';
const ecommerceDetectorName = 'opendistro-sample-ecommerce-detector';
const ecommerceFieldMappings = {
timestamp: {
type: 'date',
},
order_id: {
type: 'integer',
},
items_purchased_success: {
type: 'integer',
},
items_purchased_failure: {
type: 'integer',
},
total_revenue_usd: {
type: 'integer',
},
};
export const sampleEcommerce = {
indexName: ecommerceIndexName,
detectorName: ecommerceDetectorName,
description:
'Detect any unual increase or decrease of orders in an index containing online order data.',
icon: <EuiIcon type="package" size="l" />,
fieldMappings: ecommerceFieldMappings,
indexConfig: {
index: ecommerceIndexName,
body: {
settings: indexSettings,
mappings: {
properties: ecommerceFieldMappings,
},
},
},
detectorConfig: {
name: ecommerceDetectorName,
description: 'A sample detector to detect anomalies with ecommerce logs.',
time_field: 'timestamp',
indices: [ecommerceIndexName],
featureAttributes: [
{
feature_name: 'sum_failed_items',
feature_enabled: true,
importance: 1,
aggregationQuery: {
sum_failed_items: { sum: { field: 'items_purchased_failure' } },
},
},
{
feature_name: 'avg_total_revenue',
feature_enabled: true,
importance: 2,
aggregationQuery: {
avg_total_revenue: { avg: { field: 'total_revenue_usd' } },
},
},
{
feature_name: 'max_total_revenue',
feature_enabled: true,
importance: 3,
aggregationQuery: {
max_total_revenue: { max: { field: 'total_revenue_usd' } },
},
},
{
feature_name: 'min_total_revenue',
feature_enabled: true,
importance: 4,
aggregationQuery: {
min_total_revenue: { min: { field: 'total_revenue_usd' } },
},
},
],
uiMetadata: {
features: {
sum_failed_items: {
featureType: 'simple_aggs',
aggregationBy: 'sum',
aggregationOf: 'items_purchased_failure',
},
avg_total_revenue: {
featureType: 'simple_aggs',
aggregationBy: 'avg',
aggregationOf: 'total_revenue_usd',
},
max_total_revenue: {
featureType: 'simple_aggs',
aggregationBy: 'max',
aggregationOf: 'total_revenue_usd',
},
min_total_revenue: {
featureType: 'simple_aggs',
aggregationBy: 'min',
aggregationOf: 'total_revenue_usd',
},
},
filters: [],
},
detection_interval: {
period: {
interval: 10,
unit: 'Minutes',
},
},
window_delay: {
period: {
interval: 1,
unit: 'Minutes',
},
},
},
} as SAMPLE_DATA;

/*
*** HOST HEALTH CONSTANTS ***
*/
const hostHealthIndexName = 'opendistro-sample-host-health';
const hostHealthDetectorName = 'opendistro-sample-host-health-detector';
const hostHealthFieldMappings = {
timestamp: {
type: 'date',
},
cpu_usage_percentage: {
type: 'integer',
},
memory_usage_percentage: {
type: 'integer',
},
};
export const sampleHostHealth = {
indexName: hostHealthIndexName,
detectorName: hostHealthDetectorName,
description:
'Detect increases in CPU and memory utilization in an index containing various health metrics from a host.',
icon: <EuiIcon type="visGauge" size="l" />,
fieldMappings: hostHealthFieldMappings,
indexConfig: {
index: hostHealthIndexName,
body: {
settings: indexSettings,
mappings: {
properties: hostHealthFieldMappings,
},
},
},
detectorConfig: {
name: hostHealthDetectorName,
description:
'A sample detector to detect anomalies with logs related to the health of a host.',
time_field: 'timestamp',
indices: [hostHealthIndexName],
featureAttributes: [
{
feature_name: 'max_cpu_usage',
feature_enabled: true,
importance: 1,
aggregationQuery: {
sum_cpu_usage: { max: { field: 'cpu_usage_percentage' } },
},
},
{
feature_name: 'max_memory_usage',
feature_enabled: true,
importance: 2,
aggregationQuery: {
sum_memory_usage: { max: { field: 'memory_usage_percentage' } },
},
},
{
feature_name: 'avg_cpu_usage',
feature_enabled: true,
importance: 3,
aggregationQuery: {
avg_cpu_usage: { avg: { field: 'cpu_usage_percentage' } },
},
},
{
feature_name: 'avg_memory_usage',
feature_enabled: true,
importance: 4,
aggregationQuery: {
avg_memory_usage: { avg: { field: 'memory_usage_percentage' } },
},
},
],
uiMetadata: {
features: {
max_cpu_usage: {
featureType: 'simple_aggs',
aggregationBy: 'max',
aggregationOf: 'cpu_usage_percentage',
},
max_memory_usage: {
featureType: 'simple_aggs',
aggregationBy: 'max',
aggregationOf: 'memory_usage_percentage',
},
avg_cpu_usage: {
featureType: 'simple_aggs',
aggregationBy: 'avg',
aggregationOf: 'cpu_usage_percentage',
},
avg_memory_usage: {
featureType: 'simple_aggs',
aggregationBy: 'avg',
aggregationOf: 'memory_usage_percentage',
},
},
filters: [],
},
detection_interval: {
period: {
interval: 10,
unit: 'Minutes',
},
},
window_delay: {
period: {
interval: 1,
unit: 'Minutes',
},
},
},
} as SAMPLE_DATA;
233 changes: 233 additions & 0 deletions public/pages/SampleData/utils/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* 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 React from 'react';
import { EuiDataGrid } from '@elastic/eui';
import { CatIndex } from '../../../../server/models/types';
import { Detector, DetectorListItem } from '../../../models/interfaces';
import { SAMPLE_TYPE, ANOMALY_DETECTORS_INDEX } from '../../../utils/constants';
import {
sampleHttpResponses,
sampleEcommerce,
sampleHostHealth,
} from '../utils/constants';

export const containsDetectorsIndex = (indices: CatIndex[]) => {
return indices.map((index) => index.index).includes(ANOMALY_DETECTORS_INDEX);
};

export const containsSampleIndex = (
indices: CatIndex[],
sampleType: SAMPLE_TYPE
) => {
let indexName = '';
switch (sampleType) {
case SAMPLE_TYPE.HTTP_RESPONSES: {
indexName = sampleHttpResponses.indexName;
break;
}
case SAMPLE_TYPE.ECOMMERCE: {
indexName = sampleEcommerce.indexName;
break;
}
case SAMPLE_TYPE.HOST_HEALTH: {
indexName = sampleHostHealth.indexName;
break;
}
}
return indices.map((index) => index.index).includes(indexName);
};

export const containsSampleDetector = (
detectors: DetectorListItem[],
sampleType: SAMPLE_TYPE
) => {
let detectorName = '';
switch (sampleType) {
case SAMPLE_TYPE.HTTP_RESPONSES: {
detectorName = sampleHttpResponses.detectorName;
break;
}
case SAMPLE_TYPE.ECOMMERCE: {
detectorName = sampleEcommerce.detectorName;
break;
}
case SAMPLE_TYPE.HOST_HEALTH: {
detectorName = sampleHostHealth.detectorName;
break;
}
}
return detectors.map((detector) => detector.name).includes(detectorName);
};

export const detectorIsSample = (detector: Detector) => {
return (
detector.name === sampleHttpResponses.detectorName ||
detector.name === sampleEcommerce.detectorName ||
detector.name === sampleHostHealth.detectorName
);
};

export const getAssociatedIndex = (detector: Detector) => {
if (detector.name === sampleHttpResponses.detectorName) {
return sampleHttpResponses.indexName;
}
if (detector.name === sampleEcommerce.detectorName) {
return sampleEcommerce.indexName;
}
if (detector.name === sampleHostHealth.detectorName) {
return sampleHostHealth.indexName;
}
console.error(
'Error getting associated sample index for detector ',
detector.name
);
return '';
};

export const getDetectorId = (
detectors: DetectorListItem[],
detectorName: string
) => {
let detectorId = '';
detectors.some((detector) => {
if (detector.name === detectorName) {
detectorId = detector.id;
return true;
}
return false;
});
return detectorId;
};

const getFieldsAndTypesData = (fields: string[], types: string[]) => {
let data = [];
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
const type = types[i];
data.push({
Field: field,
Type: type,
});
}
return data;
};

const getFeaturesAndAggsAndFieldsData = (
features: string[],
aggs: string[],
fields: string[]
) => {
let data = [];
for (let i = 0; i < features.length; i++) {
const feature = features[i];
const agg = aggs[i];
const field = fields[i];
data.push({
Feature: feature,
Aggregation: agg,
'Index field': field,
});
}
return data;
};

export const getFieldsAndTypesGrid = (fields: string[], types: string[]) => {
const gridData = getFieldsAndTypesData(fields, types);
return (
<EuiDataGrid
aria-label="Index fields and types"
columns={[
{
id: 'Field',
isResizable: false,
isExpandable: false,
isSortable: false,
},
{
id: 'Type',
isResizable: false,
isExpandable: false,
isSortable: false,
},
]}
columnVisibility={{
visibleColumns: ['Field', 'Type'],
setVisibleColumns: () => {},
}}
rowCount={gridData.length}
renderCellValue={({ rowIndex, columnId }) =>
//@ts-ignore
gridData[rowIndex][columnId]
}
gridStyle={{
border: 'horizontal',
header: 'shade',
rowHover: 'highlight',
stripes: true,
}}
toolbarVisibility={false}
/>
);
};

export const getFeaturesAndAggsAndFieldsGrid = (
features: string[],
aggs: string[],
fields: string[]
) => {
const gridData = getFeaturesAndAggsAndFieldsData(features, aggs, fields);
return (
<EuiDataGrid
aria-label="Feature details"
columns={[
{
id: 'Feature',
isResizable: false,
isExpandable: false,
isSortable: false,
},
{
id: 'Aggregation',
isResizable: false,
isExpandable: false,
isSortable: false,
},
{
id: 'Index field',
isResizable: false,
isExpandable: false,
isSortable: false,
},
]}
columnVisibility={{
visibleColumns: ['Feature', 'Aggregation', 'Index field'],
setVisibleColumns: () => {},
}}
rowCount={gridData.length}
renderCellValue={({ rowIndex, columnId }) =>
//@ts-ignore
gridData[rowIndex][columnId]
}
gridStyle={{
border: 'horizontal',
header: 'shade',
rowHover: 'highlight',
stripes: true,
}}
toolbarVisibility={false}
/>
);
};
44 changes: 41 additions & 3 deletions public/pages/createDetector/containers/CreateDetector.tsx
Original file line number Diff line number Diff line change
@@ -27,8 +27,8 @@ import {
} from '@elastic/eui';
import { Formik } from 'formik';
import { get, isEmpty } from 'lodash';
import React, { Fragment, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import React, { Fragment, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { Dispatch } from 'redux';
//@ts-ignore
@@ -41,6 +41,8 @@ import {
searchDetector,
updateDetector,
} from '../../../redux/reducers/ad';
import { getIndices } from '../../../redux/reducers/elasticsearch';
import { AppState } from '../../../redux/reducers';
import { BREADCRUMBS, MAX_DETECTORS } from '../../../utils/constants';
import { getErrorMessage, validateName } from '../../../utils/utils';
import { DetectorInfo } from '../components/DetectorInfo';
@@ -52,6 +54,9 @@ import { formikToDetector } from './utils/formikToDetector';
import { Detector } from '../../../models/interfaces';
import { Settings } from '../components/Settings/Settings';
import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar';
import { CatIndex } from '../../../../server/models/types';
import { SampleDataCallout } from '../../SampleData/components/SampleDataCallout/SampleDataCallout';
import { containsDetectorsIndex } from '../../SampleData/utils/helpers';

interface CreateRouterProps {
detectorId?: string;
@@ -68,6 +73,25 @@ export function CreateDetector(props: CreateADProps) {
//In case user is refreshing Edit detector page, we'll lose existing detector state
//This will ensure to fetch the detector based on id from URL
const { detector, hasError } = useFetchDetectorInfo(detectorId);
const [sampleCalloutVisible, setSampleCalloutVisible] = useState<boolean>(
false
);
const visibleIndices = useSelector(
(state: AppState) => state.elasticsearch.indices
) as CatIndex[];

// Getting all initial indices
useEffect(() => {
const getInitialIndices = async () => {
await dispatch(getIndices(''));
};
getInitialIndices();
}, []);

// Check if the sample data callout should be visible based on detector index
useEffect(() => {
setSampleCalloutVisible(!containsDetectorsIndex(visibleIndices));
}, [visibleIndices]);

//Set breadcrumbs based on Create / Update
useEffect(() => {
@@ -196,9 +220,23 @@ export function CreateDetector(props: CreateADProps) {
}
};

const handleHideSampleCallout = () => {
setSampleCalloutVisible(false);
};

return (
<EuiPage>
<EuiPageBody>
{sampleCalloutVisible ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
<SampleDataCallout onHide={handleHideSampleCallout} />
</EuiFlexItem>
<EuiFlexItem>
<EuiSpacer size="m" />
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
@@ -211,7 +249,7 @@ export function CreateDetector(props: CreateADProps) {
initialValues={detectorToFormik(detector)}
onSubmit={handleSubmit}
>
{formikProps => (
{(formikProps) => (
<Fragment>
<DetectorInfo onValidateDetectorName={handleValidateName} />
<EuiSpacer />
Original file line number Diff line number Diff line change
@@ -7,6 +7,72 @@ exports[`<CreateDetector /> spec create detector renders the component 1`] = `
<main
class="euiPageBody"
>
<div
class="euiFlexGroup euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
class="euiFlexItem"
>
<div
class="euiCallOut euiCallOut--primary"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading euiCallOutHeader__icon"
focusable="false"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<span
class="euiCallOutHeader__title"
>
Looking to get more familiar with anomaly detection?
</span>
</div>
<div
class="euiText euiText--small"
>
<p>
Read the
<a
class="euiLink euiLink--primary"
href="https://opendistro.github.io/for-elasticsearch-docs/docs/ad/"
rel="noopener noreferrer"
target="_blank"
>
documentation
</a>
or create a
<a
class="euiLink euiLink--primary"
href="opendistro-anomaly-detection-kibana#/sample-detectors"
rel="noreferrer"
>
sample detector
</a>
to get started.
</p>
</div>
</div>
</div>
<div
class="euiFlexItem"
>
<div
class="euiSpacer euiSpacer--m"
/>
</div>
</div>
<div
class="euiPageHeader euiPageHeader--responsive"
>
14 changes: 14 additions & 0 deletions public/pages/main/Main.tsx
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import React from 'react';
import { AppState } from '../../redux/reducers';
import { CreateDetector } from '../createDetector';
import { DetectorList } from '../DetectorsList';
import { SampleData } from '../SampleData';
import { ListRouterParams } from '../DetectorsList/containers/List/List';
// @ts-ignore
import { EuiSideNav, EuiPage, EuiPageBody, EuiPageSideBar } from '@elastic/eui';
@@ -31,11 +32,13 @@ enum Navigation {
AnomalyDetection = 'Anomaly detection',
Dashboard = 'Dashboard',
Detectors = 'Detectors',
SampleDetectors = 'Sample detectors',
}

enum Pathname {
Dashboard = '/dashboard',
Detectors = '/detectors',
SampleDetectors = '/sample-detectors',
}

interface MainProps extends RouteComponentProps {}
@@ -62,6 +65,12 @@ export function Main(props: MainProps) {
href: `#${Pathname.Detectors}`,
isSelected: props.location.pathname === Pathname.Detectors,
},
{
name: Navigation.SampleDetectors,
id: 3,
href: `#${Pathname.SampleDetectors}`,
isSelected: props.location.pathname === Pathname.SampleDetectors,
},
],
},
];
@@ -84,6 +93,11 @@ export function Main(props: MainProps) {
<DetectorList {...props} />
)}
/>
<Route
exact
path={APP_PATH.SAMPLE_DETECTORS}
render={() => <SampleData />}
/>
<Route
exact
path={APP_PATH.CREATE_DETECTOR}
3 changes: 3 additions & 0 deletions public/pages/utils/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -42,6 +42,9 @@ export class SideBar extends Component<{}, SideBarState> {
this.createItem('Detectors', 2, {
href: `#${APP_PATH.LIST_DETECTORS}`,
}),
this.createItem('Sample Data', 3, {
href: `#${APP_PATH.SAMPLE_DATA}`,
}),
],
},
];
11 changes: 11 additions & 0 deletions public/pages/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -63,3 +63,14 @@ export const GET_ALL_DETECTORS_QUERY_PARAMS = {
sortDirection: SORT_DIRECTION.ASC,
sortField: 'name',
};

export const GET_SAMPLE_DETECTORS_QUERY_PARAMS = {
from: 0,
search: 'opendistro-sample',
indices: '',
size: MAX_DETECTORS,
sortDirection: SORT_DIRECTION.ASC,
sortField: 'name',
};

export const GET_SAMPLE_INDICES_QUERY = 'opendistro-sample-*';
91 changes: 90 additions & 1 deletion public/redux/reducers/elasticsearch.ts
Original file line number Diff line number Diff line change
@@ -30,6 +30,9 @@ const GET_INDICES = 'elasticsearch/GET_INDICES';
const GET_ALIASES = 'elasticsearch/GET_ALIASES';
const GET_MAPPINGS = 'elasticsearch/GET_MAPPINGS';
const SEARCH_ES = 'elasticsearch/SEARCH_ES';
const CREATE_INDEX = 'elasticsearch/CREATE_INDEX';
const BULK = 'elasticsearch/BULK';
const DELETE_INDEX = 'elasticsearch/DELETE_INDEX';

export type Mappings = {
[key: string]: any;
@@ -170,7 +173,75 @@ const reducer = handleActions<ElasticsearchState>(
...state,
requesting: false,
errorMessage: get(action, 'error.data.error', action.error),
dataTypes: {}
dataTypes: {},
}),
},
[CREATE_INDEX]: {
REQUEST: (state: ElasticsearchState): ElasticsearchState => {
return { ...state, requesting: true, errorMessage: '' };
},
SUCCESS: (
state: ElasticsearchState,
action: APIResponseAction
): ElasticsearchState => {
return {
...state,
requesting: false,
indices: action.result.data.response.indices,
};
},
FAILURE: (
state: ElasticsearchState,
action: APIErrorAction
): ElasticsearchState => ({
...state,
requesting: false,
errorMessage: action.error,
}),
},
[BULK]: {
REQUEST: (state: ElasticsearchState): ElasticsearchState => {
return { ...state, requesting: true, errorMessage: '' };
},
SUCCESS: (
state: ElasticsearchState,
action: APIResponseAction
): ElasticsearchState => {
return {
...state,
requesting: false,
};
},
FAILURE: (
state: ElasticsearchState,
action: APIErrorAction
): ElasticsearchState => ({
...state,
requesting: false,
errorMessage: action.error,
}),
},
[DELETE_INDEX]: {
REQUEST: (state: ElasticsearchState): ElasticsearchState => {
return { ...state, requesting: true, errorMessage: '' };
},
SUCCESS: (
state: ElasticsearchState,
action: APIResponseAction
): ElasticsearchState => {
return {
...state,
requesting: false,
indices: action.result.data.response.indices,
};
},
FAILURE: (
state: ElasticsearchState,
action: APIErrorAction
): ElasticsearchState => ({
...state,
requesting: false,
errorMessage: action.error,
}),
},
},
@@ -204,6 +275,24 @@ export const searchES = (requestData: any): APIAction => ({
client.post(`..${AD_NODE_API._SEARCH}`, requestData),
});

export const createIndex = (indexConfig: any): APIAction => ({
type: CREATE_INDEX,
request: (client: IHttpService) =>
client.put(`..${AD_NODE_API.CREATE_INDEX}`, { indexConfig }),
});

export const bulk = (body: any): APIAction => ({
type: BULK,
request: (client: IHttpService) =>
client.post(`..${AD_NODE_API.BULK}`, { body }),
});

export const deleteIndex = (index: string): APIAction => ({
type: DELETE_INDEX,
request: (client: IHttpService) =>
client.post(`..${AD_NODE_API.DELETE_INDEX}`, { index }),
});

export const getPrioritizedIndices = (searchKey: string): ThunkAction => async (
dispatch,
getState
72 changes: 72 additions & 0 deletions public/redux/reducers/sampleData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 {
APIAction,
APIResponseAction,
IHttpService,
} from '../middleware/types';
import handleActions from '../utils/handleActions';
import { AD_NODE_API } from '../../../utils/constants';
import { SAMPLE_TYPE } from '../../utils/constants';

const CREATE_SAMPLE_DATA = 'ad/CREATE_SAMPLE_DATA';

export interface SampleDataState {
requesting: boolean;
errorMessage: string;
}
export const initialState: SampleDataState = {
requesting: false,
errorMessage: '',
};

const reducer = handleActions<SampleDataState>(
{
[CREATE_SAMPLE_DATA]: {
REQUEST: (state: SampleDataState): SampleDataState => ({
...state,
requesting: true,
errorMessage: '',
}),
SUCCESS: (
state: SampleDataState,
action: APIResponseAction
): SampleDataState => ({
...state,
requesting: false,
}),
FAILURE: (
state: SampleDataState,
action: APIResponseAction
): SampleDataState => ({
...state,
requesting: false,
errorMessage: action.error.data.error,
}),
},
},
initialState
);

export const createSampleData = (sampleDataType: SAMPLE_TYPE): APIAction => ({
type: CREATE_SAMPLE_DATA,
request: (client: IHttpService) =>
client.post(`..${AD_NODE_API.CREATE_SAMPLE_DATA}`, {
type: sampleDataType,
}),
});

export default reducer;
17 changes: 17 additions & 0 deletions public/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ export enum DATA_TYPES {
export const BREADCRUMBS = Object.freeze({
ANOMALY_DETECTOR: { text: 'Anomaly detection', href: '#/' },
DETECTORS: { text: 'Detectors', href: '#/detectors' },
SAMPLE_DETECTORS: { text: 'Sample detectors', href: '#/sample-detectors' },
CREATE_DETECTOR: { text: 'Create detector' },
EDIT_DETECTOR: { text: 'Edit detector' },
DASHBOARD: { text: 'Dashboard', href: '#/' },
@@ -33,15 +34,25 @@ export const BREADCRUMBS = Object.freeze({
export const APP_PATH = {
DASHBOARD: '/dashboard',
LIST_DETECTORS: '/detectors',
SAMPLE_DETECTORS: '/sample-detectors',
CREATE_DETECTOR: '/create-ad/',
EDIT_DETECTOR: '/detectors/:detectorId/edit',
EDIT_FEATURES: '/detectors/:detectorId/features/',
DETECTOR_DETAIL: '/detectors/:detectorId/',
};

export const KIBANA_PATH = {
DISCOVER: '/discover',
};

export const PLUGIN_NAME = 'opendistro-anomaly-detection-kibana';

export const ALERTING_PLUGIN_NAME = 'opendistro-alerting';

export const KIBANA_NAME = 'kibana';

export const ANOMALY_DETECTORS_INDEX = '.opendistro-anomaly-detectors';

export const ANOMALY_RESULT_INDEX = '.opendistro-anomaly-results';

export const MAX_DETECTORS = 1000;
@@ -57,6 +68,12 @@ export enum DETECTOR_STATE {
UNEXPECTED_FAILURE = 'Unexpected failure',
}

export enum SAMPLE_TYPE {
HTTP_RESPONSES = 'http-responses',
HOST_HEALTH = 'host-health',
ECOMMERCE = 'ecommerce',
}

export const MAX_FEATURE_NUM = 5;

export const MAX_FEATURE_NAME_SIZE = 64;
2 changes: 2 additions & 0 deletions server/plugin.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import { default as createRouter, Router } from './router';
import registerADRoutes from './routes/ad';
import registerAlertingRoutes from './routes/alerting';
import registerElasticsearchRoute from './routes/elasticsearch';
import registerSampleDataRoutes from './routes/sampleData';

interface CoreSetup {
elasticsearch: Legacy.Plugins.elasticsearch.Plugin;
@@ -46,6 +47,7 @@ export class ADPlugin {
registerElasticsearchRoute(apiRouter);
registerADRoutes(apiRouter);
registerAlertingRoutes(apiRouter);
registerSampleDataRoutes(apiRouter);
}

public start() {}
6 changes: 4 additions & 2 deletions server/routes/ad.ts
Original file line number Diff line number Diff line change
@@ -100,7 +100,9 @@ const previewDetector = async (
try {
const { detectorId } = req.params;
//@ts-ignore
const requestBody = JSON.stringify(convertPreviewInputKeysToSnakeCase(req.payload));
const requestBody = JSON.stringify(
convertPreviewInputKeysToSnakeCase(req.payload)
);
const response = await callWithRequest(req, 'ad.previewDetector', {
detectorId,
body: requestBody,
@@ -333,7 +335,7 @@ const getDetectors = async (
query_string: {
fields: ['name', 'description'],
default_operator: 'AND',
query: `*${search.trim().split(' ').join('* *')}*`,
query: `*${search.trim().split('-').join('* *')}*`,
},
});
}
90 changes: 89 additions & 1 deletion server/routes/elasticsearch.ts
Original file line number Diff line number Diff line change
@@ -27,12 +27,16 @@ import {
ServerResponse,
} from '../models/types';
import { Router } from '../router';
import { isIndexNotFoundError } from './utils/adHelpers';

export default function(apiRouter: Router) {
export default function (apiRouter: Router) {
apiRouter.get('/_indices', getIndices);
apiRouter.get('/_aliases', getAliases);
apiRouter.get('/_mappings', getMapping);
apiRouter.post('/_search', executeSearch);
apiRouter.put('/create_index', createIndex);
apiRouter.post('/bulk', bulk);
apiRouter.post('/delete_index', deleteIndex);
}

type SearchParams = {
@@ -133,6 +137,90 @@ const getAliases = async (
}
};

const createIndex = async (
req: Request,
h: ResponseToolkit,
callWithRequest: CallClusterWithRequest
): Promise<ServerResponse<any>> => {
//@ts-ignore
const index = req.payload.indexConfig.index;
//@ts-ignore
const body = req.payload.indexConfig.body;
try {
await callWithRequest(req, 'indices.create', {
index: index,
body: body,
});
} catch (err) {
console.log('Anomaly detector - Unable to create index', err);
return { ok: false, error: err.message };
}
try {
const response: CatIndex[] = await callWithRequest(req, 'cat.indices', {
index,
format: 'json',
h: 'health,index',
});
return { ok: true, response: { indices: response } };
} catch (err) {
console.log('Anomaly detector - Unable to get indices', err);
return { ok: false, error: err.message };
}
};

const bulk = async (
req: Request,
h: ResponseToolkit,
callWithRequest: CallClusterWithRequest
): Promise<ServerResponse<GetAliasesResponse>> => {
//@ts-ignore
const body = req.payload.body;
try {
const response: any = await callWithRequest(req, 'bulk', {
body: body,
});
//@ts-ignore
return { ok: true, response: { response } };
} catch (err) {
console.log('Anomaly detector - Unable to perform bulk action', err);
return { ok: false, error: err.message };
}
};

const deleteIndex = async (
req: Request,
h: ResponseToolkit,
callWithRequest: CallClusterWithRequest
): Promise<ServerResponse<any>> => {
//@ts-ignore
const index = req.payload.index;
try {
await callWithRequest(req, 'indices.delete', {
index: index,
});
} catch (err) {
console.log(
'Anomaly detector - Unable to perform delete index action',
err
);
// Ignore the error if it's an index_not_found_exception
if (!isIndexNotFoundError(err)) {
return { ok: false, error: err.message };
}
}
try {
const response: CatIndex[] = await callWithRequest(req, 'cat.indices', {
index,
format: 'json',
h: 'health,index',
});
return { ok: true, response: { indices: response } };
} catch (err) {
console.log('Anomaly detector - Unable to get indices', err);
return { ok: false, error: err.message };
}
};

const getMapping = async (
req: Request,
h: ResponseToolkit,
78 changes: 78 additions & 0 deletions server/routes/sampleData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2019 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.
*/

//@ts-ignore
import moment from 'moment';
import { Request, ResponseToolkit } from 'hapi';
import path from 'path';
//@ts-ignore
import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch';
import { ServerResponse } from '../models/types';
import { Router } from '../router';
import { SAMPLE_TYPE } from '../../public/utils/constants';
import { loadSampleData } from '../sampleData/utils/helpers';

export default function (apiRouter: Router) {
apiRouter.post('/create_sample_data', createSampleData);
}

// Get the zip file stored in server, unzip it, and bulk insert it
const createSampleData = async (
req: Request,
h: ResponseToolkit,
callWithRequest: CallClusterWithRequest
): Promise<ServerResponse<any>> => {
//@ts-ignore
const type = req.payload.type as SAMPLE_TYPE;
try {
let filePath = '';
let indexName = '';

switch (type) {
case SAMPLE_TYPE.HTTP_RESPONSES: {
filePath = path.resolve(
__dirname,
'../sampleData/rawData/httpResponses.json.gz'
);
indexName = 'opendistro-sample-http-responses';
break;
}
case SAMPLE_TYPE.ECOMMERCE: {
filePath = path.resolve(
__dirname,
'../sampleData/rawData/ecommerce.json.gz'
);
indexName = 'opendistro-sample-ecommerce';
break;
}
case SAMPLE_TYPE.HOST_HEALTH: {
filePath = path.resolve(
__dirname,
'../sampleData/rawData/hostHealth.json.gz'
);
indexName = 'opendistro-sample-host-health';
break;
}
}

await loadSampleData(filePath, indexName, req, callWithRequest);

//@ts-ignore
return { ok: true };
} catch (err) {
console.log('Anomaly detector - Unable to load the sample data', err);
return { ok: false, error: err.message };
}
};
Binary file added server/sampleData/rawData/ecommerce.json.gz
Binary file not shown.
Binary file added server/sampleData/rawData/hostHealth.json.gz
Binary file not shown.
Binary file added server/sampleData/rawData/httpResponses.json.gz
Binary file not shown.
142 changes: 142 additions & 0 deletions server/sampleData/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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.
*/

//@ts-ignore
import moment from 'moment';
import readline from 'readline';
import { Request } from 'hapi';

import fs from 'fs';
import { createUnzip } from 'zlib';
//@ts-ignore
import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch';

const BULK_INSERT_SIZE = 500;

export const loadSampleData = (
filePath: string,
indexName: string,
req: Request,
callWithRequest: CallClusterWithRequest
) => {
return new Promise((resolve, reject) => {
let count: number = 0;
let docs: any[] = [];
let isPaused: boolean = false;
let offset = 0;
const startTime = moment(new Date().getTime()).subtract({ days: 7 });

// Create the read stream for the file. Set a smaller buffer size here to prevent it from
// getting too large, to prevent inserting too many docs at once into the index.
const readStream = fs.createReadStream(filePath, {
highWaterMark: 1024 * 4,
});
const lineStream = readline.createInterface({
input: readStream.pipe(createUnzip()),
});

// This is only ran when the end of lineStream closes normally. It is used to
// bulk insert the final batch of lines that are < BULK_INSERT_SIZE
const onClose = async () => {
if (docs.length > 0) {
try {
await bulkInsert(docs);
} catch (err) {
reject(err);
return;
}
}
resolve(count);
};
lineStream.on('close', onClose);
lineStream.on('pause', async () => {
isPaused = true;
});
lineStream.on('resume', async () => {
isPaused = false;
});
lineStream.on('line', async (doc) => {
// for the initial doc, get the timestamp to properly set an offset
if (count === 0) {
const initialTime = moment(JSON.parse(doc).timestamp);
offset = startTime.diff(initialTime);
}
count++;
docs.push(doc);

// If not currently paused: pause the stream to prevent concurrent bulk inserts
// on the cluster which could cause performance issues.
// Also, empty the current docs[] before performing the bulk insert to prevent
// buffered docs from being dropped.
if (docs.length >= BULK_INSERT_SIZE && !isPaused) {
lineStream.pause();

// save the docs to insert, and empty out current docs list
const docsToInsert = docs.slice();
docs = [];
try {
await bulkInsert(docsToInsert);
lineStream.resume();
} catch (err) {
lineStream.removeListener('close', onClose);
lineStream.close();
reject(err);
}
}
});

const bulkInsert = async (docs: any[]) => {
try {
const bulkBody = prepareBody(docs, offset);
const resp = await callWithRequest(req, 'bulk', {
body: bulkBody,
});
if (resp.errors) {
console.log('Error while bulk inserting. ', resp.errors);
return Promise.reject(
new Error('Error while bulk inserting. Please try again.')
);
}
} catch (err) {
console.log('Error while bulk inserting. ', err);
return Promise.reject(
new Error('Error while bulk inserting. Please try again.')
);
}
};

const prepareBody = (docs: string[], offset: number) => {
const bulkBody = [] as any[];
let docIdCount = count - docs.length;
docs.forEach((doc: string) => {
bulkBody.push(getDocDetails(docIdCount));
bulkBody.push(updateTimestamp(doc, offset));
docIdCount++;
});
return bulkBody;
};

const updateTimestamp = (doc: any, offset: number) => {
let docAsJSON = JSON.parse(doc);
const updatedTimestamp = docAsJSON.timestamp + offset;
docAsJSON.timestamp = updatedTimestamp;
return docAsJSON;
};

const getDocDetails = (docIdCount: number) => {
return `{ "index": { "_index": "${indexName}", "_id": ${docIdCount} } }`;
};
});
};
4 changes: 4 additions & 0 deletions utils/constants.ts
Original file line number Diff line number Diff line change
@@ -21,6 +21,10 @@ export const AD_NODE_API = Object.freeze({
_ALIASES: `${BASE_NODE_API_PATH}/_aliases`,
_MAPPINGS: `${BASE_NODE_API_PATH}/_mappings`,
DETECTOR: `${BASE_NODE_API_PATH}/detectors`,
CREATE_INDEX: `${BASE_NODE_API_PATH}/create_index`,
BULK: `${BASE_NODE_API_PATH}/bulk`,
DELETE_INDEX: `${BASE_NODE_API_PATH}/delete_index`,
CREATE_SAMPLE_DATA: `${BASE_NODE_API_PATH}/create_sample_data`,
});
export const ALERTING_NODE_API = Object.freeze({
_SEARCH: `${BASE_NODE_API_PATH}/monitors/_search`,