Skip to content
Merged
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
9 changes: 8 additions & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
"version": "3.0.0.0",
"opensearchDashboardsVersion": "3.0.0",
"configPath": ["anomaly_detection_dashboards"],
"requiredPlugins": ["navigation"],
"requiredPlugins": [
"navigation",
"uiActions",
"dashboard",
"embeddable",
"opensearchDashboardsReact",
"savedObjects"
],
"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.tsx
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import CreateAnomalyDetector from '../CreateAnomalyDetector';
import { useIndex } from '../../../utils/contextMenu/indexes';
import './styles.scss';

const Container = ({ startingFlyout, ...props }) => {
const { embeddable } = props;
console.log({ embeddable });
const index = useIndex(embeddable);
const [mode, setMode] = useState(startingFlyout);

const Flyout = {
create: CreateAnomalyDetector,
}[mode];

return (
<Flyout
{...{
...props,
setMode,
mode,
index,
}}
/>
);
};

export default Container;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import Container from './Container';

export default Container;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.context-menu {
&__flyout {
&.euiFlyout--medium {
width: 740px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import {
EuiFlexItem,
EuiFlexGroup,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiTitle,
EuiSpacer,
EuiButton,
EuiButtonEmpty,
EuiFormFieldset,
EuiCheckableCard,
} from '@elastic/eui';
import './styles.scss';
import CreateNew from './CreateNew';

function AddAnomalyDetector({
embeddable,
closeFlyout,
core,
services,
mode,
setMode,
index,
}) {
const onCreate = () => {
console.log(`Current mode: ${mode}`);
const event = new Event('createDetector');
document.dispatchEvent(event);
closeFlyout();
};

return (
<div className="add-anomaly-detector">
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 id="add-anomaly-detector__title">Add anomaly detector</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<div className="add-anomaly-detector__scroll">
<EuiFormFieldset
legend={{
display: 'hidden',
children: (
<EuiTitle>
<span>Options to create a new detector or associate an existing detector</span>
</EuiTitle>
),
}}
className="add-anomaly-detector__modes"
>
{[
{
id: 'add-anomaly-detector__create',
label: 'Create new detector',
value: 'create',
},
{
id: 'add-anomaly-detector__existing',
label: 'Associate existing detector',
value: 'existing',
},
].map((option) => (
<EuiCheckableCard
{...{
...option,
key: option.id,
name: option.id,
checked: option.value === mode,
onChange: () => setMode(option.value),
}}
/>
))}
</EuiFormFieldset>
<EuiSpacer size="m" />
{mode === 'create' && (
<CreateNew {...{ embeddable, closeFlyout, core, services, index }} />
)}
</div>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={closeFlyout}>Cancel</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onCreate} fill>
{mode === 'existing' ? 'Associate' : 'Create'} detector
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</div>
);
}

export default AddAnomalyDetector;
Loading