From 7cd38cfd31f67ff158b3d6b39c0c6043462f4807 Mon Sep 17 00:00:00 2001 From: Amit Galitzky Date: Wed, 7 Jun 2023 17:22:37 -0700 Subject: [PATCH] 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), },