diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 6868b4ed..65bfc5f7 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -3,7 +3,16 @@ "version": "3.0.0.0", "opensearchDashboardsVersion": "3.0.0", "configPath": ["anomaly_detection_dashboards"], - "requiredPlugins": ["navigation"], + "requiredPlugins": [ + "navigation", + "uiActions", + "dashboard", + "embeddable", + "opensearchDashboardsReact", + "savedObjects", + "visAugmenter", + "opensearchDashboardsUtils" + ], "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 00000000..f5de0fcd --- /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 }); + }, + }); diff --git a/public/components/ContextMenu/CreateAnomalyDetector/index.js b/public/components/ContextMenu/CreateAnomalyDetector/index.js new file mode 100644 index 00000000..e8448681 --- /dev/null +++ b/public/components/ContextMenu/CreateAnomalyDetector/index.js @@ -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 + + + + + + ); +}; diff --git a/public/components/ContextMenu/CreateAnomalyDetector/styles.scss b/public/components/ContextMenu/CreateAnomalyDetector/styles.scss new file mode 100644 index 00000000..8dc2ec9f --- /dev/null +++ b/public/components/ContextMenu/CreateAnomalyDetector/styles.scss @@ -0,0 +1,5 @@ +.create-anomaly-detector { + &__create { + align-self: flex-end; + } +} diff --git a/public/components/ContextMenu/Notifications/index.js b/public/components/ContextMenu/Notifications/index.js new file mode 100644 index 00000000..2fe066dd --- /dev/null +++ b/public/components/ContextMenu/Notifications/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { + EuiLink, + EuiText, + EuiSpacer, + EuiPanel, + EuiIcon, + EuiFormRow, +} from '@elastic/eui'; + +export const Notifications = () => ( + <> + + + + 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. + + + + + + + + Add notifications + + + + +); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx new file mode 100644 index 00000000..607c4f8e --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx @@ -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(false); + const isLoading = isModalLoading || props.isListLoading; + return ( + + + + + {'Remove association?'}  + + + + + Removing association unlinks {props.detector.name} detector from the + visualization but does not delete it. The detector association can + be restored. + + + + + {isLoading ? null : ( + + Cancel + + )} + { + setIsModalLoading(true); + props.onUnlinkDetector(); + props.onConfirm(); + }} + > + {'Remove association'} + + + + + ); +}; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyMessage/EmptyMessage.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyMessage/EmptyMessage.tsx new file mode 100644 index 00000000..aae18dc6 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyMessage/EmptyMessage.tsx @@ -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 +) => ( + No anomaly detectors to display} + titleSize="s" + data-test-subj="emptyAssociatedDetectorFlyoutMessage" + style={{ maxWidth: '45em' }} + body={ + +

+ {props.isFilterApplied + ? FILTER_TEXT + : `There are no anomaly detectors associated with ${props.embeddableTitle} visualization.`} +

+
+ } + /> +); diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx new file mode 100644 index 00000000..55edd1ad --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -0,0 +1,341 @@ +/* + * 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, { useMemo, useEffect, useState } from 'react'; +import { + EuiFlyoutHeader, + EuiTitle, + EuiSpacer, + EuiInMemoryTable, + EuiFlyoutBody, + EuiButton, + EuiFlyout, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { get, isEmpty } from 'lodash'; +import '../styles.scss'; +import { getColumns } from '../utils/helpers'; +import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from '../../../../redux/reducers'; +import { DetectorListItem } from '../../../../models/interfaces'; +import { getSavedFeatureAnywhereLoader } from '../../../../services'; +import { + GET_ALL_DETECTORS_QUERY_PARAMS, + SINGLE_DETECTOR_NOT_FOUND_MSG, +} from '../../../../pages/utils/constants'; +import { getDetectorList } from '../../../../redux/reducers/ad'; +import { + prettifyErrorMessage, + NO_PERMISSIONS_KEY_WORD, +} from '../../../../../server/utils/helpers'; +import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; +import { EmptyAssociatedDetectorFlyoutMessage } from '../components/EmptyMessage/EmptyMessage'; +import { ISavedAugmentVis } from '../../../../../../../src/plugins/vis_augmenter/public'; +import { ASSOCIATED_DETECTOR_ACTION } from '../utils/constants'; +import { ConfirmUnlinkDetectorModal } from '../components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal'; + +interface ConfirmModalState { + isOpen: boolean; + action: ASSOCIATED_DETECTOR_ACTION; + isListLoading: boolean; + isRequestingToClose: boolean; + affectedDetector: DetectorListItem; +} + +export const AssociatedDetectors = ({ embeddable, closeFlyout }) => { + const core = React.useContext(CoreServicesContext) as CoreStart; + const dispatch = useDispatch(); + const allDetectors = useSelector((state: AppState) => state.ad.detectorList); + const isRequestingFromES = useSelector( + (state: AppState) => state.ad.requesting + ); + const [isLoadingFinalDetectors, setIsLoadingFinalDetectors] = + useState(true); + const isLoading = isRequestingFromES || isLoadingFinalDetectors; + const errorGettingDetectors = useSelector( + (state: AppState) => state.ad.errorMessage + ); + const embeddableTitle = embeddable.getTitle(); + const [selectedDetectors, setSelectedDetectors] = useState( + [] as DetectorListItem[] + ); + + const [detectorToUnlink, setDetectorToUnlink] = useState( + {} as DetectorListItem + ); + const [confirmModalState, setConfirmModalState] = useState( + { + isOpen: false, + //@ts-ignore + action: null, + isListLoading: false, + isRequestingToClose: false, + affectedDetector: {} as DetectorListItem, + } + ); + + // Establish savedObjectLoader for all operations on vis augmented saved objects + const savedObjectLoader: SavedObjectLoader = getSavedFeatureAnywhereLoader(); + + useEffect(() => { + if ( + errorGettingDetectors && + !errorGettingDetectors.includes(SINGLE_DETECTOR_NOT_FOUND_MSG) + ) { + console.error(errorGettingDetectors); + core.notifications.toasts.addDanger( + typeof errorGettingDetectors === 'string' && + errorGettingDetectors.includes(NO_PERMISSIONS_KEY_WORD) + ? prettifyErrorMessage(errorGettingDetectors) + : 'Unable to get all detectors' + ); + setIsLoadingFinalDetectors(false); + } + }, [errorGettingDetectors]); + + // Update modal state if user decides to close modal + useEffect(() => { + if (confirmModalState.isRequestingToClose) { + if (isLoading) { + setConfirmModalState({ + ...confirmModalState, + isListLoading: true, + }); + } else { + setConfirmModalState({ + ...confirmModalState, + isOpen: false, + isListLoading: false, + isRequestingToClose: false, + }); + } + } + }, [confirmModalState.isRequestingToClose, isLoading]); + + useEffect(() => { + getDetectors(); + }, []); + + // Handle all changes in the assoicated detectors such as unlinking or new detectors associated + useEffect(() => { + // Gets all augmented saved objects + savedObjectLoader.findAll().then((resp: any) => { + if (resp != undefined) { + const savedAugmentObjectsArr: ISavedAugmentVis[] = get( + resp, + 'hits', + [] + ); + const curSelectedDetectors = getAssociatedDetectors( + Object.values(allDetectors), + savedAugmentObjectsArr + ); + setSelectedDetectors(curSelectedDetectors); + setIsLoadingFinalDetectors(false); + } + }); + }, [allDetectors]); + + // cross checks all the detectors that exist with all the savedAugment Objects to only display ones + // that are associated to the current visualization + const getAssociatedDetectors = ( + detectors: DetectorListItem[], + savedAugmentObjects: ISavedAugmentVis[] + ) => { + // Filter all savedAugmentObjects that aren't linked to the specific visualization + const savedAugmentForThisVisualization: ISavedAugmentVis[] = + savedAugmentObjects.filter( + (savedObj) => get(savedObj, 'visId', '') === embeddable.vis.id + ); + + // Map all detector IDs for all the found augmented vis objects + const savedAugmentDetectorsSet = new Set( + savedAugmentForThisVisualization.map((savedObject) => + get(savedObject, 'pluginResourceId', '') + ) + ); + + // filter out any detectors that aren't on the set of detectors IDs from the augmented vis objects. + const detectorsToDisplay = detectors.filter((detector) => + savedAugmentDetectorsSet.has(detector.id) + ); + return detectorsToDisplay; + }; + + const onUnlinkDetector = async () => { + setIsLoadingFinalDetectors(true); + await savedObjectLoader.findAll().then(async (resp: any) => { + if (resp != undefined) { + const savedAugmentObjects: ISavedAugmentVis[] = get(resp, 'hits', []); + // gets all the saved object for this visualization + const savedAugmentForThisVisualization: ISavedAugmentVis[] = + savedAugmentObjects.filter( + (savedObj) => get(savedObj, 'visId', '') === embeddable.vis.id + ); + + // find saved Augment object matching detector we want to unlink + // There should only be one detector and vis pairing + const savedAugmentToUnlink = savedAugmentForThisVisualization.find( + (savedObject) => + get(savedObject, 'pluginResourceId', '') === detectorToUnlink.id + ); + const savedObjectToUnlinkId = get(savedAugmentToUnlink, 'id', ''); + await savedObjectLoader + .delete(savedObjectToUnlinkId) + .catch((error) => { + core.notifications.toasts.addDanger( + prettifyErrorMessage( + `Error unlinking selected detector: ${error}` + ) + ); + }) + .finally(() => { + getDetectors(); + }); + } + }); + }; + + const getUnlinkConfirmModal = () => { + if (confirmModalState.isOpen) { + return ( + + ); + } + }; + + const handleHideModal = () => { + setConfirmModalState({ + ...confirmModalState, + isOpen: false, + }); + }; + + const handleConfirmModal = () => { + setConfirmModalState({ + ...confirmModalState, + isRequestingToClose: true, + }); + }; + + const getDetectors = async () => { + dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); + }; + + // TODO: this part is incomplete because it is pending on complete the work for associating an existing + // detector which is dependent on changes in the action.tsx code that jackie will merge in + const onAssociateExistingDetector = async () => { + console.log('inside create anomaly detector'); + }; + + const handleUnlinkDetectorAction = (detector: DetectorListItem) => { + setDetectorToUnlink(detector); + if (!isEmpty(detector)) { + setConfirmModalState({ + isOpen: true, + action: ASSOCIATED_DETECTOR_ACTION.UNLINK, + isListLoading: false, + isRequestingToClose: false, + affectedDetector: detector, + }); + } else { + core.notifications.toasts.addWarning( + 'Make sure selected detector has not been deleted' + ); + } + }; + + const columns = useMemo( + () => getColumns({ handleUnlinkDetectorAction }), + [handleUnlinkDetectorAction] + ); + + const renderEmptyMessage = () => { + if (isLoading) { + return 'Loading detectors...'; + } else if (!isEmpty(selectedDetectors)) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const tableProps = { + items: selectedDetectors, + columns, + search: { + box: { + disabled: selectedDetectors.length === 0, + incremental: true, + schema: true, + }, + }, + hasActions: true, + pagination: true, + sorting: true, + message: renderEmptyMessage(), + }; + return ( +
+ + + +

+ Associated anomaly detectors +

+
+
+ + {getUnlinkConfirmModal()} + + + +

{embeddableTitle}

+
+
+ + { + onAssociateExistingDetector(); + }} + > + Associate a detector + + +
+ + +
+
+
+ ); +}; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts new file mode 100644 index 00000000..a25a81fc --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export { AssociatedDetectors } from './containers/AssociatedDetectors'; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss new file mode 100644 index 00000000..e6520e0e --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss @@ -0,0 +1,16 @@ +@import '@elastic/eui/src/global_styling/variables/index'; + +.associated-detectors { + height: 100%; + display: flex; + flex-direction: column; + + .euiFlyoutBody__overflowContent { + height: 100%; + padding-bottom: 0; + } + + &__flex-group { + height: 100%; + } +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx new file mode 100644 index 00000000..16d43420 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx @@ -0,0 +1,14 @@ +/* + * 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. + */ + +export enum ASSOCIATED_DETECTOR_ACTION { + UNLINK, +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx new file mode 100644 index 00000000..b4668a4f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx @@ -0,0 +1,81 @@ +/* + * 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 from 'react'; +import { EuiBasicTableColumn, EuiHealth, EuiLink } from '@elastic/eui'; +import { DETECTOR_STATE } from 'server/utils/constants'; +import { stateToColorMap } from '../../../../pages/utils/constants'; +import { PLUGIN_NAME } from '../../../../utils/constants'; +import { Detector } from '../../../../models/interfaces'; + +export const renderState = (state: DETECTOR_STATE) => { + return ( + //@ts-ignore + {state} + ); +}; + +export const getColumns = ({ handleUnlinkDetectorAction }) => + [ + { + field: 'name', + name: 'Detector', + sortable: true, + truncateText: true, + width: '30%', + align: 'left', + render: (name: string, detector: Detector) => ( + + {name} + + ), + }, + { + field: 'curState', + name: 'Real-time state', + sortable: true, + align: 'left', + width: '30%', + truncateText: true, + render: renderState, + }, + { + field: 'totalAnomalies', + name: 'Anomalies/24hr', + sortable: true, + dataType: 'number', + align: 'left', + truncateText: true, + width: '30%', + }, + { + name: 'Actions', + align: 'left', + truncateText: true, + width: '10%', + actions: [ + { + type: 'icon', + name: 'Unlink Detector', + description: 'Unlink Detector', + icon: 'unlink', + onClick: handleUnlinkDetectorAction, + }, + ], + }, + ] as EuiBasicTableColumn[]; + +export const search = { + box: { + incremental: true, + schema: true, + }, +}; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/DetectorDetails/index.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/DetectorDetails/index.js new file mode 100644 index 00000000..2723804f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/DetectorDetails/index.js @@ -0,0 +1,82 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + EuiLink, + EuiText, + EuiSpacer, + EuiPanel, + EuiIcon, + EuiAccordion, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiCheckbox, +} from '@elastic/eui'; +const DetectorDetails = () => { + 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 [checked, setChecked] = useState(false); + const onCustomerResultIndexCheckboxChange = (e) => { + setChecked(e.target.checked); + }; + + return ( + <> + + + + + + + + + + + + intervalOnChange(e)} + /> + + + delayOnChange(e)} + /> + + + + + onCustomerResultIndexCheckboxChange(e)} + /> + + + ); +}; + +export default DetectorDetails; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/Features/index.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/Features/index.js new file mode 100644 index 00000000..77774a1f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/Features/index.js @@ -0,0 +1,78 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + EuiHorizontalRule, + EuiTextColor, + EuiPanel, + EuiIcon, + EuiAccordion, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; + +const Features = () => { + const aggMethodOptions = [ + { value: 'avg', text: 'AVG' }, + { value: 'sum', text: 'SUM' }, + ]; + const [aggMethodValue, setAggMethodValue] = useState( + aggMethodOptions[0].value + ); + const aggMethodOnChange = (e) => { + setAggMethodValue(e.target.value); + }; + + return ( + <> + + + + + Find anomalies based on + + + + + + Aggregation method + aggMethodOnChange(e)} + /> + + + + + + + + Find anomalies based on + + + + + + Aggregation method + aggMethodOnChange(e)} + /> + + + + {/* + +

Selected aggration method is incompatible

+
*/} +
+ + ); +}; + +export default Features; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/ShingleSize/index.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/ShingleSize/index.js new file mode 100644 index 00000000..2a3f0e0d --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/ShingleSize/index.js @@ -0,0 +1,37 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + EuiHorizontalRule, + EuiTextColor, + EuiPanel, + EuiIcon, + EuiAccordion, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; + +const ShingleSize = () => { + return ( + <> + +

+ + Set the number of intervals to consider in a detection window for + your model. The anomaly detector expects the shingle size to be in + the range of 1 and 60. The default shingle size is 8. We recommend + that you don't choose 1 unless you have two or more features. + Smaller values might increase recall but also false positives. + Larger values might be useful for ignoring noise in a signal. + +

+ + + +
+ + ); +}; + +export default ShingleSize; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.js new file mode 100644 index 00000000..c0d80471 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/index.js @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexItem, + EuiFlexGroup, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiCheckableCard, + EuiSpacer, + EuiFlyout, +} from '@elastic/eui'; +// import { +// OuiCheckableCard +// } from '@elastic/eui'; +import './styles.scss'; +import { EmbeddablePanel } from '../../../../../../src/plugins/embeddable/public'; +import DetectorDetails from './DetectorDetails'; +import Features from './Features'; + +const accordions = [ + 'detectorDetails', + 'features', + 'shingleSize', + 'alerts', + 'triggers', +].reduce((acc, cur) => ({ ...acc, [cur]: cur }), {}); + +function CreateAnomalyDetector({ embeddable }) { + const [radio, setRadio] = useState('createRadio'); + const [accordionOpen, setAccordionOpen] = useState(accordions.triggers); + + return ( +
+ + + +

Add anomaly detector

+
+
+ + + + setRadio('createRadio')} + /> + + + setRadio('associateRadio')} + /> + + + + + This is a short description of the feature to get users exicted. + Learn more in the documentation. + + + {/* + +
+ Promise.resolve([])} + getAllEmbeddableFactories={() => []} + getEmbeddableFactory={() => null} + notifications={{}} + application={{}} + overlays={{}} + inspector={{ isAvailable: () => null }} + SavedObjectFinder={() => null} + /> +
+
+ + +
DETECTOR DETAILS
+ + } + initialIsOpen={true} + forceState={accordionOpen === accordions.detectorDetails ? 'open' : 'closed'} + onToggle={() => + setAccordionOpen( + accordionOpen !== accordions.detectorDetails && accordions.detectorDetails + ) + } + > + + +
+ + +
FEATURES
+ + } + initialIsOpen={true} + // forceState={accordionOpen === accordions.detectorDetails ? 'open' : 'closed'} + // onToggle={() => + // setAccordionOpen( + // accordionOpen !== accordions.detectorDetails && accordions.detectorDetails + // ) + // } + > + +
+ +
+
*/} +
+
+
+ ); +} +export default CreateAnomalyDetector; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss new file mode 100644 index 00000000..bc3581c3 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -0,0 +1,25 @@ +.create-anomaly-detector { + .euiFlexItem.create-anomaly-detector__aside { + width: 400px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + } + + .euiAccordion__button { + align-items: flex-start; + + &:hover, + &:focus { + text-decoration: none; + + h6 { + text-decoration: underline; + } + } + } + + &__vis { + height: 400px; + } +} diff --git a/public/components/FeatureAnywhereContextMenu/FormikWrapper/index.js b/public/components/FeatureAnywhereContextMenu/FormikWrapper/index.js new file mode 100644 index 00000000..c0e664fb --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/FormikWrapper/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { useFormik, FormikProvider } from 'formik'; + +const FormikWrapper = ({ getFormikOptions, children, ...props }) => { + const formik = useFormik(getFormikOptions()); + return ( + {children} + ); +}; + +export default FormikWrapper; diff --git a/public/expressions/index.ts b/public/expressions/index.ts new file mode 100644 index 00000000..f8a2fd64 --- /dev/null +++ b/public/expressions/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export * from './overlay_anomalies'; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts new file mode 100644 index 00000000..133174fc --- /dev/null +++ b/public/expressions/overlay_anomalies.ts @@ -0,0 +1,10 @@ +// /* +// * 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. +// */ diff --git a/public/plugin.ts b/public/plugin.ts deleted file mode 100644 index 7ee985bf..00000000 --- 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 00000000..deddbd00 --- /dev/null +++ b/public/plugin.tsx @@ -0,0 +1,63 @@ +/* + * 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, +} from '../../../src/core/public'; +import { CONTEXT_MENU_TRIGGER } from '../../../src/plugins/embeddable/public'; +import { ACTION_AD, createADAction } from './action/ad_dashboard_action'; +import { PLUGIN_NAME } from './utils/constants'; +import { getActions } from './utils/contextMenu/action'; +import { setSavedFeatureAnywhereLoader } from './services'; + +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(core: CoreStart, plugins) { + setSavedFeatureAnywhereLoader(plugins.visAugmenter.savedAugmentVisLoader); + return {}; + } + + public stop() {} +} diff --git a/public/services.ts b/public/services.ts new file mode 100644 index 00000000..c2bbaf37 --- /dev/null +++ b/public/services.ts @@ -0,0 +1,35 @@ +/* + * 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. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License 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 { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; +import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; + +export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = + createGetterSetter('savedFeatureAnywhereLoader'); diff --git a/public/utils/contextMenu/action.tsx b/public/utils/contextMenu/action.tsx new file mode 100644 index 00000000..2a775c65 --- /dev/null +++ b/public/utils/contextMenu/action.tsx @@ -0,0 +1,105 @@ +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 CreateAnomalyDetector from '../../components/FeatureAnywhereContextMenu/CreateAnomalyDetector'; +import { AssociatedDetectors } from '../../components/FeatureAnywhereContextMenu/AssociatedDetectors'; +import { CoreServicesContext } from '../../components/CoreServices/CoreServices'; +import { Provider } from 'react-redux'; +import configureStore from '../../redux/configureStore'; + +// 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', + order: 200, + }, +]; + +export const getActions = ({ core }) => + [ + { + grouping, + id: 'createAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.adMenuItem.createAnomalyDetector.displayName', + { + defaultMessage: 'Create anomaly detector', + } + ), + icon: 'plusInCircle' as EuiIconType, + order: 100, + onClick: async ({ embeddable }) => { + const services = await core.getStartServices(); + const http = services[0].http; + const store = configureStore(http); + const openFlyout = services[0].overlays.openFlyout; + openFlyout( + toMountPoint( + + + + + + ), + { size: 'm' } + ); + }, + }, + { + grouping, + id: 'manageAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.alertingMenuItem.manageAnomalyDetector.displayName', + { + defaultMessage: 'Manage anomaly detector', + } + ), + icon: 'wrench' as EuiIconType, + order: 99, + onClick: async ({ embeddable }) => { + const services = await core.getStartServices(); + const http = services[0].http; + const store = configureStore(http); + const openFlyout = services[0].overlays.openFlyout; + const overlay = openFlyout( + toMountPoint( + + + overlay.close(), + core, + services, + }} + /> + + + ), + { size: 'm' } + ); + }, + }, + { + id: 'documentation', + title: i18n.translate( + 'dashboard.actions.adMenuItem.documentation.displayName', + { + defaultMessage: 'Documentation', + } + ), + icon: 'documentation' as EuiIconType, + order: 98, + onClick: () => { + window.open( + 'https://opensearch.org/docs/latest/monitoring-plugins/alerting/index/', + '_blank' + ); + }, + }, + ].map((options) => createADAction({ ...options, grouping })); diff --git a/public/utils/contextMenu/helper.js b/public/utils/contextMenu/helper.js new file mode 100644 index 00000000..2ba1729d --- /dev/null +++ b/public/utils/contextMenu/helper.js @@ -0,0 +1,13 @@ +export const getInitialValues = () => ({ + ...{ ...FORMIK_INITIAL_VALUES, name: 'Monitor 1' }, + triggers: [ + { + ...FORMIK_INITIAL_TRIGGER_CONDITION_VALUES, + name: 'New trigger', + id: Date.now(), + severity: '1', + }, + ], + monitors: getInitialMonitors(), + alerts: getInitialAlerts(), +}); diff --git a/public/utils/contextMenu/styles.scss b/public/utils/contextMenu/styles.scss new file mode 100644 index 00000000..89ee5baf --- /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; + } + } +}