From 3479e20e58959f273267d2cabb5a534dc3a47686 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Thu, 25 May 2023 17:50:53 -0700 Subject: [PATCH] Adding functionality to associate existing detector with a visualization (#484) * Adding associate existing Signed-off-by: Amit Galitzky * removed usememo, addressed other comments Signed-off-by: Amit Galitzky * merge cleanup Signed-off-by: Amit Galitzky * added integration to call on alerting Signed-off-by: Amit Galitzky * cleaned up files and added changes to check if detector is deleted in expr fn Signed-off-by: Amit Galitzky * fixing dependency and notifcations issues Signed-off-by: Amit Galitzky * removed long toast life time Signed-off-by: Amit Galitzky --------- Signed-off-by: Amit Galitzky --- .../AnywhereParentFlyout.tsx | 14 +- .../AnywhereParentFlyout/constants.ts | 13 + .../containers/AssociatedDetectors.tsx | 60 ++-- .../AddAnomalyDetector.tsx | 267 +++++++++++++---- .../containers/AssociateExisting.tsx | 272 ++++++++++++++++++ .../AssociateExisting/index.ts | 6 + .../CreateAnomalyDetector/styles.scss | 10 + public/expressions/constants.ts | 2 + public/expressions/overlay_anomalies.ts | 41 ++- public/models/interfaces.ts | 1 + public/plugin.ts | 30 +- public/services.ts | 12 +- public/utils/contextMenu/getActions.tsx | 5 +- server/utils/helpers.ts | 4 + 14 files changed, 630 insertions(+), 107 deletions(-) create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx index d2e624ca..5ab72b2d 100644 --- a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -6,7 +6,8 @@ import React, { useState } from 'react'; import { get } from 'lodash'; import AssociatedDetectors from '../AssociatedDetectors/containers/AssociatedDetectors'; import { getEmbeddable } from '../../../../public/services'; -import AddAnomalyDetector from '../CreateAnomalyDetector/AddAnomalyDetector'; +import AddAnomalyDetector from '../CreateAnomalyDetector'; +import { FLYOUT_MODES } from './constants'; const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { const embeddable = getEmbeddable().getEmbeddableFactory; @@ -15,11 +16,12 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { ]; const [mode, setMode] = useState(startingFlyout); - const [selectedDetectorId, setSelectedDetectorId] = useState(); + const [selectedDetector, setSelectedDetector] = useState(undefined); const AnywhereFlyout = { - create: AddAnomalyDetector, - associated: AssociatedDetectors, + [FLYOUT_MODES.create]: AddAnomalyDetector, + [FLYOUT_MODES.associated]: AssociatedDetectors, + [FLYOUT_MODES.existing]: AddAnomalyDetector, }[mode]; return ( @@ -29,8 +31,8 @@ const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { setMode, mode, indices, - selectedDetectorId, - setSelectedDetectorId, + selectedDetector, + setSelectedDetector, }} /> ); diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts new file mode 100644 index 00000000..fa470962 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +//created: Flyout for creating a new anomaly detector from a visualization +//associated: Flyout for listing all the associated detectors to the given visualization +//existing: Flyout for associating existing detectors with the current visualizations +export enum FLYOUT_MODES { + create = 'create', + associated = 'associated', + existing = 'existing', +} diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx index c0b4f64f..69f299cc 100644 --- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx +++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx @@ -24,6 +24,7 @@ import { DetectorListItem } from '../../../../models/interfaces'; import { getSavedFeatureAnywhereLoader, getNotifications, + getUISettings, } from '../../../../services'; import { GET_ALL_DETECTORS_QUERY_PARAMS, @@ -39,7 +40,11 @@ import { EmptyAssociatedDetectorMessage, ConfirmUnlinkDetectorModal, } from '../components'; -import { ISavedAugmentVis } from '../../../../../../../src/plugins/vis_augmenter/public'; +import { + ISavedAugmentVis, + SavedAugmentVisLoader, + getAugmentVisSavedObjs, +} from '../../../../../../../src/plugins/vis_augmenter/public'; import { ASSOCIATED_DETECTOR_ACTION } from '../utils/constants'; interface ConfirmModalState { @@ -82,8 +87,10 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { ); // Establish savedObjectLoader for all operations on vis_augment saved objects - const savedObjectLoader: SavedObjectLoader = getSavedFeatureAnywhereLoader(); + const savedObjectLoader: SavedAugmentVisLoader = + getSavedFeatureAnywhereLoader(); + const uiSettings = getUISettings(); const notifications = getNotifications(); useEffect(() => { @@ -127,15 +134,12 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { // Handles 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', - [] + // Gets all augmented saved objects that are associated to the given visualization + getAugmentVisSavedObjs(embeddable.vis.id, savedObjectLoader, uiSettings) + .then((savedAugmentObjectsArr: any) => { + if (savedAugmentObjectsArr != undefined) { + console.log( + 'savedAugmentObjectsArr: ' + JSON.stringify(savedAugmentObjectsArr) ); const curSelectedDetectors = getAssociatedDetectors( Object.values(allDetectors), @@ -156,14 +160,8 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { // that are associated to the current visualization const getAssociatedDetectors = ( detectors: DetectorListItem[], - savedAugmentObjects: ISavedAugmentVis[] + savedAugmentForThisVisualization: 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) => @@ -180,18 +178,13 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { const onUnlinkDetector = async () => { setIsLoadingFinalDetectors(true); - await savedObjectLoader.findAll().then(async (resp: any) => { - if (resp != undefined) { - // gets all the saved object for this visualization - const savedAugmentForThisVisualization: ISavedAugmentVis[] = get( - resp, - 'hits', - [] as ISavedAugmentVis[] - ).filter( - (savedObj: ISavedAugmentVis[]) => - get(savedObj, 'visId', '') === embeddable.vis.id - ); - + // Gets all augmented saved objects that are associated to the given visualization + await getAugmentVisSavedObjs( + embeddable.vis.id, + savedObjectLoader, + uiSettings + ).then(async (savedAugmentForThisVisualization: any) => { + if (savedAugmentForThisVisualization != undefined) { // find saved augment object matching detector we want to unlink // There should only be one detector and vis pairing const savedAugmentToUnlink = savedAugmentForThisVisualization.find( @@ -239,11 +232,6 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); }; - // TODO: this part is incomplete because it is pending on a different PR that will have all the associate existing changes - const openAssociateDetectorFlyout = async () => { - console.log('inside create anomaly detector'); - }; - const handleUnlinkDetectorAction = (detector: DetectorListItem) => { setDetectorToUnlink(detector); setConfirmModalState({ @@ -326,7 +314,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) { fill iconType="link" onClick={() => { - openAssociateDetectorFlyout(); + setMode('existing'); }} > Associate a detector diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx index ead4014b..307f26de 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx @@ -31,6 +31,7 @@ import { createAugmentVisSavedObject, ISavedAugmentVis, ISavedPluginResource, + SavedAugmentVisLoader, VisLayerExpressionFn, VisLayerTypes, } from '../../../../../../src/plugins/vis_augmenter/public'; @@ -80,9 +81,15 @@ import { FeatureAccordion } from '../../../../public/pages/ConfigureModel/compon import { AD_DOCS_LINK, AD_HIGH_CARDINALITY_LINK, + DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, } from '../../../../public/utils/constants'; -import { getNotifications } from '../../../../public/services'; +import { + getNotifications, + getSavedFeatureAnywhereLoader, + getUISettings, + getUiActions, +} from '../../../../public/services'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, @@ -90,8 +97,18 @@ import { VIS_LAYER_PLUGIN_TYPE, } from '../../../../public/expressions/constants'; import { formikToDetectorName, visFeatureListToFormik } from './helpers'; +import { AssociateExisting } from './AssociateExisting'; +import { mountReactNode } from '../../../../../../src/core/public/utils'; +import { FLYOUT_MODES } from '../AnywhereParentFlyout/constants'; -function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { +function AddAnomalyDetector({ + embeddable, + closeFlyout, + mode, + setMode, + selectedDetector, + setSelectedDetector, +}) { const dispatch = useDispatch(); const [queryText, setQueryText] = useState(''); useEffect(() => { @@ -148,21 +165,48 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { } }; - const handleSubmit = (formikProps) => { + const uiSettings = getUISettings(); + const savedObjectLoader: SavedAugmentVisLoader = + getSavedFeatureAnywhereLoader(); + + const getAugmentVisSavedObject = (detectorId: string) => { + const fn = { + type: VisLayerTypes.PointInTimeEvents, + name: OVERLAY_ANOMALIES, + args: { + detectorId: detectorId, + }, + } as VisLayerExpressionFn; + + const pluginResource = { + type: VIS_LAYER_PLUGIN_TYPE, + id: detectorId, + } as ISavedPluginResource; + + return { + title: embeddable.vis.title, + originPlugin: ORIGIN_PLUGIN_VIS_LAYER, + pluginResource: pluginResource, + visId: embeddable.vis.id, + visLayerExpressionFn: fn, + } as ISavedAugmentVis; + }; + + // Error handeling/notification cases listed here as many things are being done sequentially + //1. if detector is created succesfully, started succesfully and associated succesfully and alerting exists -> show end message with alerting button + //2. If detector is created succesfully, started succesfully and associated succesfully and alerting doesn't exist -> show end message with OUT alerting button + //3. If detector is created succesfully, started succesfully and fails association -> show one toast with detector created, and one toast with failed association + //4. If detector is created succesfully, fails starting and fails association -> show one toast with detector created succesfully, one toast with failed association + //5. If detector is created successfully, fails starting and fails associating -> show one toast with detector created succesfully, one toast with fail starting, one toast with failed association + //6. If detector fails creating -> show one toast with detector failed creating + const handleSubmit = async (formikProps) => { formikProps.setSubmitting(true); try { const detectorToCreate = formikToDetector(formikProps.values); dispatch(createDetector(detectorToCreate)) .then(async (response) => { - notifications.toasts.addSuccess( - `Detector created: ${formikProps.values.name}` - ); dispatch(startDetector(response.response.id)) - .then((startDetectorResponse) => { - notifications.toasts.addSuccess( - `Successfully started the real-time detector` - ); - }) + .then((startDetectorResponse) => {}) .catch((err: any) => { notifications.toasts.addDanger( prettifyErrorMessage( @@ -174,34 +218,61 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { ); }); - const fn = { - type: VisLayerTypes.PointInTimeEvents, - name: OVERLAY_ANOMALIES, - args: { - detectorId: response.response.id, - }, - } as VisLayerExpressionFn; - - const pluginResource = { - type: VIS_LAYER_PLUGIN_TYPE, - id: response.response.id, - } as ISavedPluginResource; - - const savedObjectToCreate = { - title: embeddable.vis.title, - originPlugin: ORIGIN_PLUGIN_VIS_LAYER, - pluginResource: pluginResource, - visId: embeddable.vis.id, - savedObjectType: 'visualization', - visLayerExpressionFn: fn, - } as ISavedAugmentVis; - - // TODO: catch saved object failure - const savedObject = await createAugmentVisSavedObject( - savedObjectToCreate - ); - - const saveObjectResponse = await savedObject.save({}); + const detectorId = response.response.id; + const augmentVisSavedObjectToCreate: ISavedAugmentVis = + getAugmentVisSavedObject(detectorId); + + createAugmentVisSavedObject( + augmentVisSavedObjectToCreate, + uiSettings, + savedObjectLoader + ) + .then((savedObject: any) => { + savedObject + .save({}) + .then((response: any) => { + const shingleSize = get( + formikProps.values, + 'shingleSize', + DEFAULT_SHINGLE_SIZE + ); + const detectorId = get(savedObject, 'pluginResource.id', ''); + notifications.toasts.addSuccess({ + title: `The ${formikProps.values.name} is associated with the ${title} visualization`, + text: mountReactNode( + getEverythingSuccessfulButton(detectorId, shingleSize) + ), + className: 'createdAndAssociatedSuccessToast', + }); + closeFlyout(); + }) + .catch((error) => { + console.error( + `Error associating selected detector in save process: ${error}` + ); + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector in save process: ${error}` + ) + ); + notifications.toasts.addSuccess( + `Detector created: ${formikProps.values.name}` + ); + }); + }) + .catch((error) => { + console.error( + `Error associating selected detector in create process: ${error}` + ); + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector in create process: ${error}` + ) + ); + notifications.toasts.addSuccess( + `Detector created: ${formikProps.values.name}` + ); + }); }) .catch((err: any) => { dispatch(getDetectorCount()).then((response: any) => { @@ -231,6 +302,84 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { } }; + const getEverythingSuccessfulButton = (detectorId, shingleSize) => { + return ( + +

+ Attempting to initialize the detector with historical data. This + initializing process takes approximately 1 minute if you have data in + each of the last {32 + shingleSize} consecutive intervals. +

+ {alertingExists() ? ( + + +

Set up alerts to be notified of any anomalies.

+
+ +
+ openAlerting(detectorId)}> + Set up alerts + +
+
+
+ ) : null} +
+ ); + }; + + const alertingExists = () => { + try { + const uiActionService = getUiActions(); + uiActionService.getTrigger('ALERTING_TRIGGER_AD_ID'); + return true; + } catch (e) { + console.error('No alerting trigger exists', e); + return false; + } + }; + + const openAlerting = (detectorId: string) => { + const uiActionService = getUiActions(); + uiActionService + .getTrigger('ALERTING_TRIGGER_AD_ID') + .exec({ embeddable, detectorId }); + }; + + const handleAssociate = async (detector: DetectorListItem) => { + const augmentVisSavedObjectToCreate: ISavedAugmentVis = + getAugmentVisSavedObject(detector.id); + + createAugmentVisSavedObject( + augmentVisSavedObjectToCreate, + uiSettings, + savedObjectLoader + ) + .then((savedObject: any) => { + savedObject + .save({}) + .then((response: any) => { + notifications.toasts.addSuccess({ + title: `The ${detector.name} is associated with the ${title} visualization`, + text: "The detector's anomalies do not appear on the visualization. Refresh your dashboard to update the visualization", + }); + closeFlyout(); + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage( + `Error associating selected detector: ${error}` + ) + ); + }); + }) + .catch((error) => { + notifications.toasts.addDanger( + prettifyErrorMessage(`Error associating selected detector: ${error}`) + ); + }); + }; + const validateVisDetectorName = async (detectorName: string) => { if (isEmpty(detectorName)) { return 'Detector name cannot be empty'; @@ -325,7 +474,14 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { ))} - {mode === 'create' && ( + {mode === FLYOUT_MODES.existing && ( + + )} + {mode === FLYOUT_MODES.create && (

@@ -695,16 +851,27 @@ function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode }) { Cancel - { - handleValidationAndSubmit(formikProps); - }} - > - Create Detector - + {mode === FLYOUT_MODES.existing ? ( + handleAssociate(selectedDetector)} + > + Associate detector + + ) : ( + { + handleValidationAndSubmit(formikProps); + }} + > + Create detector + + )} diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx new file mode 100644 index 00000000..ba5e12fc --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/containers/AssociateExisting.tsx @@ -0,0 +1,272 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useMemo, useState } from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiIcon, + EuiText, + EuiComboBox, + EuiLoadingSpinner, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiHorizontalRule, +} from '@elastic/eui'; +import { useDispatch, useSelector } from 'react-redux'; +import { get } from 'lodash'; +import { CoreServicesContext } from '../../../../../components/CoreServices/CoreServices'; +import { CoreStart } from '../../../../../../../../src/core/public'; +import { AppState } from '../../../../../redux/reducers'; +import { DetectorListItem } from '../../../../../models/interfaces'; +import { + GET_ALL_DETECTORS_QUERY_PARAMS, + SINGLE_DETECTOR_NOT_FOUND_MSG, +} from '../../../../../pages/utils/constants'; +import { + NO_PERMISSIONS_KEY_WORD, + prettifyErrorMessage, +} from '../../../../../../server/utils/helpers'; +import { getDetectorList } from '../../../../../redux/reducers/ad'; +import { + getSavedFeatureAnywhereLoader, + getUISettings, +} from '../../../../../services'; +import { + ISavedAugmentVis, + SavedAugmentVisLoader, + getAugmentVisSavedObjs, +} from '../../../../../../../../src/plugins/vis_augmenter/public'; +import { stateToColorMap } from '../../../../../pages/utils/constants'; +import { + BASE_DOCS_LINK, + PLUGIN_NAME, +} from '../../../../../../public/utils/constants'; +import { renderTime } from '../../../../../../public/pages/DetectorsList/utils/tableUtils'; + +interface AssociateExistingProps { + embeddableVisId: string; + selectedDetector: DetectorListItem | undefined; + setSelectedDetector(detector: DetectorListItem | undefined): void; +} + +export function AssociateExisting( + associateExistingProps: AssociateExistingProps +) { + 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 uiSettings = getUISettings(); + const [isLoadingFinalDetectors, setIsLoadingFinalDetectors] = + useState(true); + const isLoading = isRequestingFromES || isLoadingFinalDetectors; + const errorGettingDetectors = useSelector( + (state: AppState) => state.ad.errorMessage + ); + const [ + existingDetectorsAvailableToAssociate, + setExistingDetectorsAvailableToAssociate, + ] = useState([] as DetectorListItem[]); + + // Establish savedObjectLoader for all operations on vis augmented saved objects + const savedObjectLoader: SavedAugmentVisLoader = + 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]); + + // Handle all changes in the assoicated detectors such as unlinking or new detectors associated + useEffect(() => { + // Gets all augmented saved objects for the given visualization + getAugmentVisSavedObjs( + associateExistingProps.embeddableVisId, + savedObjectLoader, + uiSettings + ).then((savedAugmentObjectsArr: any) => { + if (savedAugmentObjectsArr != undefined) { + const curDetectorsToDisplayOnList = + getExistingDetectorsAvailableToAssociate( + Object.values(allDetectors), + savedAugmentObjectsArr + ); + setExistingDetectorsAvailableToAssociate(curDetectorsToDisplayOnList); + 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 getExistingDetectorsAvailableToAssociate = ( + detectors: DetectorListItem[], + savedAugmentForThisVisualization: ISavedAugmentVis[] + ) => { + // Map all detector IDs for all the found augmented vis objects + const savedAugmentDetectorsSet = new Set( + savedAugmentForThisVisualization.map((savedObject) => + get(savedObject, 'pluginResource.id', '') + ) + ); + + // detectors here is all detectors + // for each detector in all detectors return that detector if that detector ID isnt in the set + // filter out any detectors that aren't on the set of detectors IDs from the augmented vis objects. + const detectorsToDisplay = detectors.filter((detector) => { + if ( + !savedAugmentDetectorsSet.has(detector.id) && + detector.detectorType === 'SINGLE_ENTITY' + ) { + return detector; + } + }); + return detectorsToDisplay; + }; + + useEffect(() => { + getDetectors(); + }, []); + + const getDetectors = async () => { + dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); + }; + + const selectedOptions = useMemo(() => { + if ( + !existingDetectorsAvailableToAssociate || + !associateExistingProps.selectedDetector + ) { + return []; + } + + const detector = (existingDetectorsAvailableToAssociate || []).find( + (detector) => + detector.id === get(associateExistingProps.selectedDetector, 'id', '') + ); + return detector ? [{ label: detector.name }] : []; + }, [ + associateExistingProps.selectedDetector, + existingDetectorsAvailableToAssociate, + ]); + + const detector = associateExistingProps.selectedDetector; + + const options = useMemo(() => { + if (!existingDetectorsAvailableToAssociate) { + return []; + } + + return existingDetectorsAvailableToAssociate.map((detector) => ({ + label: detector.name, + })); + }, [existingDetectorsAvailableToAssociate]); + + return ( +

+ +

+ View existing anomaly detectors across your system and add the + detector(s) to a dashboard and visualization.{' '} + + Learn more + +

+
+ + +

Select detector to associate

+
+ + + Eligible detectors don't include high-cardinality detectors. + + {existingDetectorsAvailableToAssociate ? ( + { + let detector = {} as DetectorListItem | undefined; + + if (selectedOptions && selectedOptions.length) { + const match = existingDetectorsAvailableToAssociate.find( + (detector) => detector.name === selectedOptions[0].label + ); + detector = match; + } + associateExistingProps.setSelectedDetector(detector); + }} + aria-label="Select an anomaly detector to associate" + isClearable + singleSelection + placeholder="Search for an anomaly detector" + /> + ) : ( + + )} + + {detector && ( + <> + + + +

{detector.name}

+
+ + + {renderTime(detector.enabledTime)} + +
+ + + View detector page + + +
+ +
    + {[ + ['Indices', (detector) => detector.indices], + [ + 'Anomalies last 24 hours', + (detector) => detector.totalAnomalies, + ], + [ + 'Last real-time occurrence', + (detector) => renderTime(detector.lastActiveAnomaly), + ], + ].map(([label, getValue]) => ( +
  • + + {label}: {getValue(detector)} + +
  • + ))} +
+ + )} +
+ ); +} + +export default AssociateExisting; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts new file mode 100644 index 00000000..90aa3ae3 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AssociateExisting/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { AssociateExisting } from './containers/AssociateExisting'; diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss index bf457fc5..e16e3895 100644 --- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss +++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss @@ -56,3 +56,13 @@ height: 100%; min-height: 40px; } + +.euiGlobalToastList { + width: 650px; +} + +.createdAndAssociatedSuccessToast { + width: 550px; + position: relative; + right: 15px; +} diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts index 2082e349..41a79276 100644 --- a/public/expressions/constants.ts +++ b/public/expressions/constants.ts @@ -11,3 +11,5 @@ export const VIS_LAYER_PLUGIN_TYPE = 'Anomaly Detectors'; export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers'; export const OVERLAY_ANOMALIES = 'overlay_anomalies'; + +export const PLUGIN_EVENT_TYPE = 'Anomalies'; diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts index 5d639297..3ff8c128 100644 --- a/public/expressions/overlay_anomalies.ts +++ b/public/expressions/overlay_anomalies.ts @@ -29,10 +29,15 @@ import { VisLayerErrorTypes, } from '../../../../src/plugins/vis_augmenter/public'; import { PLUGIN_NAME } from '../utils/constants'; -import { NO_PERMISSIONS_KEY_WORD } from '../../server/utils/helpers'; +import { + CANT_FIND_KEY_WORD, + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD, + NO_PERMISSIONS_KEY_WORD, +} from '../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, OVERLAY_ANOMALIES, + PLUGIN_EVENT_TYPE, TYPE_OF_EXPR_VIS_LAYERS, VIS_LAYER_PLUGIN_TYPE, } from './constants'; @@ -41,6 +46,8 @@ type Input = ExprVisLayers; type Output = Promise; type Name = typeof OVERLAY_ANOMALIES; +const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted'; + interface Arguments { detectorId: string; } @@ -72,9 +79,9 @@ const getAnomalies = async ( return parsePureAnomalies(anomalySummaryResponse); }; -const getDetectorName = async (detectorId: string) => { +const getDetectorResponse = async (detectorId: string) => { const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`); - return get(resp.response, 'name', ''); + return resp; }; // This takes anomalies and returns them as vis layer of type PointInTimeEvents @@ -152,7 +159,17 @@ export const overlayAnomaliesFunction = urlPath: `${PLUGIN_NAME}#/detectors/${detectorId}/results`, //details page for detector in AD plugin }; try { - const detectorName = await getDetectorName(detectorId); + const detectorResponse = await getDetectorResponse(detectorId); + if (get(detectorResponse, 'error', '').includes(CANT_FIND_KEY_WORD)) { + throw new Error('Anomaly Detector - ' + DETECTOR_HAS_BEEN_DELETED); + } else if ( + get(detectorResponse, 'error', '').includes( + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD + ) + ) { + throw new Error(get(detectorResponse, 'error', '')); + } + const detectorName = get(detectorResponse.response, 'name', ''); if (detectorName === '') { throw new Error('Anomaly Detector - Unable to get detector'); } @@ -177,15 +194,24 @@ export const overlayAnomaliesFunction = : ([anomalyLayer] as VisLayers), }; } catch (error) { - console.log('Anomaly Detector - Unable to get anomalies: ', error); + console.error('Anomaly Detector - Unable to get anomalies: ', error); let visLayerError: VisLayerError = {} as VisLayerError; if ( typeof error === 'string' && - error.includes(NO_PERMISSIONS_KEY_WORD) + (error.includes(NO_PERMISSIONS_KEY_WORD) || + error.includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD)) ) { visLayerError = { type: VisLayerErrorTypes.PERMISSIONS_FAILURE, - message: error, //TODO: might just change this to a generic message like rest of AD plugin + message: error, + }; + } else if ( + typeof error === 'string' && + error.includes(DETECTOR_HAS_BEEN_DELETED) + ) { + visLayerError = { + type: VisLayerErrorTypes.RESOURCE_DELETED, + message: error, }; } else { visLayerError = { @@ -204,6 +230,7 @@ export const overlayAnomaliesFunction = pluginResource: ADPluginResource, events: [], error: visLayerError, + pluginEventType: PLUGIN_EVENT_TYPE, } as PointInTimeEventsVisLayer; return { type: TYPE_OF_EXPR_VIS_LAYERS, diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index f8fbc248..eff5ead5 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -217,6 +217,7 @@ export type DetectorListItem = { lastActiveAnomaly: number; lastUpdateTime: number; enabledTime?: number; + detectorType?: string; }; export type EntityData = { diff --git a/public/plugin.ts b/public/plugin.ts index 0c45e15e..83fd40eb 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -13,6 +13,7 @@ import { AppMountParameters, CoreSetup, CoreStart, + NotificationsSetup, NotificationsStart, Plugin, } from '../../../src/core/public'; @@ -30,11 +31,19 @@ import { setEmbeddable, setNotifications, setOverlays, - setSavedFeatureAnywhereLoader + setSavedFeatureAnywhereLoader, + setUiActions, + setUISettings, } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; -import { VisAugmenterStart } from '../../../src/plugins/vis_augmenter/public'; - +import { + VisAugmenterSetup, + VisAugmenterStart, +} from '../../../src/plugins/vis_augmenter/public'; +import { + UiActionsSetup, + UiActionsStart, +} from '../../../src/plugins/ui_actions/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -42,14 +51,19 @@ declare module '../../../src/plugins/ui_actions/public' { } } +//TODO: there is currently no savedAugmentVisLoader in VisAugmentSetup interface, this needs to be fixed export interface AnomalyDetectionSetupDeps { embeddable: EmbeddableSetup; + notifications: NotificationsSetup; + visAugmenter: VisAugmenterSetup; + //uiActions: UiActionsSetup; } export interface AnomalyDetectionStartDeps { embeddable: EmbeddableStart; notifications: NotificationsStart; visAugmenter: VisAugmenterStart; + uiActions: UiActionsStart; } export class AnomalyDetectionOpenSearchDashboardsPlugin @@ -72,6 +86,12 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin }, }); + // // set embeddable plugin for feature anywhere create flyout + // setEmbeddable(embeddable); + + // // set vis argumenter loader for feature anywhere associated flyout + // setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); + // Set the HTTP client so it can be pulled into expression fns to make // direct server-side calls setClient(core.http); @@ -91,12 +111,14 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin public start( core: CoreStart, - { embeddable, visAugmenter }: AnomalyDetectionStartDeps + { embeddable, visAugmenter, uiActions }: AnomalyDetectionStartDeps ): AnomalyDetectionOpenSearchDashboardsPluginStart { + setUISettings(core.uiSettings); setEmbeddable(embeddable); setOverlays(core.overlays); setSavedFeatureAnywhereLoader(visAugmenter.savedAugmentVisLoader); setNotifications(core.notifications); + setUiActions(uiActions); return {}; } } diff --git a/public/services.ts b/public/services.ts index 1908f443..7e0d7843 100644 --- a/public/services.ts +++ b/public/services.ts @@ -5,15 +5,17 @@ import { CoreStart, + IUiSettingsClient, NotificationsStart, OverlayStart, } from '../../../src/core/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; -import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public'; export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = - createGetterSetter('savedFeatureAnywhereLoader'); + createGetterSetter('savedFeatureAnywhereLoader'); export const [getClient, setClient] = createGetterSetter('http'); @@ -26,3 +28,9 @@ export const [getOverlays, setOverlays] = export const [getNotifications, setNotifications] = createGetterSetter('Notifications'); + +export const [getUiActions, setUiActions] = + createGetterSetter('UIActions'); + +export const [getUISettings, setUISettings] = + createGetterSetter('UISettings'); diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index 0c1302e4..cccfd399 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -15,6 +15,7 @@ import configureStore from '../../redux/configureStore'; import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; import { AD_DOCS_LINK, APM_TRACE } from '../constants'; import { getClient, getOverlays } from '../../../public/services'; +import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; // This is used to create all actions in the same context menu const grouping: Action['grouping'] = [ @@ -58,7 +59,7 @@ export const getActions = () => { ), icon: 'plusInCircle' as EuiIconType, order: 100, - onClick: getOnClick('create'), + onClick: getOnClick(FLYOUT_MODES.create), }, { grouping, @@ -71,7 +72,7 @@ export const getActions = () => { ), icon: 'gear' as EuiIconType, order: 99, - onClick: getOnClick('associated'), + onClick: getOnClick(FLYOUT_MODES.associated), }, { id: 'documentationAnomalyDetector', diff --git a/server/utils/helpers.ts b/server/utils/helpers.ts index 035d2c74..15c80b3e 100644 --- a/server/utils/helpers.ts +++ b/server/utils/helpers.ts @@ -66,6 +66,10 @@ const PERMISSIONS_ERROR_PATTERN = export const NO_PERMISSIONS_KEY_WORD = 'no permissions'; +export const DOES_NOT_HAVE_PERMISSIONS_KEY_WORD = 'does not have permissions'; + +export const CANT_FIND_KEY_WORD = "Can't find"; + export const prettifyErrorMessage = (rawErrorMessage: string) => { if (isEmpty(rawErrorMessage) || rawErrorMessage === 'undefined') { return 'Unknown error is returned.';