diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 6868b4ed1..a88771b0a 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -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 diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx new file mode 100644 index 000000000..0d8c43894 --- /dev/null +++ b/public/action/ad_dashboard_action.tsx @@ -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 }); + }, + }); \ No newline at end of file diff --git a/public/components/ContextMenu/CreateAnomalyDetector/index.tsx b/public/components/ContextMenu/CreateAnomalyDetector/index.tsx new file mode 100644 index 000000000..f84c2ba37 --- /dev/null +++ b/public/components/ContextMenu/CreateAnomalyDetector/index.tsx @@ -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( + + flyout.close() }} + /> + + ) + ); + + // Close context menu + closeMenu(); + }; + + return ( + <> + + + {name.value} + + + + Detector interval: 10 minutes; Window delay: 1 minute + + + + {/* not sure about the select features part */} + + + + + + + + + + Advanced settings + + + + + + Create + + + + + + ); +}; \ No newline at end of file diff --git a/public/components/ContextMenu/CreateAnomalyDetector/styles.scss b/public/components/ContextMenu/CreateAnomalyDetector/styles.scss new file mode 100644 index 000000000..2316fd381 --- /dev/null +++ b/public/components/ContextMenu/CreateAnomalyDetector/styles.scss @@ -0,0 +1,5 @@ +.create-anomaly-detector { + &__create { + align-self: flex-end; + } +} \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/Container/Container.js b/public/components/FeatureAnywhereContextMenu/Container/Container.js new file mode 100644 index 000000000..61c0abef7 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/Container/Container.js @@ -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 ( + + ); +}; + +export default Container; diff --git a/public/components/FeatureAnywhereContextMenu/Container/index.js b/public/components/FeatureAnywhereContextMenu/Container/index.js new file mode 100644 index 000000000..6a0f64ace --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/Container/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Container from './Container'; + +export default Container; diff --git a/public/components/FeatureAnywhereContextMenu/Container/styles.scss b/public/components/FeatureAnywhereContextMenu/Container/styles.scss new file mode 100644 index 000000000..376c85572 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/Container/styles.scss @@ -0,0 +1,7 @@ +.context-menu { + &__flyout { + &.euiFlyout--medium { + width: 740px; + } + } +} diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx new file mode 100644 index 000000000..5f3a7b14f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -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 ( +
+ + +

Add anomaly detector

+
+
+ +
+ + Options to create a new detector or associate an existing detector + + ), + }} + 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) => ( + setMode(option.value), + }} + /> + ))} + + + {mode === 'create' && ( + + )} +
+
+ + + + Cancel + + + + {mode === 'existing' ? 'Associate' : 'Create'} detector + + + + +
+ ); +} + +export default AddAnomalyDetector; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js new file mode 100644 index 000000000..b626ebe22 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js @@ -0,0 +1,474 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiIcon, + EuiText, + EuiSwitch, + EuiLoadingSpinner, + EuiPanel, + EuiAccordion, + EuiFormRow, + EuiFieldText, + EuiCheckbox, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, + EuiCallOut + } from '@elastic/eui'; +import { EmbeddablePanel } from '../../../../../../../src/plugins/embeddable/public'; +import './styles.scss'; +import EnhancedAccordion from '../../EnhancedAccordion'; + +function CreateNew({ embeddable, closeFlyout, core, services, index }) { + const [isShowVis, setIsShowVis] = useState(false); + const title = embeddable.getTitle(); + const history = { + location: { pathname: '/create-detector', search: '', hash: '', state: undefined }, + push: (value) => console.log('pushed', value), + goBack: closeFlyout, + }; + const createMonitorProps = { + ...history, + history, + httpClient: core.http, + // This is not expected to be used + setFlyout: () => null, + notifications: core.notifications, + isDarkMode: core.isDarkMode, + notificationService: services.notificationService, + edit: false, + updateMonitor: () => null, + staticContext: undefined, + isMinimal: true, + defaultName: `${title} anomaly detector 1`, + defaultIndex: index, + defaultTimeField: embeddable.vis.params.time_field, + isDefaultTriggerEnabled: true, + }; + + const intervalOptions = [ + { value: 'option_one', text: '10 minutes' }, + { value: 'option_two', text: '1 minutes' }, + { value: 'option_three', text: '5 minutes' }, + ]; + const [intervalValue, setIntervalalue] = useState(intervalOptions[0].value); + const intervalOnChange = (e) => { + setIntervalalue(e.target.value); + }; + + const delayOptions = [ + { value: 'option_one', text: '10 minutes' }, + { value: 'option_two', text: '1 minutes' }, + { value: 'option_three', text: '5 minutes' }, + ]; + const [delayValue, setDelayValue] = useState(delayOptions[0].value); + const delayOnChange = (e) => { + setDelayValue(e.target.value); + }; + + const detectorNameFromVis = embeddable.vis.title + ' anomaly detector 1'; + const featureList = embeddable.vis.params.series; + console.log("feature list: " + featureList) + console.log("feature name: " + featureList[0].label) + + const anomaliesOptions = [ + { value: 'option_one', text: 'Field value' }, + { value: 'option_two', text: 'Custom expression' }, + ]; + const [anomaliesValue, setAnomaliesValue] = useState(anomaliesOptions[0].value); + const anomaliesOnChange = (e) => { + setAnomaliesValue(e.target.value); + }; + + const aggMethodOptions = [ + { value: 'avg', text: 'AVG' }, + { value: 'sum', text: 'SUM' }, + ]; + const [aggMethodValue, setAggMethodValue] = useState( + featureList[0].metrics[0].type + ); + const aggMethodOnChange = (e) => { + setAggMethodValue(e.target.value); + }; + + const [shingleSizeValue, setShingleSizeValue] = useState(''); + + const shingleSizeOnChange = (e) => { + setShingleSizeValue(e.target.value); + }; + + const [checked, setChecked] = useState(false); + const onCustomerResultIndexCheckboxChange = (e) => { + setChecked(e.target.checked); + }; + + const detectorDetailsOnToggle = (isOpen) => { + setIsOpen(isOpen ? 'open' : 'closed'); + }; + + const [isOpen, setIsOpen] = useState('open'); + + return ( +
+ +

+ Create and configure an anomaly detector to automatically detect anomalies in your data and + to view real-time results on the visualization. {' '} + + Learn more + +

+
+ +
+ +

+ + {title} +

+
+ setIsShowVis(!isShowVis)} + /> +
+
+ + Promise.resolve([])} + inspector={{ isAvailable: () => false }} + hideHeader + isRetained + isBorderless + /> +
+ + +

Detector details

+
+ + {/* {!index && } */} + {/* Do not initialize until index is available */} + + {/* +

+ Detector interval: 10 minutes; Window delay: 1 minutesss +

+ + }> + + + + + + intervalOnChange(e)} + /> + + + delayOnChange(e)} + /> + + +
*/} + + + + + + Detector interval: 10 minutes; Window delay: 1 minute + + + + + + + + + intervalOnChange(e)} + /> + + + delayOnChange(e)} + /> + + + + + + + + + + + + + + + + +

+ Set number of intervals in the model's detection window. +

+
+ + +

+ The anomaly detector expects the single size to be between 1 and 60. The default shingle size + is 8. We recommend that you don't choose 1 unless you have 2 or more features. Smaller values + might increase recall but also false positives. Larger values might be useful for ignoring + noise in a signal. + + Learn more + +

+
+ + + shingleSizeOnChange(e)} + aria-label="intervals" + /> + +
+
+ + + + + +

+ Store detector results to our own index. +

+
+ + + + onCustomerResultIndexCheckboxChange(e)} + /> +
+
+ + + + + +

+ Split a single time series into multiple time series based on categorical fields. +

+
+ + + +

+ The dashboard does not support high-cardinality detectors. + + Learn more + +

+
+
+
+ + +
+
+ + + + +

Model Features

+
+ + + + + + + + + + + + + + anomaliesOnChange(e)} + /> + + + + + + + + + + + + + aggMethodOnChange(e)} + /> + + + + + + + +

+ Field: {featureList[0].metrics[0].field}, Aggregation method: {featureList[0].metrics[0].type} +

+
+
+ + + + + + + + + anomaliesOnChange(e)} + /> + + + + + + + + + + + + + aggMethodOnChange(e)} + /> + + + + + + + + +

+ Field: {featureList[1].metrics[0].field}, Aggregation method: {featureList[1].metrics[0].type} +

+
+
+ + + + + + + + + anomaliesOnChange(e)} + /> + + + + + + + + + + + + + aggMethodOnChange(e)} + /> + + + + + + + + +

+ Field: {featureList[2].metrics[0].field}, Aggregation method: {featureList[2].metrics[0].type} +

+
+
+ +
+
+
+ ); +} + +export default CreateNew; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/index.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/index.js new file mode 100644 index 000000000..95b553cce --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import CreateNew from './CreateNew'; + +export default CreateNew; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/styles.scss new file mode 100644 index 000000000..5e0d1c20f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/styles.scss @@ -0,0 +1,29 @@ +.create-new { + &__vis { + height: 400px; + + &--hidden { + display: none; + } + } + + &__title-and-toggle { + display: flex; + justify-content: space-between; + } + + &__title-icon { + margin-right: 10px; + vertical-align: middle; + } + + .visualization { + padding: 0; + } + + &__frequency { + span { + text-transform: lowercase; + } + } +} diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx new file mode 100644 index 000000000..cacc501e0 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import AddAnomalyDetector from './AddAnomalyDetector'; + +export default AddAnomalyDetector; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss new file mode 100644 index 000000000..a5cf2471a --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -0,0 +1,24 @@ +.add-anomaly-detector { + height: 100%; + display: flex; + flex-direction: column; + + .euiFlyoutBody__overflowContent { + height: 100%; + padding-bottom: 0; + } + + .euiFlexItem.add-anomaly-detector__scroll { + overflow-y: auto; + } + + &__flex-group { + height: 100%; + } + + &__modes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + } +} diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js new file mode 100644 index 000000000..3edbb24a5 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +const DocumentationTitle = () => ( + + + + {i18n.translate('dashboard.actions.adMenuItem.documentation.displayName', { + defaultMessage: 'Documentation', + })} + + + + + + +); + +export default DocumentationTitle; diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.js b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.js new file mode 100644 index 000000000..a60ab0b46 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import DocumentationTitle from './DocumentationTitle'; + +export default DocumentationTitle; diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js new file mode 100644 index 000000000..b745d3310 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiButtonIcon, + EuiButtonEmpty, + EuiAccordion, + EuiPanel, +} from '@elastic/eui'; +import './styles.scss'; + +const EnhancedAccordion = ({ + id, + title, + subTitle, + isOpen, + onToggle, + children, + isButton, + iconType, + extraAction, +}) => ( +
+
+ +
+
+ {!isButton && ( + {extraAction}
} + forceState={isOpen ? 'open' : 'closed'} + onToggle={onToggle} + buttonContent={ +
+ +

{title}

+
+ + {subTitle && ( + <> + + {subTitle} + + )} +
+ } + > + + {children} + + + )} + {isButton && ( + + + )} +
+ +); + +export default EnhancedAccordion; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js new file mode 100644 index 000000000..23d43a83c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import EnhancedAccordion from './EnhancedAccordion'; + +describe('EnhancedAccordion', () => { + test('renders', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.js b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.js new file mode 100644 index 000000000..3226d34c2 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import EnhancedAccordion from './EnhancedAccordion'; + +export default EnhancedAccordion; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss new file mode 100644 index 000000000..88ca19a17 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss @@ -0,0 +1,28 @@ +.enhanced-accordion { + &__arrow { + transition: rotate .3s; + rotate: 0deg; + + &--open { + rotate: 90deg; + } + + &--hidden { + visibility: hidden; + } + } + + &__title { + padding: 12px 16px; + } + + &__extra { + padding-right: 16px; + } + + &__button { + width: 100%; + height: 100%; + min-height: 50px; + } +} \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.js b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.js new file mode 100644 index 000000000..f2b5902ae --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { + EuiHorizontalRule, + EuiTitle, + EuiAccordion, + EuiSpacer, + EuiPanel, + EuiTextColor, + EuiText, +} from '@elastic/eui'; +import './styles.scss'; + +function MinimalAccordion({ id, isOpen, onToggle, title, subTitle, children, isUsingDivider }) { + return ( +
+ {isUsingDivider && ( + <> + + + + )} + + +
{title}
+
+ {subTitle && ( + + {subTitle} + + )} + + } + forceState={isOpen ? 'open' : 'closed'} + onToggle={onToggle} + > + + {children} + +
+
+ ); +} + +export default MinimalAccordion; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.test.js b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.test.js new file mode 100644 index 000000000..3da83605c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.test.js @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import MinimalAccordion from './MinimalAccordion'; + +describe('MinimalAccordion', () => { + test('renders', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.js b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.js new file mode 100644 index 000000000..b4e1d5392 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/index.js @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import MinimalAccordion from './MinimalAccordion'; + +export default MinimalAccordion; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss new file mode 100644 index 000000000..0afb10cf7 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/styles.scss @@ -0,0 +1,24 @@ +.minimal-accordion { + .euiAccordion__button { + align-items: flex-start; + + &:hover, + &:focus { + text-decoration: none; + + .minimal-accordion__title { + text-decoration: underline; + } + } + } + + &__title { + margin-top: -5px; + font-weight: 400; + } + + &__panel { + padding-left: 28px; + padding-bottom: 0; + } +} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts deleted file mode 100644 index 7ee985bff..000000000 --- a/public/plugin.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from '../../../src/core/public'; -import { - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart, -} from '.'; - -export class AnomalyDetectionOpenSearchDashboardsPlugin - implements - Plugin< - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart - > -{ - constructor(private readonly initializerContext: PluginInitializerContext) { - // can retrieve config from initializerContext - } - - public setup( - core: CoreSetup - ): AnomalyDetectionOpenSearchDashboardsPluginSetup { - core.application.register({ - id: 'anomaly-detection-dashboards', - title: 'Anomaly Detection', - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: 5000, - mount: async (params: AppMountParameters) => { - const { renderApp } = await import('./anomaly_detection_app'); - const [coreStart, depsStart] = await core.getStartServices(); - return renderApp(coreStart, params); - }, - }); - return {}; - } - - public start( - core: CoreStart - ): AnomalyDetectionOpenSearchDashboardsPluginStart { - return {}; - } -} diff --git a/public/plugin.tsx b/public/plugin.tsx new file mode 100644 index 000000000..d192a5f09 --- /dev/null +++ b/public/plugin.tsx @@ -0,0 +1,59 @@ +/* + * 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 { + AppMountParameters, + CoreSetup, + Plugin, + PluginInitializerContext, +} from '../../../src/core/public'; +import { CONTEXT_MENU_TRIGGER } from '../../../src/plugins/embeddable/public'; +import { ACTION_AD } from './action/ad_dashboard_action'; +import { PLUGIN_NAME } from './utils/constants'; +import { getActions } from './utils/contextMenu/getActions'; + +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_AD]: {}; + } +} + +export class AnomalyDetectionOpenSearchDashboardsPlugin implements Plugin { + public setup(core: CoreSetup, plugins) { + core.application.register({ + id: PLUGIN_NAME, + title: 'Anomaly Detection', + category: { + id: 'opensearch', + label: 'OpenSearch Plugins', + order: 2000, + }, + order: 5000, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./anomaly_detection_app'); + const [coreStart] = await core.getStartServices(); + return renderApp(coreStart, params); + }, + }); + + // Create context menu actions. Pass core, to access service for flyouts. + const actions = getActions({ core }); + + // Add actions to uiActions + actions.forEach((action) => { + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); + }); + } + + public start() {} + + public stop() {} +} \ No newline at end of file diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx new file mode 100644 index 000000000..fc659f186 --- /dev/null +++ b/public/utils/contextMenu/getActions.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { Action } from '../../../../../src/plugins/ui_actions/public'; +import { createADAction } from '../../action/ad_dashboard_action'; +import Container from '../../components/FeatureAnywhereContextMenu/Container'; +import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle'; + +// This is used to create all actions in the same context menu +const grouping: Action['grouping'] = [ + { + id: 'ad-dashboard-context-menu', + getDisplayName: () => 'Anomaly Detector', + getIconType: () => 'apmTrace', + }, +]; + +export const getActions = ({ core, plugins }) => { + const getOnClick = + (startingFlyout) => + async ({ embeddable }) => { + const services = await core.getStartServices(); + const openFlyout = services[0].overlays.openFlyout; + const overlay = openFlyout( + toMountPoint( + overlay.close(), + core, + services, + }} + /> + ), + { size: 'm', className: 'context-menu__flyout' } + ); + }; + + return [ + { + grouping, + id: 'createAnomalyDetector', + title: i18n.translate('dashboard.actions.adMenuItem.createAnomalyDetector.displayName', { + defaultMessage: 'Create anomaly detector', + }), + icon: 'plusInCircle' as EuiIconType, + order: 100, + onClick: getOnClick('create'), + }, + { + grouping, + id: 'associatedAnomalyDetector', + title: i18n.translate('dashboard.actions.adMenuItem.associatedAnomalyDetector.displayName', { + defaultMessage: 'Associated anomaly detector', + }), + icon: 'gear' as EuiIconType, + order: 99, + onClick: getOnClick('associated'), + }, + { + id: 'documentation', + title: , + icon: 'documentation' as EuiIconType, + order: 98, + onClick: () => { + window.open( + 'https://opensearch.org/docs/latest/monitoring-plugins/anomaly-detection/index/', + '_blank' + ); + }, + }, + ].map((options) => createADAction({ ...options, grouping })); +} \ No newline at end of file diff --git a/public/utils/contextMenu/indexes.ts b/public/utils/contextMenu/indexes.ts new file mode 100644 index 000000000..e8476a6e1 --- /dev/null +++ b/public/utils/contextMenu/indexes.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react'; + +export const useIndex = (embeddable) => { + const [index, setIndex] = useState(); + + useEffect(() => { + const getIndex = async () => { + await new Promise((resolve) => { + setTimeout(resolve, 4000); + }); + + const newIndex = [ + { health: 'green', label: 'opensearch_dashboards_sample_data_logs', status: 'open' }, + ]; + + setIndex(newIndex); + }; + + getIndex(); + }, [embeddable]); + + return index; +}; diff --git a/public/utils/contextMenu/styles.scss b/public/utils/contextMenu/styles.scss new file mode 100644 index 000000000..69ebfcb91 --- /dev/null +++ b/public/utils/contextMenu/styles.scss @@ -0,0 +1,25 @@ +@import '@elastic/eui/src/global_styling/variables/index'; + +.ad-dashboards-context-menu { + &__text-content { + &:hover { + text-decoration: none; + } + } + + &__no-action { + cursor: default; + + &:hover, + &:active { + text-decoration: none; + background-color: inherit; + } + } + + &__view-events-text { + h5 { + color: inherit; + } + } +} \ No newline at end of file