Skip to content
Closed
Show file tree
Hide file tree
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
13 changes: 12 additions & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
"version": "3.0.0.0",
"opensearchDashboardsVersion": "3.0.0",
"configPath": ["anomaly_detection_dashboards"],
"requiredPlugins": ["navigation"],
"requiredPlugins": [
"navigation",
"uiActions",
"dashboard",
"embeddable",
"opensearchDashboardsReact",
"savedObjects",
"visAugmenter",
"opensearchDashboardsUtils",
"expressions",
"data"
],
"optionalPlugins": [],
"server": true,
"ui": true
Expand Down
74 changes: 74 additions & 0 deletions public/action/ad_dashboard_action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { IEmbeddable } from '../../../../src/plugins/dashboard/public/embeddable_plugin';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
} from '../../../../src/plugins/dashboard/public';
import {
IncompatibleActionError,
createAction,
Action,
} from '../../../../src/plugins/ui_actions/public';
import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public';
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';

export const ACTION_AD = 'ad';

function isDashboard(
embeddable: IEmbeddable
): embeddable is DashboardContainer {
return embeddable.type === DASHBOARD_CONTAINER_TYPE;
}

export interface ActionContext {
embeddable: IEmbeddable;
}

export interface CreateOptions {
grouping: Action['grouping'];
title: string;
icon: EuiIconType;
id: string;
order: number;
onClick: Function;
}

export const createADAction = ({
grouping,
title,
icon,
id,
order,
onClick,
}: CreateOptions) =>
createAction({
id,
order,
getDisplayName: ({ embeddable }: ActionContext) => {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
return title;
},
getIconType: () => icon,
type: ACTION_AD,
grouping,
isCompatible: async ({ embeddable }: ActionContext) => {
const paramsType = embeddable.vis?.params?.type;
const seriesParams = embeddable.vis?.params?.seriesParams || [];
const series = embeddable.vis?.params?.series || [];
const isLineGraph =
seriesParams.find((item) => item.type === 'line') ||
series.find((item) => item.chart_type === 'line');
const isValidVis = isLineGraph && paramsType !== 'table';
return Boolean(
embeddable.parent && isDashboard(embeddable.parent) && isValidVis
);
},
execute: async ({ embeddable }: ActionContext) => {
if (!isReferenceOrValueEmbeddable(embeddable)) {
throw new IncompatibleActionError();
}

onClick({ embeddable });
},
});
91 changes: 91 additions & 0 deletions public/components/ContextMenu/CreateAnomalyDetector/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import {
EuiLink,
EuiText,
EuiHorizontalRule,
EuiSpacer,
EuiPanel,
EuiIcon,
EuiFlexItem,
EuiFlexGroup,
EuiButton,
} from '@elastic/eui';
import { useField, useFormikContext } from 'formik';
import Notifications from '../Notifications';
import FormikWrapper from '../../../utils/contextMenu/FormikWrapper';
import './styles.scss';
import { toMountPoint } from '../../../../../../src/plugins/opensearch_dashboards_react/public';

export const CreateAnomalyDetector = (props) => {
const { overlays, closeMenu } = props;
const { values } = useFormikContext();
const [name] = useField('name');

const onOpenAdvanced = () => {
// Prepare advanced flyout with new formik provider of current values
const getFormikOptions = () => ({
initialValues: values,
onSubmit: (values) => {
console.log(values);
},
});

const flyout = overlays.openFlyout(
toMountPoint(
<FormikWrapper {...{ getFormikOptions }}>
<CreateAnomalyDetectorExpanded
{...{ ...props, onClose: () => flyout.close() }}
/>
</FormikWrapper>
)
);

// Close context menu
closeMenu();
};

return (
<>
<EuiPanel hasBorder={false} hasShadow={false}>
<EuiText size="s">
<strong>{name.value}</strong>
</EuiText>
<EuiSpacer size="xs" />
<EuiText size="xs">
Detector interval: 10 minutes; Window delay: 1 minute
</EuiText>
<EuiSpacer />

{/* not sure about the select features part */}

<Notifications />
</EuiPanel>
<EuiHorizontalRule margin="none" />
<EuiPanel hasBorder={false} hasShadow={false}>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>
<EuiText size="s">
<EuiLink onClick={onOpenAdvanced}>
<EuiIcon type="menuLeft" /> Advanced settings
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
type="submit"
data-test-subj="createDetectorButtonFlyout"
className="create-anomaly-detector__create"
fill={true}
size="s"
isLoading={formikProps.isSubmitting}
//@ts-ignore
onClick={formikProps.handleSubmit}
>
Create
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.create-anomaly-detector {
&__create {
align-self: flex-end;
}
}
31 changes: 31 additions & 0 deletions public/components/ContextMenu/Notifications/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import {
EuiLink,
EuiText,
EuiSpacer,
EuiPanel,
EuiIcon,
EuiFormRow,
} from '@elastic/eui';

export const Notifications = () => (
<>
<EuiFormRow label="Notifications">
<EuiPanel color="subdued" hasBorder={false} hasShadow={false}>
<EuiText size="xs">
The anomalies will appear on the visualization when the anomaly grade
is above 0.7 and anomaly confidence is below 0.7. Additional
notification can be configured.
</EuiText>
</EuiPanel>
</EuiFormRow>
<EuiSpacer size="s" />
<EuiPanel color="subdued" hasBorder={false} hasShadow={false}>
<EuiText size="s">
<EuiLink href="#">
<EuiIcon type="plusInCircle" /> Add notifications
</EuiLink>
</EuiText>
</EuiPanel>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import React, { useState } from 'react';
import {
EuiText,
EuiOverlayMask,
EuiButton,
EuiButtonEmpty,
EuiModal,
EuiModalHeader,
EuiModalFooter,
EuiModalBody,
EuiModalHeaderTitle,
} from '@elastic/eui';
import { DetectorListItem } from '../../../../../models/interfaces';
import { EuiSpacer } from '@elastic/eui';

interface ConfirmUnlinkDetectorModalProps {
detector: DetectorListItem;
onUnlinkDetector(): void;
onHide(): void;
onConfirm(): void;
isListLoading: boolean;
}

export const ConfirmUnlinkDetectorModal = (
props: ConfirmUnlinkDetectorModalProps
) => {
const [isModalLoading, setIsModalLoading] = useState<boolean>(false);
const isLoading = isModalLoading || props.isListLoading;
return (
<EuiOverlayMask>
<EuiModal
data-test-subj="startDetectorsModal"
onClose={props.onHide}
maxWidth={450}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
{'Remove association?'}&nbsp;
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
Removing association unlinks {props.detector.name} detector from the
visualization but does not delete it. The detector association can
be restored.
</EuiText>
<EuiSpacer size="s" />
</EuiModalBody>
<EuiModalFooter>
{isLoading ? null : (
<EuiButtonEmpty
data-test-subj="cancelButton"
onClick={props.onHide}
>
Cancel
</EuiButtonEmpty>
)}
<EuiButton
data-test-subj="confirmButton"
color="primary"
fill
isLoading={isLoading}
onClick={async () => {
setIsModalLoading(true);
props.onUnlinkDetector();
props.onConfirm();
}}
>
{'Remove association'}
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
import React from 'react';

const FILTER_TEXT = 'There are no detectors matching your search';

interface EmptyDetectorProps {
isFilterApplied: boolean;
embeddableTitle: string;
}

export const EmptyAssociatedDetectorFlyoutMessage = (
props: EmptyDetectorProps
) => (
<EuiEmptyPrompt
title={<h3>No anomaly detectors to display</h3>}
titleSize="s"
data-test-subj="emptyAssociatedDetectorFlyoutMessage"
style={{ maxWidth: '45em' }}
body={
<EuiText>
<p>
{props.isFilterApplied
? FILTER_TEXT
: `There are no anomaly detectors associated with ${props.embeddableTitle} visualization.`}
</p>
</EuiText>
}
/>
);
Loading