diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index a88771b0a..2242614c7 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -4,12 +4,15 @@ "opensearchDashboardsVersion": "3.0.0", "configPath": ["anomaly_detection_dashboards"], "requiredPlugins": [ - "navigation", "uiActions", "dashboard", "embeddable", "opensearchDashboardsReact", - "savedObjects" + "savedObjects", + "visAugmenter", + "opensearchDashboardsUtils", + "data", + "expressions" ], "optionalPlugins": [], "server": true, diff --git a/package.json b/package.json index a68755228..a710256e2 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "babel-polyfill": "^6.26.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", - "lint-staged": "^9.2.0", + "lint-staged": "^10.0.0", "moment": "^2.24.0", "redux-mock-store": "^1.5.3", "start-server-and-test": "^1.11.7" @@ -38,7 +38,7 @@ "dependencies": { "babel-polyfill": "^6.26.0", "brace": "0.11.1", - "formik": "^2.2.5", + "formik": "^2.2.6", "plotly.js-dist": "^1.57.1", "prettier": "^2.1.1", "react-plotly.js": "^2.4.0", diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx index 0d8c43894..f5de0fcd9 100644 --- a/public/action/ad_dashboard_action.tsx +++ b/public/action/ad_dashboard_action.tsx @@ -71,4 +71,4 @@ export const createADAction = ({ onClick({ embeddable }); }, - }); \ No newline at end of file + }); diff --git a/public/components/ContextMenu/CreateAnomalyDetector/index.tsx b/public/components/ContextMenu/CreateAnomalyDetector/index.tsx deleted file mode 100644 index f84c2ba37..000000000 --- a/public/components/ContextMenu/CreateAnomalyDetector/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -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 deleted file mode 100644 index 2316fd381..000000000 --- a/public/components/ContextMenu/CreateAnomalyDetector/styles.scss +++ /dev/null @@ -1,5 +0,0 @@ -.create-anomaly-detector { - &__create { - align-self: flex-end; - } -} \ No newline at end of file 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 000000000..ec700bcba --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx @@ -0,0 +1,80 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 000000000..7e110e27f --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/EmptyMessage/EmptyMessage.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 000000000..cfa670c0c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -0,0 +1,337 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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; +} + +function 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 + + +
+ + +
+
+
+ ); +} + +export default AssociatedDetectors; diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts new file mode 100644 index 000000000..394836499 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 000000000..192c2cda2 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +@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/CreateAnomalyDetector/CreateNew/index.js b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx similarity index 56% rename from public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/index.js rename to public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx index 95b553cce..37236349b 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/index.js +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/constants.tsx @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import CreateNew from './CreateNew'; - -export default CreateNew; +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 000000000..1b1550860 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/utils/helpers.tsx @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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/Container/Container.js b/public/components/FeatureAnywhereContextMenu/Container/Container.js index 61c0abef7..96b88bdcf 100644 --- a/public/components/FeatureAnywhereContextMenu/Container/Container.js +++ b/public/components/FeatureAnywhereContextMenu/Container/Container.js @@ -1,16 +1,17 @@ import React, { useState } from 'react'; -import CreateAnomalyDetector from '../CreateAnomalyDetector'; -import { useIndex } from '../../../utils/contextMenu/indexes'; import './styles.scss'; +import AddAnomalyDetector from '../CreateAnomalyDetector'; +import AssociatedDetectors from '../AssociatedDetectors/containers/AssociatedDetectors'; const Container = ({ startingFlyout, ...props }) => { const { embeddable } = props; console.log({ embeddable }); - const index = useIndex(embeddable); + const index = [{ label: embeddable?.vis?.data?.indexPattern?.title }]; const [mode, setMode] = useState(startingFlyout); const Flyout = { - create: CreateAnomalyDetector, + create: AddAnomalyDetector, + associated: AssociatedDetectors, }[mode]; return ( diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index 5f3a7b14f..69e9d6733 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -3,22 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useState } from 'react'; import { - EuiFlexItem, - EuiFlexGroup, EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiTitle, - EuiSpacer, EuiButton, - EuiButtonEmpty, EuiFormFieldset, EuiCheckableCard, + EuiSpacer, + EuiIcon, + EuiText, + EuiSwitch, + EuiButtonIcon, + EuiFormRow, + EuiFieldText, + EuiCheckbox, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, + EuiFieldNumber, + EuiCallOut, + EuiButtonEmpty, } from '@elastic/eui'; import './styles.scss'; -import CreateNew from './CreateNew'; +import { + createAugmentVisSavedObject, + ISavedAugmentVis, + VisLayerExpressionFn, +} from '../../../../../../src/plugins/vis_augmenter/public'; +import { useDispatch, useSelector } from 'react-redux'; +import { snakeCase, find } from 'lodash'; +import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik'; +import { + createDetector, + startDetector, +} from '../../../../public/redux/reducers/ad'; +import { EmbeddablePanel } from '../../../../../../src/plugins/embeddable/public'; +import './styles.scss'; +import EnhancedAccordion from '../EnhancedAccordion'; +import MinimalAccordion from '../MinimalAccordion'; +import { Detector, UNITS } from '../../../../public/models/interfaces'; +import { AppState } from '../../../../public/redux/reducers'; +import { AGGREGATION_TYPES } from '../../../../public/pages/ConfigureModel/utils/constants'; +import { DataFilterList } from '../../../../public/pages/DefineDetector/components/DataFilterList/DataFilterList'; function AddAnomalyDetector({ embeddable, @@ -29,74 +58,598 @@ function AddAnomalyDetector({ setMode, index, }) { - const onCreate = () => { - console.log(`Current mode: ${mode}`); - const event = new Event('createDetector'); - document.dispatchEvent(event); - closeFlyout(); + const dispatch = useDispatch(); + const isLoading = useSelector((state: AppState) => state.ad.requesting); + + const [isShowVis, setIsShowVis] = useState(false); + const [accordionsOpen, setAccordionsOpen] = useState({}); + const [detectorNameFromVis, setDetectorNameFromVis] = useState( + formikToDetectorName(embeddable.vis.title) + ); + const [intervalValue, setIntervalalue] = useState(10); + const [delayValue, setDelayValue] = useState(1); + + const onAccordionToggle = (key) => { + const newAccordionsOpen = { ...accordionsOpen }; + newAccordionsOpen[key] = !accordionsOpen[key]; + setAccordionsOpen(newAccordionsOpen); + }; + + const title = embeddable.getTitle(); + + const intervalOnChange = (e) => { + setIntervalalue(e.target.value); + }; + + const delayOnChange = (e) => { + setDelayValue(e.target.value); + }; + + const aggList = embeddable.vis.data.aggs.aggs.filter( + (feature) => feature.schema == 'metric' + ); + + const featureList = aggList.filter( + (feature, index) => index < (aggList.length < 5 ? aggList.length : 5) + ); + console.log('featureList: ', JSON.stringify(featureList)); + + // console.log("feature list size: " + featureList.length) + // console.log("feature name: " + featureList[0].data.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 AGGREGATION_TYPES = [{ value: 'sum', text: 'sum()' }]; + + const [aggMethodValue, setAggMethodValue] = useState(); + const aggMethodOnChange = (e) => { + setAggMethodValue(e.target.value); + }; + + const [shingleSizeValue, setShingleSizeValue] = useState(8); + + const shingleSizeOnChange = (e) => { + setShingleSizeValue(e.target.value); + }; + + const [checked, setChecked] = useState(false); + const onCustomerResultIndexCheckboxChange = (e) => { + setChecked(e.target.checked); }; + const getFeatureNameFromParams = (id) => { + let name = find(embeddable.vis.params.seriesParams, function (param) { + if (param.data.id === id) { + return true; + } + }); + return name.data.label; + }; + + let defaultFeatureList = []; + featureList.map((feature) => + defaultFeatureList.push({ + id: feature.id, + featureName: getFeatureNameFromParams(feature.id), + field: feature.params.field.name, + aggMethod: feature.type.title, + }) + ); + + const [feautreListToRender, setFeatureListToRender] = + useState(defaultFeatureList); + + const handleDeleteFeature = (id) => { + setFeatureListToRender( + feautreListToRender.filter((feature) => feature.id !== id) + ); + }; + + const handleAddFeature = () => { + let uuid = Math.floor(100000 + Math.random() * 900000); + const emptyFeatureComponenet = { + id: uuid, + featureName: 'feature_' + uuid, + field: 'byte', + aggMethod: 'avg', + }; + setFeatureListToRender([...feautreListToRender, emptyFeatureComponenet]); + }; + + const handleSubmit = () => { + try { + dispatch(createDetector(initialDetectorValue)).then(async (response) => { + console.log('detector id here: ' + response.response.id); + dispatch(startDetector(response.response.id)).then( + (startDetectorResponse) => { + core.notifications.toasts.addSuccess( + `Detector created: ${initialDetectorValue.name}` + ); + } + ); + enum VisLayerTypes { + PointInTimeEvents = 'PointInTimeEvents', + } + const fn = { + type: VisLayerTypes.PointInTimeEvents, + name: 'overlay_anomalies', + args: { + detectorId: response.response.id, + }, + } as VisLayerExpressionFn; + + const savedObjectToCreate = { + title: embeddable.vis.title, + pluginResourceId: response.response.id, + visId: embeddable.vis.id, + savedObjectType: 'visualization', + visLayerExpressionFn: fn, + } as ISavedAugmentVis; + + const savedObject = await createAugmentVisSavedObject( + savedObjectToCreate + ); + + const saveObjectResponse = await savedObject.save({}); + console.log('response: ' + JSON.stringify(saveObjectResponse)); + }); + closeFlyout(); + } catch (e) { + console.log('errrrrror: ' + e); + } finally { + } + }; + + const initialDetectorValue = { + name: detectorNameFromVis, + indices: formikToIndicesArray(embeddable.vis.data.aggs.indexPattern.title), + timeField: embeddable.vis.data.indexPattern.timeFieldName, + detectionInterval: { + period: { interval: intervalValue, unit: UNITS.MINUTES }, + }, + windowDelay: { + period: { interval: delayValue, unit: UNITS.MINUTES }, + }, + shingleSize: shingleSizeValue, + featureAttributes: formikToFeatureAttributes(featureList), + filterQuery: { match_all: {} }, + description: '', + resultIndex: undefined, + }; + + function formikToDetectorName(title) { + const detectorName = + title + + '_anomaly_detector_' + + Math.floor(100000 + Math.random() * 900000); + detectorName.replace(/ /g, '_'); + return detectorName; + } + + function formikToIndicesArray(indexString) { + return [indexString]; + } + + function formikToFeatureAttributes(values) { + //@ts-ignore + return values.map(function (value) { + return { + featureId: value.id, + featureName: getFeatureNameFromParams(value.id), + featureEnabled: true, + importance: 1, + aggregationQuery: formikToAggregation(value), + }; + }); + } + + function formikToAggregation(value) { + return { + [snakeCase(getFeatureNameFromParams(value.id))]: { + sum: { field: value.params.field.name }, + }, + }; + } + + console.log('initialDetectorValue: ', JSON.stringify(initialDetectorValue)); + 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 - - - - + + {(formikProps) => ( +
+ + +

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' && ( +
+ +

+ 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 */} + + onAccordionToggle('detectorDetails')} + subTitle={ + +

+ Detector interval: {intervalValue} minutes; Window + delay: {delayValue} minute +

+
+ } + > + + + setDetectorNameFromVis(e.target.value) + } + /> + + + + + + intervalOnChange(e)} + /> + + + +

minutes

+
+
+
+
+ + + + + delayOnChange(e)} + /> + + + +

minutes

+
+
+
+
+
+ + + + + onAccordionToggle('advancedConfiguration') + } + initialIsOpen={false} + > + + + + +

Source: {embeddable.vis.params.index_pattern}

+
+ + {}}> + + Add data filter + + {/* */} +
+ + + + +

+ 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" + /> +
+ + + + + + onCustomerResultIndexCheckboxChange(e) + } + /> + + + + +

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

+
+
+
+ + + +

Model Features

+
+ + + onAccordionToggle('modelFeatures')} + initialIsOpen={true} + > + + + {feautreListToRender.map((feature, index) => { + return ( + handleDeleteFeature(feature.id)} + /> + } + > + + + + + anomaliesOnChange(e)} + /> + + + + + + + + + + + + + aggMethodOnChange(e)} + /> + + + + + ); + })} + + +
+ handleAddFeature()} + iconType="plusInCircle" + > + Add feature + +
+ +
+ )} +
+
+ + + + Cancel + + + + {mode === 'existing' ? 'Associate' : 'Create'} detector + + + + +
+ )} +
); } diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js deleted file mode 100644 index b626ebe22..000000000 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/CreateNew.js +++ /dev/null @@ -1,474 +0,0 @@ -/* - * 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/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/styles.scss deleted file mode 100644 index 5e0d1c20f..000000000 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/CreateNew/styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -.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/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss index a5cf2471a..4a63bc768 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -22,3 +22,33 @@ gap: 12px; } } + +.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; + } +} + +.featureButton { + width: 100%; + height: 40px; +} diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js index 3edbb24a5..3ee81e656 100644 --- a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/DocumentationTitle.js @@ -11,9 +11,12 @@ const DocumentationTitle = () => ( - {i18n.translate('dashboard.actions.adMenuItem.documentation.displayName', { - defaultMessage: 'Documentation', - })} + {i18n.translate( + 'dashboard.actions.adMenuItem.documentation.displayName', + { + defaultMessage: 'Documentation', + } + )} diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js index b745d3310..61752b3c8 100644 --- a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.js @@ -19,17 +19,18 @@ const EnhancedAccordion = ({ isButton, iconType, extraAction, + initialIsOpen, }) => (
@@ -37,9 +38,12 @@ const EnhancedAccordion = ({ {extraAction}
} + extraAction={ +
{extraAction}
+ } forceState={isOpen ? 'open' : 'closed'} onToggle={onToggle} + initialIsOpen={initialIsOpen} buttonContent={
- + > )}
); -export default EnhancedAccordion; \ No newline at end of file +export default EnhancedAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js index 23d43a83c..07cce2cbe 100644 --- a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/EnhancedAccordion.test.js @@ -12,4 +12,4 @@ describe('EnhancedAccordion', () => { 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 index 3226d34c2..0b994f5f5 100644 --- a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.js +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/index.js @@ -5,4 +5,4 @@ import EnhancedAccordion from './EnhancedAccordion'; -export default EnhancedAccordion; \ No newline at end of file +export default EnhancedAccordion; diff --git a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss index 88ca19a17..de1ad4b9a 100644 --- a/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss +++ b/public/components/FeatureAnywhereContextMenu/EnhancedAccordion/styles.scss @@ -1,6 +1,6 @@ .enhanced-accordion { &__arrow { - transition: rotate .3s; + transition: rotate 0.3s; rotate: 0deg; &--open { @@ -25,4 +25,4 @@ 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 index f2b5902ae..530065ca1 100644 --- a/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.js +++ b/public/components/FeatureAnywhereContextMenu/MinimalAccordion/MinimalAccordion.js @@ -10,7 +10,14 @@ import { } from '@elastic/eui'; import './styles.scss'; -function MinimalAccordion({ id, isOpen, onToggle, title, subTitle, children, isUsingDivider }) { +function MinimalAccordion({ + id, + title, + subTitle, + children, + isUsingDivider, + extraAction, +}) { return (
{isUsingDivider && ( @@ -33,8 +40,9 @@ function MinimalAccordion({ id, isOpen, onToggle, title, subTitle, children, isU )} } - forceState={isOpen ? 'open' : 'closed'} - onToggle={onToggle} + extraAction={ +
{extraAction}
+ } > ; + +interface Arguments { + detectorId: string; +} + +const name = 'overlay_anomalies'; + +export type OverlayAnomaliesExpressionFunctionDefinition = + ExpressionFunctionDefinition<'overlay_anomalies', Input, Arguments, Output>; + +// This gets all the needed anomalies for the given detector ID and time range +const getAnomalies = async ( + detectorId: string, + startTime: number, + endTime: number +): Promise => { + const anomalySummaryQuery = getAnomalySummaryQuery( + startTime, + endTime, + detectorId, + undefined, + true + ); + + const anomalySummaryResponse = await getClient().post( + `..${AD_NODE_API.DETECTOR}/results/_search`, + { + body: JSON.stringify(anomalySummaryQuery), + } + ); + + return parsePureAnomalies(anomalySummaryResponse); +}; + +const getDetectorName = async (detectorId: string) => { + const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); + return get(resp.response, 'name', ''); +}; + +// This takes anomalies and returns them as vis layer of type PointInTimeEvents +const convertAnomaliesToPointInTimeEventsVisLayer = ( + anomalies: AnomalyData[], + ADPluginResource: PluginResource +): PointInTimeEventsVisLayer => { + const events = anomalies.map((anomaly: AnomalyData) => { + return { + timestamp: anomaly.startTime + (anomaly.endTime - anomaly.startTime) / 2, + metadata: {}, + }; + }); + return { + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + type: VisLayerTypes.PointInTimeEvents, + pluginResource: ADPluginResource, + events: events, + } as PointInTimeEventsVisLayer; +}; + +/* + * This function defines the Anomaly Detection expression function of type vis_layers. + * The expression-fn defined takes an argument of detectorId and an array of VisLayers as input, + * it then returns back the VisLayers array with an additional vislayer composed of anomalies. + * + * The purpose of this function is to allow us on the visualization rendering to gather additional + * overlays from an associated plugin resource such as an anomaly detector in this occasion. The VisLayers will + * now have anomaly data as one of its VisLayers. + * + * To create the new added VisLayer the function uses the detectorId and daterange from the search context + * to fetch anomalies. Next, the anomalies are mapped into events based on timestamps in order to convert them to a + * PointInTimeEventsVisLayer. + * + * If there are any errors fetching the anomalies the function will return a VisLayerError in the + * VisLayer detailing the error type. + */ +export const overlayAnomaliesFunction = + (): OverlayAnomaliesExpressionFunctionDefinition => ({ + name, + type: TYPE_OF_EXPR_VIS_LAYERS, + inputTypes: [TYPE_OF_EXPR_VIS_LAYERS], + help: i18n.translate('data.functions.overlay_anomalies.help', { + defaultMessage: 'Add an anomaly vis layer', + }), + args: { + detectorId: { + types: ['string'], + default: '""', + help: '', + }, + }, + + async fn(input, args, context): Promise { + // Parsing all of the args & input + const detectorId = get(args, 'detectorId', ''); + const timeRange = get( + context, + 'searchContext.timeRange', + '' + ) as TimeRange; + const origVisLayers = get(input, 'layers', [] as VisLayers) as VisLayers; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const startTimeInMillis = parsedTimeRange?.min?.unix() + ? parsedTimeRange?.min?.unix() * 1000 + : undefined; + const endTimeInMillis = parsedTimeRange?.max?.unix() + ? parsedTimeRange?.max?.unix() * 1000 + : undefined; + var ADPluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: detectorId, + name: '', + urlPath: `${PLUGIN_NAME}#/detectors/${detectorId}/results`, //details page for detector in AD plugin + }; + try { + const detectorName = await getDetectorName(detectorId); + if (detectorName === '') { + throw new Error('Anomaly Detector - Unable to get detector'); + } + ADPluginResource.name = detectorName; + + if (startTimeInMillis === undefined || endTimeInMillis === undefined) { + throw new RangeError('start or end time invalid'); + } + const anomalies = await getAnomalies( + detectorId, + startTimeInMillis, + endTimeInMillis + ); + + console.log('anomalies: ' + JSON.stringify(anomalies)); + const anomalyLayer = convertAnomaliesToPointInTimeEventsVisLayer( + anomalies, + ADPluginResource + ); + return { + type: TYPE_OF_EXPR_VIS_LAYERS, + layers: origVisLayers + ? origVisLayers.concat(anomalyLayer) + : ([anomalyLayer] as VisLayers), + }; + } catch (error) { + console.log('Anomaly Detector - Unable to get anomalies: ', error); + let visLayerError: VisLayerError = {} as VisLayerError; + if ( + typeof error === 'string' && + error.includes(NO_PERMISSIONS_KEY_WORD) + ) { + visLayerError = { + type: VisLayerErrorTypes.PERMISSIONS_FAILURE, + message: error, //TODO: might just change this to a generic message like rest of AD plugin + }; + } else { + visLayerError = { + type: VisLayerErrorTypes.FETCH_FAILURE, + message: + error === 'string' + ? error + : error instanceof Error + ? error.message + : '', + }; + } + const anomalyErrorLayer = { + type: VisLayerTypes.PointInTimeEvents, + originPlugin: PLUGIN_NAME, + pluginResource: ADPluginResource, + events: [], + error: visLayerError, + } as PointInTimeEventsVisLayer; + return { + type: TYPE_OF_EXPR_VIS_LAYERS, + layers: origVisLayers + ? origVisLayers.concat(anomalyErrorLayer) + : ([anomalyErrorLayer] as VisLayers), + }; + } + }, + }); diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index f8fbc2489..42cc83964 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -191,7 +191,7 @@ export type Detector = { windowDelay: { period: Schedule }; detectionInterval: { period: Schedule }; shingleSize: number; - uiMetadata: UiMetaData; + uiMetadata?: UiMetaData; lastUpdateTime: number; enabled?: boolean; enabledTime?: number; diff --git a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx index 1055c2d44..eac5800df 100644 --- a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx +++ b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx @@ -174,6 +174,7 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { setIsCreatingDetector(true); formikHelpers.setSubmitting(true); const detectorToCreate = formikToDetector(values); + console.log('detector to create: ', detectorToCreate); dispatch(createDetector(detectorToCreate)) .then((response: any) => { core.notifications.toasts.addSuccess( diff --git a/public/plugin.tsx b/public/plugin.tsx index d192a5f09..2f48bae43 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -19,6 +19,9 @@ 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'; +import { setSavedFeatureAnywhereLoader } from './services'; +import { overlayAnomaliesFunction } from './expressions/overlay_anomalies'; +import { setClient } from './services'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -51,9 +54,19 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin implements Plugin { actions.forEach((action) => { plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); }); - } - public start() {} + // Set the HTTP client so it can be pulled into expression fns to make + // direct server-side calls + setClient(core.http); + + // registers the expression function used to render anomalies on an Augmented Visualization + plugins.expressions.registerFunction(overlayAnomaliesFunction); + return {}; + } + public start(core: CoreStart, plugins) { + setSavedFeatureAnywhereLoader(plugins.visAugmenter.savedAugmentVisLoader); + return {}; + } public stop() {} -} \ No newline at end of file +} diff --git a/public/services.ts b/public/services.ts new file mode 100644 index 000000000..d9161693e --- /dev/null +++ b/public/services.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from '../../../src/core/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; +import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; + +export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = + createGetterSetter('savedFeatureAnywhereLoader'); + +export const [getClient, setClient] = + createGetterSetter('http'); diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index fc659f186..04fbbaaee 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -6,6 +6,9 @@ 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'; +import { Provider } from 'react-redux'; +import { CoreServicesContext } from '../../../public/components/CoreServices/CoreServices'; +import configureStore from '../../redux/configureStore'; // This is used to create all actions in the same context menu const grouping: Action['grouping'] = [ @@ -22,55 +25,67 @@ export const getActions = ({ core, plugins }) => { async ({ embeddable }) => { const services = await core.getStartServices(); const openFlyout = services[0].overlays.openFlyout; + const http = services[0].http; + const store = configureStore(http); const overlay = openFlyout( toMountPoint( - overlay.close(), - core, - services, - }} - /> + + + overlay.close(), + core, + services, + }} + /> + + ), { size: 'm', className: 'context-menu__flyout' } ); }; - return [ - { - grouping, - id: 'createAnomalyDetector', - title: i18n.translate('dashboard.actions.adMenuItem.createAnomalyDetector.displayName', { + 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', { + } + ), + 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' - ); - }, + } + ), + icon: 'gear' as EuiIconType, + order: 99, + onClick: getOnClick('associated'), + }, + { + id: 'documentationAnomalyDetector', + title: , + icon: 'documentation' as EuiIconType, + order: 98, + onExecute: () => { + window.open( + 'https://opensearch.org/docs/latest/monitoring-plugins/anomaly-detection/index/', + '_blank' + ); }, - ].map((options) => createADAction({ ...options, grouping })); -} \ No newline at end of file + }, + ].map((options) => createADAction({ ...options, grouping })); +}; diff --git a/public/utils/contextMenu/indexes.ts b/public/utils/contextMenu/indexes.ts index e8476a6e1..b3f0ed065 100644 --- a/public/utils/contextMenu/indexes.ts +++ b/public/utils/contextMenu/indexes.ts @@ -10,7 +10,11 @@ export const useIndex = (embeddable) => { }); const newIndex = [ - { health: 'green', label: 'opensearch_dashboards_sample_data_logs', status: 'open' }, + { + health: 'green', + label: 'opensearch_dashboards_sample_data_logs', + status: 'open', + }, ]; setIndex(newIndex); diff --git a/public/utils/contextMenu/styles.scss b/public/utils/contextMenu/styles.scss index 69ebfcb91..89ee5baf7 100644 --- a/public/utils/contextMenu/styles.scss +++ b/public/utils/contextMenu/styles.scss @@ -22,4 +22,4 @@ color: inherit; } } -} \ No newline at end of file +} diff --git a/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.7.0.0.md b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.7.0.0.md new file mode 100644 index 000000000..dc6c1a532 --- /dev/null +++ b/release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.7.0.0.md @@ -0,0 +1,7 @@ +## Version 2.7.0.0 Release Notes + +Compatible with OpenSearch Dashboards 2.7.0 + +### Enhancements + +* Run prettier command against all files ([#444](https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/pull/444))