From 296b44d811d7b0adbe4ef8218d6f84a2757ac484 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Tue, 24 Sep 2024 11:20:14 -0700 Subject: [PATCH] Revert "Add suggest anomaly detector action to discover page (#849) (#873)" (#882) This reverts commit e8035458fd7624c0c54b94e757a4c4ae3762ac6a. --- opensearch_dashboards.json | 3 +- .../SuggestAnomalyDetector.test.tsx | 449 --------- .../DiscoverAction/SuggestAnomalyDetector.tsx | 863 ------------------ .../FeatureAccordion/FeatureAccordion.tsx | 1 - public/plugin.ts | 33 +- public/redux/reducers/__tests__/ad.test.ts | 20 +- .../reducers/__tests__/opensearch.test.ts | 4 +- public/redux/reducers/ad.ts | 39 +- public/redux/reducers/opensearch.ts | 2 +- public/services.ts | 11 +- public/utils/contextMenu/getActions.tsx | 33 +- server/utils/constants.ts | 2 - 12 files changed, 47 insertions(+), 1413 deletions(-) delete mode 100644 public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx delete mode 100644 public/components/DiscoverAction/SuggestAnomalyDetector.tsx diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index e740b28b..aa749836 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -7,8 +7,7 @@ ], "optionalPlugins": [ "dataSource", - "dataSourceManagement", - "assistantDashboards" + "dataSourceManagement" ], "requiredPlugins": [ "opensearchDashboardsUtils", diff --git a/public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx b/public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx deleted file mode 100644 index 4e0de621..00000000 --- a/public/components/DiscoverAction/SuggestAnomalyDetector.test.tsx +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import React from 'react'; -import { render, waitFor } from '@testing-library/react'; - -import { CoreServicesContext } from '../CoreServices/CoreServices'; -import { coreServicesMock, httpClientMock } from '../../../test/mocks'; -import { - HashRouter as Router, - RouteComponentProps, - Route, - Switch, -} from 'react-router-dom'; -import { Provider } from 'react-redux'; - -import configureStore from '../../redux/configureStore'; -import SuggestAnomalyDetector from './SuggestAnomalyDetector'; -import userEvent from '@testing-library/user-event'; -import { HttpFetchOptionsWithPath } from '../../../../../src/core/public'; -import { getAssistantClient, getQueryService } from '../../services'; - -const notifications = { - toasts: { - addDanger: jest.fn().mockName('addDanger'), - addSuccess: jest.fn().mockName('addSuccess'), - } -}; - -const getNotifications = () => { - return notifications; -} - -jest.mock('../../services', () => ({ - ...jest.requireActual('../../services'), - getNotifications: getNotifications, - getQueryService: jest.fn().mockReturnValue({ - queryString: { - getQuery: jest.fn(), - }, - }), - getAssistantClient: jest.fn().mockReturnValue({ - executeAgentByName: jest.fn(), - }) -})); - -const renderWithRouter = () => ({ - ...render( - - - - ( - - - - )} - /> - - - - ), -}); - -describe('GenerateAnomalyDetector spec', () => { - describe('Renders failed', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders with invalid dataset type', async () => { - const queryService = getQueryService(); - queryService.queryString.getQuery.mockReturnValue({ - dataset: { - id: undefined, - title: undefined, - type: 'INDEX' - }, - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Unsupported dataset type' - ); - }); - }); - - it('renders empty component', async () => { - const queryService = getQueryService(); - queryService.queryString.getQuery.mockReturnValue({ - dataset: { - id: undefined, - title: undefined, - type: 'INDEX_PATTERN' - }, - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Cannot extract complete index info from the context' - ); - }); - }); - }); - - describe('Renders loading component', () => { - beforeEach(() => { - jest.clearAllMocks(); - const queryService = getQueryService(); - queryService.queryString.getQuery.mockReturnValue({ - dataset: { - id: 'test-pattern', - title: 'test-pattern', - type: 'INDEX_PATTERN', - timeFieldName: '@timestamp', - }, - }); - - }); - - it('renders with empty generated parameters', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: '' } - ] - } - ] - } - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Generate parameters for creating anomaly detector failed, reason: Error: Cannot get generated parameters!' - ); - }); - }); - - it('renders with empty parameter', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"\",\"aggregationMethod\":\"\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Generate parameters for creating anomaly detector failed, reason: Error: Cannot find aggregation field, aggregation method or data fields!' - ); - }); - }); - - it('renders with empty aggregation field or empty aggregation method', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\",\",\"aggregationMethod\":\",\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Generate parameters for creating anomaly detector failed, reason: Error: The generated aggregation field or aggregation method is empty!' - ); - }); - }); - - it('renders with different number of aggregation methods and fields', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"a,b\",\"aggregationMethod\":\"avg\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Generate parameters for creating anomaly detector failed, reason: Error: The number of aggregation fields and the number of aggregation methods are different!' - ); - }); - }); - - it('renders component completely', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"opensearch_dashboards_sample_data_logs\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(queryByText('Create detector')).not.toBeNull(); - expect(queryByText('Detector details')).not.toBeNull(); - expect(queryByText('Advanced configuration')).not.toBeNull(); - expect(queryByText('Model Features')).not.toBeNull(); - }); - }); - - }); - - describe('Test API calls', () => { - beforeEach(() => { - jest.clearAllMocks(); - const queryService = getQueryService(); - queryService.queryString.getQuery.mockReturnValue({ - dataset: { - id: 'test-pattern', - title: 'test-pattern', - type: 'INDEX_PATTERN', - timeFieldName: '@timestamp', - }, - }); - }); - - it('All API calls execute successfully', async () => { - httpClientMock.post = jest.fn((pathOrOptions: string | HttpFetchOptionsWithPath) => { - const url = typeof pathOrOptions === 'string' ? pathOrOptions : pathOrOptions.path; - switch (url) { - case '/api/anomaly_detectors/detectors': - return Promise.resolve({ - ok: true, - response: { - id: 'test' - } - }); - default: - return Promise.resolve({ - ok: true - }); - } - }); - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"test-pattern\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - - const { queryByText, getByTestId } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - await waitFor(() => { - expect(queryByText('Generating parameters...')).toBeNull(); - expect(queryByText('Create detector')).not.toBeNull(); - expect(queryByText('Detector details')).not.toBeNull(); - expect(queryByText('Advanced configuration')).not.toBeNull(); - expect(queryByText('Model Features')).not.toBeNull(); - }); - - userEvent.click(getByTestId("SuggestAnomalyDetectorCreateButton")); - - await waitFor(() => { - expect(httpClientMock.post).toHaveBeenCalledTimes(2); - expect(getNotifications().toasts.addSuccess).toHaveBeenCalledTimes(1); - }); - }); - - it('Generate parameters failed', async () => { - (getAssistantClient().executeAgentByName as jest.Mock).mockRejectedValueOnce('Generate parameters failed'); - - const { queryByText } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Generate parameters for creating anomaly detector failed, reason: Generate parameters failed' - ); - }); - }); - - it('Create anomaly detector failed', async () => { - httpClientMock.post = jest.fn((pathOrOptions: string | HttpFetchOptionsWithPath) => { - const url = typeof pathOrOptions === 'string' ? pathOrOptions : pathOrOptions.path; - switch (url) { - case '/api/anomaly_detectors/detectors': - return Promise.resolve({ - ok: false, - error: 'Create anomaly detector failed' - }); - default: - return Promise.resolve({ - ok: true - }); - } - }); - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"test-pattern\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - httpClientMock.get = jest.fn().mockResolvedValue({ - ok: true, - response: { - count: 0 - }, - }); - - const { queryByText, getByTestId } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(queryByText('Generating parameters...')).toBeNull(); - expect(queryByText('Create detector')).not.toBeNull(); - expect(queryByText('Detector details')).not.toBeNull(); - expect(queryByText('Advanced configuration')).not.toBeNull(); - expect(queryByText('Model Features')).not.toBeNull(); - }); - - userEvent.click(getByTestId("SuggestAnomalyDetectorCreateButton")); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Create anomaly detector failed' - ); - }); - }); - - - it('Start anomaly detector failed', async () => { - httpClientMock.post = jest.fn((pathOrOptions: string | HttpFetchOptionsWithPath) => { - const url = typeof pathOrOptions === 'string' ? pathOrOptions : pathOrOptions.path; - switch (url) { - case '/api/anomaly_detectors/detectors': - return Promise.resolve({ - ok: true, - response: { - id: 'test' - } - }); - case '/api/anomaly_detectors/detectors/test/start': - return Promise.resolve({ - ok: false, - error: 'Start anomaly detector failed' - }); - default: - return Promise.resolve({ - ok: true - }); - } - }); - - httpClientMock.get = jest.fn().mockResolvedValue({ - ok: true, - response: { - count: 0 - }, - }); - - (getAssistantClient().executeAgentByName as jest.Mock).mockResolvedValueOnce({ - body: { - inference_results: [ - { - output: [ - { result: "{\"index\":\"test-pattern\",\"categoryField\":\"ip\",\"aggregationField\":\"responseLatency,response\",\"aggregationMethod\":\"avg,sum\",\"dateFields\":\"utc_time,timestamp\"}" } - ] - } - ] - } - }); - - - const { queryByText, getByTestId } = renderWithRouter(); - expect(queryByText('Suggested anomaly detector')).not.toBeNull(); - - await waitFor(() => { - expect(queryByText('Generating parameters...')).toBeNull(); - expect(queryByText('Create detector')).not.toBeNull(); - expect(queryByText('Detector details')).not.toBeNull(); - expect(queryByText('Advanced configuration')).not.toBeNull(); - expect(queryByText('Model Features')).not.toBeNull(); - }); - - userEvent.click(getByTestId("SuggestAnomalyDetectorCreateButton")); - - await waitFor(() => { - expect(getNotifications().toasts.addDanger).toHaveBeenCalledTimes(1); - expect(getNotifications().toasts.addDanger).toHaveBeenCalledWith( - 'Start anomaly detector failed' - ); - }); - }); - }); -}); diff --git a/public/components/DiscoverAction/SuggestAnomalyDetector.tsx b/public/components/DiscoverAction/SuggestAnomalyDetector.tsx deleted file mode 100644 index 96e60234..00000000 --- a/public/components/DiscoverAction/SuggestAnomalyDetector.tsx +++ /dev/null @@ -1,863 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useEffect, Fragment } from 'react'; -import { - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiTitle, - EuiButton, - EuiSpacer, - EuiText, - EuiFormRow, - EuiFieldText, - EuiCheckbox, - EuiFlexItem, - EuiFlexGroup, - EuiFieldNumber, - EuiCallOut, - EuiButtonEmpty, - EuiPanel, - EuiComboBox, -} from '@elastic/eui'; -import '../FeatureAnywhereContextMenu/CreateAnomalyDetector/styles.scss'; -import { useDispatch, useSelector } from 'react-redux'; -import { isEmpty, get } from 'lodash'; -import { - Field, - FieldArray, - FieldArrayRenderProps, - FieldProps, - Formik, -} from 'formik'; -import { - createDetector, - getDetectorCount, - matchDetector, - startDetector, -} from '../../redux/reducers/ad'; -import { - getError, - getErrorMessage, - isInvalid, - validateCategoryField, - validateDetectorName, - validateNonNegativeInteger, - validatePositiveInteger, -} from '../../utils/utils'; -import { - CUSTOM_AD_RESULT_INDEX_PREFIX, - MAX_DETECTORS, - SUGGEST_ANOMALY_DETECTOR_CONFIG_ID, -} from '../../../server/utils/constants'; -import { - focusOnFirstWrongFeature, - getCategoryFields, - initialFeatureValue, - validateFeatures, -} from '../../pages/ConfigureModel/utils/helpers'; -import { formikToDetector } from '../../pages/ReviewAndCreate/utils/helpers'; -import { FormattedFormRow } from '../FormattedFormRow/FormattedFormRow'; -import { FeatureAccordion } from '../../pages/ConfigureModel/components/FeatureAccordion'; -import { AD_DOCS_LINK, DEFAULT_SHINGLE_SIZE, MAX_FEATURE_NUM, PLUGIN_NAME } from '../../utils/constants'; -import { getAssistantClient, getNotifications, getQueryService } from '../../services'; -import { prettifyErrorMessage } from '../../../server/utils/helpers'; -import EnhancedAccordion from '../FeatureAnywhereContextMenu/EnhancedAccordion'; -import MinimalAccordion from '../FeatureAnywhereContextMenu/MinimalAccordion'; -import { DataFilterList } from '../../pages/DefineDetector/components/DataFilterList/DataFilterList'; -import { FEATURE_TYPE } from '../../models/interfaces'; -import { FeaturesFormikValues } from '../../pages/ConfigureModel/models/interfaces'; -import { getMappings } from '../../redux/reducers/opensearch'; -import { mountReactNode } from '../../../../../src/core/public/utils'; -import { formikToDetectorName } from '../FeatureAnywhereContextMenu/CreateAnomalyDetector/helpers'; -import { DEFAULT_DATA } from '../../../../../src/plugins/data/common'; -import { AppState } from '../../redux/reducers'; - -export interface GeneratedParameters { - categoryField: string; - features: FeaturesFormikValues[]; - dateFields: string[]; -} - -function SuggestAnomalyDetector({ - closeFlyout, -}: { - closeFlyout: any; -}) { - const dispatch = useDispatch(); - const notifications = getNotifications(); - const assistantClient = getAssistantClient(); - - const queryString = getQueryService().queryString; - const dataset = queryString.getQuery().dataset || queryString.getDefaultQuery().dataset; - const datasetType = dataset.type; - if (datasetType != DEFAULT_DATA.SET_TYPES.INDEX_PATTERN && datasetType != DEFAULT_DATA.SET_TYPES.INDEX) { - notifications.toasts.addDanger( - 'Unsupported dataset type' - ); - return <>; - } - - const indexPatternId = dataset.id; - // indexName could be a index pattern or a concrete index - const indexName = dataset.title; - const timeFieldName = dataset.timeFieldName; - if (!indexPatternId || !indexName || !timeFieldName) { - notifications.toasts.addDanger( - 'Cannot extract complete index info from the context' - ); - return <>; - } - - const dataSourceId = dataset.dataSource?.id; - const [isLoading, setIsLoading] = useState(true); - const [buttonName, setButtonName] = useState( - 'Generating parameters...' - ); - const [categoryFieldEnabled, setCategoryFieldEnabled] = - useState(false); - - const [accordionsOpen, setAccordionsOpen] = useState>({ modelFeatures: true }); - const [intervalValue, setIntervalalue] = useState(10); - const [delayValue, setDelayValue] = useState(1); - const [enabled, setEnabled] = useState(false); - const [detectorName, setDetectorName] = useState( - formikToDetectorName(indexName.substring(0, 40)) - ); - const indexDataTypes = useSelector( - (state: AppState) => state.opensearch.dataTypes - ); - const categoricalFields = getCategoryFields(indexDataTypes); - - const dateFields = get(indexDataTypes, 'date', []) as string[]; - const dateNanoFields = get(indexDataTypes, 'date_nanos', []) as string[]; - const allDateFields = dateFields.concat(dateNanoFields); - - // let LLM to generate parameters for creating anomaly detector - async function getParameters() { - try { - const executeAgentResponse = await - assistantClient.executeAgentByName(SUGGEST_ANOMALY_DETECTOR_CONFIG_ID, { index: indexName }, { dataSourceId } - ); - const rawGeneratedParameters = executeAgentResponse?.body?.inference_results?.[0]?.output?.[0]?.result; - if (!rawGeneratedParameters) { - throw new Error('Cannot get generated parameters!'); - } - - const generatedParameters = formatGeneratedParameters(JSON.parse(rawGeneratedParameters)); - if (generatedParameters.features.length == 0) { - throw new Error('Generated parameters have empty model features!'); - } - - initialDetectorValue.featureList = generatedParameters.features; - initialDetectorValue.categoryFieldEnabled = !!generatedParameters.categoryField; - initialDetectorValue.categoryField = initialDetectorValue.categoryFieldEnabled ? [generatedParameters.categoryField] : []; - - setIsLoading(false); - setButtonName('Create detector'); - setCategoryFieldEnabled(!!generatedParameters.categoryField); - } catch (error) { - notifications.toasts.addDanger( - 'Generate parameters for creating anomaly detector failed, reason: ' + error - ); - } - } - - const formatGeneratedParameters = function (rawGeneratedParameters: any): GeneratedParameters { - const categoryField = rawGeneratedParameters['categoryField']; - - const rawAggregationFields = rawGeneratedParameters['aggregationField']; - const rawAggregationMethods = rawGeneratedParameters['aggregationMethod']; - const rawDataFields = rawGeneratedParameters['dateFields']; - if (!rawAggregationFields || !rawAggregationMethods || !rawDataFields) { - throw new Error('Cannot find aggregation field, aggregation method or data fields!'); - } - const aggregationFields = - rawAggregationFields.split(','); - const aggregationMethods = - rawAggregationMethods.split(','); - const dateFields = rawDataFields.split(','); - - if (aggregationFields.length != aggregationMethods.length) { - throw new Error('The number of aggregation fields and the number of aggregation methods are different!'); - } - - const featureList = aggregationFields.map((field: string, index: number) => { - const method = aggregationMethods[index]; - if (!field || !method) { - throw new Error('The generated aggregation field or aggregation method is empty!'); - } - const aggregationOption = { - label: field, - }; - const feature: FeaturesFormikValues = { - featureName: `feature_${field}`, - featureType: FEATURE_TYPE.SIMPLE, - featureEnabled: true, - aggregationQuery: '', - aggregationBy: aggregationMethods[index], - aggregationOf: [aggregationOption], - }; - return feature; - }); - - return { - categoryField: categoryField, - features: featureList, - dateFields: dateFields, - }; - }; - - useEffect(() => { - async function fetchData() { - await dispatch(getMappings(indexName, dataSourceId)); - await getParameters(); - } - fetchData(); - }, []); - - const onDetectorNameChange = (e: any, field: any) => { - field.onChange(e); - setDetectorName(e.target.value); - }; - - const onAccordionToggle = (key: string) => { - const newAccordionsOpen = { ...accordionsOpen }; - newAccordionsOpen[key] = !accordionsOpen[key]; - setAccordionsOpen(newAccordionsOpen); - }; - - const onIntervalChange = (e: any, field: any) => { - field.onChange(e); - setIntervalalue(e.target.value); - }; - - const onDelayChange = (e: any, field: any) => { - field.onChange(e); - setDelayValue(e.target.value); - }; - - const handleValidationAndSubmit = (formikProps: any) => { - if (formikProps.values.featureList.length !== 0) { - formikProps.setFieldTouched('featureList', true); - formikProps.validateForm().then(async (errors: any) => { - if (!isEmpty(errors)) { - focusOnFirstWrongFeature(errors, formikProps.setFieldTouched); - notifications.toasts.addDanger( - 'One or more input fields is invalid.' - ); - } else { - handleSubmit(formikProps); - } - }); - } else { - notifications.toasts.addDanger('One or more features are required.'); - } - }; - - const handleSubmit = async (formikProps: any) => { - formikProps.setSubmitting(true); - try { - const detectorToCreate = formikToDetector(formikProps.values); - await dispatch(createDetector(detectorToCreate, dataSourceId)) - .then(async (response: any) => { - const detectorId = response.response.id; - dispatch(startDetector(detectorId, dataSourceId)) - .then(() => { }) - .catch((err: any) => { - notifications.toasts.addDanger( - prettifyErrorMessage( - getErrorMessage( - err, - 'There was a problem starting the real-time detector' - ) - ) - ); - }); - - const shingleSize = get( - formikProps.values, - 'shingleSize', - DEFAULT_SHINGLE_SIZE - ); - notifications.toasts.addSuccess({ - title: mountReactNode( - - Detector created: { - e.preventDefault(); - const url = `../${PLUGIN_NAME}#/detectors/${detectorId}`; - window.open(url, '_blank'); - }} style={{ textDecoration: 'underline' }}>{formikProps.values.name} - - ), - text: mountReactNode( - -

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

-
- ), - className: 'createdAndAssociatedSuccessToast', - }); - - }) - .catch((err: any) => { - dispatch(getDetectorCount(dataSourceId)).then((response: any) => { - const totalDetectors = get(response, 'response.count', 0); - if (totalDetectors === MAX_DETECTORS) { - notifications.toasts.addDanger( - 'Cannot create detector - limit of ' + - MAX_DETECTORS + - ' detectors reached' - ); - } else { - notifications.toasts.addDanger( - prettifyErrorMessage( - getErrorMessage( - err, - 'There was a problem creating the detector' - ) - ) - ); - } - }); - }); - closeFlyout(); - } catch (e) { - } finally { - formikProps.setSubmitting(false); - } - }; - - const validateAnomalyDetectorName = async (detectorName: string) => { - if (isEmpty(detectorName)) { - return 'Detector name cannot be empty'; - } else { - const error = validateDetectorName(detectorName); - if (error) { - return error; - } - const resp = await dispatch(matchDetector(detectorName, dataSourceId)); - const match = get(resp, 'response.match', false); - if (!match) { - return undefined; - } - //If more than one detectors found, duplicate exists. - if (match) { - return 'Duplicate detector name'; - } - } - }; - - let initialDetectorValue = { - name: detectorName, - index: [{ label: indexName }], - timeField: timeFieldName, - interval: intervalValue, - windowDelay: delayValue, - shingleSize: DEFAULT_SHINGLE_SIZE, - filterQuery: { match_all: {} }, - description: 'Created based on the OpenSearch Assistant', - resultIndex: undefined, - filters: [], - featureList: [] as FeaturesFormikValues[], - categoryFieldEnabled: false, - categoryField: [] as string[], - realTime: true, - historical: false, - }; - - return ( -
- - {(formikProps) => ( - <> - - -

- Suggested anomaly detector -

-
- - - Create an anomaly detector based on the parameters(model features and categorical field) suggested by OpenSearch Assistant. - -
- -
- -

Detector details

-
- - - onAccordionToggle('detectorDetails')} - subTitle={ - -

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

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

minute(s)

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

minute(s)

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

Source: {'test'}

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

intervals

-
-
-
-
- )} -
-
- - - - {({ field, form }: FieldProps) => ( - - - { - if (enabled) { - form.setFieldValue('resultIndex', ''); - } - setEnabled(!enabled); - }} - /> - - - {enabled ? ( - - - - ) : null} - - {enabled ? ( - - - - - - ) : null} - - )} - - - - - - {({ field, form }: FieldProps) => ( - - - { - if (categoryFieldEnabled) { - form.setFieldValue('categoryField', []); - } - setCategoryFieldEnabled(!categoryFieldEnabled); - }} - /> - - {categoryFieldEnabled ? ( - - - - ) : null} - {categoryFieldEnabled ? ( - - - { - return { - label: value, - }; - }) - } - options={categoricalFields?.map((field) => { - return { - label: field, - }; - })} - onBlur={() => { - form.setFieldTouched('categoryField', true); - }} - onChange={(options) => { - const selection = options.map( - (option) => option.label - ); - if (!isEmpty(selection)) { - if (selection.length <= 2) { - form.setFieldValue( - 'categoryField', - selection - ); - } - } else { - form.setFieldValue('categoryField', []); - } - }} - singleSelection={false} - isClearable={true} - /> - - - ) : null} - - )} - - - - - - {({ field, form }: FieldProps) => ( - - { - return { - label: field, - }; - })} - onBlur={() => { - form.setFieldTouched('timeField', true); - }} - onChange={(options) => { - form.setFieldValue( - 'timeField', - get(options, '0.label') - ); - }} - selectedOptions={ - field.value - ? [ - { - label: field.value, - }, - ] - : [{ label: timeFieldName }] - } - singleSelection={{ asPlainText: true }} - isClearable={false} - /> - - )} - - -
- - - -

Model Features

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

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

-
-
- ); - }} -
-
- -
-
- - - - Cancel - - - { - handleValidationAndSubmit(formikProps); - }} - > - {buttonName} - - - - - - )} -
-
- ); -} - -export default SuggestAnomalyDetector; diff --git a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx index 53fd616c..545dda63 100644 --- a/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx +++ b/public/pages/ConfigureModel/components/FeatureAccordion/FeatureAccordion.tsx @@ -113,7 +113,6 @@ export const FeatureAccordion = (props: FeatureAccordionProps) => { if (props.displayMode === 'flyout') { return ( { }, }); expect(httpMockedClient.get).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/${detectorId}` + `..${BASE_NODE_API_PATH}/detectors/${detectorId}` ); }); test('should invoke [REQUEST, FAILURE]', async () => { @@ -76,7 +76,7 @@ describe('detector reducer actions', () => { errorMessage: 'Not found', }); expect(httpMockedClient.get).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/${detectorId}` + `..${BASE_NODE_API_PATH}/detectors/${detectorId}` ); } }); @@ -104,7 +104,7 @@ describe('detector reducer actions', () => { }, }); expect(httpMockedClient.delete).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/${expectedDetector.id}` + `..${BASE_NODE_API_PATH}/detectors/${expectedDetector.id}` ); }); test('should invoke [REQUEST, FAILURE]', async () => { @@ -129,7 +129,7 @@ describe('detector reducer actions', () => { errorMessage: 'Detector is consumed by Monitor', }); expect(httpMockedClient.delete).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/${expectedDetector.id}` + `..${BASE_NODE_API_PATH}/detectors/${expectedDetector.id}` ); } }); @@ -162,7 +162,7 @@ describe('detector reducer actions', () => { }, }); expect(httpMockedClient.post).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors`, + `..${BASE_NODE_API_PATH}/detectors`, { body: JSON.stringify(expectedDetector), } @@ -190,7 +190,7 @@ describe('detector reducer actions', () => { errorMessage: 'Internal server error', }); expect(httpMockedClient.post).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors`, + `..${BASE_NODE_API_PATH}/detectors`, { body: JSON.stringify(expectedDetector), } @@ -230,7 +230,7 @@ describe('detector reducer actions', () => { }, }); expect(httpMockedClient.put).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/${detectorId}`, + `..${BASE_NODE_API_PATH}/detectors/${detectorId}`, { body: JSON.stringify(randomDetector) } ); }); @@ -258,7 +258,7 @@ describe('detector reducer actions', () => { errorMessage: 'Internal server error', }); expect(httpMockedClient.post).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors`, + `..${BASE_NODE_API_PATH}/detectors`, randomDetector, { params: { @@ -298,7 +298,7 @@ describe('detector reducer actions', () => { ), }); expect(httpMockedClient.post).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors/_search`, + `..${BASE_NODE_API_PATH}/detectors/_search`, { body: JSON.stringify(query), } @@ -328,7 +328,7 @@ describe('detector reducer actions', () => { errorMessage: 'Internal server error', }); expect(httpMockedClient.post).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/detectors`, + `..${BASE_NODE_API_PATH}/detectors`, randomDetector ); } diff --git a/public/redux/reducers/__tests__/opensearch.test.ts b/public/redux/reducers/__tests__/opensearch.test.ts index ee3e5fc0..18d75ae2 100644 --- a/public/redux/reducers/__tests__/opensearch.test.ts +++ b/public/redux/reducers/__tests__/opensearch.test.ts @@ -175,7 +175,7 @@ describe('opensearch reducer actions', () => { }, }); expect(httpMockedClient.get).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/_mappings`, + `..${BASE_NODE_API_PATH}/_mappings`, { query: { indices: [] }, } @@ -202,7 +202,7 @@ describe('opensearch reducer actions', () => { errorMessage: 'Something went wrong', }); expect(httpMockedClient.get).toHaveBeenCalledWith( - `${BASE_NODE_API_PATH}/_mappings`, + `..${BASE_NODE_API_PATH}/_mappings`, { query: { indices: [] }, } diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index a1a689d1..3fa06ad3 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -374,8 +374,9 @@ export const createDetector = ( dataSourceId: string = '' ): APIAction => { const url = dataSourceId - ? `${AD_NODE_API.DETECTOR}/${dataSourceId}` - : `${AD_NODE_API.DETECTOR}`; + ? `..${AD_NODE_API.DETECTOR}/${dataSourceId}` + : `..${AD_NODE_API.DETECTOR}`; + return { type: CREATE_DETECTOR, request: (client: HttpSetup) => @@ -390,7 +391,7 @@ export const validateDetector = ( validationType: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/_validate/${validationType}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/_validate/${validationType}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -406,7 +407,7 @@ export const getDetector = ( detectorId: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -421,7 +422,7 @@ export const getDetectorList = ( ): APIAction => { const dataSourceId = queryParams.dataSourceId || ''; - const baseUrl = `${AD_NODE_API.DETECTOR}/_list`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/_list`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -435,7 +436,7 @@ export const getDetectorList = ( export const searchDetector = (requestBody: any): APIAction => ({ type: SEARCH_DETECTOR, request: (client: HttpSetup) => - client.post(`${AD_NODE_API.DETECTOR}/_search`, { + client.post(`..${AD_NODE_API.DETECTOR}/_search`, { body: JSON.stringify(requestBody), }), }); @@ -445,7 +446,7 @@ export const updateDetector = ( requestBody: Detector, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -462,7 +463,7 @@ export const deleteDetector = ( detectorId: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -476,7 +477,7 @@ export const startDetector = ( detectorId: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}/start`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/start`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -492,7 +493,7 @@ export const startHistoricalDetector = ( startTime: number, endTime: number ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}/start` : `${baseUrl}/start`; @@ -516,7 +517,7 @@ export const stopDetector = ( detectorId: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}/stop/${false}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${false}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -530,7 +531,7 @@ export const stopHistoricalDetector = ( detectorId: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -543,16 +544,16 @@ export const stopHistoricalDetector = ( export const getDetectorProfile = (detectorId: string): APIAction => ({ type: GET_DETECTOR_PROFILE, request: (client: HttpSetup) => - client.get(`${AD_NODE_API.DETECTOR}/${detectorId}/_profile`), + client.get(`..${AD_NODE_API.DETECTOR}/${detectorId}/_profile`), detectorId, }); export const matchDetector = ( - detectorName: string, + detectorName: string, dataSourceId: string = '' ): APIAction => { - const baseUrl = `${AD_NODE_API.DETECTOR}/${detectorName}/_match`; - const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorName}/_match`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { type: MATCH_DETECTOR, @@ -561,9 +562,9 @@ export const matchDetector = ( }; export const getDetectorCount = (dataSourceId: string = ''): APIAction => { - const url = dataSourceId ? - `${AD_NODE_API.DETECTOR}/_count/${dataSourceId}` : - `${AD_NODE_API.DETECTOR}/_count`; + const url = dataSourceId ? + `..${AD_NODE_API.DETECTOR}/_count/${dataSourceId}` : + `..${AD_NODE_API.DETECTOR}/_count`; return { type: GET_DETECTOR_COUNT, diff --git a/public/redux/reducers/opensearch.ts b/public/redux/reducers/opensearch.ts index 046b2c19..77667b74 100644 --- a/public/redux/reducers/opensearch.ts +++ b/public/redux/reducers/opensearch.ts @@ -371,7 +371,7 @@ export const getMappings = ( return { type: GET_MAPPINGS, request: (client: HttpSetup) => - client.get(`${url}`, { + client.get(`..${url}`, { query: { indices: searchKey }, }), }; diff --git a/public/services.ts b/public/services.ts index d582ac59..0c3d45dd 100644 --- a/public/services.ts +++ b/public/services.ts @@ -16,7 +16,6 @@ import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_u import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; -import { AssistantPublicPluginStart } from '../../../plugins/dashboards-assistant/public/'; export interface DataSourceEnabled { enabled: boolean; @@ -46,12 +45,6 @@ export const [getUISettings, setUISettings] = export const [getQueryService, setQueryService] = createGetterSetter('Query'); -export const [getAssistantEnabled, setAssistantEnabled] = - createGetterSetter('AssistantClient'); - -export const [getAssistantClient, setAssistantClient] = - createGetterSetter('AssistantClient'); - export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter('SavedObjectsClient'); @@ -61,10 +54,10 @@ export const [getDataSourceManagementPlugin, setDataSourceManagementPlugin] = export const [getDataSourceEnabled, setDataSourceEnabled] = createGetterSetter('DataSourceEnabled'); -export const [getNavigationUI, setNavigationUI] = +export const [getNavigationUI, setNavigationUI] = createGetterSetter('navigation'); -export const [getApplication, setApplication] = +export const [getApplication, setApplication] = createGetterSetter('application'); // This is primarily used for mocking this module and each of its fns in tests. diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx index 6bb6bf3e..f58a7a9e 100644 --- a/public/utils/contextMenu/getActions.tsx +++ b/public/utils/contextMenu/getActions.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiIconType } from '@elastic/eui'; import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; -import { Action, createAction } from '../../../../../src/plugins/ui_actions/public'; +import { Action } from '../../../../../src/plugins/ui_actions/public'; import { createADAction } from '../../action/ad_dashboard_action'; import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/AnywhereParentFlyout'; import { Provider } from 'react-redux'; @@ -16,9 +16,6 @@ import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/Docu import { AD_FEATURE_ANYWHERE_LINK, ANOMALY_DETECTION_ICON } from '../constants'; import { getClient, getOverlays } from '../../../public/services'; import { FLYOUT_MODES } from '../../../public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/constants'; -import SuggestAnomalyDetector from '../../../public/components/DiscoverAction/SuggestAnomalyDetector'; - -export const ACTION_SUGGEST_AD = 'suggestAnomalyDetector'; // This is used to create all actions in the same context menu const grouping: Action['grouping'] = [ @@ -90,31 +87,3 @@ export const getActions = () => { }, ].map((options) => createADAction({ ...options, grouping })); }; - -export const getSuggestAnomalyDetectorAction = () => { - const onClick = async function () { - const overlayService = getOverlays(); - const openFlyout = overlayService.openFlyout; - const store = configureStore(getClient()); - const overlay = openFlyout( - toMountPoint( - - overlay.close()} - /> - - ) - ); - } - - return createAction({ - id: 'suggestAnomalyDetector', - order: 100, - type: ACTION_SUGGEST_AD, - getDisplayName: () => 'Suggest anomaly detector', - getIconType: () => ANOMALY_DETECTION_ICON, - execute: async () => { - onClick(); - }, - }); -} diff --git a/server/utils/constants.ts b/server/utils/constants.ts index 1a756187..ac3c887a 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -132,5 +132,3 @@ export const HISTORICAL_TASK_TYPES = [ ]; export const CUSTOM_AD_RESULT_INDEX_PREFIX = 'opensearch-ad-plugin-result-'; - -export const SUGGEST_ANOMALY_DETECTOR_CONFIG_ID = 'os_suggest_ad';