/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect, Fragment } from 'react'; import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, EuiTitle, EuiButton, EuiFormFieldset, EuiCheckableCard, EuiSpacer, EuiIcon, EuiText, EuiSwitch, EuiFormRow, EuiFieldText, EuiCheckbox, EuiFlexItem, EuiFlexGroup, EuiFieldNumber, EuiCallOut, EuiButtonEmpty, EuiPanel, } from '@elastic/eui'; import './styles.scss'; import { createAugmentVisSavedObject, fetchVisEmbeddable, ISavedAugmentVis, ISavedPluginResource, SavedAugmentVisLoader, VisLayerExpressionFn, VisLayerTypes, } from '../../../../../../src/plugins/vis_augmenter/public'; import { useDispatch } from 'react-redux'; import { isEmpty, get } from 'lodash'; import { Field, FieldArray, FieldArrayRenderProps, FieldProps, Formik, } from 'formik'; import { createDetector, getDetectorCount, matchDetector, startDetector, } from '../../../../public/redux/reducers/ad'; import { EmbeddableRenderer, ErrorEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import './styles.scss'; import EnhancedAccordion from '../EnhancedAccordion'; import MinimalAccordion from '../MinimalAccordion'; import { DataFilterList } from '../../../../public/pages/DefineDetector/components/DataFilterList/DataFilterList'; import { getError, getErrorMessage, isInvalid, validateDetectorName, validateNonNegativeInteger, validatePositiveInteger, } from '../../../../public/utils/utils'; import { CUSTOM_AD_RESULT_INDEX_PREFIX, MAX_DETECTORS, } from '../../../../server/utils/constants'; import { focusOnFirstWrongFeature, initialFeatureValue, validateFeatures, } from '../../../../public/pages/ConfigureModel/utils/helpers'; import { getIndices, getMappings, } from '../../../../public/redux/reducers/opensearch'; import { formikToDetector } from '../../../../public/pages/ReviewAndCreate/utils/helpers'; import { FormattedFormRow } from '../../../../public/components/FormattedFormRow/FormattedFormRow'; import { FeatureAccordion } from '../../../../public/pages/ConfigureModel/components/FeatureAccordion'; import { AD_DOCS_LINK, AD_HIGH_CARDINALITY_LINK, DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, } from '../../../../public/utils/constants'; import { getEmbeddable, getNotifications, getSavedFeatureAnywhereLoader, getUISettings, getUiActions, getQueryService, } from '../../../../public/services'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { ORIGIN_PLUGIN_VIS_LAYER, OVERLAY_ANOMALIES, VIS_LAYER_PLUGIN_TYPE, PLUGIN_AUGMENTATION_ENABLE_SETTING, PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING, } 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'; import { DetectorListItem } from '../../../../public/models/interfaces'; import { VisualizeEmbeddable } from '../../../../../../src/plugins/visualizations/public'; function AddAnomalyDetector({ embeddable, closeFlyout, mode, setMode, selectedDetector, setSelectedDetector, }) { const dispatch = useDispatch(); const [queryText, setQueryText] = useState(''); const [generatedEmbeddable, setGeneratedEmbeddable] = useState< VisualizeEmbeddable | ErrorEmbeddable >(); useEffect(() => { const getInitialIndices = async () => { await dispatch(getIndices(queryText)); }; getInitialIndices(); dispatch(getMappings(embeddable.vis.data.aggs.indexPattern.title)); const createEmbeddable = async () => { const visEmbeddable = await fetchVisEmbeddable( embeddable.vis.id, getEmbeddable(), getQueryService() ); setGeneratedEmbeddable(visEmbeddable); }; createEmbeddable(); }, []); const [isShowVis, setIsShowVis] = useState(false); const [accordionsOpen, setAccordionsOpen] = useState({ modelFeatures: true }); const [detectorNameFromVis, setDetectorNameFromVis] = useState( formikToDetectorName(embeddable.vis.title) ); const [intervalValue, setIntervalalue] = useState(10); const [delayValue, setDelayValue] = useState(1); const [enabled, setEnabled] = useState(false); const [associationLimitReached, setAssociationLimitReached] = useState(false); const title = embeddable.getTitle(); const onAccordionToggle = (key) => { const newAccordionsOpen = { ...accordionsOpen }; newAccordionsOpen[key] = !accordionsOpen[key]; setAccordionsOpen(newAccordionsOpen); }; const onDetectorNameChange = (e, field) => { field.onChange(e); setDetectorNameFromVis(e.target.value); }; const onIntervalChange = (e, field) => { field.onChange(e); setIntervalalue(e.target.value); }; const onDelayChange = (e, field) => { field.onChange(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 < MAX_FEATURE_NUM ? aggList.length : MAX_FEATURE_NUM) ); const notifications = getNotifications(); const handleValidationAndSubmit = (formikProps) => { if (formikProps.values.featureList.length !== 0) { formikProps.setFieldTouched('featureList', true); formikProps.validateForm().then(async (errors) => { if (!isEmpty(errors)) { focusOnFirstWrongFeature(errors, formikProps.setFieldTouched); notifications.toasts.addDanger( 'One or more input fields is invalid.' ); } else { const isAugmentationEnabled = uiSettings.get( PLUGIN_AUGMENTATION_ENABLE_SETTING ); if (!isAugmentationEnabled) { notifications.toasts.addDanger( 'Visualization augmentation is disabled, please enable visualization:enablePluginAugmentation.' ); } else { const maxAssociatedCount = uiSettings.get( PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING ); await savedObjectLoader .findAll('', 100, [], { type: 'visualization', id: embeddable.vis.id as string, }) .then(async (resp) => { if (resp !== undefined) { const savedObjectsForThisVisualization = get( resp, 'hits', [] ); if ( maxAssociatedCount <= savedObjectsForThisVisualization.length ) { notifications.toasts.addDanger( `Cannot create the detector and associate it to the visualization due to the limit of the max amount of associated plugin resources (${maxAssociatedCount}) with ${savedObjectsForThisVisualization.length} associated to the visualization` ); } else { handleSubmit(formikProps); } } }); } } }); } else { notifications.toasts.addDanger('One or more features are required.'); } }; const uiSettings = getUISettings(); const savedObjectLoader: SavedAugmentVisLoader = getSavedFeatureAnywhereLoader(); let maxAssociatedCount = uiSettings.get( PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING ); useEffect(async () => { // Gets all augmented saved objects await savedObjectLoader .findAll('', 100, [], { type: 'visualization', id: embeddable.vis.id as string, }) .then(async (resp) => { if (resp !== undefined) { const savedObjectsForThisVisualization = get(resp, 'hits', []); if (maxAssociatedCount <= savedObjectsForThisVisualization.length) { setAssociationLimitReached(true); } else { setAssociationLimitReached(false); } } }); }, []); const getEmbeddableSection = () => { 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)} />
); }; 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); await dispatch(createDetector(detectorToCreate)) .then(async (response) => { dispatch(startDetector(response.response.id)) .then((startDetectorResponse) => {}) .catch((err: any) => { notifications.toasts.addDanger( prettifyErrorMessage( getErrorMessage( err, 'There was a problem starting the real-time detector' ) ) ); }); const detectorId = response.response.id; const augmentVisSavedObjectToCreate: ISavedAugmentVis = getAugmentVisSavedObject(detectorId); await createAugmentVisSavedObject( augmentVisSavedObjectToCreate, savedObjectLoader, uiSettings ) .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) => { const totalDetectors = get(response, 'response.count', 0); if (totalDetectors === MAX_DETECTORS) { notifications.toasts.addDanger( 'Cannot create detector - limit of ' + MAX_DETECTORS + ' detectors reached' ); } else { notifications.toasts.addDanger( prettifyErrorMessage( getErrorMessage( err, 'There was a problem creating the detector' ) ) ); } }); }); closeFlyout(); } catch (e) { } finally { formikProps.setSubmitting(false); } }; 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, savedObjectLoader, uiSettings ) .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)); }); }) .catch((error) => { notifications.toasts.addDanger(prettifyErrorMessage(error)); }); }; const validateVisDetectorName = async (detectorName: string) => { if (isEmpty(detectorName)) { return 'Detector name cannot be empty'; } else { const error = validateDetectorName(detectorName); if (error) { return error; } const resp = await dispatch(matchDetector(detectorName)); const match = get(resp, 'response.match', false); if (!match) { return undefined; } //If more than one detectors found, duplicate exists. if (match) { return 'Duplicate detector name'; } } }; const initialDetectorValue = { name: detectorNameFromVis, index: [{ label: embeddable.vis.data.aggs.indexPattern.title }], timeField: embeddable.vis.data.indexPattern.timeFieldName, interval: intervalValue, windowDelay: delayValue, shingleSize: 8, filterQuery: { match_all: {} }, description: 'Created based on ' + embeddable.vis.title, resultIndex: undefined, filters: [], featureList: visFeatureListToFormik( featureList, embeddable.vis.params.seriesParams ), categoryFieldEnabled: false, realTime: true, historical: false, }; return (
{(formikProps) => ( <>

Add anomaly detector

{associationLimitReached ? (
Adding more objects may affect cluster performance and prevent dashboards from rendering properly. Remove associations before adding new ones. {getEmbeddableSection()}
) : (
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 === FLYOUT_MODES.existing && ( )} {mode === FLYOUT_MODES.create && (
{getEmbeddableSection()}

Detector details

onAccordionToggle('detectorDetails')} subTitle={

Detector interval: {intervalValue} minute(s); Window delay: {delayValue} minute(s)

} > {({ field, form }: FieldProps) => ( onDetectorNameChange(e, field)} /> )} {({ field, form }: FieldProps) => ( onIntervalChange(e, field) } />

minute(s)

)}
{({ field, form }: FieldProps) => ( onDelayChange(e, field)} />

minute(s)

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

Source:{' '} {embeddable.vis.data.aggs.indexPattern.title}

{({ field, form }: FieldProps) => (

intervals

)}
{({ field, form }: FieldProps) => ( { if (enabled) { form.setFieldValue('resultIndex', ''); } setEnabled(!enabled); }} /> {enabled ? ( ) : null} {enabled ? ( ) : null} )}

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

Model Features

onAccordionToggle('modelFeatures')} > {({ push, remove, form: { values }, }: FieldArrayRenderProps) => { return ( {values.featureList.map( (feature: any, index: number) => ( { remove(index); }} index={index} feature={feature} handleChange={formikProps.handleChange} displayMode="flyout" /> ) )} = MAX_FEATURE_NUM } onClick={() => { push(initialFeatureValue()); }} > Add another feature

You can add up to{' '} {Math.max( MAX_FEATURE_NUM - values.featureList.length, 0 )}{' '} more features.

); }}
)}
)}
Cancel {mode === FLYOUT_MODES.existing ? ( handleAssociate(selectedDetector)} > Associate detector ) : ( { handleValidationAndSubmit(formikProps); }} > Create detector )} )}
); } export default AddAnomalyDetector;