@@ -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.';
From 5e76e44dc2fbe76acb71dfe683ecc04ea4a39186 Mon Sep 17 00:00:00 2001
From: Amit Galitzky
Date: Fri, 26 May 2023 10:02:27 -0700
Subject: [PATCH 24/43] switched argument order (#494)
Signed-off-by: Amit Galitzky
---
.../CreateAnomalyDetector/AddAnomalyDetector.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
index 307f26de..9e490801 100644
--- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
+++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
@@ -224,8 +224,8 @@ function AddAnomalyDetector({
createAugmentVisSavedObject(
augmentVisSavedObjectToCreate,
- uiSettings,
- savedObjectLoader
+ savedObjectLoader,
+ uiSettings
)
.then((savedObject: any) => {
savedObject
@@ -352,8 +352,8 @@ function AddAnomalyDetector({
createAugmentVisSavedObject(
augmentVisSavedObjectToCreate,
- uiSettings,
- savedObjectLoader
+ savedObjectLoader,
+ uiSettings
)
.then((savedObject: any) => {
savedObject
From 9308a4ccb120399202446982f396186ae8dcb793 Mon Sep 17 00:00:00 2001
From: Jackie Han
Date: Tue, 30 May 2023 13:54:15 -0700
Subject: [PATCH 25/43] add jest transform config (#497)
* add jest transform config
Signed-off-by: Jackie Han
* add license header
Signed-off-by: Jackie Han
* update file as .ts file
Signed-off-by: Jackie Han
* add comment on jest config file
Signed-off-by: Jackie Han
* add more comments
Signed-off-by: Jackie Han
* cleanup
Signed-off-by: Jackie Han
* update file export
Signed-off-by: Jackie Han
---------
Signed-off-by: Jackie Han
---
test/jest.config.js | 10 ++++++++++
test/mocks/transformMock.ts | 27 +++++++++++++++++++++++++++
2 files changed, 37 insertions(+)
create mode 100644 test/mocks/transformMock.ts
diff --git a/test/jest.config.js b/test/jest.config.js
index c726f920..242924cc 100644
--- a/test/jest.config.js
+++ b/test/jest.config.js
@@ -44,4 +44,14 @@ module.exports = {
testPathIgnorePatterns: ['/build/', '/node_modules/'],
transformIgnorePatterns: ['/node_modules'],
globalSetup: '/global-setup.js',
+
+ /**
+ * This configuration specifies different file extensions
+ * and the corresponding transformers to be used
+ */
+ transform: {
+ '\\.[jt]sx?$': 'babel-jest',
+ '^.+\\.svg$': '/test/mocks/transformMock.ts',
+ '^.+\\.html$': '/test/mocks/transformMock.ts',
+ },
};
diff --git a/test/mocks/transformMock.ts b/test/mocks/transformMock.ts
new file mode 100644
index 00000000..ac888d71
--- /dev/null
+++ b/test/mocks/transformMock.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * The transform configuration in Jest allows you to
+ * specify custom transformation logic for specific file types during testing.
+ */
+module.exports = {
+ /**
+ * This function is responsible for transforming the file.
+ * @returns the string module.exports = {};, which is an empty CommonJS module.
+ */
+ process() {
+ return {
+ code: `module.exports = {};`,
+ };
+ },
+ /**
+ * The cache key helps Jest determine if a file needs to be retransformed or if it can use the cached transformation result.
+ * @returns a unique string that serves as a cache key for the transformation.
+ */
+ getCacheKey() {
+ return 'svgTransform';
+ },
+};
\ No newline at end of file
From db4e039c03d2b67d0ea385180e1db1cb29bd33c0 Mon Sep 17 00:00:00 2001
From: Amit Galitzky
Date: Wed, 31 May 2023 16:31:33 -0700
Subject: [PATCH 26/43] Merging main branch into featureAnywhere (#499)
* Add branch constants in CI workflow (#345)
Signed-off-by: Tyler Ohlsen
* Bump decode-uri-component (#359)
Signed-off-by: Tyler Ohlsen
* removed duplicate popout icon and ran prettier (#382)
Signed-off-by: Amit Galitzky
* Change detector out of time range modal warning into a callout warning (#384)
Signed-off-by: Jackie Han
Signed-off-by: Jackie Han
* Fix undefined entity list when heatmap is empty (#383)
Signed-off-by: Tyler Ohlsen
* Updated MAINTAINERS.md to match recommended opensearch-project format. (#388)
Signed-off-by: dblock
* Add windows env to integration test workflow (#390)
Signed-off-by: Tyler Ohlsen
* Bump json5 to 2.2.3 (#393)
Signed-off-by: Tyler Ohlsen
* Add 2.5 release notes (#395)
Signed-off-by: Tyler Ohlsen
* Update cold start message (#398)
Signed-off-by: Kaituo Li
* upgrade filter bug (#402)
Previously, we didn't actually add filter type when loading old detector. This PR fixed that.
Testing done:
1. added a unit tes
2. verified e2et
Signed-off-by: Kaituo Li
* Changed required minimum intervals in cold start message (#411)
Required minimum intervals to finish cold start is related to shingle size. This PR adds the shingle size in the computation and puts the result in the cold start message.
Testing done:
1. verified changing shingle size will change the message.
Signed-off-by: Kaituo Li
* Bump @sideway/formula to 3.0.1 (#418)
Signed-off-by: Tyler Ohlsen
* Remove auto_expand_replicas override in sample data indices (#423)
Signed-off-by: Tyler Ohlsen
* Created untriaged issue workflow. (#422)
Signed-off-by: dblock
* Add 2.6 release notes (#429)
Signed-off-by: Tyler Ohlsen
* Fix Node.js and Yarn installation in CI (#433)
Signed-off-by: Miki
* run prettier command against all files (#444)
Signed-off-by: Jackie Han
* Add 2.7 release notes (#456)
Signed-off-by: Jackie Han
* updating maintainers and code owners (#476)
Signed-off-by: Amit Galitzky
* fixing test to pass with node 18 (#491)
Signed-off-by: Amit Galitzky
---------
Signed-off-by: Tyler Ohlsen
Signed-off-by: Amit Galitzky
Signed-off-by: Jackie Han
Signed-off-by: dblock
Signed-off-by: Kaituo Li
Signed-off-by: Miki
Co-authored-by: Tyler Ohlsen
Co-authored-by: Jackie Han
Co-authored-by: Daniel (dB.) Doubrovkine
Co-authored-by: Kaituo Li
Co-authored-by: Miki
---
.github/CODEOWNERS | 3 +--
MAINTAINERS.md | 15 ++++++++++++---
.../__tests__/NameAndDescription.test.tsx | 10 +++++++---
...-detection-dashboards.release-notes-2.7.0.0.md | 7 +++++++
4 files changed, 27 insertions(+), 8 deletions(-)
create mode 100644 release-notes/opensearch-anomaly-detection-dashboards.release-notes-2.7.0.0.md
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index cd025f06..48711d5f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1 @@
-# This should match the owning team set up in https://github.com/orgs/opensearch-project/teams
-* @opensearch-project/anomaly-detection
\ No newline at end of file
+* @ohltyler @kaituo @jackiehanyang @amitgalitz @sean-zheng-amazon
\ No newline at end of file
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index bafb14f5..6a406fb4 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -4,9 +4,18 @@ This document contains a list of maintainers in this repo. See [opensearch-proje
## Current Maintainers
-| Maintainer | GitHub ID | Affiliation |
-| ----------------------- | ------------------------------------------------------- | ----------- |
-| Tyler Ohlsen | [ohltyler](https://github.com/ohltyler) | Amazon |
+| Maintainer | GitHub ID | Affiliation |
+| ----------------------- | -------------------------------------------------------- | ----------- |
+| Tyler Ohlsen | [ohltyler](https://github.com/ohltyler) | Amazon |
+| Kaituo Li | [kaituo](https://github.com/kaituo) | Amazon |
+| Jackie Han | [jackiehanyang](https://github.com/jackiehanyang) | Amazon |
+| Amit Galitzky | [amitgalitz](https://github.com/amitgalitz) | Amazon |
+| Sean Zheng | [sean-zheng-amazon](https://github.com/sean-zheng-amazon)| Amazon |
+
+## Emeritus Maintainers
+
+| Maintainer | GitHub ID | Affiliation |
+| ----------------- | ------------------------------------------------------- | ----------- |
| Yaliang | [ylwu-amzn](https://github.com/ylwu-amzn) | Amazon |
| Yizhe Liu | [yizheliu-amazon](https://github.com/yizheliu-amazon) | Amazon |
| Vijayan Balasubramanian | [VijayanB](https://github.com/VijayanB) | Amazon |
diff --git a/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx b/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx
index 32b564fa..dbd5908c 100644
--- a/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx
+++ b/public/pages/DefineDetector/components/NameAndDescription/__tests__/NameAndDescription.test.tsx
@@ -29,11 +29,14 @@ describe(' spec', () => {
expect(container.firstChild).toMatchSnapshot();
});
test('shows error for detector name input when toggling focus/blur', async () => {
- const handleValidateName = jest.fn().mockImplementation(() => {
- throw 'Required';
+ const handleValidateName = jest.fn().mockImplementationOnce(() => {
+ return 'Required';
});
const { queryByText, findByText, getByPlaceholderText } = render(
-
+
{() => (
@@ -49,6 +52,7 @@ describe('
spec', () => {
expect(handleValidateName).toHaveBeenCalledTimes(1);
expect(findByText('Required')).not.toBeNull();
});
+
test('shows error for detector description input when toggling focus/bur', async () => {
const { queryByText, findByText, getByPlaceholderText } = render(
Date: Wed, 7 Jun 2023 17:22:37 -0700
Subject: [PATCH 27/43] Unit tests for expression function and additional
components (#503)
* adding UT for expression function and some components
Signed-off-by: Amit Galitzky
* moved helper functions to separate files, cleaned up other tests
Signed-off-by: Amit Galitzky
* custom result bug fix along with a few others
Signed-off-by: Amit Galitzky
* revert historical boolean
Signed-off-by: Amit Galitzky
* add pluginEventType when no error
Signed-off-by: Amit Galitzky
---------
Signed-off-by: Amit Galitzky
---
.../ConfirmUnlinkDetectorModal.tsx | 6 +-
.../ConfirmUnlinkDetectorModal.test.tsx | 69 +++++++
.../EmptyAssociatedDetectorMessage.test.tsx | 40 ++++
...ptyAssociatedDetectorMessage.test.tsx.snap | 69 +++++++
.../containers/AssociatedDetectors.tsx | 8 +-
.../AssociatedDetectors/styles.scss | 4 +
.../__tests__/overlay_anomalies.test.ts | 152 +++++++++++++++
public/expressions/constants.ts | 4 +
public/expressions/helpers.ts | 139 ++++++++++++++
public/expressions/overlay_anomalies.ts | 112 ++---------
.../__tests__/AnomaliesLiveCharts.test.tsx | 4 +-
.../Dashboard/utils/__tests__/utils.test.tsx | 18 +-
.../__tests__/anomalyResultUtils.test.ts | 18 +-
public/pages/utils/__tests__/constants.ts | 177 +++++++++++++++++-
public/utils/contextMenu/getActions.tsx | 4 +-
15 files changed, 698 insertions(+), 126 deletions(-)
create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/ConfirmUnlinkDetectorModal.test.tsx
create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx
create mode 100644 public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap
create mode 100644 public/expressions/__tests__/overlay_anomalies.test.ts
create mode 100644 public/expressions/helpers.ts
diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx
index 25687ed5..98d5d155 100644
--- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx
+++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/ConfirmUnlinkDetectorModal/ConfirmUnlinkDetectorModal.tsx
@@ -34,7 +34,7 @@ export const ConfirmUnlinkDetectorModal = (
return (
@@ -52,14 +52,14 @@ export const ConfirmUnlinkDetectorModal = (
{isLoading ? null : (
Cancel
)}
{
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const testDetectors = [
+ {
+ id: 'detectorId1',
+ name: 'test-detector-1',
+ },
+ {
+ id: 'detectorId2',
+ name: 'test-detector-2',
+ },
+ ] as DetectorListItem[];
+
+ const ConfirmUnlinkDetectorModalProps = {
+ detector: testDetectors[0],
+ onHide: jest.fn(),
+ onConfirm: jest.fn(),
+ onUnlinkDetector: jest.fn(),
+ isListLoading: false,
+ };
+
+ test('renders the component correctly', () => {
+ const { container, getByText } = render(
+
+ );
+ getByText('Remove association?');
+ getByText(
+ 'Removing association unlinks test-detector-1 detector from the visualization but does not delete it. The detector association can be restored.'
+ );
+ });
+ test('should call onConfirm() when closing', async () => {
+ const { container, getByText, getByTestId } = render(
+
+ );
+ getByText('Remove association?');
+ userEvent.click(getByTestId('confirmUnlinkButton'));
+ expect(ConfirmUnlinkDetectorModalProps.onConfirm).toHaveBeenCalled();
+ });
+ test('should call onConfirm() when closing', async () => {
+ const { container, getByText, getByTestId } = render(
+
+ );
+ getByText('Remove association?');
+ userEvent.click(getByTestId('confirmUnlinkButton'));
+ expect(ConfirmUnlinkDetectorModalProps.onConfirm).toHaveBeenCalled();
+ });
+ test('should call onHide() when closing', async () => {
+ const { getByTestId } = render(
+
+ );
+ userEvent.click(getByTestId('cancelUnlinkButton'));
+ expect(ConfirmUnlinkDetectorModalProps.onHide).toHaveBeenCalled();
+ });
+});
diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx
new file mode 100644
index 00000000..21b684be
--- /dev/null
+++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/EmptyAssociatedDetectorMessage.test.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { findAllByTestId, render, waitFor } from '@testing-library/react';
+import { EmptyAssociatedDetectorMessage } from '../index';
+import { getRandomDetector } from '../../../../../../public/redux/reducers/__tests__/utils';
+import { DetectorListItem } from '../../../../../../public/models/interfaces';
+import userEvent from '@testing-library/user-event';
+
+describe('ConfirmUnlinkDetectorModal spec', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders the component with filter applied', () => {
+ const { container, getByText } = render(
+
+ );
+ getByText('There are no detectors matching your search');
+ expect(container).toMatchSnapshot();
+ });
+ test('renders the component with filter applied', () => {
+ const { container, getByText } = render(
+
+ );
+ getByText(
+ 'There are no anomaly detectors associated with test-title visualization.'
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap
new file mode 100644
index 00000000..15c1a6c3
--- /dev/null
+++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/components/__tests__/__snapshots__/EmptyAssociatedDetectorMessage.test.tsx.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConfirmUnlinkDetectorModal spec renders the component with filter applied 1`] = `
+
+
+
+ No anomaly detectors to display
+
+
+
+
+
+
+ There are no detectors matching your search
+
+
+
+
+
+
+`;
+
+exports[`ConfirmUnlinkDetectorModal spec renders the component with filter applied 2`] = `
+
+
+
+ No anomaly detectors to display
+
+
+
+
+
+
+ There are no anomaly detectors associated with test-title visualization.
+
+
+
+
+
+
+`;
diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx
index 69f299cc..0829f4ec 100644
--- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx
+++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/containers/AssociatedDetectors.tsx
@@ -35,7 +35,6 @@ import {
prettifyErrorMessage,
NO_PERMISSIONS_KEY_WORD,
} from '../../../../../server/utils/helpers';
-import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public';
import {
EmptyAssociatedDetectorMessage,
ConfirmUnlinkDetectorModal,
@@ -138,9 +137,6 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {
getAugmentVisSavedObjs(embeddable.vis.id, savedObjectLoader, uiSettings)
.then((savedAugmentObjectsArr: any) => {
if (savedAugmentObjectsArr != undefined) {
- console.log(
- 'savedAugmentObjectsArr: ' + JSON.stringify(savedAugmentObjectsArr)
- );
const curSelectedDetectors = getAssociatedDetectors(
Object.values(allDetectors),
savedAugmentObjectsArr
@@ -287,7 +283,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {
-
+
Associated anomaly detectors
@@ -306,7 +302,7 @@ function AssociatedDetectors({ embeddable, closeFlyout, setMode }) {
- {embeddableTitle}
+ Visualization: {embeddableTitle}
diff --git a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss
index 6598f00e..0c3fe230 100644
--- a/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss
+++ b/public/components/FeatureAnywhereContextMenu/AssociatedDetectors/styles.scss
@@ -16,4 +16,8 @@
&__flex-group {
height: 100%;
}
+
+ &__associate-button {
+ flex: 0 0 auto;
+ }
}
diff --git a/public/expressions/__tests__/overlay_anomalies.test.ts b/public/expressions/__tests__/overlay_anomalies.test.ts
new file mode 100644
index 00000000..d55ef2e4
--- /dev/null
+++ b/public/expressions/__tests__/overlay_anomalies.test.ts
@@ -0,0 +1,152 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { setClient } from '../../services';
+import { httpClientMock } from '../../../test/mocks';
+import {
+ convertAnomaliesToPointInTimeEventsVisLayer,
+ getAnomalies,
+ getVisLayerError,
+ getDetectorResponse,
+} from '../helpers';
+import {
+ ANOMALY_RESULT_SUMMARY,
+ ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ NO_ANOMALIES_RESULT_RESPONSE,
+ PARSED_ANOMALIES,
+ SELECTED_DETECTORS,
+} from '../../pages/utils/__tests__/constants';
+import {
+ DETECTOR_HAS_BEEN_DELETED,
+ START_OR_END_TIME_INVALID_ERROR,
+ VIS_LAYER_PLUGIN_TYPE,
+} from '../constants';
+import { PLUGIN_NAME } from '../../utils/constants';
+import { VisLayerErrorTypes } from '../../../../../src/plugins/vis_augmenter/public';
+import { DOES_NOT_HAVE_PERMISSIONS_KEY_WORD } from '../../../server/utils/helpers';
+
+describe('overlay_anomalies spec', () => {
+ setClient(httpClientMock);
+
+ const ADPluginResource = {
+ type: VIS_LAYER_PLUGIN_TYPE,
+ id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ name: 'test-1',
+ urlPath: `${PLUGIN_NAME}#/detectors/${ANOMALY_RESULT_SUMMARY_DETECTOR_ID}/results`, //details page for detector in AD plugin
+ };
+
+ describe('getAnomalies()', () => {
+ test('One anomaly', async () => {
+ httpClientMock.post = jest.fn().mockResolvedValue(ANOMALY_RESULT_SUMMARY);
+ const receivedAnomalies = await getAnomalies(
+ ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ 1589258564789,
+ 1589258684789,
+ ''
+ );
+ expect(receivedAnomalies).toStrictEqual(PARSED_ANOMALIES);
+ });
+ test('No Anomalies', async () => {
+ httpClientMock.post = jest
+ .fn()
+ .mockResolvedValue(NO_ANOMALIES_RESULT_RESPONSE);
+ const receivedAnomalies = await getAnomalies(
+ ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ 1589258564789,
+ 1589258684789,
+ ''
+ );
+ expect(receivedAnomalies).toStrictEqual([]);
+ });
+ test('Failed response', async () => {
+ httpClientMock.post = jest.fn().mockResolvedValue({ ok: false });
+ const receivedAnomalies = await getAnomalies(
+ ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ 1589258564789,
+ 1589258684789,
+ ''
+ );
+ expect(receivedAnomalies).toStrictEqual([]);
+ });
+ });
+ describe('getDetectorResponse()', () => {
+ test('get detector', async () => {
+ httpClientMock.get = jest
+ .fn()
+ .mockResolvedValue({ ok: true, response: SELECTED_DETECTORS[0] });
+ const receivedAnomalies = await getDetectorResponse(
+ 'gtU2l4ABuV34PY9ITTdm'
+ );
+ expect(receivedAnomalies).toStrictEqual({
+ ok: true,
+ response: SELECTED_DETECTORS[0],
+ });
+ });
+ });
+ describe('convertAnomaliesToPointInTimeEventsVisLayer()', () => {
+ test('convert anomalies to PointInTimeEventsVisLayer', async () => {
+ const expectedTimeStamp =
+ PARSED_ANOMALIES[0].startTime +
+ (PARSED_ANOMALIES[0].endTime - PARSED_ANOMALIES[0].startTime) / 2;
+ const expectedPointInTimeEventsVisLayer = {
+ events: [
+ {
+ metadata: {},
+ timestamp: expectedTimeStamp,
+ },
+ ],
+ originPlugin: 'anomalyDetectionDashboards',
+ pluginResource: {
+ id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ name: 'test-1',
+ type: 'Anomaly Detectors',
+ urlPath: `anomaly-detection-dashboards#/detectors/${ANOMALY_RESULT_SUMMARY_DETECTOR_ID}/results`,
+ },
+ type: 'PointInTimeEvents',
+ };
+ const pointInTimeEventsVisLayer =
+ await convertAnomaliesToPointInTimeEventsVisLayer(
+ PARSED_ANOMALIES,
+ ADPluginResource
+ );
+ expect(pointInTimeEventsVisLayer).toStrictEqual(
+ expectedPointInTimeEventsVisLayer
+ );
+ });
+ });
+ describe('getErrorLayerVisLayer()', () => {
+ test('get resource deleted ErrorVisLayer', async () => {
+ const error = new Error(
+ 'Anomaly Detector - ' + DETECTOR_HAS_BEEN_DELETED
+ );
+ const expectedVisLayerError = {
+ type: VisLayerErrorTypes.RESOURCE_DELETED,
+ message: error.message,
+ };
+ const visLayerError = await getVisLayerError(error);
+ expect(visLayerError).toStrictEqual(expectedVisLayerError);
+ });
+ test('get no permission ErrorVisLayer', async () => {
+ const error = new Error(
+ 'Anomaly Detector - ' + DOES_NOT_HAVE_PERMISSIONS_KEY_WORD
+ );
+ const expectedVisLayerError = {
+ type: VisLayerErrorTypes.PERMISSIONS_FAILURE,
+ message: error.message,
+ };
+ const visLayerError = await getVisLayerError(error);
+ expect(visLayerError).toStrictEqual(expectedVisLayerError);
+ });
+ test('get fetch issue ErrorVisLayer', async () => {
+ const error = new Error(START_OR_END_TIME_INVALID_ERROR);
+ const expectedVisLayerError = {
+ type: VisLayerErrorTypes.FETCH_FAILURE,
+ message: error.message,
+ };
+ const visLayerError = await getVisLayerError(error);
+ expect(visLayerError).toStrictEqual(expectedVisLayerError);
+ });
+ });
+});
diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts
index 41a79276..066795c0 100644
--- a/public/expressions/constants.ts
+++ b/public/expressions/constants.ts
@@ -13,3 +13,7 @@ export const TYPE_OF_EXPR_VIS_LAYERS = 'vis_layers';
export const OVERLAY_ANOMALIES = 'overlay_anomalies';
export const PLUGIN_EVENT_TYPE = 'Anomalies';
+
+export const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted';
+
+export const START_OR_END_TIME_INVALID_ERROR = 'start or end time invalid';
diff --git a/public/expressions/helpers.ts b/public/expressions/helpers.ts
new file mode 100644
index 00000000..ed3db94b
--- /dev/null
+++ b/public/expressions/helpers.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ getAnomalySummaryQuery,
+ parsePureAnomalies,
+} from '../pages/utils/anomalyResultUtils';
+import { AD_NODE_API } from '../../utils/constants';
+import { AnomalyData } from '../models/interfaces';
+import { getClient } from '../services';
+import {
+ PluginResource,
+ PointInTimeEventsVisLayer,
+ VisLayerError,
+ VisLayerErrorTypes,
+ VisLayerTypes,
+} from '../../../../src/plugins/vis_augmenter/public';
+import {
+ DETECTOR_HAS_BEEN_DELETED,
+ ORIGIN_PLUGIN_VIS_LAYER,
+ PLUGIN_EVENT_TYPE,
+} from './constants';
+import {
+ DOES_NOT_HAVE_PERMISSIONS_KEY_WORD,
+ NO_PERMISSIONS_KEY_WORD,
+} from '../../server/utils/helpers';
+import { get } from 'lodash';
+
+// This gets all the needed anomalies for the given detector ID and time range
+export const getAnomalies = async (
+ detectorId: string,
+ startTime: number,
+ endTime: number,
+ resultIndex: string
+): Promise => {
+ const anomalySummaryQuery = getAnomalySummaryQuery(
+ startTime,
+ endTime,
+ detectorId,
+ undefined,
+ false
+ );
+ let anomalySummaryResponse;
+ if (resultIndex === '') {
+ anomalySummaryResponse = await getClient().post(
+ `..${AD_NODE_API.DETECTOR}/results/_search`,
+ {
+ body: JSON.stringify(anomalySummaryQuery),
+ }
+ );
+ } else {
+ anomalySummaryResponse = await getClient().post(
+ `..${AD_NODE_API.DETECTOR}/results/_search/${resultIndex}/true`,
+ {
+ body: JSON.stringify(anomalySummaryQuery),
+ }
+ );
+ }
+
+ return parsePureAnomalies(anomalySummaryResponse);
+};
+
+export const getDetectorResponse = async (detectorId: string) => {
+ const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`);
+ return resp;
+};
+
+// This takes anomalies and returns them as vis layer of type PointInTimeEvents
+export 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,
+ pluginEventType: PLUGIN_EVENT_TYPE
+ } as PointInTimeEventsVisLayer;
+};
+
+const checkIfPermissionErrors = (error): boolean => {
+ return typeof error === 'string'
+ ? error.includes(NO_PERMISSIONS_KEY_WORD) ||
+ error.includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD)
+ : get(error, 'message', '').includes(NO_PERMISSIONS_KEY_WORD) ||
+ get(error, 'message', '').includes(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD);
+};
+
+const checkIfDeletionErrors = (error): boolean => {
+ return typeof error === 'string'
+ ? error.includes(DETECTOR_HAS_BEEN_DELETED)
+ : get(error, 'message', '').includes(DETECTOR_HAS_BEEN_DELETED);
+};
+
+//Helps convert any possible errors into either permission, deletion or fetch related failures
+export const getVisLayerError = (error): VisLayerError => {
+ let visLayerError: VisLayerError = {} as VisLayerError;
+ if (checkIfPermissionErrors(error)) {
+ visLayerError = {
+ type: VisLayerErrorTypes.PERMISSIONS_FAILURE,
+ message:
+ error === 'string'
+ ? error
+ : error instanceof Error
+ ? get(error, 'message', '')
+ : '',
+ };
+ } else if (checkIfDeletionErrors(error)) {
+ visLayerError = {
+ type: VisLayerErrorTypes.RESOURCE_DELETED,
+ message:
+ error === 'string'
+ ? error
+ : error instanceof Error
+ ? get(error, 'message', '')
+ : '',
+ };
+ } else {
+ visLayerError = {
+ type: VisLayerErrorTypes.FETCH_FAILURE,
+ message:
+ error === 'string'
+ ? error
+ : error instanceof Error
+ ? get(error, 'message', '')
+ : '',
+ };
+ }
+ return visLayerError;
+};
diff --git a/public/expressions/overlay_anomalies.ts b/public/expressions/overlay_anomalies.ts
index 3ff8c128..0df420b2 100644
--- a/public/expressions/overlay_anomalies.ts
+++ b/public/expressions/overlay_anomalies.ts
@@ -3,51 +3,43 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { get, isEmpty } from 'lodash';
+import { get } from 'lodash';
import { i18n } from '@osd/i18n';
import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/public';
import {
VisLayerTypes,
VisLayers,
ExprVisLayers,
- PluginResource,
} from '../../../../src/plugins/vis_augmenter/public';
import {
TimeRange,
calculateBounds,
} from '../../../../src/plugins/data/common';
-import {
- getAnomalySummaryQuery,
- parsePureAnomalies,
-} from '../pages/utils/anomalyResultUtils';
-import { AD_NODE_API } from '../../utils/constants';
-import { AnomalyData } from '../models/interfaces';
-import { getClient } from '../services';
-import {
- PointInTimeEventsVisLayer,
- VisLayerError,
- VisLayerErrorTypes,
-} from '../../../../src/plugins/vis_augmenter/public';
+import { PointInTimeEventsVisLayer } from '../../../../src/plugins/vis_augmenter/public';
import { PLUGIN_NAME } from '../utils/constants';
import {
CANT_FIND_KEY_WORD,
DOES_NOT_HAVE_PERMISSIONS_KEY_WORD,
- NO_PERMISSIONS_KEY_WORD,
} from '../../server/utils/helpers';
import {
- ORIGIN_PLUGIN_VIS_LAYER,
+ DETECTOR_HAS_BEEN_DELETED,
OVERLAY_ANOMALIES,
PLUGIN_EVENT_TYPE,
+ START_OR_END_TIME_INVALID_ERROR,
TYPE_OF_EXPR_VIS_LAYERS,
VIS_LAYER_PLUGIN_TYPE,
} from './constants';
+import {
+ convertAnomaliesToPointInTimeEventsVisLayer,
+ getAnomalies,
+ getDetectorResponse,
+ getVisLayerError,
+} from './helpers';
type Input = ExprVisLayers;
type Output = Promise;
type Name = typeof OVERLAY_ANOMALIES;
-const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted';
-
interface Arguments {
detectorId: string;
}
@@ -55,54 +47,6 @@ interface Arguments {
export type OverlayAnomaliesExpressionFunctionDefinition =
ExpressionFunctionDefinition;
-// 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,
- false
- );
-
- const anomalySummaryResponse = await getClient().post(
- `..${AD_NODE_API.DETECTOR}/results/_search`,
- {
- body: JSON.stringify(anomalySummaryQuery),
- }
- );
-
- return parsePureAnomalies(anomalySummaryResponse);
-};
-
-const getDetectorResponse = async (detectorId: string) => {
- const resp = await getClient().get(`..${AD_NODE_API.DETECTOR}/${detectorId}`);
- return resp;
-};
-
-// 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,
@@ -170,18 +114,20 @@ export const overlayAnomaliesFunction =
throw new Error(get(detectorResponse, 'error', ''));
}
const detectorName = get(detectorResponse.response, 'name', '');
+ const resultIndex = get(detectorResponse.response, 'resultIndex', '');
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');
+ throw new RangeError(START_OR_END_TIME_INVALID_ERROR);
}
const anomalies = await getAnomalies(
detectorId,
startTimeInMillis,
- endTimeInMillis
+ endTimeInMillis,
+ resultIndex
);
const anomalyLayer = convertAnomaliesToPointInTimeEventsVisLayer(
anomalies,
@@ -195,35 +141,7 @@ export const overlayAnomaliesFunction =
};
} catch (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(DOES_NOT_HAVE_PERMISSIONS_KEY_WORD))
- ) {
- visLayerError = {
- type: VisLayerErrorTypes.PERMISSIONS_FAILURE,
- message: error,
- };
- } else if (
- typeof error === 'string' &&
- error.includes(DETECTOR_HAS_BEEN_DELETED)
- ) {
- visLayerError = {
- type: VisLayerErrorTypes.RESOURCE_DELETED,
- message: error,
- };
- } else {
- visLayerError = {
- type: VisLayerErrorTypes.FETCH_FAILURE,
- message:
- error === 'string'
- ? error
- : error instanceof Error
- ? error.message
- : '',
- };
- }
+ const visLayerError = getVisLayerError(error);
const anomalyErrorLayer = {
type: VisLayerTypes.PointInTimeEvents,
originPlugin: PLUGIN_NAME,
diff --git a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx
index 5c497688..23f65912 100644
--- a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx
+++ b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx
@@ -6,7 +6,7 @@
import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { AnomaliesLiveChart } from '../AnomaliesLiveChart';
-import { selectedDetectors } from '../../../../pages/utils/__tests__/constants';
+import { SELECTED_DETECTORS } from '../../../../pages/utils/__tests__/constants';
import { Provider } from 'react-redux';
import { coreServicesMock } from '../../../../../test/mocks';
import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices';
@@ -45,7 +45,7 @@ describe(' spec', () => {
const { container, getByTestId, getAllByText, getByText } = render(
-
+
);
diff --git a/public/pages/Dashboard/utils/__tests__/utils.test.tsx b/public/pages/Dashboard/utils/__tests__/utils.test.tsx
index 7694dea8..0f805217 100644
--- a/public/pages/Dashboard/utils/__tests__/utils.test.tsx
+++ b/public/pages/Dashboard/utils/__tests__/utils.test.tsx
@@ -8,16 +8,10 @@ import {
getLatestAnomalyResultsByTimeRange,
getLatestAnomalyResultsForDetectorsByTimeRange,
} from '../utils';
-import { httpClientMock, coreServicesMock } from '../../../../../test/mocks';
import {
- Detector,
- FeatureAttributes,
- DetectorListItem,
-} from '../../../../models/interfaces';
-import {
- selectedDetectors,
- anomalyResultQuery,
- anomalyResultQueryPerDetector,
+ SELECTED_DETECTORS,
+ ANOMALY_RESULT_QUERY,
+ ANOMALY_RESULT_QUERY_PER_DETECTOR,
} from '../../../../pages/utils/__tests__/constants';
const anomalyResult = {
detector_id: 'gtU2l4ABuV34PY9ITTdm',
@@ -114,14 +108,14 @@ describe('get latest anomaly result by time range', () => {
'opensearch-ad-plugin-result-*',
false
);
- expect(response[0]).toStrictEqual(anomalyResultQuery);
+ expect(response[0]).toStrictEqual(ANOMALY_RESULT_QUERY);
}, 10000);
});
describe('get latest anomaly result for detectors', () => {
test('get latest by detectors and time range ', async () => {
const response = await getLatestAnomalyResultsForDetectorsByTimeRange(
jest.fn(),
- selectedDetectors,
+ SELECTED_DETECTORS,
'30m',
jest.fn().mockResolvedValue(searchResponseGetLatestAnomalyResults),
-1,
@@ -131,6 +125,6 @@ describe('get latest anomaly result for detectors', () => {
'opensearch-ad-plugin-result-*',
false
);
- expect(response[0]).toStrictEqual(anomalyResultQueryPerDetector);
+ expect(response[0]).toStrictEqual(ANOMALY_RESULT_QUERY_PER_DETECTOR);
}, 10000);
});
diff --git a/public/pages/utils/__tests__/anomalyResultUtils.test.ts b/public/pages/utils/__tests__/anomalyResultUtils.test.ts
index 65ebc8d6..914c4c1f 100644
--- a/public/pages/utils/__tests__/anomalyResultUtils.test.ts
+++ b/public/pages/utils/__tests__/anomalyResultUtils.test.ts
@@ -12,9 +12,16 @@
import {
getFeatureMissingDataAnnotations,
getFeatureDataPointsForDetector,
+ parsePureAnomalies,
} from '../anomalyResultUtils';
import { getRandomDetector } from '../../../redux/reducers/__tests__/utils';
-import { UNITS, Detector, FeatureAttributes } from '../../../models/interfaces';
+import {
+ UNITS,
+ Detector,
+ FeatureAttributes,
+ AnomalyData,
+} from '../../../models/interfaces';
+import { ANOMALY_RESULT_SUMMARY, PARSED_ANOMALIES } from './constants';
describe('anomalyResultUtils', () => {
let randomDetector_20_min: Detector;
@@ -563,4 +570,13 @@ describe('anomalyResultUtils', () => {
).toEqual([]);
});
});
+
+ describe('parsePureAnomalies()', () => {
+ test('parse anomalies', async () => {
+ const parsedPureAnomalies: AnomalyData[] = await parsePureAnomalies(
+ ANOMALY_RESULT_SUMMARY
+ );
+ expect(parsedPureAnomalies).toStrictEqual(PARSED_ANOMALIES);
+ });
+ });
});
diff --git a/public/pages/utils/__tests__/constants.ts b/public/pages/utils/__tests__/constants.ts
index a43dd4bc..e5c3d916 100644
--- a/public/pages/utils/__tests__/constants.ts
+++ b/public/pages/utils/__tests__/constants.ts
@@ -71,20 +71,20 @@ export const FAKE_ENTITY_ANOMALY_SUMMARIES = {
anomalySummaries: [FAKE_ENTITY_ANOMALY_SUMMARY],
} as EntityAnomalySummaries;
-export const anomalyResultQuery = {
+export const ANOMALY_RESULT_QUERY = {
anomaly_grade: 0.10949221682655441,
data_start_time: 1651817250642,
data_end_time: 1651817310642,
detector_id: 'gtU2l4ABuV34PY9ITTdm',
};
-export const anomalyResultQueryPerDetector = {
+export const ANOMALY_RESULT_QUERY_PER_DETECTOR = {
anomaly_grade: 0.10949221682655441,
data_start_time: 1651817250642,
data_end_time: 1651817310642,
detector_id: 'gtU2l4ABuV34PY9ITTdm',
name: 'test3',
};
-export const selectedDetectors = [
+export const SELECTED_DETECTORS = [
{
id: 'gtU2l4ABuV34PY9ITTdm',
name: 'test2',
@@ -134,3 +134,174 @@ export const selectedDetectors = [
lastUpdateTime: 1651818220194,
},
] as DetectorListItem[];
+
+export const ANOMALY_RESULT_SUMMARY_DETECTOR_ID: string =
+ 'hNX8l4ABuV34PY9I1EAZ';
+
+export const ANOMALY_RESULT_SUMMARY = {
+ ok: true,
+ response: {
+ took: 1,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 255,
+ relation: 'eq',
+ },
+ max_score: 0,
+ hits: [
+ {
+ _index: '.opendistro-anomaly-results-history-2022.05.06-1',
+ _id: ANOMALY_RESULT_SUMMARY_DETECTOR_ID,
+ _version: 1,
+ _seq_no: 2980,
+ _primary_term: 1,
+ _score: 0,
+ _source: {
+ relevant_attribution: [
+ {
+ feature_id: 'j-fObYgB3BV2P4BXAga2',
+ data: 0,
+ },
+ {
+ feature_id: 'kOfObYgB3BV2P4BXAga5',
+ data: 1,
+ },
+ ],
+ detector_id: 'gtU2l4ABuV34PY9ITTdm',
+ data_start_time: 1651817250642,
+ data_end_time: 1651817310642,
+ feature_data: [
+ {
+ feature_id: 'j-fObYgB3BV2P4BXAga2',
+ feature_name: 'sum_http_4xx',
+ data: 0,
+ },
+ {
+ feature_id: 'kOfObYgB3BV2P4BXAga5',
+ feature_name: 'sum_http_5xx',
+ data: 3,
+ },
+ ],
+ execution_start_time: 1651817370642,
+ execution_end_time: 1651817370649,
+ anomaly_score: 0.44207098120965693,
+ anomaly_grade: 0.10949221682655441,
+ confidence: 0.9821335094192676,
+ },
+ expected_values: [
+ {
+ likelihood: 1,
+ value_list: [
+ {
+ feature_id: 'j-fObYgB3BV2P4BXAga2',
+ data: 0,
+ },
+ {
+ feature_id: 'kOfObYgB3BV2P4BXAga5',
+ data: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ aggregations: {
+ max_confidence: {
+ value: 0.9669652473591948,
+ },
+ max_anomaly_grade: {
+ value: 1,
+ },
+ max_data_end_time: {
+ value: 1685424000000,
+ value_as_string: '2023-05-30T05:20:00.000Z',
+ },
+ avg_anomaly_grade: {
+ value: 1,
+ },
+ min_confidence: {
+ value: 0.41885100904406947,
+ },
+ count_anomalies: {
+ value: 1,
+ },
+ min_anomaly_grade: {
+ value: 1,
+ },
+ },
+ },
+};
+
+export const NO_ANOMALIES_RESULT_RESPONSE = {
+ ok: true,
+ response: {
+ took: 13,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ },
+ hits: {
+ total: {
+ value: 0,
+ relation: 'eq',
+ },
+ max_score: null,
+ hits: [],
+ },
+ aggregations: {
+ max_confidence: {
+ value: null,
+ },
+ max_anomaly_grade: {
+ value: null,
+ },
+ max_data_end_time: {
+ value: null,
+ },
+ avg_anomaly_grade: {
+ value: null,
+ },
+ min_confidence: {
+ value: null,
+ },
+ count_anomalies: {
+ value: 0,
+ },
+ min_anomaly_grade: {
+ value: null,
+ },
+ },
+ },
+};
+
+export const PARSED_ANOMALIES: AnomalyData[] = [
+ {
+ anomalyGrade: 0.11,
+ confidence: 0.98,
+ contributions: {
+ 'j-fObYgB3BV2P4BXAga2': {
+ attribution: undefined,
+ name: 'sum_http_4xx',
+ },
+ kOfObYgB3BV2P4BXAga5: {
+ attribution: 1,
+ name: 'sum_http_5xx',
+ },
+ },
+ endTime: 1651817310642,
+ entity: undefined,
+ plotTime: 1651817310642,
+ startTime: 1651817250642,
+ },
+];
diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx
index cccfd399..e10e61b2 100644
--- a/public/utils/contextMenu/getActions.tsx
+++ b/public/utils/contextMenu/getActions.tsx
@@ -67,10 +67,10 @@ export const getActions = () => {
title: i18n.translate(
'dashboard.actions.adMenuItem.associatedAnomalyDetector.displayName',
{
- defaultMessage: 'Associated anomaly detector',
+ defaultMessage: 'Associated detectors',
}
),
- icon: 'gear' as EuiIconType,
+ icon: 'kqlSelector' as EuiIconType,
order: 99,
onClick: getOnClick(FLYOUT_MODES.associated),
},
From 65676497ed825361beac8af8c31798b53299f0b9 Mon Sep 17 00:00:00 2001
From: Jackie Han
Date: Thu, 8 Jun 2023 09:48:25 -0700
Subject: [PATCH 28/43] Fix various bugs (#504)
* fix bug bash bugs
Signed-off-by: Jackie Han
* bug fix
Signed-off-by: Jackie Han
* yarn prettier
Signed-off-by: Jackie Han
* bug fix
Signed-off-by: Jackie Han
* bug fixes
Signed-off-by: Jackie Han
* clean up code
Signed-off-by: Jackie Han
* removed unused snapshot
Signed-off-by: Jackie Han
---------
Signed-off-by: Jackie Han
Signed-off-by: Jackie Han
---
.../AddAnomalyDetector.tsx | 75 +++++++++++++++----
.../CreateAnomalyDetector/helpers.tsx | 57 ++++++++++----
public/expressions/constants.ts | 6 ++
.../FeatureAccordion/FeatureAccordion.tsx | 15 +++-
.../components/FeatureAccordion/styles.scss | 3 +
public/utils/contextMenu/getActions.tsx | 4 +-
6 files changed, 128 insertions(+), 32 deletions(-)
create mode 100644 public/pages/ConfigureModel/components/FeatureAccordion/styles.scss
diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
index 9e490801..766cee4e 100644
--- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
+++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/AddAnomalyDetector.tsx
@@ -70,6 +70,7 @@ import {
import {
focusOnFirstWrongFeature,
initialFeatureValue,
+ validateFeatures,
} from '../../../../public/pages/ConfigureModel/utils/helpers';
import {
getIndices,
@@ -95,6 +96,8 @@ 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';
@@ -157,11 +160,55 @@ function AddAnomalyDetector({
const notifications = getNotifications();
const handleValidationAndSubmit = (formikProps) => {
- if (!isEmpty(formikProps.errors)) {
- focusOnFirstWrongFeature(formikProps.errors, formikProps.setFieldTouched);
- notifications.toasts.addDanger('One or more input fields is invalid');
+ 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().then(async (resp) => {
+ if (resp !== undefined) {
+ const savedAugmentObjects = get(resp, 'hits', []);
+ // gets all the saved object for this visualization
+ const savedObjectsForThisVisualization =
+ savedAugmentObjects.filter(
+ (savedObj) =>
+ get(savedObj, 'visId', '') === embeddable.vis.id
+ );
+ 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 {
- handleSubmit(formikProps);
+ notifications.toasts.addDanger(
+ 'One or more features are required.'
+ );
}
};
@@ -203,7 +250,7 @@ function AddAnomalyDetector({
formikProps.setSubmitting(true);
try {
const detectorToCreate = formikToDetector(formikProps.values);
- dispatch(createDetector(detectorToCreate))
+ await dispatch(createDetector(detectorToCreate))
.then(async (response) => {
dispatch(startDetector(response.response.id))
.then((startDetectorResponse) => {})
@@ -222,7 +269,7 @@ function AddAnomalyDetector({
const augmentVisSavedObjectToCreate: ISavedAugmentVis =
getAugmentVisSavedObject(detectorId);
- createAugmentVisSavedObject(
+ await createAugmentVisSavedObject(
augmentVisSavedObjectToCreate,
savedObjectLoader,
uiSettings
@@ -408,7 +455,7 @@ function AddAnomalyDetector({
windowDelay: delayValue,
shingleSize: 8,
filterQuery: { match_all: {} },
- description: '',
+ description: 'Created based on ' + embeddable.vis.title,
resultIndex: undefined,
filters: [],
featureList: visFeatureListToFormik(
@@ -426,6 +473,7 @@ function AddAnomalyDetector({
initialValues={initialDetectorValue}
onSubmit={handleSubmit}
validateOnChange={true}
+ validate={validateFeatures}
>
{(formikProps) => (
<>
@@ -532,8 +580,8 @@ function AddAnomalyDetector({
subTitle={
- Detector interval: {intervalValue} minutes; Window
- delay: {delayValue} minutes
+ Detector interval: {intervalValue} minute(s); Window
+ delay: {delayValue} minute(s)
}
@@ -584,7 +632,7 @@ function AddAnomalyDetector({
- minutes
+ minute(s)
@@ -618,7 +666,7 @@ function AddAnomalyDetector({
- minutes
+ minute(s)
@@ -788,8 +836,6 @@ function AddAnomalyDetector({
isOpen={accordionsOpen.modelFeatures}
onToggle={() => onAccordionToggle('modelFeatures')}
>
-
-
{({
push,
@@ -811,6 +857,8 @@ function AddAnomalyDetector({
/>
)
)}
+
+
+
)}
diff --git a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx
index 5c69015c..4a990b67 100644
--- a/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx
+++ b/public/components/FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers.tsx
@@ -1,9 +1,12 @@
-import { dispatch } from 'd3';
-import { matchDetector } from 'public/redux/reducers/ad';
-import { validateDetectorName } from 'public/utils/utils';
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
import { FEATURE_TYPE } from '../../../../public/models/interfaces';
import { FeaturesFormikValues } from '../../../../public/pages/ConfigureModel/models/interfaces';
-import { find, get, isEmpty, snakeCase } from 'lodash';
+import { find, snakeCase } from 'lodash';
+import { AGGREGATION_TYPES } from '../../../../public/pages/ConfigureModel/utils/constants';
export function visFeatureListToFormik(
featureList,
@@ -17,7 +20,7 @@ export function visFeatureListToFormik(
featureType: FEATURE_TYPE.SIMPLE,
importance: 1,
newFeature: false,
- aggregationBy: 'sum',
+ aggregationBy: visAggregationTypeToFormik(feature),
aggregationOf: visAggregationToFormik(feature),
aggregationQuery: JSON.stringify(
visAggregationQueryToFormik(feature, seriesParams)
@@ -44,18 +47,40 @@ const getFeatureNameFromVisParams = (id, seriesParams) => {
};
function visAggregationToFormik(value) {
- return [
- {
- label: value.params.field.name,
- type: 'number',
- },
- ];
+ if (Object.values(value.params).length !== 0) {
+ return [
+ {
+ label: value.params?.field?.name,
+ type: value.type,
+ },
+ ];
+ }
+ // for count type of vis, there's no field name in the embeddable-vis schema
+ return [];
}
function visAggregationQueryToFormik(value, seriesParams) {
- return {
- [snakeCase(getFeatureNameFromVisParams(value.id, seriesParams))]: {
- sum: { field: value.params.field.name },
- },
- };
+ if (Object.values(value.params).length !== 0) {
+ return {
+ [snakeCase(getFeatureNameFromVisParams(value.id, seriesParams))]: {
+ [visAggregationTypeToFormik(value)]: {
+ field: value.params?.field?.name,
+ },
+ },
+ };
+ }
+ // for count type of vis, there's no field name in the embeddable-vis schema
+ // return '' as the custom expression query
+ return '';
+}
+
+function visAggregationTypeToFormik(feature) {
+ const aggType = feature.__type.name;
+ if (AGGREGATION_TYPES.some((type) => type.value === aggType)) {
+ return aggType;
+ }
+ if (aggType === 'count') {
+ return 'value_count';
+ }
+ return 'sum';
}
diff --git a/public/expressions/constants.ts b/public/expressions/constants.ts
index 066795c0..71d696bc 100644
--- a/public/expressions/constants.ts
+++ b/public/expressions/constants.ts
@@ -14,6 +14,12 @@ export const OVERLAY_ANOMALIES = 'overlay_anomalies';
export const PLUGIN_EVENT_TYPE = 'Anomalies';
+export const PLUGIN_AUGMENTATION_ENABLE_SETTING =
+ 'visualization:enablePluginAugmentation';
+
+export const PLUGIN_AUGMENTATION_MAX_OBJECTS_SETTING =
+ 'visualization:enablePluginAugmentation.maxPluginObjects';
+
export const DETECTOR_HAS_BEEN_DELETED = 'detector has been deleted';
export const START_OR_END_TIME_INVALID_ERROR = 'start or end time invalid';
diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx
index f1805858..a819ed8f 100644
--- a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx
+++ b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx
@@ -22,6 +22,7 @@ import {
EuiCheckbox,
EuiButtonIcon,
} from '@elastic/eui';
+import './styles.scss';
import { Field, FieldProps } from 'formik';
import {
required,
@@ -80,6 +81,18 @@ export const FeatureAccordion = (props: FeatureAccordionProps) => {
};
const featureButtonContent = (feature: any, index: number) => {
+ if (props.displayMode === 'flyout') {
+ return (
+
+ );
+ }
return (