From af40fa6ad7e880eeb7d4cc5b3b94d0ee6290d232 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 29 Apr 2024 12:08:20 -0700 Subject: [PATCH] merge feature/mds branch into main (#739) * Support MDS on List, Detail, Dashboard, Overview pages (#722) * update snapshots Signed-off-by: Jackie Han * add snpshot Signed-off-by: Jackie Han * add data source client Signed-off-by: Jackie Han * test * neo List Detectors page Signed-off-by: Jackie Han * add opensearch_dashboards.json file Signed-off-by: Jackie Han * neo List Detectors page Signed-off-by: Jackie Han * test Signed-off-by: Jackie Han * Support MDS on DetectorDetails page Signed-off-by: Jackie Han * change 4/7 Signed-off-by: Jackie Han * version change Signed-off-by: Jackie Han * list detector list change Signed-off-by: Jackie Han * Support MDS on List, Detail, Dashboard, Overview pages Signed-off-by: Jackie Han * change version back to 3.0 Signed-off-by: Jackie Han * revert version change Signed-off-by: Jackie Han * make dataSourceId optional in DetectorListItem type Signed-off-by: Jackie Han * update imports Signed-off-by: Jackie Han * remove used function Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * add getter and setter for dataSource plugin Signed-off-by: Jackie Han * read dataSourceId from the url instead of passing props Signed-off-by: Jackie Han * addressing comments and run prettier Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * make getDataSourceManagementPlugin() optional Signed-off-by: Jackie Han * add comment Signed-off-by: Jackie Han * make dataSourceId type safe Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han * add MDS support for detector creation&Update flow (#728) * update snapshots Signed-off-by: Jackie Han * add snpshot Signed-off-by: Jackie Han * add data source client Signed-off-by: Jackie Han * test * neo List Detectors page Signed-off-by: Jackie Han * add opensearch_dashboards.json file Signed-off-by: Jackie Han * neo List Detectors page Signed-off-by: Jackie Han * test Signed-off-by: Jackie Han * Support MDS on DetectorDetails page Signed-off-by: Jackie Han * change 4/7 Signed-off-by: Jackie Han * version change Signed-off-by: Jackie Han * list detector list change Signed-off-by: Jackie Han * Support MDS on List, Detail, Dashboard, Overview pages Signed-off-by: Jackie Han * change version back to 3.0 Signed-off-by: Jackie Han * revert version change Signed-off-by: Jackie Han * make dataSourceId optional in DetectorListItem type Signed-off-by: Jackie Han * update imports Signed-off-by: Jackie Han * remove used function Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * add getter and setter for dataSource plugin Signed-off-by: Jackie Han * read dataSourceId from the url instead of passing props Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * addressing comments and run prettier Signed-off-by: Jackie Han * addressing comments in neo1 branch Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * make getDataSourceManagementPlugin() optional Signed-off-by: Jackie Han * add comment Signed-off-by: Jackie Han * make dataSourceId type safe Signed-off-by: Jackie Han * add MDS support on creation page Signed-off-by: Jackie Han * updated methods to get data source query param from url Signed-off-by: Jackie Han * using helper to construct url with dataSourceId Signed-off-by: Jackie Han * add update page, and update urls on detector detail page Signed-off-by: Jackie Han * update update call router Signed-off-by: Jackie Han * update reducer and router Signed-off-by: Jackie Han * clean up Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * update enums Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han * update actionOption attribution for all data soure components (#732) Signed-off-by: Jackie Han * Add MDS UTs and bug fixes (#734) * working on ut Signed-off-by: Jackie Han * update ut Signed-off-by: Jackie Han * update UTs and add bug fixes Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han * bug fixes (#737) Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han (cherry picked from commit 48acb93b9e3fcd85ed3131a57a3602797d079fda) --- opensearch_dashboards.json | 1 + package.json | 6 +- public/anomaly_detection_app.tsx | 6 +- .../CreateDetectorButtons.tsx | 14 +- public/models/interfaces.ts | 6 + .../containers/AnomalyDetailsChart.tsx | 24 +- .../__tests__/AnomaliesChart.test.tsx | 19 +- .../__tests__/AnomalyDetailsChart.test.tsx | 56 ++- .../__tests__/AnomalyOccurrenceChart.test.tsx | 57 ++- .../containers/ConfigureModel.tsx | 131 ++++-- .../containers/SampleAnomalies.tsx | 6 + .../__tests__/ConfigureModel.test.tsx | 9 + .../containers/CreateDetectorSteps.tsx | 5 +- .../hooks/useFetchDetectorInfo.ts | 7 +- .../Components/AnomaliesDistribution.tsx | 6 + .../Components/AnomaliesLiveChart.tsx | 18 +- .../__tests__/EmptyDashboard.test.tsx | 17 +- .../EmptyDashboard.test.tsx.snap | 4 +- .../__tests__/AnomaliesLiveCharts.test.tsx | 28 +- .../Components/utils/DashboardHeader.tsx | 14 +- .../Dashboard/Container/DashboardOverview.tsx | 129 +++++- public/pages/Dashboard/utils/utils.tsx | 14 +- .../components/__tests__/DataFilter.test.tsx | 4 +- .../components/Datasource/DataSource.tsx | 20 +- .../NameAndDescription/NameAndDescription.tsx | 2 +- .../components/Timestamp/Timestamp.tsx | 11 +- .../containers/DefineDetector.tsx | 181 +++++++-- .../__tests__/DefineDetector.test.tsx | 10 + .../containers/DetectorConfig.tsx | 9 +- .../containers/DetectorDetail.tsx | 112 +++-- .../hooks/useFetchMonitorInfo.ts | 9 +- .../DetectorJobs/containers/DetectorJobs.tsx | 67 ++- .../__tests__/DetectorJobs.test.tsx | 8 + .../containers/AnomalyHistory.tsx | 48 ++- .../containers/AnomalyResults.tsx | 41 +- .../containers/AnomalyResultsLiveChart.tsx | 8 +- .../__tests__/EmptyMessage.test.tsx | 33 +- .../__snapshots__/EmptyMessage.test.tsx.snap | 4 +- .../ConfirmDeleteDetectorsModal.test.tsx | 4 +- .../DetectorsList/containers/List/List.tsx | 156 +++++-- .../containers/List/__tests__/List.test.tsx | 12 +- .../utils/__tests__/helpers.test.ts | 3 + .../utils/__tests__/tableUtils.test.tsx | 16 +- public/pages/DetectorsList/utils/helpers.ts | 14 +- .../pages/DetectorsList/utils/tableUtils.tsx | 226 ++++++----- .../containers/HistoricalDetectorResults.tsx | 21 +- .../SampleDataBox/SampleDataBox.tsx | 8 +- .../__tests__/SampleDataBox.test.tsx | 41 +- .../__snapshots__/SampleDataBox.test.tsx.snap | 2 +- .../containers/AnomalyDetectionOverview.tsx | 170 ++++++-- .../AnomalyDetectionOverview.test.tsx | 33 +- .../AnomalyDetectionOverview.test.tsx.snap | 11 +- .../containers/ReviewAndCreate.tsx | 92 ++++- .../__tests__/ReviewAndCreate.test.tsx | 8 + public/pages/main/Main.tsx | 105 +++-- public/pages/utils/anomalyResultUtils.ts | 2 + public/pages/utils/constants.ts | 1 + public/pages/utils/helpers.ts | 80 +++- public/plugin.ts | 15 +- .../reducers/__tests__/anomalyResults.test.ts | 11 +- public/redux/reducers/ad.ts | 248 ++++++++---- public/redux/reducers/alerting.ts | 43 +- public/redux/reducers/anomalyResults.ts | 97 +++-- public/redux/reducers/liveAnomalyResults.ts | 39 +- public/redux/reducers/opensearch.ts | 96 +++-- public/redux/reducers/previewAnomalies.ts | 19 +- public/redux/reducers/sampleData.ts | 18 +- public/services.ts | 16 + public/utils/constants.ts | 12 +- server/models/types.ts | 5 + server/plugin.ts | 27 +- server/routes/ad.ts | 382 +++++++++++++----- server/routes/alerting.ts | 44 +- server/routes/opensearch.ts | 117 ++++-- server/routes/sampleData.ts | 17 +- server/sampleData/utils/helpers.ts | 21 +- server/utils/constants.ts | 11 + server/utils/helpers.ts | 26 ++ yarn.lock | 10 +- 79 files changed, 2571 insertions(+), 852 deletions(-) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 139f9fa7..a2eda9ee 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -5,6 +5,7 @@ "configPath": [ "anomaly_detection_dashboards" ], + "optionalPlugins": ["dataSource","dataSourceManagement"], "requiredPlugins": [ "opensearchDashboardsUtils", "expressions", diff --git a/package.json b/package.json index 68169de2..dc247b92 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,13 @@ "devDependencies": { "@testing-library/user-event": "^12.1.6", "@types/react-plotly.js": "^2.6.0", - "@types/redux-mock-store": "^1.0.1", + "@types/redux-mock-store": "^1.0.6", "babel-polyfill": "^6.26.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", "lint-staged": "^9.2.0", "moment": "^2.24.0", - "redux-mock-store": "^1.5.3", + "redux-mock-store": "^1.5.4", "start-server-and-test": "^1.11.7" }, "dependencies": { @@ -56,4 +56,4 @@ "browserify-sign": "^4.2.2", "axios": "^1.6.1" } -} \ No newline at end of file +} diff --git a/public/anomaly_detection_app.tsx b/public/anomaly_detection_app.tsx index dfbc4591..326cf18b 100644 --- a/public/anomaly_detection_app.tsx +++ b/public/anomaly_detection_app.tsx @@ -29,13 +29,17 @@ export function renderApp(coreStart: CoreStart, params: AppMountParameters) { } else { require('@elastic/charts/dist/theme_only_light.css'); } + ReactDOM.render( ( -
+
)} /> diff --git a/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx b/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx index 087789f4..551bb709 100644 --- a/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx +++ b/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx @@ -12,14 +12,24 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import React from 'react'; import { APP_PATH, PLUGIN_NAME } from '../../utils/constants'; +import { useLocation } from 'react-router-dom'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../pages/utils/helpers'; export const CreateDetectorButtons = () => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + + const createDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(`${APP_PATH.CREATE_DETECTOR}`, dataSourceId, false); + + const sampleDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(`${APP_PATH.OVERVIEW}`, dataSourceId, false); + return ( Try a sample detector @@ -29,7 +39,7 @@ export const CreateDetectorButtons = () => { Create detector diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index eff5ead5..3d836207 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -14,6 +14,7 @@ import { DATA_TYPES } from '../utils/constants'; import { DETECTOR_STATE } from '../../server/utils/constants'; import { Duration } from 'moment'; import moment from 'moment'; +import { MDSQueryParams } from '../../server/models/types'; export type FieldInfo = { label: string; @@ -326,3 +327,8 @@ export interface ValidationSettingResponse { message: string; validationType: string; } + +export interface MDSStates { + queryParams: MDSQueryParams; + selectedDataSourceId: string | undefined; +} diff --git a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx index e2116e30..74661b67 100644 --- a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx +++ b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx @@ -81,7 +81,10 @@ import { } from '../utils/constants'; import { HeatmapCell } from './AnomalyHeatmapChart'; import { ANOMALY_AGG, MIN_END_TIME, MAX_END_TIME } from '../../utils/constants'; -import { MAX_HISTORICAL_AGG_RESULTS } from '../../../utils/constants'; +import { + DATA_SOURCE_ID, + MAX_HISTORICAL_AGG_RESULTS, +} from '../../../utils/constants'; import { searchResults } from '../../../redux/reducers/anomalyResults'; import { DAY_IN_MILLI_SECS, @@ -89,6 +92,8 @@ import { DETECTOR_STATE, } from '../../../../server/utils/constants'; import { ENTITY_COLORS } from '../../DetectorResults/utils/constants'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface AnomalyDetailsChartProps { onDateRangeChange( @@ -118,6 +123,9 @@ interface AnomalyDetailsChartProps { export const AnomalyDetailsChart = React.memo( (props: AnomalyDetailsChartProps) => { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [showAlertsFlyout, setShowAlertsFlyout] = useState(false); const [alertAnnotations, setAlertAnnotations] = useState([]); const [isLoadingAlerts, setIsLoadingAlerts] = useState(false); @@ -174,7 +182,9 @@ export const AnomalyDetailsChart = React.memo( zoomRange.endDate, taskId ); - dispatch(searchResults(anomalyDataRangeQuery, resultIndex, true)) + dispatch( + searchResults(anomalyDataRangeQuery, resultIndex, dataSourceId, true) + ) .then((response: any) => { // Only retrieve buckets that are in the anomaly results range. This is so // we don't show aggregate results for where there is no data at all @@ -193,7 +203,9 @@ export const AnomalyDetailsChart = React.memo( taskId, selectedAggId ); - dispatch(searchResults(historicalAggQuery, resultIndex, true)) + dispatch( + searchResults(historicalAggQuery, resultIndex, dataSourceId, true) + ) .then((response: any) => { const aggregatedAnomalies = parseHistoricalAggregatedAnomalies( response, @@ -229,7 +241,9 @@ export const AnomalyDetailsChart = React.memo( zoomRange.endDate, taskId ); - dispatch(searchResults(anomalyDataRangeQuery, resultIndex, true)) + dispatch( + searchResults(anomalyDataRangeQuery, resultIndex, dataSourceId, true) + ) .then((response: any) => { const dataStartDate = get( response, @@ -318,7 +332,7 @@ export const AnomalyDetailsChart = React.memo( try { setIsLoadingAlerts(true); const result = await dispatch( - searchAlerts(monitorId, startDateTime, endDateTime) + searchAlerts(monitorId, startDateTime, endDateTime, dataSourceId) ); setIsLoadingAlerts(false); setTotalAlerts(get(result, 'response.totalAlerts')); diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx index aebc1ea2..02f63fcd 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx @@ -22,6 +22,8 @@ import { FAKE_ANOMALIES_RESULT, FAKE_DATE_RANGE, } from '../../../../pages/utils/__tests__/constants'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const DEFAULT_PROPS = { onDateRangeChange: jest.fn(), @@ -47,17 +49,32 @@ const DEFAULT_PROPS = { entityAnomalySummaries: [], } as AnomaliesChartProps; +const history = createMemoryHistory(); + const renderDataFilter = (chartProps: AnomaliesChartProps) => ({ ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('renders the component for sample / preview', () => { console.error = jest.fn(); const { getByText, getAllByText } = renderDataFilter({ diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx index fbf1e66d..5bed2db9 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx @@ -22,6 +22,18 @@ import { } from '../../../../pages/utils/__tests__/constants'; import { INITIAL_ANOMALY_SUMMARY } from '../../utils/constants'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); const renderAnomalyOccurenceChart = ( isNotSample: boolean, @@ -30,27 +42,41 @@ const renderAnomalyOccurenceChart = ( ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); + test('renders the component in case of Sample Anomaly', () => { console.error = jest.fn(); const { getByText } = renderAnomalyOccurenceChart(false, false); diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx index 802d7d6c..94b4ffd4 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx @@ -16,6 +16,8 @@ import { mockedStore } from '../../../../redux/utils/testUtils'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { coreServicesMock } from '../../../../../test/mocks'; import { AnomalyOccurrenceChart } from '../AnomalyOccurrenceChart'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; import { FAKE_ANOMALY_DATA, FAKE_DATE_RANGE, @@ -23,6 +25,16 @@ import { import { INITIAL_ANOMALY_SUMMARY } from '../../utils/constants'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); + const renderAnomalyOccurenceChart = ( isNotSample: boolean, isHCDetector: boolean @@ -30,28 +42,41 @@ const renderAnomalyOccurenceChart = ( ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('renders the component in case of Sample Anomaly', () => { console.error = jest.fn(); const { getByText } = renderAnomalyOccurenceChart(false, false); diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index b2a21696..6d2996e8 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -26,13 +26,13 @@ import { } from '@elastic/eui'; import { FormikProps, Formik } from 'formik'; import { get, isEmpty } from 'lodash'; -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, ReactElement } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; import { AppState } from '../../../redux/reducers'; import { getMappings } from '../../../redux/reducers/opensearch'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; -import { BREADCRUMBS, BASE_DOCS_LINK } from '../../../utils/constants'; +import { BREADCRUMBS, BASE_DOCS_LINK, MDS_BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { updateDetector } from '../../../redux/reducers/ad'; import { @@ -49,7 +49,7 @@ import { Features } from '../components/Features'; import { CategoryField } from '../components/CategoryField'; import { AdvancedSettings } from '../components/AdvancedSettings'; import { SampleAnomalies } from './SampleAnomalies'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { Detector } from '../../../models/interfaces'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; @@ -58,6 +58,17 @@ import { ModelConfigurationFormikValues } from '../models/interfaces'; import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/interfaces'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { getErrorMessage } from '../../../utils/utils'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface ConfigureModelRouterProps { detectorId?: string; @@ -70,14 +81,20 @@ interface ConfigureModelProps initialValues?: ModelConfigurationFormikValues; setInitialValues?(initialValues: ModelConfigurationFormikValues): void; detectorDefinitionValues?: DetectorDefinitionFormikValues; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function ConfigureModel(props: ConfigureModelProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourceEnabled().enabled; + const dataSourceId = MDSQueryParams.dataSourceId; + useHideSideNavBar(true, false); const detectorId = get(props, 'match.params.detectorId', ''); - const { detector, hasError } = useFetchDetectorInfo(detectorId); + const { detector, hasError } = useFetchDetectorInfo(detectorId, dataSourceId); const indexDataTypes = useSelector( (state: AppState) => state.opensearch.dataTypes ); @@ -100,33 +117,60 @@ export function ConfigureModel(props: ConfigureModelProps) { setIsHCDetector(true); } if (detector?.indices) { - dispatch(getMappings(detector.indices[0])); + dispatch(getMappings(detector.indices[0], dataSourceId)); } }, [detector]); useEffect(() => { - if (props.isEdit) { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { - text: detector && detector.name ? detector.name : '', - href: `#/detectors/${detectorId}`, - }, - BREADCRUMBS.EDIT_MODEL_CONFIGURATION, - ]); + if (dataSourceEnabled) { + if (props.isEdit) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { + text: detector && detector.name ? detector.name : '', + href: constructHrefWithDataSourceId(`#/detectors/${detectorId}`, dataSourceId, false) + }, + MDS_BREADCRUMBS.EDIT_MODEL_CONFIGURATION, + ]); + } else { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + MDS_BREADCRUMBS.CREATE_DETECTOR, + ]); + } } else { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (props.isEdit) { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { + text: detector && detector.name ? detector.name : '', + href: `#/detectors/${detectorId}`, + }, + BREADCRUMBS.EDIT_MODEL_CONFIGURATION, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } } }, [detector]); useEffect(() => { if (hasError) { - props.history.push('/detectors'); + if(dataSourceEnabled) { + props.history.push( + constructHrefWithDataSourceId('/detectors', dataSourceId, false) + ); + } + else { + props.history.push('/detectors'); + } } }, [hasError]); @@ -178,12 +222,18 @@ export function ConfigureModel(props: ConfigureModelProps) { }; const handleUpdateDetector = async (detectorToUpdate: Detector) => { - dispatch(updateDetector(detectorId, detectorToUpdate)) + dispatch(updateDetector(detectorId, detectorToUpdate, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector updated: ${response.response.name}` ); - props.history.push(`/detectors/${detectorId}/configurations/`); + props.history.push( + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + dataSourceId, + false + ) + ); }) .catch((err: any) => { core.notifications.toasts.addDanger( @@ -207,6 +257,24 @@ export function ConfigureModel(props: ConfigureModelProps) { ...props.initialValues, } as CreateDetectorFormikValues); + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { if (props.isEdit) { props.history.push( - `/detectors/${detectorId}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + dataSourceId, + false + ) ); } else { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); } }} > diff --git a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx index a6ad30ad..096316cd 100644 --- a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx +++ b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx @@ -49,6 +49,8 @@ import { BASE_DOCS_LINK } from '../../../utils/constants'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface SampleAnomaliesProps { detector: Detector; @@ -62,6 +64,9 @@ interface SampleAnomaliesProps { export function SampleAnomalies(props: SampleAnomaliesProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); const [isLoading, setIsLoading] = useState(false); @@ -150,6 +155,7 @@ export function SampleAnomalies(props: SampleAnomaliesProps) { periodStart: dateRange.startDate.valueOf(), periodEnd: dateRange.endDate.valueOf(), detector: detector, + dataSourceId: dataSourceId, }) ); setIsLoading(false); diff --git a/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx b/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx index 77a9e370..6cd845bd 100644 --- a/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx +++ b/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx @@ -25,6 +25,14 @@ import { CoreServicesContext } from '../../../../components/CoreServices/CoreSer import { INITIAL_DETECTOR_DEFINITION_VALUES } from '../../../DefineDetector/utils/constants'; import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( @@ -34,6 +42,7 @@ const renderWithRouter = (isEdit: boolean = false) => ({ render={(props: RouteComponentProps) => ( void; +} export const CreateDetectorSteps = (props: CreateDetectorStepsProps) => { useHideSideNavBar(true, false); diff --git a/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts b/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts index af3cc4e9..a686775b 100644 --- a/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts +++ b/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts @@ -21,7 +21,8 @@ import { getMappings } from '../../../redux/reducers/opensearch'; // 1. Get detector // 2. Gets index mapping export const useFetchDetectorInfo = ( - detectorId: string + detectorId: string, + dataSourceId: string ): { detector: Detector; hasError: boolean; @@ -43,10 +44,10 @@ export const useFetchDetectorInfo = ( useEffect(() => { const fetchDetector = async () => { if (!detector) { - await dispatch(getDetector(detectorId)); + await dispatch(getDetector(detectorId, dataSourceId)); } if (selectedIndices) { - await dispatch(getMappings(selectedIndices)); + await dispatch(getMappings(selectedIndices, dataSourceId)); } }; if (detectorId) { diff --git a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx index b02878fe..d9749777 100644 --- a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx +++ b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx @@ -33,6 +33,8 @@ import { get, isEmpty } from 'lodash'; import { AD_DOC_FIELDS } from '../../../../server/utils/constants'; import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../utils/constants'; import { searchResults } from '../../../redux/reducers/anomalyResults'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface AnomaliesDistributionChartProps { selectedDetectors: DetectorListItem[]; } @@ -41,6 +43,9 @@ export const AnomaliesDistributionChart = ( props: AnomaliesDistributionChartProps ) => { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [anomalyDistribution, setAnomalyDistribution] = useState( [] as object[] @@ -66,6 +71,7 @@ export const AnomaliesDistributionChart = ( await getAnomalyDistributionForDetectorsByTimeRange( searchResults, props.selectedDetectors, + dataSourceId, timeRange, dispatch, 0, diff --git a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx index a0176b22..5e6e5f72 100644 --- a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx +++ b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx @@ -51,9 +51,15 @@ import { getLatestAnomalyResultsForDetectorsByTimeRange, getLatestAnomalyResultsByTimeRange, } from '../utils/utils'; -import { MAX_ANOMALIES, SPACE_STR } from '../../../utils/constants'; +import { + DATA_SOURCE_ID, + MAX_ANOMALIES, + SPACE_STR, +} from '../../../utils/constants'; import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../utils/constants'; import { searchResults } from '../../../redux/reducers/anomalyResults'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface AnomaliesLiveChartProps { selectedDetectors: DetectorListItem[]; @@ -68,7 +74,9 @@ const MAX_LIVE_DETECTORS = 10; export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const dispatch = useDispatch(); - + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [liveTimeRange, setLiveTimeRange] = useState({ startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), @@ -102,7 +110,8 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { 1, true, ALL_CUSTOM_AD_RESULT_INDICES, - false + false, + dataSourceId ); } catch (err) { console.log( @@ -126,7 +135,8 @@ export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { MAX_LIVE_DETECTORS, false, ALL_CUSTOM_AD_RESULT_INDICES, - false + false, + dataSourceId ); setLiveAnomalyData(latestLiveAnomalyResult); diff --git a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/EmptyDashboard.test.tsx b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/EmptyDashboard.test.tsx index d3a9c5de..f66b3f4b 100644 --- a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/EmptyDashboard.test.tsx +++ b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/EmptyDashboard.test.tsx @@ -12,11 +12,26 @@ import React from 'react'; import { render } from '@testing-library/react'; import { EmptyDashboard } from '../EmptyDashboard'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); describe(' spec', () => { describe('Empty results', () => { test('renders component with empty message', async () => { - const { container } = render(); + const { container } = render( + + + ); expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap index 67762b0d..e71ff3ba 100644 --- a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap +++ b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap @@ -72,7 +72,7 @@ exports[` spec Empty results renders component with empty messa @@ -93,7 +93,7 @@ exports[` spec Empty results renders component with empty messa diff --git a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx index 23f65912..fd043c49 100644 --- a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx +++ b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx @@ -11,6 +11,17 @@ import { Provider } from 'react-redux'; import { coreServicesMock } from '../../../../../test/mocks'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { mockedStore } from '../../../../redux/utils/testUtils'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const anomalyResponse = [ { ok: true, @@ -41,11 +52,26 @@ jest.mock('../../utils/utils', () => ({ visualizeAnomalyResultForXYChart: jest.fn(), })); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('AnomaliesLiveChart with Sample anomaly data', async () => { + const history = createMemoryHistory(); + const { container, getByTestId, getAllByText, getByText } = render( - + + + ); diff --git a/public/pages/Dashboard/Components/utils/DashboardHeader.tsx b/public/pages/Dashboard/Components/utils/DashboardHeader.tsx index ed4fa87e..f39cf74f 100644 --- a/public/pages/Dashboard/Components/utils/DashboardHeader.tsx +++ b/public/pages/Dashboard/Components/utils/DashboardHeader.tsx @@ -17,12 +17,22 @@ import { EuiPageHeader, EuiTitle, } from '@elastic/eui'; -import { PLUGIN_NAME, APP_PATH } from '../../../../utils/constants'; +import { + PLUGIN_NAME, + APP_PATH, +} from '../../../../utils/constants'; +import { useLocation } from 'react-router-dom'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../../../pages/utils/helpers'; export interface DashboardHeaderProps { hasDetectors: boolean; } export const DashboardHeader = (props: DashboardHeaderProps) => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const createDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(APP_PATH.CREATE_DETECTOR, dataSourceId, false); + return ( @@ -35,7 +45,7 @@ export const DashboardHeader = (props: DashboardHeaderProps) => { Create detector diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 67b3d433..ff57d99e 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -9,14 +9,15 @@ * GitHub history for details. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, useMemo } from 'react'; import { AnomaliesLiveChart } from '../Components/AnomaliesLiveChart'; import { AnomaliesDistributionChart } from '../Components/AnomaliesDistribution'; +import queryString from 'querystring'; import { useDispatch, useSelector } from 'react-redux'; import { get, isEmpty, cloneDeep } from 'lodash'; -import { DetectorListItem } from '../../../models/interfaces'; +import { DetectorListItem, MDSStates } from '../../../models/interfaces'; import { getIndices, getAliases } from '../../../redux/reducers/opensearch'; import { getDetectorList } from '../../../redux/reducers/ad'; import { @@ -29,17 +30,25 @@ import { } from '@elastic/eui'; import { AnomalousDetectorsList } from '../Components/AnomalousDetectorsList'; import { - GET_ALL_DETECTORS_QUERY_PARAMS, ALL_DETECTORS_MESSAGE, ALL_DETECTOR_STATES_MESSAGE, ALL_INDICES_MESSAGE, } from '../utils/constants'; import { AppState } from '../../../redux/reducers'; -import { CatIndex, IndexAlias } from '../../../../server/models/types'; -import { getVisibleOptions } from '../../utils/helpers'; -import { BREADCRUMBS } from '../../../utils/constants'; +import { + CatIndex, + IndexAlias, +} from '../../../../server/models/types'; +import { + getAllDetectorsQueryParamsWithDataSourceId, + getDataSourceFromURL, + getVisibleOptions, +} from '../../utils/helpers'; +import { BREADCRUMBS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; -import { getDetectorStateOptions } from '../../DetectorsList/utils/helpers'; +import { + getDetectorStateOptions, +} from '../../DetectorsList/utils/helpers'; import { DashboardHeader } from '../Components/utils/DashboardHeader'; import { EmptyDashboard } from '../Components/EmptyDashboard/EmptyDashboard'; import { @@ -47,9 +56,22 @@ import { NO_PERMISSIONS_KEY_WORD, } from '../../../../server/utils/helpers'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; +import { DataSourceSelectableConfig } from '../../../../../../src/plugins/data_source_management/public'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { RouteComponentProps } from 'react-router-dom'; + +interface OverviewProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; + landingDataSourceId: string | undefined; +} -export function DashboardOverview() { +export function DashboardOverview(props: OverviewProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); const adState = useSelector((state: AppState) => state.ad); @@ -58,6 +80,8 @@ export function DashboardOverview() { const errorGettingDetectors = adState.errorMessage; const isLoadingDetectors = adState.requesting; + const dataSourceEnabled = getDataSourceEnabled().enabled; + const [currentDetectors, setCurrentDetectors] = useState( Object.values(allDetectorList) ); @@ -65,6 +89,14 @@ export function DashboardOverview() { const [selectedDetectorsName, setSelectedDetectorsName] = useState( [] as string[] ); + const queryParams = getDataSourceFromURL(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ + queryParams, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, + }); + const getDetectorOptions = (detectorsIdMap: { [key: string]: DetectorListItem; }) => { @@ -108,6 +140,20 @@ export function DashboardOverview() { setAllDetectorStatesSelected(isEmpty(selectedStates)); }; + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + if (dataSourceEnabled && dataSourceId === undefined) { + getNotifications().toasts.addDanger( + prettifyErrorMessage('Unable to set data source.') + ); + } else { + setMDSOverviewState({ + queryParams: dataSourceId, + selectedDataSourceId: dataSourceId, + }); + } + }; + const opensearchState = useSelector((state: AppState) => state.opensearch); const [selectedIndices, setSelectedIndices] = useState([] as string[]); @@ -157,14 +203,30 @@ export function DashboardOverview() { }; const intializeDetectors = async () => { - dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); - dispatch(getIndices('')); - dispatch(getAliases('')); + dispatch( + getDetectorList( + getAllDetectorsQueryParamsWithDataSourceId( + MDSOverviewState.selectedDataSourceId + ) + ) + ); + dispatch(getIndices('', MDSOverviewState.selectedDataSourceId)); + dispatch(getAliases('', MDSOverviewState.selectedDataSourceId)); }; useEffect(() => { + const { history, location } = props; + if (dataSourceEnabled) { + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); + } intializeDetectors(); - }, []); + }, [MDSOverviewState]); useEffect(() => { if (errorGettingDetectors) { @@ -179,10 +241,17 @@ export function DashboardOverview() { }, [errorGettingDetectors]); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DASHBOARD, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(MDSOverviewState.selectedDataSourceId), + MDS_BREADCRUMBS.DASHBOARD(MDSOverviewState.selectedDataSourceId), + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DASHBOARD, + ]); + } }); useEffect(() => { @@ -197,9 +266,35 @@ export function DashboardOverview() { ); }, [selectedDetectorsName, selectedIndices, selectedDetectorStates]); + let renderDataSourceComponent = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = useMemo(() => { + return ( + + handleDataSourceChange(dataSources), + }} + /> + ); + }, [getSavedObjectsClient(), getNotifications(), props.setActionMenu]); + } + return (
+ {dataSourceEnabled && renderDataSourceComponent} 0} /> {isLoadingDetectors ? (
diff --git a/public/pages/Dashboard/utils/utils.tsx b/public/pages/Dashboard/utils/utils.tsx index 451d2b90..86414003 100644 --- a/public/pages/Dashboard/utils/utils.tsx +++ b/public/pages/Dashboard/utils/utils.tsx @@ -433,6 +433,7 @@ export const getLatestAnomalyResultsByTimeRange = async ( func: ( request: any, resultIndex: string, + dataSourceId: string, onlyQueryCustomResultIndex: boolean ) => APIAction, timeRange: string, @@ -441,7 +442,8 @@ export const getLatestAnomalyResultsByTimeRange = async ( anomalySize: number, checkLastIndexOnly: boolean, resultIndex: string, - onlyQueryCustomResultIndex: boolean + onlyQueryCustomResultIndex: boolean, + dataSourceId: string ): Promise => { let from = 0; let anomalyResults = [] as object[]; @@ -457,6 +459,7 @@ export const getLatestAnomalyResultsByTimeRange = async ( checkLastIndexOnly ), resultIndex, + dataSourceId, onlyQueryCustomResultIndex ) ); @@ -489,6 +492,7 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( func: ( request: any, resultIndex: string, + dataSourceId: string, onlyQueryCustomResultIndex: boolean ) => APIAction, selectedDetectors: DetectorListItem[], @@ -499,7 +503,8 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( detectorNum: number, checkLastIndexOnly: boolean, resultIndex: string, - onlyQueryCustomResultIndex: boolean + onlyQueryCustomResultIndex: boolean, + dataSourceId: string ): Promise => { const detectorAndIdMap = buildDetectorAndIdMap(selectedDetectors); let from = 0; @@ -516,6 +521,7 @@ export const getLatestAnomalyResultsForDetectorsByTimeRange = async ( checkLastIndexOnly ), resultIndex, + dataSourceId, onlyQueryCustomResultIndex ) ); @@ -605,9 +611,11 @@ export const getAnomalyDistributionForDetectorsByTimeRange = async ( func: ( request: any, resultIndex: string, + dataSourceId: string, onlyQueryCustomResultIndex: boolean ) => APIAction, selectedDetectors: DetectorListItem[], + dataSourceId: string, timeRange: string, dispatch: Dispatch, threshold: number, @@ -638,7 +646,7 @@ export const getAnomalyDistributionForDetectorsByTimeRange = async ( const finalQuery = Object.assign({}, getResultQuery, anomaly_dist_aggs); const result = await dispatch( - func(finalQuery, resultIndex, onlyQueryCustomResultIndex) + func(finalQuery, resultIndex, dataSourceId, onlyQueryCustomResultIndex) ); const detectorsAggResults = get( diff --git a/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx b/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx index 4eb5f4cd..2b42c5e1 100644 --- a/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx +++ b/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx @@ -150,7 +150,7 @@ describe('dataFilter', () => { getAllByText('cpu'); }); userEvent.click(getByTestId('cancelFilter0Button')); - }, 10000); + }, 30000); test('renders data filter, click on custom', async () => { const { container, getByText, getByTestId } = renderWithProvider(); getByText('Create custom label?'); @@ -164,5 +164,5 @@ describe('dataFilter', () => { getByText('Use query DSL'); }); userEvent.click(getByTestId('cancelFilter0Button')); - }, 10000); + }, 30000); }); diff --git a/public/pages/DefineDetector/components/Datasource/DataSource.tsx b/public/pages/DefineDetector/components/Datasource/DataSource.tsx index dd268dde..4fbc882d 100644 --- a/public/pages/DefineDetector/components/Datasource/DataSource.tsx +++ b/public/pages/DefineDetector/components/Datasource/DataSource.tsx @@ -24,7 +24,11 @@ import { } from '../../../../redux/reducers/opensearch'; import { getError, isInvalid } from '../../../../utils/utils'; import { IndexOption } from './IndexOption'; -import { getVisibleOptions, sanitizeSearchText } from '../../../utils/helpers'; +import { + getDataSourceFromURL, + getVisibleOptions, + sanitizeSearchText, +} from '../../../utils/helpers'; import { validateIndex } from '../../../utils/validate'; import { DataFilterList } from '../DataFilterList/DataFilterList'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; @@ -32,6 +36,7 @@ import { DetectorDefinitionFormikValues } from '../../models/interfaces'; import { ModelConfigurationFormikValues } from '../../../ConfigureModel/models/interfaces'; import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../../ConfigureModel/utils/constants'; import { FILTER_TYPES } from '../../../../models/interfaces'; +import { useLocation } from 'react-router-dom'; interface DataSourceProps { formikProps: FormikProps; @@ -45,6 +50,9 @@ interface DataSourceProps { export function DataSource(props: DataSourceProps) { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [indexName, setIndexName] = useState( props.formikProps.values.index[0]?.label ); @@ -52,10 +60,10 @@ export function DataSource(props: DataSourceProps) { const opensearchState = useSelector((state: AppState) => state.opensearch); useEffect(() => { const getInitialIndices = async () => { - await dispatch(getIndices(queryText)); + await dispatch(getIndices(queryText, dataSourceId)); }; getInitialIndices(); - }, []); + }, [dataSourceId]); useEffect(() => { setIndexName(props.formikProps.values.index[0]?.label); @@ -65,7 +73,7 @@ export function DataSource(props: DataSourceProps) { if (searchValue !== queryText) { const sanitizedQuery = sanitizeSearchText(searchValue); setQueryText(sanitizedQuery); - await dispatch(getPrioritizedIndices(sanitizedQuery)); + await dispatch(getPrioritizedIndices(sanitizedQuery, dataSourceId)); } }, 300); @@ -73,7 +81,7 @@ export function DataSource(props: DataSourceProps) { const indexName = get(selectedOptions, '0.label', ''); setIndexName(indexName); if (indexName !== '') { - dispatch(getMappings(indexName)); + dispatch(getMappings(indexName, dataSourceId)); } if (indexName !== props.origIndex) { if (props.setNewIndexSelected) { @@ -167,4 +175,4 @@ export function DataSource(props: DataSourceProps) { /> ); -} +} \ No newline at end of file diff --git a/public/pages/DefineDetector/components/NameAndDescription/NameAndDescription.tsx b/public/pages/DefineDetector/components/NameAndDescription/NameAndDescription.tsx index cd90e6bc..a85cf3b0 100644 --- a/public/pages/DefineDetector/components/NameAndDescription/NameAndDescription.tsx +++ b/public/pages/DefineDetector/components/NameAndDescription/NameAndDescription.tsx @@ -72,4 +72,4 @@ function NameAndDescription(props: NameAndDescriptionProps) { ); } -export default NameAndDescription; +export default NameAndDescription; \ No newline at end of file diff --git a/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx b/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx index 16cf8594..48431fe8 100644 --- a/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx +++ b/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx @@ -18,9 +18,13 @@ import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; import { AppState } from '../../../../redux/reducers'; import { getPrioritizedIndices } from '../../../../redux/reducers/opensearch'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; -import { sanitizeSearchText } from '../../../utils/helpers'; +import { + getDataSourceFromURL, + sanitizeSearchText, +} from '../../../utils/helpers'; import { getError, isInvalid, required } from '../../../../utils/utils'; import { DetectorDefinitionFormikValues } from '../../models/interfaces'; +import { useLocation } from 'react-router-dom'; interface TimestampProps { formikProps: FormikProps; @@ -28,6 +32,9 @@ interface TimestampProps { export function Timestamp(props: TimestampProps) { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const opensearchState = useSelector((state: AppState) => state.opensearch); const selectedIndex = get(props, 'formikProps.values.index.0.label', ''); const isRemoteIndex = selectedIndex.includes(':'); @@ -37,7 +44,7 @@ export function Timestamp(props: TimestampProps) { if (searchValue !== queryText) { const sanitizedQuery = sanitizeSearchText(searchValue); setQueryText(sanitizedQuery); - await dispatch(getPrioritizedIndices(sanitizedQuery)); + await dispatch(getPrioritizedIndices(sanitizedQuery, dataSourceId)); } }, 300); diff --git a/public/pages/DefineDetector/containers/DefineDetector.tsx b/public/pages/DefineDetector/containers/DefineDetector.tsx index 459553c0..13de30cc 100644 --- a/public/pages/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/DefineDetector/containers/DefineDetector.tsx @@ -9,8 +9,14 @@ * GitHub history for details. */ -import React, { Fragment, useEffect, useState } from 'react'; -import { RouteComponentProps } from 'react-router'; +import React, { + Fragment, + ReactElement, + useEffect, + useMemo, + useState, +} from 'react'; +import { RouteComponentProps, useLocation } from 'react-router'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { FormikProps, Formik } from 'formik'; @@ -30,10 +36,10 @@ import { import { updateDetector, matchDetector } from '../../../redux/reducers/ad'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { APIAction } from '../../../redux/middleware/types'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; -import { BREADCRUMBS } from '../../../utils/constants'; +import { BREADCRUMBS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { getErrorMessage, validateDetectorName } from '../../../utils/utils'; import { NameAndDescription } from '../components/NameAndDescription'; import { DataSource } from '../components/Datasource/DataSource'; @@ -46,10 +52,22 @@ import { clearModelConfiguration, } from '../utils/helpers'; import { DetectorDefinitionFormikValues } from '../models/interfaces'; -import { Detector, FILTER_TYPES } from '../../../models/interfaces'; +import { Detector, FILTER_TYPES, MDSStates } from '../../../models/interfaces'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { ModelConfigurationFormikValues } from 'public/pages/ConfigureModel/models/interfaces'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceSelectableConfig, DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import queryString from 'querystring'; interface DefineDetectorRouterProps { detectorId?: string; @@ -62,16 +80,27 @@ interface DefineDetectorProps initialValues?: DetectorDefinitionFormikValues; setInitialValues?(initialValues: DetectorDefinitionFormikValues): void; setModelConfigValues?(initialValues: ModelConfigurationFormikValues): void; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export const DefineDetector = (props: DefineDetectorProps) => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const dataSourceEnabled = getDataSourceEnabled().enabled; + const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch>(); useHideSideNavBar(true, false); const detectorId: string = get(props, 'match.params.detectorId', ''); - const { detector, hasError } = useFetchDetectorInfo(detectorId); + const { detector, hasError } = useFetchDetectorInfo(detectorId, dataSourceId); const [newIndexSelected, setNewIndexSelected] = useState(false); + const [MDSCreateState, setMDSCreateState] = useState({ + queryParams: MDSQueryParams, + selectedDataSourceId: dataSourceId === undefined? undefined : dataSourceId, + }); + // To handle backward compatibility, we need to pass some fields via // props to the subcomponents so they can render correctly // @@ -97,33 +126,60 @@ export const DefineDetector = (props: DefineDetectorProps) => { // Set breadcrumbs based on create / update useEffect(() => { - const createOrEditBreadcrumb = props.isEdit - ? BREADCRUMBS.EDIT_DETECTOR - : BREADCRUMBS.CREATE_DETECTOR; - let breadCrumbs = [ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - createOrEditBreadcrumb, - ]; - if (detector && detector.name) { - breadCrumbs.splice(2, 0, { - text: detector.name, - //@ts-ignore - href: `#/detectors/${detectorId}`, - }); + if (dataSourceEnabled) { + const createOrEditBreadcrumb = props.isEdit + ? MDS_BREADCRUMBS.EDIT_DETECTOR + : MDS_BREADCRUMBS.CREATE_DETECTOR; + let breadCrumbs = [ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + createOrEditBreadcrumb, + ]; + if (detector && detector.name) { + breadCrumbs.splice(2, 0, { + text: detector.name, + //@ts-ignore + href: `#/detectors/${detectorId}/?dataSourceId=${dataSourceId}`, + }); + } + core.chrome.setBreadcrumbs(breadCrumbs); + } else { + const createOrEditBreadcrumb = props.isEdit + ? BREADCRUMBS.EDIT_DETECTOR + : BREADCRUMBS.CREATE_DETECTOR; + let breadCrumbs = [ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + createOrEditBreadcrumb, + ]; + if (detector && detector.name) { + breadCrumbs.splice(2, 0, { + text: detector.name, + //@ts-ignore + href: `#/detectors/${detectorId}`, + }); + } + core.chrome.setBreadcrumbs(breadCrumbs); } - core.chrome.setBreadcrumbs(breadCrumbs); }); // If no detector found with ID, redirect it to list useEffect(() => { + const { history, location } = props; + const updatedParams = { + dataSourceId: MDSCreateState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); if (props.isEdit && hasError) { core.notifications.toasts.addDanger( 'Unable to find the detector for editing' ); - props.history.push(`/detectors`); + props.history.push(constructHrefWithDataSourceId(`/detectors`, dataSourceId, false)); } - }, [props.isEdit]); + }, [props.isEdit, MDSCreateState]); const handleValidateName = async (detectorName: string) => { if (isEmpty(detectorName)) { @@ -134,7 +190,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { return error; } //TODO::Avoid making call if value is same - const resp = await dispatch(matchDetector(detectorName)); + const resp = await dispatch(matchDetector(detectorName, dataSourceId)); const match = get(resp, 'response.match', false); if (!match) { return undefined; @@ -196,12 +252,13 @@ export const DefineDetector = (props: DefineDetectorProps) => { const preparedDetector = newIndexSelected ? clearModelConfiguration(detectorToUpdate) : detectorToUpdate; - dispatch(updateDetector(detectorId, preparedDetector)) + dispatch(updateDetector(detectorId, preparedDetector, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector updated: ${response.response.name}` ); - props.history.push(`/detectors/${detectorId}/configurations/`); + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, dataSourceId, false)); }) .catch((err: any) => { core.notifications.toasts.addDanger( @@ -218,6 +275,63 @@ export const DefineDetector = (props: DefineDetectorProps) => { } }; + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + if (dataSourceEnabled && dataSourceId === undefined) { + getNotifications().toasts.addDanger( + prettifyErrorMessage('Unable to set data source.') + ); + } else { + setMDSCreateState({ + queryParams: dataSourceId, + selectedDataSourceId: dataSourceId, + }); + } + }; + + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + if (props.isEdit) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } else { + const DataSourceMenu = + getDataSourceManagementPlugin().ui.getDataSourceMenu(); + renderDataSourceComponent = useMemo(() => { + return ( + + handleDataSourceChange(dataSources), + }} + /> + ); + }, [getSavedObjectsClient(), getNotifications(), props.setActionMenu]); + } + } + + + return ( { > {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { onClick={() => { if (props.isEdit) { props.history.push( - `/detectors/${detectorId}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + MDSCreateState.selectedDataSourceId, + false + ) ); } else { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + MDSCreateState.selectedDataSourceId, + false + ) + ); } }} > diff --git a/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx b/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx index 84f9a212..0ba9421f 100644 --- a/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx +++ b/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx @@ -27,6 +27,14 @@ import { testDetectorDefinitionValues, } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouterEmpty = (isEdit: boolean = false) => ({ ...render( @@ -36,6 +44,7 @@ const renderWithRouterEmpty = (isEdit: boolean = false) => ({ render={(props: RouteComponentProps) => ( ({ render={(props: RouteComponentProps) => ( state.ad.detectors[props.detectorId] ); useEffect(() => { - dispatch(getDetector(props.detectorId)); + dispatch(getDetector(props.detectorId, dataSourceId)); }, []); return ( diff --git a/public/pages/DetectorDetail/containers/DetectorDetail.tsx b/public/pages/DetectorDetail/containers/DetectorDetail.tsx index 20f7344f..f2184e15 100644 --- a/public/pages/DetectorDetail/containers/DetectorDetail.tsx +++ b/public/pages/DetectorDetail/containers/DetectorDetail.tsx @@ -24,10 +24,16 @@ import { EuiLoadingSpinner, EuiButton, } from '@elastic/eui'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { get, isEmpty } from 'lodash'; -import { RouteComponentProps, Switch, Route, Redirect } from 'react-router-dom'; +import { + RouteComponentProps, + Switch, + Route, + Redirect, + useLocation, +} from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; @@ -42,7 +48,7 @@ import { import { getIndices } from '../../../redux/reducers/opensearch'; import { getErrorMessage, Listener } from '../../../utils/utils'; import { darkModeEnabled } from '../../../utils/opensearchDashboardsUtils'; -import { BREADCRUMBS } from '../../../utils/constants'; +import { BREADCRUMBS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { DetectorControls } from '../components/DetectorControls'; import { ConfirmModal } from '../components/ConfirmModal/ConfirmModal'; import { useFetchMonitorInfo } from '../hooks/useFetchMonitorInfo'; @@ -58,12 +64,21 @@ import { import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { CatIndex } from '../../../../server/models/types'; import { containsIndex } from '../utils/helpers'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface DetectorRouterProps { detectorId?: string; } -interface DetectorDetailProps - extends RouteComponentProps {} +interface DetectorDetailProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; +} const tabs = [ { @@ -103,10 +118,18 @@ export const DetectorDetail = (props: DetectorDetailProps) => { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); const detectorId = get(props, 'match.params.detectorId', '') as string; + const dataSourceEnabled = getDataSourceEnabled().enabled; + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const { detector, hasError, isLoadingDetector, errorMessage } = - useFetchDetectorInfo(detectorId); - const { monitor, fetchMonitorError, isLoadingMonitor } = - useFetchMonitorInfo(detectorId); + useFetchDetectorInfo(detectorId, dataSourceId); + const { monitor } = useFetchMonitorInfo( + detectorId, + dataSourceId, + dataSourceEnabled + ); const visibleIndices = useSelector( (state: AppState) => state.opensearch.indices ) as CatIndex[]; @@ -156,7 +179,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { // detector starts, result index recreated or user switches tabs to re-fetch detector) useEffect(() => { const getInitialIndices = async () => { - await dispatch(getIndices('')).catch((error: any) => { + await dispatch(getIndices('', dataSourceId)).catch((error: any) => { console.error(error); core.notifications.toasts.addDanger('Error getting all indices'); }); @@ -174,17 +197,25 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ? prettifyErrorMessage(errorMessage) : 'Unable to find the detector' ); - props.history.push('/detectors'); + props.history.push(constructHrefWithDataSourceId('/detectors', dataSourceId, false)); } }, [hasError]); useEffect(() => { if (detector) { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { text: detector ? detector.name : '' }, - ]); + if(dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { text: detector ? detector.name : '' }, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { text: detector ? detector.name : '' }, + ]); + } } }, [detector]); @@ -201,7 +232,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.CONFIGURATIONS, }); - props.history.push(`/detectors/${detectorId}/configurations`); + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations`, dataSourceId, false) + ); }, []); const handleSwitchToHistoricalTab = useCallback(() => { @@ -209,7 +242,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.HISTORICAL, }); - props.history.push(`/detectors/${detectorId}/historical`); + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/historical`, dataSourceId, false) + ); }, []); const handleTabChange = (route: DETECTOR_DETAIL_TABS) => { @@ -217,7 +252,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: route, }); - props.history.push(`/detectors/${detectorId}/${route}`); + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/${route}`, dataSourceId, false) + ); }; const hideMonitorCalloutModal = () => { @@ -245,9 +282,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { const listener: Listener = { onSuccess: () => { if (detectorDetailModel.showStopDetectorModalFor === 'detector') { - props.history.push(`/detectors/${detectorId}/edit`); + props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/edit`, dataSourceId, false)); } else { - props.history.push(`/detectors/${detectorId}/features`); + props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/features`, dataSourceId, false)); } hideStopDetectorModal(); }, @@ -261,8 +298,8 @@ export const DetectorDetail = (props: DetectorDetailProps) => { // Await for the start detector call to succeed before displaying toast. // Don't wait for get detector call; the page will be updated // via hooks automatically when the new detector info is returned. - await dispatch(startDetector(detectorId)); - dispatch(getDetector(detectorId)); + await dispatch(startDetector(detectorId, dataSourceId)); + dispatch(getDetector(detectorId, dataSourceId)); core.notifications.toasts.addSuccess( `Successfully started the detector job` ); @@ -278,10 +315,10 @@ export const DetectorDetail = (props: DetectorDetailProps) => { const handleStopAdJob = async (detectorId: string, listener?: Listener) => { try { if (isRTJobRunning) { - await dispatch(stopDetector(detectorId)); + await dispatch(stopDetector(detectorId, dataSourceId)); } if (isHistoricalJobRunning) { - await dispatch(stopHistoricalDetector(detectorId)); + await dispatch(stopHistoricalDetector(detectorId, dataSourceId)); } core.notifications.toasts.addSuccess( `Successfully stopped the ${runningJobsAsString}` @@ -302,10 +339,10 @@ export const DetectorDetail = (props: DetectorDetailProps) => { const handleDelete = useCallback(async (detectorId: string) => { try { - await dispatch(deleteDetector(detectorId)); + await dispatch(deleteDetector(detectorId, dataSourceId)); core.notifications.toasts.addSuccess(`Successfully deleted the detector`); hideDeleteDetectorModal(); - props.history.push('/detectors'); + props.history.push(constructHrefWithDataSourceId('/detectors', dataSourceId, false)); } catch (err) { core.notifications.toasts.addDanger( prettifyErrorMessage( @@ -322,7 +359,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, showStopDetectorModalFor: 'detector', }) - : props.history.push(`/detectors/${detectorId}/edit`); + : props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/edit`, dataSourceId, false)); }; const handleEditFeature = () => { @@ -331,7 +368,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, showStopDetectorModalFor: 'features', }) - : props.history.push(`/detectors/${detectorId}/features`); + : props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/features`, dataSourceId, false)); }; const lightStyles = { @@ -350,6 +387,24 @@ export const DetectorDetail = (props: DetectorDetailProps) => { > ) : null; + let renderDataSourceComponent = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {!isEmpty(detector) && !hasError ? ( @@ -361,6 +416,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { : { ...lightStyles, flexGrow: 'unset' }), }} > + {dataSourceEnabled && renderDataSourceComponent} { const dispatch = useDispatch(); useEffect(() => { - const fetchAdMonitors = async () => { - await dispatch(searchMonitors()); - }; - fetchAdMonitors(); + dispatch(searchMonitors(dataSourceId)); }, []); const isMonitorRequesting = useSelector( diff --git a/public/pages/DetectorJobs/containers/DetectorJobs.tsx b/public/pages/DetectorJobs/containers/DetectorJobs.tsx index 22599da4..435b4dc8 100644 --- a/public/pages/DetectorJobs/containers/DetectorJobs.tsx +++ b/public/pages/DetectorJobs/containers/DetectorJobs.tsx @@ -23,25 +23,41 @@ import { } from '@elastic/eui'; import { FormikProps, Formik } from 'formik'; import { isEmpty } from 'lodash'; -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, ReactElement } from 'react'; import { BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { DetectorJobsFormikValues } from '../models/interfaces'; import { RealTimeJob } from '../components/RealTimeJob'; import { HistoricalJob } from '../components/HistoricalJob'; import { convertTimestampToNumber } from '../../../utils/utils'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface DetectorJobsProps extends RouteComponentProps { setStep?(stepNumber: number): void; initialValues: DetectorJobsFormikValues; setInitialValues(initialValues: DetectorJobsFormikValues): void; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function DetectorJobs(props: DetectorJobsProps) { const core = React.useContext(CoreServicesContext) as CoreStart; + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourceEnabled().enabled; + const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); const [realTime, setRealTime] = useState( @@ -57,11 +73,19 @@ export function DetectorJobs(props: DetectorJobsProps) { }, []); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } }, []); const handleFormValidation = async ( @@ -103,6 +127,24 @@ export function DetectorJobs(props: DetectorJobsProps) { props.setInitialValues(values); }; + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); }} > Cancel diff --git a/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx b/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx index ac8e9b7b..cea842df 100644 --- a/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx +++ b/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx @@ -24,6 +24,14 @@ import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { INITIAL_DETECTOR_JOB_VALUES } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( diff --git a/public/pages/DetectorResults/containers/AnomalyHistory.tsx b/public/pages/DetectorResults/containers/AnomalyHistory.tsx index e9a355df..4030340b 100644 --- a/public/pages/DetectorResults/containers/AnomalyHistory.tsx +++ b/public/pages/DetectorResults/containers/AnomalyHistory.tsx @@ -65,7 +65,7 @@ import { TOP_CHILD_ENTITIES_TO_FETCH, } from '../utils/constants'; import { MIN_IN_MILLI_SECS } from '../../../../server/utils/constants'; -import { MAX_ANOMALIES } from '../../../utils/constants'; +import { DATA_SOURCE_ID, MAX_ANOMALIES } from '../../../utils/constants'; import { searchResults, getDetectorResults, @@ -95,6 +95,8 @@ import { import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface AnomalyHistoryProps { detector: Detector; @@ -126,6 +128,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { props.isHistorical && props.detector?.detectionDateRange ? props.detector.detectionDateRange.endTime : moment().valueOf(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [dateRange, setDateRange] = useState({ startDate: initialStartDate, endDate: initialEndDate, @@ -223,7 +228,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { taskId.current, modelId ); - return dispatch(searchResults(params, resultIndex, true)); + return dispatch( + searchResults(params, resultIndex, dataSourceId, true) + ); }) : []; @@ -252,7 +259,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { modelId ); const anomalySummaryResponse = await dispatch( - searchResults(anomalySummaryQuery, resultIndex, true) + searchResults(anomalySummaryQuery, resultIndex, dataSourceId, true) ); allPureAnomalies.push(parsePureAnomalies(anomalySummaryResponse)); } @@ -275,7 +282,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { taskId.current, modelId ); - return dispatch(searchResults(params, resultIndex, true)); + return dispatch( + searchResults(params, resultIndex, dataSourceId, true) + ); } ); @@ -308,7 +317,12 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { modelId ); const bucketizedAnomalyResultResponse = await dispatch( - searchResults(bucketizedAnomalyResultsQuery, resultIndex, true) + searchResults( + bucketizedAnomalyResultsQuery, + resultIndex, + dataSourceId, + true + ) ); allBucketizedAnomalyResults.push( parseBucketizedAnomalyResults(bucketizedAnomalyResultResponse) @@ -408,7 +422,14 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { ); const detectorResultResponse = props.isHistorical ? await dispatch( - getDetectorResults(taskId.current, params, true, resultIndex, true) + getDetectorResults( + taskId.current, + dataSourceId, + params, + true, + resultIndex, + true + ) ).catch((error: any) => { setIsLoading(false); setIsLoadingAnomalyResults(false); @@ -417,6 +438,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { : await dispatch( getDetectorResults( props.detector.id, + dataSourceId, params, false, resultIndex, @@ -536,6 +558,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { const result = await dispatch( getTopAnomalyResults( detectorId, + dataSourceId, get(props, 'isHistorical', false), query ) @@ -553,7 +576,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { props.isHistorical, taskId.current ); - const result = await dispatch(searchResults(query, resultIndex, true)); + const result = await dispatch( + searchResults(query, resultIndex, dataSourceId, true) + ); topEntityAnomalySummaries = parseTopEntityAnomalySummaryResults( result, isMultiCategory @@ -572,7 +597,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { props.isHistorical, taskId.current ); - return dispatch(searchResults(entityResultQuery, resultIndex, true)); + return dispatch( + searchResults(entityResultQuery, resultIndex, dataSourceId, true) + ); } ); @@ -644,6 +671,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { return dispatch( getDetectorResults( props.isHistorical ? taskId.current : props.detector?.id, + dataSourceId, params, props.isHistorical ? true : false, resultIndex, @@ -722,7 +750,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { heatmapCell.entityList ); - const result = await dispatch(searchResults(query, resultIndex, true)); + const result = await dispatch( + searchResults(query, resultIndex, dataSourceId, true) + ); // Gets top child entities as an Entity[][], // where each entry in the array is a unique combination of entity values diff --git a/public/pages/DetectorResults/containers/AnomalyResults.tsx b/public/pages/DetectorResults/containers/AnomalyResults.tsx index 827570ce..b09b18b2 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -28,11 +28,13 @@ import { import { get, isEmpty } from 'lodash'; import React, { useEffect, Fragment, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps, useLocation } from 'react-router'; import { AppState } from '../../../redux/reducers'; import { BREADCRUMBS, + DATA_SOURCE_ID, FEATURE_DATA_POINTS_WINDOW, + MDS_BREADCRUMBS, MISSING_FEATURE_DATA_SEVERITY, } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; @@ -67,6 +69,8 @@ import { SampleIndexDetailsCallout } from '../../Overview/components/SampleIndex import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { DEFAULT_SHINGLE_SIZE } from '../../../utils/constants'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; +import { getDataSourceEnabled } from '../../../services'; interface AnomalyResultsProps extends RouteComponentProps { detectorId: string; @@ -83,18 +87,30 @@ export function AnomalyResults(props: AnomalyResultsProps) { const detector = useSelector( (state: AppState) => state.ad.detectors[detectorId] ); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourceEnabled().enabled; + const dataSourceId = MDSQueryParams.dataSourceId; useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { text: detector ? detector.name : '' }, - ]); - dispatch(getDetector(detectorId)); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { text: detector ? detector.name : '' }, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { text: detector ? detector.name : '' }, + ]); + } + dispatch(getDetector(detectorId, dataSourceId)); }, []); const fetchDetector = async () => { - dispatch(getDetector(detectorId)); + dispatch(getDetector(detectorId, dataSourceId)); }; useEffect(() => { @@ -247,7 +263,14 @@ export function AnomalyResults(props: AnomalyResultsProps) { try { const resultIndex = get(detector, 'resultIndex', ''); const detectorResultResponse = await dispatch( - getDetectorResults(detectorId, params, false, resultIndex, true) + getDetectorResults( + detectorId, + dataSourceId, + params, + false, + resultIndex, + true + ) ); const featuresData = get( detectorResultResponse, diff --git a/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx b/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx index 7ba1d2fb..8d9449f2 100644 --- a/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx @@ -52,11 +52,12 @@ import { } from '../../AnomalyCharts/utils/constants'; import { LIVE_ANOMALY_CHART_THEME } from '../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; -import { dateFormatter } from '../../utils/helpers'; +import { dateFormatter, getDataSourceFromURL } from '../../utils/helpers'; import { darkModeEnabled } from '../../../utils/opensearchDashboardsUtils'; import { EuiIcon } from '@elastic/eui'; import { formatAnomalyNumber } from '../../../../server/utils/helpers'; import { getDetectorLiveResults } from '../../../redux/reducers/liveAnomalyResults'; +import { useLocation } from 'react-router-dom'; interface AnomalyResultsLiveChartProps { detector: Detector; @@ -66,6 +67,9 @@ export const AnomalyResultsLiveChart = ( props: AnomalyResultsLiveChartProps ) => { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [firstLoading, setFirstLoading] = useState(true); const [isFullScreen, setIsFullScreen] = useState(false); @@ -133,6 +137,7 @@ export const AnomalyResultsLiveChart = ( await dispatch( getDetectorLiveResults( detectorId, + dataSourceId, queryParams, false, resultIndex, @@ -161,6 +166,7 @@ export const AnomalyResultsLiveChart = ( getLiveAnomalyResults( dispatch, props.detector.id, + dataSourceId, detectionInterval, LIVE_CHART_CONFIG.MONITORING_INTERVALS, resultIndex, diff --git a/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx b/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx index db9cfebf..af8c0feb 100644 --- a/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx +++ b/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx @@ -11,16 +11,41 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { EmptyDetectorMessage } from '../EmptyMessage'; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); describe('Empty results', () => { test('renders component with empty message', () => { + const history = createMemoryHistory(); + const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Create detector'); diff --git a/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap b/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap index ed30a454..a429c5f9 100644 --- a/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap +++ b/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap @@ -33,7 +33,7 @@ exports[` spec Empty results renders component with empt @@ -54,7 +54,7 @@ exports[` spec Empty results renders component with empt diff --git a/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx b/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx index fa9987e4..1af99b47 100644 --- a/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx +++ b/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx @@ -82,7 +82,7 @@ describe(' spec', () => { expect(defaultDeleteProps.onStopDetectors).not.toHaveBeenCalled(); expect(defaultDeleteProps.onDeleteDetectors).not.toHaveBeenCalled(); expect(defaultDeleteProps.onConfirm).not.toHaveBeenCalled(); - }, 10000); + }, 5000); test('should have delete button enabled if delete typed', async () => { const { getByTestId, getByPlaceholderText } = render( @@ -93,7 +93,7 @@ describe(' spec', () => { userEvent.click(getByTestId('confirmButton')); await waitFor(() => {}); expect(defaultDeleteProps.onConfirm).toHaveBeenCalled(); - }, 10000); + }, 5000); test('should not show callout if no detectors are running', async () => { const { queryByText } = render( diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index 864f9951..a54f14c9 100644 --- a/public/pages/DetectorsList/containers/List/List.tsx +++ b/public/pages/DetectorsList/containers/List/List.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { debounce, get, isEmpty } from 'lodash'; import queryString from 'querystring'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RouteComponentProps } from 'react-router'; import { @@ -42,15 +42,19 @@ import { getIndices, getPrioritizedIndices, } from '../../../../redux/reducers/opensearch'; -import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants'; +import { APP_PATH, MDS_BREADCRUMBS, PLUGIN_NAME } from '../../../../utils/constants'; import { DETECTOR_STATE } from '../../../../../server/utils/constants'; -import { getVisibleOptions, sanitizeSearchText } from '../../../utils/helpers'; +import { + constructHrefWithDataSourceId, + getAllDetectorsQueryParamsWithDataSourceId, + getVisibleOptions, + sanitizeSearchText, +} from '../../../utils/helpers'; import { EmptyDetectorMessage } from '../../components/EmptyMessage/EmptyMessage'; import { ListFilters } from '../../components/ListFilters/ListFilters'; import { MAX_DETECTORS, MAX_SELECTED_INDICES, - GET_ALL_DETECTORS_QUERY_PARAMS, ALL_DETECTOR_STATES, ALL_INDICES, SINGLE_DETECTOR_NOT_FOUND_MSG, @@ -65,7 +69,7 @@ import { filterAndSortDetectors, getDetectorsToDisplay, } from '../../../utils/helpers'; -import { staticColumn } from '../../utils/tableUtils'; +import { getColumns } from '../../utils/tableUtils'; import { DETECTOR_ACTION } from '../../utils/constants'; import { getTitleWithCount, Listener } from '../../../../utils/utils'; import { ListActions } from '../../components/ListActions/ListActions'; @@ -78,8 +82,15 @@ import { NO_PERMISSIONS_KEY_WORD, prettifyErrorMessage, } from '../../../../../server/utils/helpers'; -import { CoreStart } from '../../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../../src/core/public'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; +import { DataSourceOption, DataSourceSelectableConfig } from '../../../../../../../src/plugins/data_source_management/public'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../../services'; export interface ListRouterParams { from: string; @@ -88,13 +99,17 @@ export interface ListRouterParams { indices: string; sortDirection: SORT_DIRECTION; sortField: string; + dataSourceId: string; +} +interface ListProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; } -interface ListProps extends RouteComponentProps {} interface ListState { page: number; queryParams: GetDetectorsQueryParams; selectedDetectorStates: DETECTOR_STATE[]; selectedIndices: string[]; + selectedDataSourceId: string | undefined; } interface ConfirmModalState { isOpen: boolean; @@ -123,6 +138,8 @@ export const DetectorList = (props: ListProps) => { (state: AppState) => state.ad.requesting ); + const dataSourceEnabled = getDataSourceEnabled().enabled; + const [selectedDetectors, setSelectedDetectors] = useState( [] as DetectorListItem[] ); @@ -152,19 +169,10 @@ export const DetectorList = (props: ListProps) => { isStopDisabled: false, }); - // Getting all initial indices - const [indexQuery, setIndexQuery] = useState(''); - useEffect(() => { - const getInitialIndices = async () => { - await dispatch(getIndices(indexQuery)); - }; - getInitialIndices(); - }, []); - // Getting all initial monitors useEffect(() => { const getInitialMonitors = async () => { - dispatch(searchMonitors()); + dispatch(searchMonitors(state.selectedDataSourceId)); }; getInitialMonitors(); }, []); @@ -198,24 +206,53 @@ export const DetectorList = (props: ListProps) => { selectedIndices: queryParams.indices ? queryParams.indices.split(',') : ALL_INDICES, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, }); // Set breadcrumbs on page initialization useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - ]); - }, []); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(state.selectedDataSourceId), + MDS_BREADCRUMBS.DETECTORS(state.selectedDataSourceId), + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + ]); + } + }, [state.selectedDataSourceId]); + + // Getting all initial indices + const [indexQuery, setIndexQuery] = useState(''); + useEffect(() => { + const getInitialIndices = async () => { + await dispatch(getIndices(indexQuery, state.selectedDataSourceId)); + }; + getInitialIndices(); + }, [state.selectedDataSourceId]); // Refresh data if user change any parameters / filter / sort useEffect(() => { const { history, location } = props; - const updatedParams = { - ...state.queryParams, - indices: state.selectedIndices.join(','), + let updatedParams = { from: state.page * state.queryParams.size, - }; + size: state.queryParams.size, + search: state.queryParams.search, + indices: state.selectedIndices.join(','), + sortDirection: state.queryParams.sortDirection, + sortField: state.queryParams.sortField, + } as GetDetectorsQueryParams; + + if (dataSourceEnabled) { + updatedParams = { + ...updatedParams, + dataSourceId: state.selectedDataSourceId, + } + } history.replace({ ...location, @@ -223,12 +260,14 @@ export const DetectorList = (props: ListProps) => { }); setIsLoadingFinalDetectors(true); + getUpdatedDetectors(); }, [ state.page, state.queryParams, state.selectedDetectorStates, state.selectedIndices, + state.selectedDataSourceId, ]); // Handle all filtering / sorting of detectors @@ -273,7 +312,11 @@ export const DetectorList = (props: ListProps) => { }, [confirmModalState.isRequestingToClose, isLoading]); const getUpdatedDetectors = async () => { - dispatch(getDetectorList(GET_ALL_DETECTORS_QUERY_PARAMS)); + dispatch( + getDetectorList( + getAllDetectorsQueryParamsWithDataSourceId(state.selectedDataSourceId) + ) + ); }; const handlePageChange = (pageNumber: number) => { @@ -315,7 +358,9 @@ export const DetectorList = (props: ListProps) => { if (searchValue !== indexQuery) { const sanitizedQuery = sanitizeSearchText(searchValue); setIndexQuery(sanitizedQuery); - await dispatch(getPrioritizedIndices(sanitizedQuery)); + await dispatch( + getPrioritizedIndices(sanitizedQuery, state.selectedDataSourceId) + ); setState((state) => ({ ...state, page: 0, @@ -455,7 +500,7 @@ export const DetectorList = (props: ListProps) => { DETECTOR_ACTION.START ).map((detector) => detector.id); const promises = validIds.map(async (id: string) => { - return dispatch(startDetector(id)); + return dispatch(startDetector(id, state.selectedDataSourceId)); }); await Promise.all(promises) .then(() => { @@ -482,7 +527,7 @@ export const DetectorList = (props: ListProps) => { DETECTOR_ACTION.STOP ).map((detector) => detector.id); const promises = validIds.map(async (id: string) => { - return dispatch(stopDetector(id)); + return dispatch(stopDetector(id, state.selectedDataSourceId)); }); await Promise.all(promises) .then(() => { @@ -514,7 +559,7 @@ export const DetectorList = (props: ListProps) => { DETECTOR_ACTION.DELETE ).map((detector) => detector.id); const promises = validIds.map(async (id: string) => { - return dispatch(deleteDetector(id)); + return dispatch(deleteDetector(id, state.selectedDataSourceId)); }); await Promise.all(promises) .then(() => { @@ -552,6 +597,22 @@ export const DetectorList = (props: ListProps) => { }); }; + const handleDataSourceChange = (dataSources: DataSourceOption[]) => { + const dataSourceId = dataSources[0].id; + + if (dataSourceEnabled && dataSourceId === undefined) { + getNotifications().toasts.addDanger( + prettifyErrorMessage('Unable to set data source.') + ); + } else { + setState((prevState) => ({ + ...prevState, + page: 0, + selectedDataSourceId: dataSourceId, + })); + } + }; + const getConfirmModal = () => { if (confirmModalState.isOpen) { //@ts-ignore @@ -626,9 +687,38 @@ export const DetectorList = (props: ListProps) => { const confirmModal = getConfirmModal(); + let renderDataSourceComponent = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = useMemo(() => { + return ( + + handleDataSourceChange(dataSources), + }} + /> + ); + }, [getSavedObjectsClient(), getNotifications(), props.setActionMenu]); + } + + const columns = getColumns(state.selectedDataSourceId); + + const createDetectorUrl =`${PLUGIN_NAME}#` + constructHrefWithDataSourceId(APP_PATH.CREATE_DETECTOR, state.selectedDataSourceId, false); + return ( + {dataSourceEnabled && renderDataSourceComponent} { Create detector , @@ -686,7 +776,7 @@ export const DetectorList = (props: ListProps) => { monitors list page. */ itemId={getItemId} - columns={staticColumn} + columns={columns} onChange={handleTableChange} isSelectable={true} selection={selection} diff --git a/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx b/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx index e932f4b2..3bc8c268 100644 --- a/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx +++ b/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx @@ -31,6 +31,14 @@ import { DetectorList, ListRouterParams } from '../List'; import { DETECTOR_STATE } from '../../../../../../server/utils/constants'; import { CoreServicesContext } from '../../../../../components/CoreServices/CoreServices'; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = ( initialAdState: Detectors = initialDetectorsState ) => ({ @@ -43,7 +51,9 @@ const renderWithRouter = ( path="/detectors" render={(props: RouteComponentProps) => ( - + )} /> diff --git a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts index 0cf68c84..979396e6 100644 --- a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts +++ b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts @@ -30,6 +30,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, + dataSourceId: undefined, }); }); test('should default values if missing from queryParams', () => { @@ -42,6 +43,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, + dataSourceId: undefined, }); }); test('should return queryParams from location', () => { @@ -57,6 +59,7 @@ describe('helpers spec', () => { indices: 'someIndex', sortField: 'name', sortDirection: SORT_DIRECTION.DESC, + dataSourceId: undefined, }); }); }); diff --git a/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx b/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx index 327767dc..9496cd2a 100644 --- a/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx +++ b/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx @@ -9,43 +9,43 @@ * GitHub history for details. */ -import { staticColumn } from '../../utils/tableUtils'; +import { getColumns } from '../../utils/tableUtils'; import { render } from '@testing-library/react'; describe('tableUtils spec', () => { describe('should render the column titles', () => { test('detector name column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[0].name); getByText('Detector'); }); test('indices column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[1].name); getByText('Indices'); }); test('detector state column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[2].name); getByText('Real-time state'); }); test('historical analysis column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[3].name); getByText('Historical analysis'); }); test('anomalies last 24 hrs column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[4].name); getByText('Anomalies last 24 hours'); }); test('last RT occurrence column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[5].name); getByText('Last real-time occurrence'); }); test('last started time column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[6].name); getByText('Last started'); }); diff --git a/public/pages/DetectorsList/utils/helpers.ts b/public/pages/DetectorsList/utils/helpers.ts index 89d9075c..7e59bdb5 100644 --- a/public/pages/DetectorsList/utils/helpers.ts +++ b/public/pages/DetectorsList/utils/helpers.ts @@ -21,8 +21,15 @@ import { DETECTOR_ACTION } from '../utils/constants'; export const getURLQueryParams = (location: { search: string; }): GetDetectorsQueryParams => { - const { from, size, search, indices, sortField, sortDirection } = - queryString.parse(location.search) as { [key: string]: string }; + const { + from, + size, + search, + indices, + sortField, + sortDirection, + dataSourceId, + } = queryString.parse(location.search) as { [key: string]: string }; return { // @ts-ignore from: isNaN(parseInt(from, 10)) @@ -40,9 +47,12 @@ export const getURLQueryParams = (location: { typeof sortDirection !== 'string' ? DEFAULT_QUERY_PARAMS.sortDirection : (sortDirection as SORT_DIRECTION), + dataSourceId: dataSourceId === undefined ? undefined : dataSourceId, }; }; + + // For realtime detectors: cannot have 'Finished' state export const getDetectorStateOptions = () => { return Object.values(DETECTOR_STATE) diff --git a/public/pages/DetectorsList/utils/tableUtils.tsx b/public/pages/DetectorsList/utils/tableUtils.tsx index b1e4b8bd..5f354214 100644 --- a/public/pages/DetectorsList/utils/tableUtils.tsx +++ b/public/pages/DetectorsList/utils/tableUtils.tsx @@ -51,114 +51,120 @@ export const renderState = (state: DETECTOR_STATE) => { ); }; -export const staticColumn = [ - { - field: 'name', - name: ( - - Detector{''} - - ), - sortable: true, - truncateText: true, - textOnly: true, - align: 'left', - width: '15%', - render: (name: string, detector: Detector) => ( - - {name} - - ), - }, - { - field: 'indices', - name: ( - - Indices{''} - - ), - sortable: true, - truncateText: true, - textOnly: true, - align: 'left', - width: '15%', - render: renderIndices, - }, - { - field: 'curState', - name: ( - - Real-time state{''} - - ), - sortable: true, - dataType: 'string', - align: 'left', - width: '12%', - truncateText: false, - render: renderState, - }, - { - field: 'task', - name: ( - - Historical analysis{''} - - ), - sortable: true, - truncateText: true, - textOnly: true, - align: 'left', - width: '15%', - render: (name: string, detector: Detector) => { - return !isEmpty(detector.taskId) ? ( - - View results - - ) : ( - - - ); +export function getColumns(dataSourceId) { + return [ + { + field: 'name', + name: ( + + Detector{''} + + ), + sortable: true, + truncateText: true, + textOnly: true, + align: 'left', + width: '15%', + render: (name: string, detector: Detector) => { + let href = `${PLUGIN_NAME}#/detectors/${detector.id}/results`; + if (dataSourceId) { + href += `?dataSourceId=${dataSourceId}`; + } + return {name}; + }, }, - }, - { - field: 'totalAnomalies', - name: ( - - Anomalies last 24 hours{''} - - ), - sortable: true, - dataType: 'number', - align: 'right', - width: '16%', - truncateText: false, - }, - { - field: 'lastActiveAnomaly', - name: ( - - Last real-time occurrence{''} - - ), - sortable: true, - dataType: 'date', - truncateText: false, - align: 'left', - width: '16%', - render: renderTime, - }, - { - field: 'enabledTime', - name: ( - - Last started{''} - - ), - sortable: true, - dataType: 'date', - truncateText: false, - align: 'left', - width: '16%', - render: renderTime, - }, -] as EuiBasicTableColumn[]; + { + field: 'indices', + name: ( + + Indices{''} + + ), + sortable: true, + truncateText: true, + textOnly: true, + align: 'left', + width: '15%', + render: renderIndices, + }, + { + field: 'curState', + name: ( + + Real-time state{''} + + ), + sortable: true, + dataType: 'string', + align: 'left', + width: '12%', + truncateText: false, + render: renderState, + }, + { + field: 'task', + name: ( + + Historical analysis{''} + + ), + sortable: true, + truncateText: true, + textOnly: true, + align: 'left', + width: '15%', + render: (name: string, detector: Detector) => { + if (!isEmpty(detector.taskId)) { + let href = `${PLUGIN_NAME}#/detectors/${detector.id}/historical`; + if (dataSourceId) { + href += `?dataSourceId=${dataSourceId}`; + } + return View results; + } else { + return -; + } + }, + }, + { + field: 'totalAnomalies', + name: ( + + Anomalies last 24 hours{''} + + ), + sortable: true, + dataType: 'number', + align: 'right', + width: '16%', + truncateText: false, + }, + { + field: 'lastActiveAnomaly', + name: ( + + Last real-time occurrence{''} + + ), + sortable: true, + dataType: 'date', + truncateText: false, + align: 'left', + width: '16%', + render: renderTime, + }, + { + field: 'enabledTime', + name: ( + + Last started{''} + + ), + sortable: true, + dataType: 'date', + truncateText: false, + align: 'left', + width: '16%', + render: renderTime, + }, + ] as EuiBasicTableColumn[]; +} diff --git a/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx b/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx index bc8e80e3..b295bfd7 100644 --- a/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx +++ b/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx @@ -23,7 +23,7 @@ import { } from '@elastic/eui'; import { get, isEmpty } from 'lodash'; import React, { useEffect, Fragment, useState } from 'react'; -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps, useLocation } from 'react-router'; import { useSelector, useDispatch } from 'react-redux'; import { darkModeEnabled } from '../../../utils/opensearchDashboardsUtils'; import { AppState } from '../../../redux/reducers'; @@ -50,6 +50,7 @@ import { import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { EmptyHistoricalDetectorResults } from '../components/EmptyHistoricalDetectorResults'; import { HistoricalDetectorCallout } from '../components/HistoricalDetectorCallout'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface HistoricalDetectorResultsProps extends RouteComponentProps { detectorId: string; @@ -62,6 +63,9 @@ export function HistoricalDetectorResults( const isDark = darkModeEnabled(); const dispatch = useDispatch(); const detectorId: string = get(props, 'match.params.detectorId', ''); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const adState = useSelector((state: AppState) => state.ad); const allDetectors = adState.detectors; @@ -81,7 +85,7 @@ export function HistoricalDetectorResults( const fetchDetector = async () => { try { - await dispatch(getDetector(detectorId)); + await dispatch(getDetector(detectorId, dataSourceId)); } catch {} }; @@ -113,7 +117,14 @@ export function HistoricalDetectorResults( const startHistoricalTask = async (startTime: number, endTime: number) => { try { - dispatch(startHistoricalDetector(props.detectorId, startTime, endTime)) + dispatch( + startHistoricalDetector( + props.detectorId, + dataSourceId, + startTime, + endTime + ) + ) .then((response: any) => { setTaskId(get(response, 'response._id')); core.notifications.toasts.addSuccess( @@ -141,9 +152,9 @@ export function HistoricalDetectorResults( const onStopDetector = async () => { try { setIsStoppingDetector(true); - await dispatch(stopHistoricalDetector(detectorId)); + await dispatch(stopHistoricalDetector(detectorId, dataSourceId)); await waitForMs(HISTORICAL_DETECTOR_STOP_THRESHOLD); - dispatch(getDetector(detectorId)).then((response: any) => { + dispatch(getDetector(detectorId, dataSourceId)).then((response: any) => { if (get(response, 'response.curState') !== DETECTOR_STATE.DISABLED) { throw 'please try again.'; } else { diff --git a/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx b/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx index 0f544d27..98d8a469 100644 --- a/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx +++ b/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx @@ -22,6 +22,8 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { PLUGIN_NAME } from '../../../../utils/constants'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../../pages/utils/helpers'; interface SampleDataBoxProps { title: string; @@ -37,6 +39,10 @@ interface SampleDataBoxProps { } export const SampleDataBox = (props: SampleDataBoxProps) => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + return (
{ {props.isDataLoaded ? ( View detector and sample data diff --git a/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx b/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx index ef32c66b..d5afb538 100644 --- a/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx +++ b/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx @@ -13,6 +13,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { EuiIcon } from '@elastic/eui'; import { SampleDataBox } from '../SampleDataBox'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const defaultProps = { title: 'Sample title', @@ -25,11 +27,33 @@ const defaultProps = { detectorId: 'sample-detector-id', }; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); describe('Data not loaded', () => { test('renders component', () => { + const history = createMemoryHistory(); const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); @@ -39,8 +63,13 @@ describe(' spec', () => { }); describe('Data is loading', () => { test('renders component', () => { + const history = createMemoryHistory(); const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); @@ -50,8 +79,14 @@ describe(' spec', () => { }); describe('Data is loaded', () => { test('renders component', () => { + const history = createMemoryHistory(); + const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); diff --git a/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap b/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap index fbec5767..4cdf707a 100644 --- a/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap +++ b/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap @@ -98,7 +98,7 @@ exports[` spec Data is loaded renders component 1`] = ` View detector and sample data diff --git a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx index 5d84779e..f1082976 100644 --- a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx +++ b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx @@ -17,24 +17,21 @@ import { EuiFlexItem, EuiFlexGroup, EuiLink, - EuiIcon, EuiButton, EuiLoadingSpinner, EuiFlexGrid, } from '@elastic/eui'; -import React, { Fragment, useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { APP_PATH, BREADCRUMBS, PLUGIN_NAME, BASE_DOCS_LINK, + MDS_BREADCRUMBS, } from '../../../utils/constants'; import { SAMPLE_TYPE } from '../../../../server/utils/constants'; -import { - GET_SAMPLE_DETECTORS_QUERY_PARAMS, - GET_SAMPLE_INDICES_QUERY, -} from '../../utils/constants'; +import { GET_SAMPLE_INDICES_QUERY } from '../../utils/constants'; import { AppState } from '../../../redux/reducers'; import { getDetectorList } from '../../../redux/reducers/ad'; import { createSampleData } from '../../../redux/reducers/sampleData'; @@ -54,27 +51,44 @@ import { import { SampleDataBox } from '../components/SampleDataBox/SampleDataBox'; import { SampleDetailsFlyout } from '../components/SampleDetailsFlyout/SampleDetailsFlyout'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import ContentPanel from '../../../components/ContentPanel/ContentPanel'; import { CreateWorkflowStepDetails } from '../components/CreateWorkflowStepDetails'; import { CreateWorkflowStepSeparator } from '../components/CreateWorkflowStepSeparator'; +import { DataSourceSelectableConfig } from '../../../../../../src/plugins/data_source_management/public'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../../public/services'; +import { RouteComponentProps } from 'react-router-dom'; +import queryString from 'querystring'; +import { getDataSourceFromURL, getSampleDetectorsQueryParamsWithDataSouceId } from '../../../../public/pages/utils/helpers'; +import { MDSStates } from '../../../models/interfaces'; -interface AnomalyDetectionOverviewProps { - isLoadingDetectors: boolean; +interface AnomalyDetectionOverviewProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; + landingDataSourceId: string | undefined; } export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const core = React.useContext(CoreServicesContext) as CoreStart; + const isLoadingSampleDetectors = useSelector( + (state: AppState) => state.ad.requesting + ); + const isLoadingSampleIndices = useSelector( + (state: AppState) => state.opensearch.requesting + ); const dispatch = useDispatch(); const visibleSampleIndices = useSelector( (state: AppState) => state.opensearch.indices ); - const allSampleDetectors = Object.values( useSelector((state: AppState) => state.ad.detectorList) ); - + const dataSourceEnabled = getDataSourceEnabled().enabled; const [isLoadingHttpData, setIsLoadingHttpData] = useState(false); const [isLoadingEcommerceData, setIsLoadingEcommerceData] = useState(false); @@ -87,30 +101,59 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const [showHostHealthDetailsFlyout, setShowHostHealthDetailsFlyout] = useState(false); - const getAllSampleDetectors = async () => { - await dispatch(getDetectorList(GET_SAMPLE_DETECTORS_QUERY_PARAMS)).catch( - (error: any) => { - console.error('Error getting all detectors: ', error); - } - ); - }; - - const getAllSampleIndices = async () => { - await dispatch(getIndices(GET_SAMPLE_INDICES_QUERY)).catch((error: any) => { - console.error('Error getting all indices: ', error); - }); - }; + const queryParams = getDataSourceFromURL(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ + queryParams, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, + }); // Set breadcrumbs on page initialization useEffect(() => { - core.chrome.setBreadcrumbs([BREADCRUMBS.ANOMALY_DETECTOR]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([MDS_BREADCRUMBS.ANOMALY_DETECTOR(MDSOverviewState.selectedDataSourceId)]); + } else { + core.chrome.setBreadcrumbs([BREADCRUMBS.ANOMALY_DETECTOR]); + } }, []); // Getting all initial sample detectors & indices useEffect(() => { - getAllSampleDetectors(); - getAllSampleIndices(); - }, []); + const { history, location } = props; + if (dataSourceEnabled && props.landingDataSourceId !== undefined) { + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); + } + fetchData(); + }, [MDSOverviewState]); + + // fetch smaple detectors and sample indices + const fetchData = async () => { + await dispatch( + getDetectorList( + getSampleDetectorsQueryParamsWithDataSouceId( + MDSOverviewState.selectedDataSourceId + ) + ) + ).catch((error: any) => { + console.error('Error getting sample detectors: ', error); + }); + await dispatch( + getIndices( + GET_SAMPLE_INDICES_QUERY, + MDSOverviewState.selectedDataSourceId + ) + ).catch((error: any) => { + console.error('Error getting sample indices: ', error); + }); + }; // Create and populate sample index, create and start sample detector const handleLoadData = async ( @@ -125,7 +168,9 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Create the index (if it doesn't exist yet) if (!containsSampleIndex(visibleSampleIndices, sampleType)) { - await dispatch(createIndex(indexConfig)).catch((error: any) => { + await dispatch( + createIndex(indexConfig, MDSOverviewState.selectedDataSourceId) + ).catch((error: any) => { errorDuringAction = true; errorMessage = 'Error creating sample index. ' + prettifyErrorMessage(error); @@ -135,7 +180,9 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Get the sample data from the server and bulk insert if (!errorDuringAction) { - await dispatch(createSampleData(sampleType)).catch((error: any) => { + await dispatch( + createSampleData(sampleType, MDSOverviewState.selectedDataSourceId) + ).catch((error: any) => { errorDuringAction = true; errorMessage = prettifyErrorMessage(error.message); console.error('Error bulk inserting data: ', errorMessage); @@ -144,11 +191,15 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Create the detector if (!errorDuringAction) { - await dispatch(createDetector(detectorConfig)) + await dispatch( + createDetector(detectorConfig, MDSOverviewState.selectedDataSourceId) + ) .then(function (response: any) { const detectorId = response.response.id; // Start the detector - dispatch(startDetector(detectorId)).catch((error: any) => { + dispatch( + startDetector(detectorId, MDSOverviewState.selectedDataSourceId) + ).catch((error: any) => { errorDuringAction = true; errorMessage = prettifyErrorMessage(error.message); console.error('Error starting sample detector: ', errorMessage); @@ -160,9 +211,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { console.error('Error creating sample detector: ', errorMessage); }); } - - getAllSampleDetectors(); - getAllSampleIndices(); + fetchData(); setLoadingState(false); if (!errorDuringAction) { core.notifications.toasts.addSuccess( @@ -175,13 +224,60 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { } }; - return props.isLoadingDetectors ? ( + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + + if (dataSourceEnabled && dataSourceId === undefined) { + getNotifications().toasts.addDanger( + prettifyErrorMessage('Unable to set data source.') + ); + } else { + setMDSOverviewState({ + queryParams: dataSourceId, + selectedDataSourceId: dataSourceId, + }); + } + }; + + let renderDataSourceComponent = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = useMemo(() => { + return ( + + handleDataSourceChange(dataSources), + }} + /> + ); + }, [getSavedObjectsClient, getNotifications, props.setActionMenu]); + } + + const createDetectorUrl = + `${PLUGIN_NAME}#` + + (dataSourceEnabled + ? `${APP_PATH.CREATE_DETECTOR}?dataSourceId=${MDSOverviewState.selectedDataSourceId}` + : `${APP_PATH.CREATE_DETECTOR}`); + + return isLoadingSampleDetectors && isLoadingSampleIndices ? (
) : ( + {dataSourceEnabled && renderDataSourceComponent} @@ -191,7 +287,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { Create detector diff --git a/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx b/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx index 60536abc..88411861 100644 --- a/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx +++ b/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx @@ -18,17 +18,23 @@ import { Redirect, Route, Switch, + RouteComponentProps, } from 'react-router-dom'; import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; import configureStore from '../../../../redux/configureStore'; -import { - Detectors, - initialDetectorsState, -} from '../../../../redux/reducers/ad'; import { sampleHttpResponses } from '../../../Overview/utils/constants'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; -const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + + +const renderWithRouter = () => ({ ...render( @@ -36,11 +42,11 @@ const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ ( + render={(props: RouteComponentProps) => ( {' '} + setActionMenu={jest.fn()} + {...props}/> )} /> @@ -52,6 +58,17 @@ const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); jest.clearAllMocks(); describe('No sample detectors created', () => { test('renders component', async () => { diff --git a/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap b/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap index 1cdefe53..0182660d 100644 --- a/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap +++ b/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap @@ -24,7 +24,7 @@ exports[` spec No sample detectors created renders c
spec No sample detectors created renders c
- `; @@ -785,7 +784,7 @@ exports[` spec Some detectors created renders compon spec Some detectors created renders compon - `; @@ -1541,7 +1539,7 @@ exports[` spec Some detectors created renders compon spec Some detectors created renders compon View detector and sample data @@ -2290,6 +2288,5 @@ exports[` spec Some detectors created renders compon - `; diff --git a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx index 1055c2d4..81ca40de 100644 --- a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx +++ b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx @@ -30,13 +30,13 @@ import { } from '../../../redux/reducers/ad'; import { Formik, FormikHelpers } from 'formik'; import { get, isEmpty } from 'lodash'; -import React, { Fragment, useEffect, useState } from 'react'; -import { RouteComponentProps } from 'react-router'; +import React, { Fragment, ReactElement, useEffect, useState } from 'react'; +import { RouteComponentProps, useLocation } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../redux/reducers'; -import { BREADCRUMBS, MAX_DETECTORS } from '../../../utils/constants'; +import { BREADCRUMBS, MAX_DETECTORS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/interfaces'; import { DetectorDefinitionFields } from '../components/DetectorDefinitionFields'; @@ -53,15 +53,31 @@ import { ValidationSettingResponse, VALIDATION_ISSUE_TYPES, } from '../../../models/interfaces'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourceEnabled, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface ReviewAndCreateProps extends RouteComponentProps { setStep(stepNumber: number): void; values: CreateDetectorFormikValues; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function ReviewAndCreate(props: ReviewAndCreateProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const dataSourceEnabled = getDataSourceEnabled().enabled; useHideSideNavBar(true, false); // This variable indicates if validate API declared detector settings as valid for AD creation @@ -90,7 +106,9 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { // meaning validation has passed and succesful callout will display or validation has failed // and callouts displaying what the issue is will be displayed instead. useEffect(() => { - dispatch(validateDetector(formikToDetector(props.values), 'model')) + dispatch( + validateDetector(formikToDetector(props.values), 'model', dataSourceId) + ) .then((resp: any) => { if (isEmpty(Object.keys(resp.response))) { setValidDetectorSettings(true); @@ -159,11 +177,19 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { }, []); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + MDS_BREADCRUMBS.CREATE_DETECTOR, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } }, []); const handleSubmit = async ( @@ -174,14 +200,14 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { setIsCreatingDetector(true); formikHelpers.setSubmitting(true); const detectorToCreate = formikToDetector(values); - dispatch(createDetector(detectorToCreate)) + dispatch(createDetector(detectorToCreate, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector created: ${detectorToCreate.name}` ); // Optionally start RT job if (get(props, 'values.realTime', true)) { - dispatch(startDetector(response.response.id)) + dispatch(startDetector(response.response.id, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Successfully started the real-time detector` @@ -209,7 +235,12 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { ); dispatch( //@ts-ignore - startHistoricalDetector(response.response.id, startTime, endTime) + startHistoricalDetector( + response.response.id, + dataSourceId, + startTime, + endTime + ) ) .then((response: any) => { core.notifications.toasts.addSuccess( @@ -229,11 +260,15 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { } props.history.push( - `/detectors/${response.response.id}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${response.response.id}/configurations/`, + dataSourceId, + false + ) ); }) .catch((err: any) => { - dispatch(getDetectorCount()).then((response: any) => { + dispatch(getDetectorCount(dataSourceId)).then((response: any) => { const totalDetectors = get(response, 'response.count', 0); if (totalDetectors === MAX_DETECTORS) { core.notifications.toasts.addDanger( @@ -262,6 +297,24 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { // Converting to detector for passing to the fields const detectorToCreate = formikToDetector(props.values); + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); }} > Cancel diff --git a/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx b/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx index f3209fff..35cac0f0 100644 --- a/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx +++ b/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx @@ -27,6 +27,14 @@ import { INITIAL_DETECTOR_JOB_VALUES } from '../../../DetectorJobs/utils/constan import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../../ConfigureModel/utils/constants'; import { INITIAL_DETECTOR_DEFINITION_VALUES } from '../../../DefineDetector/utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( diff --git a/public/pages/main/Main.tsx b/public/pages/main/Main.tsx index 8f1ef972..728bdcc6 100644 --- a/public/pages/main/Main.tsx +++ b/public/pages/main/Main.tsx @@ -23,8 +23,10 @@ import { DefineDetector } from '../DefineDetector/containers/DefineDetector'; import { ConfigureModel } from '../ConfigureModel/containers/ConfigureModel'; import { DashboardOverview } from '../Dashboard/Container/DashboardOverview'; import { CoreServicesConsumer } from '../../components/CoreServices/CoreServices'; -import { CoreStart } from '../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../src/core/public'; import { AnomalyDetectionOverview } from '../Overview'; +import { getURLQueryParams } from '../DetectorsList/utils/helpers'; +import { constructHrefWithDataSourceId } from '../utils/helpers'; enum Navigation { AnomalyDetection = 'Anomaly detection', @@ -32,33 +34,50 @@ enum Navigation { Detectors = 'Detectors', } -interface MainProps extends RouteComponentProps {} +interface MainProps extends RouteComponentProps { + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; +} export function Main(props: MainProps) { + const { setHeaderActionMenu } = props; + const hideSideNavBar = useSelector( (state: AppState) => state.adApp.hideSideNavBar ); const adState = useSelector((state: AppState) => state.ad); const totalDetectors = adState.totalDetectors; - const errorGettingDetectors = adState.errorMessage; - const isLoadingDetectors = adState.requesting; + const queryParams = getURLQueryParams(props.location); + const dataSourceId = queryParams.dataSourceId === undefined ? undefined : queryParams.dataSourceId; + const sideNav = [ { name: Navigation.AnomalyDetection, id: 0, - href: `#${APP_PATH.OVERVIEW}`, + href: constructHrefWithDataSourceId( + APP_PATH.OVERVIEW, + dataSourceId, + true + ), items: [ { name: Navigation.Dashboard, id: 1, - href: `#${APP_PATH.DASHBOARD}`, + href: constructHrefWithDataSourceId( + APP_PATH.DASHBOARD, + dataSourceId, + true + ), isSelected: props.location.pathname === APP_PATH.DASHBOARD, }, { name: Navigation.Detectors, id: 2, - href: `#${APP_PATH.LIST_DETECTORS}`, + href: constructHrefWithDataSourceId( + APP_PATH.LIST_DETECTORS, + dataSourceId, + true + ), isSelected: props.location.pathname === APP_PATH.LIST_DETECTORS, }, ], @@ -77,67 +96,89 @@ export function Main(props: MainProps) { } + render={(props: RouteComponentProps) => ( + + )} /> ) => ( - + )} /> ( - + )} /> ( - + )} /> ( - + )} /> ( - - )} - /> - ( - + )} /> ( + render={(props: RouteComponentProps) => ( )} /> - - {totalDetectors > 0 ? ( - // - - ) : ( - - )} - + + totalDetectors > 0 ? ( + + ) : ( + + ) + } + /> diff --git a/public/pages/utils/anomalyResultUtils.ts b/public/pages/utils/anomalyResultUtils.ts index acb0b955..cc3408b6 100644 --- a/public/pages/utils/anomalyResultUtils.ts +++ b/public/pages/utils/anomalyResultUtils.ts @@ -96,6 +96,7 @@ export const getQueryParamsForLiveAnomalyResults = ( export const getLiveAnomalyResults = ( dispatch: Dispatch, detectorId: string, + dataSourceId: string, detectionInterval: number, intervals: number, resultIndex: string, @@ -108,6 +109,7 @@ export const getLiveAnomalyResults = ( dispatch( getDetectorLiveResults( detectorId, + dataSourceId, queryParams, false, resultIndex, diff --git a/public/pages/utils/constants.ts b/public/pages/utils/constants.ts index e6f9fc0c..ad03e153 100644 --- a/public/pages/utils/constants.ts +++ b/public/pages/utils/constants.ts @@ -53,6 +53,7 @@ export const DEFAULT_QUERY_PARAMS = { size: 20, sortDirection: SORT_DIRECTION.ASC, sortField: 'name', + dataSourceId: '', }; export const GET_ALL_DETECTORS_QUERY_PARAMS = { diff --git a/public/pages/utils/helpers.ts b/public/pages/utils/helpers.ts index 6bfb3bd6..7a581c97 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -8,14 +8,19 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ - -import { CatIndex, IndexAlias } from '../../../server/models/types'; +import queryString from 'query-string'; +import { + CatIndex, + IndexAlias, + MDSQueryParams, +} from '../../../server/models/types'; import sortBy from 'lodash/sortBy'; import { DetectorListItem } from '../../models/interfaces'; -import { SORT_DIRECTION } from '../../../server/utils/constants'; -import { ALL_INDICES, ALL_DETECTOR_STATES } from './constants'; +import { DETECTORS_QUERY_PARAMS, SORT_DIRECTION } from '../../../server/utils/constants'; +import { ALL_INDICES, ALL_DETECTOR_STATES, MAX_DETECTORS, DEFAULT_QUERY_PARAMS } from './constants'; import { DETECTOR_STATE } from '../../../server/utils/constants'; import { timeFormatter } from '@elastic/charts'; +import { getDataSourceEnabled, getDataSourcePlugin } from '../../services'; export function sanitizeSearchText(searchValue: string): string { if (!searchValue || searchValue == '*') { @@ -112,3 +117,70 @@ export const formatNumber = (data: any) => { return ''; } }; + +export const getAllDetectorsQueryParamsWithDataSourceId = ( + dataSourceId: string = '' +) => ({ + from: 0, + search: '', + indices: '', + size: MAX_DETECTORS, + sortDirection: SORT_DIRECTION.ASC, + sortField: 'name', + dataSourceId, +}); + +export const getSampleDetectorsQueryParamsWithDataSouceId = ( + dataSourceId: string = '' +) => ({ + from: 0, + search: 'sample', + indices: '', + size: MAX_DETECTORS, + sortDirection: SORT_DIRECTION.ASC, + sortField: 'name', + dataSourceId, +}); + +export const getDataSourceFromURL = (location: { + search: string; +}): MDSQueryParams => { + const queryParams = queryString.parse(location.search); + const dataSourceId = queryParams.dataSourceId; + return { dataSourceId: typeof dataSourceId === 'string' ? dataSourceId : '' }; +}; + +export const constructHrefWithDataSourceId = ( + basePath: string, + dataSourceId: string = '', + withHash: Boolean +): string => { + const dataSourceEnabled = getDataSourceEnabled().enabled; + const url = new URLSearchParams(); + + // Set up base parameters for '/detectors' + if (basePath.startsWith('/detectors')) { + url.set(DETECTORS_QUERY_PARAMS.FROM, DEFAULT_QUERY_PARAMS.from.toString()); + url.set(DETECTORS_QUERY_PARAMS.SIZE, DEFAULT_QUERY_PARAMS.size.toString()); + url.set(DETECTORS_QUERY_PARAMS.SEARCH, DEFAULT_QUERY_PARAMS.search); + url.set(DETECTORS_QUERY_PARAMS.INDICES, DEFAULT_QUERY_PARAMS.indices); + url.set(DETECTORS_QUERY_PARAMS.SORT_FIELD, DEFAULT_QUERY_PARAMS.sortField); + url.set(DETECTORS_QUERY_PARAMS.SORT_DIRECTION, SORT_DIRECTION.ASC) + if (dataSourceEnabled) { + url.set(DETECTORS_QUERY_PARAMS.DATASOURCEID, ''); + } + } + + if (dataSourceEnabled && dataSourceId !== undefined) { + url.set('dataSourceId', dataSourceId); + } + + // we share this helper function to construct the href with dataSourceId + // some places we need to return the url with hash, some places we don't need to + // so adding this flag to indicate if we want to return the url with hash + if (withHash) { + return `#${basePath}?${url.toString()}`; + } + + return `${basePath}?${url.toString()}`; +}; diff --git a/public/plugin.ts b/public/plugin.ts index 25ea0ebf..ed771c26 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -35,6 +35,9 @@ import { setUiActions, setUISettings, setQueryService, + setSavedObjectsClient, + setDataSourceManagementPlugin, + setDataSourceEnabled, } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; import { @@ -43,6 +46,8 @@ import { } from '../../../src/plugins/vis_augmenter/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/public'; declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -55,7 +60,8 @@ export interface AnomalyDetectionSetupDeps { embeddable: EmbeddableSetup; notifications: NotificationsSetup; visAugmenter: VisAugmenterSetup; - //uiActions: UiActionsSetup; + dataSourceManagement: DataSourceManagementPluginSetup; + dataSource: DataSourcePluginSetup; } export interface AnomalyDetectionStartDeps { @@ -92,6 +98,12 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin // direct server-side calls setClient(core.http); + setDataSourceManagementPlugin(plugins.dataSourceManagement); + + const enabled = !!plugins.dataSource; + + setDataSourceEnabled({ enabled }); + // Create context menu actions const actions = getActions(); @@ -116,6 +128,7 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin setNotifications(core.notifications); setUiActions(uiActions); setQueryService(data.query); + setSavedObjectsClient(core.savedObjects.client); return {}; } } diff --git a/public/redux/reducers/__tests__/anomalyResults.test.ts b/public/redux/reducers/__tests__/anomalyResults.test.ts index bc873208..e668e2d1 100644 --- a/public/redux/reducers/__tests__/anomalyResults.test.ts +++ b/public/redux/reducers/__tests__/anomalyResults.test.ts @@ -20,6 +20,14 @@ import reducer, { initialDetectorsState, } from '../anomalyResults'; +jest.mock('../../../services', () => ({ + ...jest.requireActual('../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe('anomaly results reducer actions', () => { let store: MockStore; beforeEach(() => { @@ -45,6 +53,7 @@ describe('anomaly results reducer actions', () => { await store.dispatch( getDetectorResults( tempDetectorId, + '', queryParams, false, resultIndex, @@ -87,7 +96,7 @@ describe('anomaly results reducer actions', () => { }; try { await store.dispatch( - getDetectorResults(tempDetectorId, queryParams, false, '', false) + getDetectorResults(tempDetectorId, '', queryParams, false, '', false) ); } catch (e) { const actions = store.getActions(); diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index d4d12ae6..3fa06ad3 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -369,39 +369,69 @@ const reducer = handleActions( initialDetectorsState ); -export const createDetector = (requestBody: Detector): APIAction => ({ - type: CREATE_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}`, { - body: JSON.stringify(requestBody), - }), -}); +export const createDetector = ( + requestBody: Detector, + dataSourceId: string = '' +): APIAction => { + const url = dataSourceId + ? `..${AD_NODE_API.DETECTOR}/${dataSourceId}` + : `..${AD_NODE_API.DETECTOR}`; + + return { + type: CREATE_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify(requestBody), + }), + }; +}; export const validateDetector = ( requestBody: Detector, - validationType: string -): APIAction => ({ - type: VALIDATE_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/_validate/${validationType}`, { - body: JSON.stringify(requestBody), - }), -}); + validationType: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/_validate/${validationType}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const getDetector = (detectorId: string): APIAction => ({ - type: GET_DETECTOR, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/${detectorId}`), - detectorId, -}); + return { + type: VALIDATE_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify(requestBody), + }), + }; +} + +export const getDetector = ( + detectorId: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: GET_DETECTOR, + request: (client: HttpSetup) => client.get(url), + detectorId, + }; +}; export const getDetectorList = ( queryParams: GetDetectorsQueryParams -): APIAction => ({ - type: GET_DETECTOR_LIST, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}`, { query: queryParams }), -}); +): APIAction => { + const dataSourceId = queryParams.dataSourceId || ''; + + const baseUrl = `..${AD_NODE_API.DETECTOR}/_list`; + const url = dataSourceId + ? `${baseUrl}/${dataSourceId}` + : baseUrl; + + return { + type: GET_DETECTOR_LIST, + request: (client: HttpSetup) => client.get(url, { query: queryParams }), + }; +}; export const searchDetector = (requestBody: any): APIAction => ({ type: SEARCH_DETECTOR, @@ -413,61 +443,103 @@ export const searchDetector = (requestBody: any): APIAction => ({ export const updateDetector = ( detectorId: string, - requestBody: Detector -): APIAction => ({ - type: UPDATE_DETECTOR, - request: (client: HttpSetup) => - client.put(`..${AD_NODE_API.DETECTOR}/${detectorId}`, { - body: JSON.stringify(requestBody), - }), - detectorId, -}); + requestBody: Detector, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const deleteDetector = (detectorId: string): APIAction => ({ - type: DELETE_DETECTOR, - request: (client: HttpSetup) => - client.delete(`..${AD_NODE_API.DETECTOR}/${detectorId}`), - detectorId, -}); + return { + type: UPDATE_DETECTOR, + request: (client: HttpSetup) => + client.put(url, { + body: JSON.stringify(requestBody), + }), + detectorId, + }; +} -export const startDetector = (detectorId: string): APIAction => ({ - type: START_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/start`), - detectorId, -}); +export const deleteDetector = ( + detectorId: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: DELETE_DETECTOR, + request: (client: HttpSetup) => client.delete(url), + detectorId, + }; +}; + +export const startDetector = ( + detectorId: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/start`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: START_DETECTOR, + request: (client: HttpSetup) => client.post(url), + detectorId, + }; +}; export const startHistoricalDetector = ( detectorId: string, + dataSourceId: string = '', startTime: number, endTime: number -): APIAction => ({ - type: START_HISTORICAL_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/start`, { - body: JSON.stringify({ - startTime: startTime, - endTime: endTime, - }), - }), - detectorId, - startTime, - endTime, -}); +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId + ? `${baseUrl}/${dataSourceId}/start` + : `${baseUrl}/start`; -export const stopDetector = (detectorId: string): APIAction => ({ - type: STOP_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${false}`), - detectorId, -}); + return { + type: START_HISTORICAL_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify({ + startTime: startTime, + endTime: endTime, + }), + }), + detectorId, + startTime, + endTime, + }; +}; -export const stopHistoricalDetector = (detectorId: string): APIAction => ({ - type: STOP_HISTORICAL_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`), - detectorId, -}); +export const stopDetector = ( + detectorId: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${false}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: STOP_DETECTOR, + request: (client: HttpSetup) => client.post(url), + detectorId, + }; +}; + +export const stopHistoricalDetector = ( + detectorId: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: STOP_HISTORICAL_DETECTOR, + request: (client: HttpSetup) => client.post(url), + detectorId, + }; +}; export const getDetectorProfile = (detectorId: string): APIAction => ({ type: GET_DETECTOR_PROFILE, @@ -476,16 +548,28 @@ export const getDetectorProfile = (detectorId: string): APIAction => ({ detectorId, }); -export const matchDetector = (detectorName: string): APIAction => ({ - type: MATCH_DETECTOR, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/${detectorName}/_match`), -}); +export const matchDetector = ( + detectorName: string, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorName}/_match`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const getDetectorCount = (): APIAction => ({ - type: GET_DETECTOR_COUNT, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/_count`, {}), -}); + return { + type: MATCH_DETECTOR, + request: (client: HttpSetup) => client.get(url), + }; +}; + +export const getDetectorCount = (dataSourceId: string = ''): APIAction => { + const url = dataSourceId ? + `..${AD_NODE_API.DETECTOR}/_count/${dataSourceId}` : + `..${AD_NODE_API.DETECTOR}/_count`; + + return { + type: GET_DETECTOR_COUNT, + request: (client: HttpSetup) => client.get(url), + }; +}; export default reducer; diff --git a/public/redux/reducers/alerting.ts b/public/redux/reducers/alerting.ts index 04e8895c..788e6f83 100644 --- a/public/redux/reducers/alerting.ts +++ b/public/redux/reducers/alerting.ts @@ -94,25 +94,36 @@ const reducer = handleActions( initialDetectorsState ); -export const searchMonitors = (): APIAction => ({ - type: SEARCH_MONITORS, - request: (client: HttpSetup) => client.post(`..${ALERTING_NODE_API._SEARCH}`), -}); +export const searchMonitors = (dataSourceId: string = ''): APIAction => { + const baseUrl = `..${ALERTING_NODE_API._SEARCH}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: SEARCH_MONITORS, + request: (client: HttpSetup) => client.post(url), + }; +}; export const searchAlerts = ( monitorId: string, startTime: number, - endTime: number -): APIAction => ({ - type: SEARCH_ALERTS, - request: (client: HttpSetup) => - client.get(`..${ALERTING_NODE_API.ALERTS}`, { - query: { - monitorId: monitorId, - startTime: startTime, - endTime: endTime, - }, - }), -}); + endTime: number, + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${ALERTING_NODE_API.ALERTS}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: SEARCH_ALERTS, + request: (client: HttpSetup) => + client.get(url, { + query: { + monitorId: monitorId, + startTime: startTime, + endTime: endTime, + }, + }), + }; +} export default reducer; diff --git a/public/redux/reducers/anomalyResults.ts b/public/redux/reducers/anomalyResults.ts index 4c5d5f50..bf643a3c 100644 --- a/public/redux/reducers/anomalyResults.ts +++ b/public/redux/reducers/anomalyResults.ts @@ -94,70 +94,65 @@ const reducer = handleActions( export const getDetectorResults = ( id: string, + dataSourceId: string = '', queryParams: any, isHistorical: boolean, resultIndex: string, onlyQueryCustomResultIndex: boolean -): APIAction => - !resultIndex - ? { - type: DETECTOR_RESULTS, - request: (client: HttpSetup) => - client.get( - `..${AD_NODE_API.DETECTOR}/${id}/results/${isHistorical}`, - { - query: queryParams, - } - ), - } - : { - type: DETECTOR_RESULTS, - request: (client: HttpSetup) => - client.get( - `..${AD_NODE_API.DETECTOR}/${id}/results/${isHistorical}/${resultIndex}/${onlyQueryCustomResultIndex}`, - { - query: queryParams, - } - ), - }; +): APIAction => { + let url = `..${AD_NODE_API.DETECTOR}/${id}/results/${isHistorical}`; + + if (resultIndex) { + url += `/${resultIndex}/${onlyQueryCustomResultIndex}`; + } + + if (dataSourceId) { + url += `/${dataSourceId}`; + } + + return { + type: DETECTOR_RESULTS, + request: (client: HttpSetup) => client.get(url, { query: queryParams }), + }; +}; export const searchResults = ( requestBody: any, resultIndex: string, + dataSourceId: string = '', onlyQueryCustomResultIndex: boolean -): APIAction => - !resultIndex - ? { - type: SEARCH_ANOMALY_RESULTS, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/results/_search`, { - body: JSON.stringify(requestBody), - }), - } - : { - type: SEARCH_ANOMALY_RESULTS, - request: (client: HttpSetup) => - client.post( - `..${AD_NODE_API.DETECTOR}/results/_search/${resultIndex}/${onlyQueryCustomResultIndex}`, - { - body: JSON.stringify(requestBody), - } - ), - }; +): APIAction => { + let baseUrl = `..${AD_NODE_API.DETECTOR}/results/_search`; + + if (resultIndex) { + baseUrl += `/${resultIndex}/${onlyQueryCustomResultIndex}`; + } + + if (dataSourceId) { + baseUrl += `/${dataSourceId}`; + } + + return { + type: SEARCH_ANOMALY_RESULTS, + request: (client: HttpSetup) => + client.post(baseUrl, { body: JSON.stringify(requestBody) }), + }; +}; export const getTopAnomalyResults = ( detectorId: string, + dataSourceId: string = '', isHistorical: boolean, requestBody: any -): APIAction => ({ - type: GET_TOP_ANOMALY_RESULTS, - request: (client: HttpSetup) => - client.post( - `..${AD_NODE_API.DETECTOR}/${detectorId}/_topAnomalies/${isHistorical}`, - { - body: JSON.stringify(requestBody), - } - ), -}); +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/_topAnomalies/${isHistorical}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: GET_TOP_ANOMALY_RESULTS, + request: (client: HttpSetup) => + client.post(url, { body: JSON.stringify(requestBody) }), + }; +}; export default reducer; diff --git a/public/redux/reducers/liveAnomalyResults.ts b/public/redux/reducers/liveAnomalyResults.ts index a41f7f2d..aa06efef 100644 --- a/public/redux/reducers/liveAnomalyResults.ts +++ b/public/redux/reducers/liveAnomalyResults.ts @@ -57,31 +57,26 @@ const reducer = handleActions( export const getDetectorLiveResults = ( detectorId: string, + dataSourceId: string = '', queryParams: DetectorResultsQueryParams, isHistorical: boolean, resultIndex: string, onlyQueryCustomResultIndex: boolean -): APIAction => - !resultIndex - ? { - type: DETECTOR_LIVE_RESULTS, - request: (client: HttpSetup) => - client.get( - `..${AD_NODE_API.DETECTOR}/${detectorId}/results/${isHistorical}`, - { - query: queryParams, - } - ), - } - : { - type: DETECTOR_LIVE_RESULTS, - request: (client: HttpSetup) => - client.get( - `..${AD_NODE_API.DETECTOR}/${detectorId}/results/${isHistorical}/${resultIndex}/${onlyQueryCustomResultIndex}`, - { - query: queryParams, - } - ), - }; +): APIAction => { + let url = `..${AD_NODE_API.DETECTOR}/${detectorId}/results/${isHistorical}`; + + if (resultIndex) { + url += `/${resultIndex}/${onlyQueryCustomResultIndex}`; + } + + if (dataSourceId) { + url += `/${dataSourceId}`; + } + + return { + type: DETECTOR_LIVE_RESULTS, + request: (client: HttpSetup) => client.get(url, { query: queryParams }), + }; +}; export default reducer; diff --git a/public/redux/reducers/opensearch.ts b/public/redux/reducers/opensearch.ts index 17a807b4..fd153bca 100644 --- a/public/redux/reducers/opensearch.ts +++ b/public/redux/reducers/opensearch.ts @@ -21,6 +21,7 @@ import { getPathsPerDataType } from './mapper'; import { CatIndex, IndexAlias } from '../../../server/models/types'; import { AD_NODE_API } from '../../../utils/constants'; import { get } from 'lodash'; +import { data } from 'jquery'; const GET_INDICES = 'opensearch/GET_INDICES'; const GET_ALIASES = 'opensearch/GET_ALIASES'; @@ -246,25 +247,42 @@ const reducer = handleActions( initialState ); -export const getIndices = (searchKey: string = ''): APIAction => ({ - type: GET_INDICES, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API._INDICES}`, { query: { index: searchKey } }), -}); +export const getIndices = (searchKey = '', dataSourceId: string = '') => { + const baseUrl = `..${AD_NODE_API._INDICES}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const getAliases = (searchKey: string = ''): APIAction => ({ - type: GET_ALIASES, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API._ALIASES}`, { query: { alias: searchKey } }), -}); + return { + type: GET_INDICES, + request: (client: HttpSetup) => + client.get(url, { query: { index: searchKey } }), + }; +}; -export const getMappings = (searchKey: string = ''): APIAction => ({ - type: GET_MAPPINGS, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API._MAPPINGS}`, { - query: { index: searchKey }, - }), -}); +export const getAliases = ( + searchKey: string = '', + dataSourceId: string = '' +): APIAction => { + const baseUrl = `..${AD_NODE_API._ALIASES}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: GET_ALIASES, + request: (client: HttpSetup) => + client.get(url, { query: { alias: searchKey } }), + }; +}; + +export const getMappings = (searchKey: string = '', dataSourceId: string = ''): APIAction => { + const url = dataSourceId ? `${AD_NODE_API._MAPPINGS}/${dataSourceId}` : AD_NODE_API._MAPPINGS; + + return { + type: GET_MAPPINGS, + request: (client: HttpSetup) => + client.get(`..${url}`, { + query: { index: searchKey }, + }), + }; +}; export const searchOpenSearch = (requestData: any): APIAction => ({ type: SEARCH_OPENSEARCH, @@ -274,19 +292,29 @@ export const searchOpenSearch = (requestData: any): APIAction => ({ }), }); -export const createIndex = (indexConfig: any): APIAction => ({ - type: CREATE_INDEX, - request: (client: HttpSetup) => - client.put(`..${AD_NODE_API.CREATE_INDEX}`, { - body: JSON.stringify(indexConfig), - }), -}); +export const createIndex = (indexConfig: any, dataSourceId: string = ''): APIAction => { + const url = dataSourceId + ? `${AD_NODE_API.CREATE_INDEX}/${dataSourceId}` + : AD_NODE_API.CREATE_INDEX; + return { + type: CREATE_INDEX, + request: (client: HttpSetup) => + client.put(`..${url}`, { + body: JSON.stringify(indexConfig), + }), + }; +}; -export const bulk = (body: any): APIAction => ({ - type: BULK, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.BULK}`, { body: JSON.stringify(body) }), -}); +export const bulk = (body: any, dataSourceId: string = ''): APIAction => { + const url = dataSourceId + ? `${AD_NODE_API.BULK}/${dataSourceId}` + : AD_NODE_API.BULK; + return { + type: BULK, + request: (client: HttpSetup) => + client.post(`..${url}`, { body: JSON.stringify(body) }), + }; +}; export const deleteIndex = (index: string): APIAction => ({ type: DELETE_INDEX, @@ -295,11 +323,11 @@ export const deleteIndex = (index: string): APIAction => ({ }); export const getPrioritizedIndices = - (searchKey: string): ThunkAction => + (searchKey: string, dataSourceId: string = ''): ThunkAction => async (dispatch, getState) => { //Fetch Indices and Aliases with text provided - await dispatch(getIndices(searchKey)); - await dispatch(getAliases(searchKey)); + await dispatch(getIndices(searchKey, dataSourceId)); + await dispatch(getAliases(searchKey, dataSourceId)); const osState = getState().opensearch; const exactMatchedIndices = osState.indices; const exactMatchedAliases = osState.aliases; @@ -311,8 +339,8 @@ export const getPrioritizedIndices = }; } else { //No results found for exact match, append wildCard and get partial matches if exists - await dispatch(getIndices(`${searchKey}*`)); - await dispatch(getAliases(`${searchKey}*`)); + await dispatch(getIndices(`${searchKey}*`, dataSourceId)); + await dispatch(getAliases(`${searchKey}*`, dataSourceId)); const osState = getState().opensearch; const partialMatchedIndices = osState.indices; const partialMatchedAliases = osState.aliases; diff --git a/public/redux/reducers/previewAnomalies.ts b/public/redux/reducers/previewAnomalies.ts index 9ee231f7..01e36f29 100644 --- a/public/redux/reducers/previewAnomalies.ts +++ b/public/redux/reducers/previewAnomalies.ts @@ -59,12 +59,17 @@ const reducer = handleActions( initialDetectorsState ); -export const previewDetector = (requestBody: any): APIAction => ({ - type: PREVIEW_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/preview`, { - body: JSON.stringify(requestBody), - }), -}); +export const previewDetector = (requestBody: any, dataSourceId: string = ''): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/preview`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: PREVIEW_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify(requestBody), + }), + }; +} export default reducer; diff --git a/public/redux/reducers/sampleData.ts b/public/redux/reducers/sampleData.ts index 549516a6..576c515e 100644 --- a/public/redux/reducers/sampleData.ts +++ b/public/redux/reducers/sampleData.ts @@ -54,10 +54,18 @@ const reducer = handleActions( initialState ); -export const createSampleData = (sampleDataType: SAMPLE_TYPE): APIAction => ({ - type: CREATE_SAMPLE_DATA, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.CREATE_SAMPLE_DATA}/${sampleDataType}`), -}); +export const createSampleData = ( + sampleDataType: SAMPLE_TYPE, + dataSourceId: string = '' +): APIAction => { + const url = dataSourceId + ? `..${AD_NODE_API.CREATE_SAMPLE_DATA}/${sampleDataType}/${dataSourceId}` + : `..${AD_NODE_API.CREATE_SAMPLE_DATA}/${sampleDataType}`; + + return { + type: CREATE_SAMPLE_DATA, + request: (client: HttpSetup) => client.post(url), + }; +}; export default reducer; diff --git a/public/services.ts b/public/services.ts index ef899307..dbe060e6 100644 --- a/public/services.ts +++ b/public/services.ts @@ -10,10 +10,16 @@ import { OverlayStart, } from '../../../src/core/public'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/public'; + +export interface DataSourceEnabled { + enabled: boolean; +} export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = createGetterSetter('savedFeatureAnywhereLoader'); @@ -39,6 +45,15 @@ export const [getUISettings, setUISettings] = export const [getQueryService, setQueryService] = createGetterSetter('Query'); +export const [getSavedObjectsClient, setSavedObjectsClient] = + createGetterSetter('SavedObjectsClient'); + +export const [getDataSourceManagementPlugin, setDataSourceManagementPlugin] = + createGetterSetter('DataSourceManagement'); + +export const [getDataSourceEnabled, setDataSourceEnabled] = + createGetterSetter('DataSourceEnabled'); + // This is primarily used for mocking this module and each of its fns in tests. export default { getSavedFeatureAnywhereLoader, @@ -49,4 +64,5 @@ export default { getOverlays, setUISettings, setQueryService, + getSavedObjectsClient, }; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 6f244704..fa3022d6 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -26,6 +26,15 @@ export const BREADCRUMBS = Object.freeze({ EDIT_MODEL_CONFIGURATION: { text: 'Edit model configuration' }, }); +export const MDS_BREADCRUMBS = Object.freeze({ + ANOMALY_DETECTOR: (dataSourceId?: string) => ({ text: 'Anomaly detection', href: `#/?dataSourceId=${dataSourceId}` }), + DETECTORS: (dataSourceId?: string) => ({ text: 'Detectors', href: `#/detectors?dataSourceId=${dataSourceId}` }), + CREATE_DETECTOR: { text: 'Create detector' }, + EDIT_DETECTOR: { text: 'Edit detector' }, + DASHBOARD: (dataSourceId?: string) => ({ text: 'Dashboard', href: `#/dashboard?dataSourceId=${dataSourceId}` }), + EDIT_MODEL_CONFIGURATION: { text: 'Edit model configuration' }, +}); + export const APP_PATH = { DASHBOARD: '/dashboard', LIST_DETECTORS: '/detectors', @@ -33,7 +42,6 @@ export const APP_PATH = { EDIT_DETECTOR: '/detectors/:detectorId/edit', EDIT_FEATURES: '/detectors/:detectorId/features/', DETECTOR_DETAIL: '/detectors/:detectorId/', - CREATE_DETECTOR_STEPS: '/create-detector-steps', OVERVIEW: '/overview', }; @@ -98,3 +106,5 @@ export enum MISSING_FEATURE_DATA_SEVERITY { export const SPACE_STR = ' '; export const ANOMALY_DETECTION_ICON = 'anomalyDetection'; + +export const DATA_SOURCE_ID = 'dataSourceId'; diff --git a/server/models/types.ts b/server/models/types.ts index 24fa7bb3..c1d679b0 100644 --- a/server/models/types.ts +++ b/server/models/types.ts @@ -89,6 +89,7 @@ export type GetDetectorsQueryParams = { indices?: string; sortDirection: SORT_DIRECTION; sortField: string; + dataSourceId?: string; }; export type GetAdMonitorsQueryParams = { @@ -108,6 +109,10 @@ export type DetectorResultsQueryParams = { dateRangeFilter?: DateRangeFilter; }; +export type MDSQueryParams = { + dataSourceId: string; +}; + export type Entity = { name: string; value: string; diff --git a/server/plugin.ts b/server/plugin.ts index 88a573cb..a6dfc3b0 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -35,6 +35,13 @@ import SampleDataService, { registerSampleDataRoutes, } from './routes/sampleData'; import { DEFAULT_HEADERS } from './utils/constants'; +import { DataSourcePluginSetup } from '../../../src/plugins/data_source/server/types'; +import { DataSourceManagementPlugin } from '../../../src/plugins/data_source_management/public'; + +export interface ADPluginSetupDependencies { + dataSourceManagement?: ReturnType; + dataSource?: DataSourcePluginSetup; +} export class AnomalyDetectionOpenSearchDashboardsPlugin implements @@ -50,7 +57,10 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin this.logger = initializerContext.logger.get(); this.globalConfig$ = initializerContext.config.legacy.globalConfig$; } - public async setup(core: CoreSetup) { + public async setup( + core: CoreSetup, + { dataSource }: ADPluginSetupDependencies + ) { // Get any custom/overridden headers const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const { customHeaders, ...rest } = globalConfig.opensearch; @@ -65,6 +75,13 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin } ); + const dataSourceEnabled = !!dataSource; + + if (dataSourceEnabled) { + dataSource.registerCustomApiSchema(adPlugin); + dataSource.registerCustomApiSchema(alertingPlugin); + } + // Create router const apiRouter: Router = createRouter( core.http.createRouter(), @@ -72,10 +89,10 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin ); // Create services & register with OpenSearch client - const adService = new AdService(client); - const alertingService = new AlertingService(client); - const opensearchService = new OpenSearchService(client); - const sampleDataService = new SampleDataService(client); + const adService = new AdService(client, dataSourceEnabled); + const alertingService = new AlertingService(client, dataSourceEnabled); + const opensearchService = new OpenSearchService(client, dataSourceEnabled); + const sampleDataService = new SampleDataService(client, dataSourceEnabled); // Register server routes with the service registerADRoutes(apiRouter, adService); diff --git a/server/routes/ad.ts b/server/routes/ad.ts index 4c9eb54e..ed42e7e9 100644 --- a/server/routes/ad.ts +++ b/server/routes/ad.ts @@ -24,6 +24,7 @@ import { CUSTOM_AD_RESULT_INDEX_PREFIX, } from '../utils/constants'; import { + getClientBasedOnDataSource, mapKeysDeep, toCamel, toFixedNumberForAnomaly, @@ -59,53 +60,132 @@ type PutDetectorParams = { }; export function registerADRoutes(apiRouter: Router, adService: AdService) { + // create detector apiRouter.post('/detectors', adService.putDetector); + apiRouter.post('/detectors/{dataSourceId}', adService.putDetector); + + // put detector apiRouter.put('/detectors/{detectorId}', adService.putDetector); + apiRouter.put( + '/detectors/{detectorId}/{dataSourceId}', + adService.putDetector + ); + apiRouter.post('/detectors/_search', adService.searchDetector); - apiRouter.post('/detectors/results/_search/', adService.searchResults); + + // post search anomaly results apiRouter.post('/detectors/results/_search', adService.searchResults); + apiRouter.post( + '/detectors/results/_search/{dataSourceId}', + adService.searchResults + ); apiRouter.post( '/detectors/results/_search/{resultIndex}/{onlyQueryCustomResultIndex}', adService.searchResults ); - apiRouter.get('/detectors/{detectorId}', adService.getDetector); - apiRouter.get('/detectors', adService.getDetectors); + apiRouter.post( + '/detectors/results/_search/{resultIndex}/{onlyQueryCustomResultIndex}/{dataSourceId}', + adService.searchResults + ); + + // list detectors + apiRouter.get('/detectors/_list', adService.getDetectors); + apiRouter.get('/detectors/_list/{dataSourceId}', adService.getDetectors); + + // preview detector apiRouter.post('/detectors/preview', adService.previewDetector); + apiRouter.post('/detectors/preview/{dataSourceId}', adService.previewDetector); + + // get detector anomaly results apiRouter.get( '/detectors/{id}/results/{isHistorical}/{resultIndex}/{onlyQueryCustomResultIndex}', adService.getAnomalyResults ); + apiRouter.get( + '/detectors/{id}/results/{isHistorical}/{resultIndex}/{onlyQueryCustomResultIndex}/{dataSourceId}', + adService.getAnomalyResults + ); apiRouter.get( '/detectors/{id}/results/{isHistorical}', adService.getAnomalyResults ); + apiRouter.get( + '/detectors/{id}/results/{isHistorical}/{dataSourceId}', + adService.getAnomalyResults + ); + + // delete detector apiRouter.delete('/detectors/{detectorId}', adService.deleteDetector); + apiRouter.delete( + '/detectors/{detectorId}/{dataSourceId}', + adService.deleteDetector + ); + + // start detector apiRouter.post('/detectors/{detectorId}/start', adService.startDetector); + apiRouter.post( + '/detectors/{detectorId}/start/{dataSourceId}', + adService.startDetector + ); + + // stop detector apiRouter.post( '/detectors/{detectorId}/stop/{isHistorical}', adService.stopDetector ); + apiRouter.post( + '/detectors/{detectorId}/stop/{isHistorical}/{dataSourceId}', + adService.stopDetector + ); + apiRouter.get( '/detectors/{detectorId}/_profile', adService.getDetectorProfile ); + + // get detector + apiRouter.get('/detectors/{detectorId}', adService.getDetector); + apiRouter.get( + '/detectors/{detectorId}/{dataSourceId}', + adService.getDetector + ); + + // match detector apiRouter.get('/detectors/{detectorName}/_match', adService.matchDetector); + apiRouter.get('/detectors/{detectorName}/_match/{dataSourceId}', adService.matchDetector); + + // get detector count apiRouter.get('/detectors/_count', adService.getDetectorCount); + apiRouter.get('/detectors/_count/{dataSourceId}', adService.getDetectorCount); + + // post get top anomaly results apiRouter.post( '/detectors/{detectorId}/_topAnomalies/{isHistorical}', adService.getTopAnomalyResults ); + apiRouter.post( + '/detectors/{detectorId}/_topAnomalies/{isHistorical}/{dataSourceId}', + adService.getTopAnomalyResults + ); + + // validate detector apiRouter.post( '/detectors/_validate/{validationType}', adService.validateDetector ); + apiRouter.post( + '/detectors/_validate/{validationType}/{dataSourceId}', + adService.validateDetector + ); } export default class AdService { private client: any; + dataSourceEnabled: boolean; - constructor(client: any) { + constructor(client: any, dataSourceEnabled: boolean) { this.client = client; + this.dataSourceEnabled = dataSourceEnabled; } deleteDetector = async ( @@ -115,11 +195,19 @@ export default class AdService { ): Promise> => { try { const { detectorId } = request.params as { detectorId: string }; - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.deleteDetector', { - detectorId, - }); + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest('ad.deleteDetector', { + detectorId, + }); + return opensearchDashboardsResponse.ok({ body: { ok: true, @@ -143,12 +231,21 @@ export default class AdService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const requestBody = JSON.stringify( convertPreviewInputKeysToSnakeCase(request.body) ); - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.previewDetector', { + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const response = await callWithRequest( + 'ad.previewDetector', { body: requestBody, }); const transformedKeys = mapKeysDeep(response, toCamel); @@ -177,6 +274,8 @@ export default class AdService { ): Promise> => { try { const { detectorId } = request.params as { detectorId: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + //@ts-ignore const ifSeqNo = request.body.seqNo; //@ts-ignore @@ -193,16 +292,20 @@ export default class AdService { }; let response; + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + if (isNumber(ifSeqNo) && isNumber(ifPrimaryTerm)) { - response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.updateDetector', params); + response = await callWithRequest('ad.updateDetector', params); } else { - response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.createDetector', { - body: params.body, - }); + response = await callWithRequest('ad.createDetector', { + body: params.body, + }); } const resp = { ...response.anomaly_detector, @@ -236,12 +339,21 @@ export default class AdService { let { validationType } = request.params as { validationType: string; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const requestBody = JSON.stringify( convertPreviewInputKeysToSnakeCase(request.body) ); - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.validateDetector', { + const response = await callWithRequest( + 'ad.validateDetector', { body: requestBody, validationType: validationType, }); @@ -269,11 +381,18 @@ export default class AdService { ): Promise> => { try { const { detectorId } = request.params as { detectorId: string }; - const detectorResponse = await this.client - .asScoped(request) - .callAsCurrentUser('ad.getDetector', { - detectorId, - }); + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const detectorResponse = await callWithRequest('ad.getDetector', { + detectorId, + }); // Populating static detector fields const staticFields = { @@ -290,16 +409,21 @@ export default class AdService { let realtimeTasksResponse = {} as any; let historicalTasksResponse = {} as any; try { - realtimeTasksResponse = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchTasks', { - body: getLatestTaskForDetectorQuery(detectorId, true), - }); - historicalTasksResponse = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchTasks', { - body: getLatestTaskForDetectorQuery(detectorId, false), - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + realtimeTasksResponse = await callWithRequest('ad.searchTasks', { + body: getLatestTaskForDetectorQuery(detectorId, true), + }); + + historicalTasksResponse = await callWithRequest('ad.searchTasks', { + body: getLatestTaskForDetectorQuery(detectorId, false), + }); } catch (err) { if (!isIndexNotFoundError(err)) { throw err; @@ -364,6 +488,7 @@ export default class AdService { ): Promise> => { try { const { detectorId } = request.params as { detectorId: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; //@ts-ignore const startTime = request.body?.startTime; //@ts-ignore @@ -382,9 +507,16 @@ export default class AdService { requestPath = 'ad.startHistoricalDetector'; } - const response = await this.client - .asScoped(request) - .callAsCurrentUser(requestPath, requestParams); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest(requestPath, requestParams); + return opensearchDashboardsResponse.ok({ body: { ok: true, @@ -412,16 +544,25 @@ export default class AdService { detectorId: string; isHistorical: any; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + isHistorical = JSON.parse(isHistorical) as boolean; const requestPath = isHistorical ? 'ad.stopHistoricalDetector' : 'ad.stopDetector'; - const response = await this.client - .asScoped(request) - .callAsCurrentUser(requestPath, { - detectorId, - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest(requestPath, { + detectorId, + }); + return opensearchDashboardsResponse.ok({ body: { ok: true, @@ -520,6 +661,8 @@ export default class AdService { resultIndex: string; onlyQueryCustomResultIndex: boolean; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + if ( !resultIndex || !resultIndex.startsWith(CUSTOM_AD_RESULT_INDEX_PREFIX) @@ -532,18 +675,23 @@ export default class AdService { onlyQueryCustomResultIndex: onlyQueryCustomResultIndex, } as {}; const requestBody = JSON.stringify(request.body); + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const response = !resultIndex - ? await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchResults', { - body: requestBody, - }) - : await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchResultsFromCustomResultIndex', { - ...requestParams, - body: requestBody, - }); + ? await callWithRequest('ad.searchResults', { + body: requestBody, + }) + : await callWithRequest('ad.searchResultsFromCustomResultIndex', { + ...requestParams, + body: requestBody, + }); return opensearchDashboardsResponse.ok({ body: { ok: true, @@ -579,6 +727,7 @@ export default class AdService { indices = '', sortDirection = SORT_DIRECTION.DESC, sortField = 'name', + dataSourceId = '', } = request.query as GetDetectorsQueryParams; const mustQueries = []; if (search.trim()) { @@ -621,9 +770,17 @@ export default class AdService { }, }, }; - const response: any = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchDetector', { body: requestBody }); + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const response = await callWithRequest('ad.searchDetector', { + body: requestBody, + }); const totalDetectors = get(response, 'hits.total.value', 0); @@ -655,9 +812,10 @@ export default class AdService { resultIndex: CUSTOM_AD_RESULT_INDEX_PREFIX + '*', onlyQueryCustomResultIndex: 'false', } as {}; - const aggregationResult = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchResultsFromCustomResultIndex', { + + const aggregationResult = await callWithRequest( + 'ad.searchResultsFromCustomResultIndex', + { ...requestParams, body: getResultAggregationQuery(allDetectorIds, { from, @@ -667,7 +825,8 @@ export default class AdService { search, indices, }), - }); + } + ); const aggsDetectors = get( aggregationResult, 'aggregations.unique_detectors.buckets', @@ -720,16 +879,12 @@ export default class AdService { let realtimeTasksResponse = {} as any; let historicalTasksResponse = {} as any; try { - realtimeTasksResponse = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchTasks', { - body: getLatestDetectorTasksQuery(true), - }); - historicalTasksResponse = await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchTasks', { - body: getLatestDetectorTasksQuery(false), - }); + realtimeTasksResponse = await callWithRequest('ad.searchTasks', { + body: getLatestDetectorTasksQuery(true), + }); + historicalTasksResponse = await callWithRequest('ad.searchTasks', { + body: getLatestDetectorTasksQuery(false), + }); } catch (err) { if (!isIndexNotFoundError(err)) { throw err; @@ -816,6 +971,8 @@ export default class AdService { resultIndex: string; onlyQueryCustomResultIndex: boolean; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + if ( !resultIndex || !resultIndex.startsWith(CUSTOM_AD_RESULT_INDEX_PREFIX) @@ -946,18 +1103,23 @@ export default class AdService { resultIndex: resultIndex, onlyQueryCustomResultIndex: onlyQueryCustomResultIndex, } as {}; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const response = !resultIndex - ? await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchResults', { - body: requestBody, - }) - : await this.client - .asScoped(request) - .callAsCurrentUser('ad.searchResultsFromCustomResultIndex', { - ...requestParams, - body: requestBody, - }); + ? await callWithRequest('ad.searchResults', { + body: requestBody, + }) + : await callWithRequest('ad.searchResultsFromCustomResultIndex', { + ...requestParams, + body: requestBody, + }); const totalResults: number = get(response, 'hits.total.value', 0); @@ -1046,17 +1208,25 @@ export default class AdService { detectorId: string; isHistorical: any; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + isHistorical = JSON.parse(isHistorical) as boolean; const requestPath = isHistorical ? 'ad.topHistoricalAnomalyResults' : 'ad.topAnomalyResults'; - const response = await this.client - .asScoped(request) - .callAsCurrentUser(requestPath, { - detectorId: detectorId, - body: request.body, - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest(requestPath, { + detectorId: detectorId, + body: request.body, + }); return opensearchDashboardsResponse.ok({ body: { @@ -1082,9 +1252,18 @@ export default class AdService { ): Promise> => { try { const { detectorName } = request.params as { detectorName: string }; - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.matchDetector', { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'ad.matchDetector', { detectorName, }); return opensearchDashboardsResponse.ok({ @@ -1107,9 +1286,18 @@ export default class AdService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.detectorCount'); + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'ad.detectorCount'); return opensearchDashboardsResponse.ok({ body: { ok: true, diff --git a/server/routes/alerting.ts b/server/routes/alerting.ts index 2616656d..59fce0ae 100644 --- a/server/routes/alerting.ts +++ b/server/routes/alerting.ts @@ -22,20 +22,29 @@ import { OpenSearchDashboardsResponseFactory, IOpenSearchDashboardsResponse, } from '../../../../src/core/server'; +import { getClientBasedOnDataSource } from '../utils/helpers'; export function registerAlertingRoutes( apiRouter: Router, alertingService: AlertingService ) { apiRouter.post('/monitors/_search', alertingService.searchMonitors); + apiRouter.post( + '/monitors/_search/{dataSourceId}', + alertingService.searchMonitors + ); + apiRouter.get('/monitors/alerts', alertingService.searchAlerts); + apiRouter.get('/monitors/alerts/{dataSourceId}', alertingService.searchAlerts); } export default class AlertingService { private client: any; + dataSourceEnabled: boolean; - constructor(client: any) { + constructor(client: any, dataSourceEnabled: boolean) { this.client = client; + this.dataSourceEnabled = dataSourceEnabled; } searchMonitors = async ( @@ -44,6 +53,8 @@ export default class AlertingService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const requestBody = { size: MAX_MONITORS, query: { @@ -71,9 +82,19 @@ export default class AlertingService { }, }, }; - const response: SearchResponse = await this.client - .asScoped(request) - .callAsCurrentUser('alerting.searchMonitors', { body: requestBody }); + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response: SearchResponse = await callWithRequest( + 'alerting.searchMonitors', + { body: requestBody } + ); const totalMonitors = get(response, 'hits.total.value', 0); const allMonitors = get(response, 'hits.hits', []).reduce( (acc: any, monitor: any) => ({ @@ -123,9 +144,18 @@ export default class AlertingService { startTime?: number; endTime?: number; }; - const response = await this.client - .asScoped(request) - .callAsCurrentUser('alerting.searchAlerts', { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = callWithRequest( + 'alerting.searchAlerts', { monitorId: monitorId, startTime: startTime, endTime: endTime, diff --git a/server/routes/opensearch.ts b/server/routes/opensearch.ts index 8a52c46e..3a8444d9 100644 --- a/server/routes/opensearch.ts +++ b/server/routes/opensearch.ts @@ -27,6 +27,7 @@ import { OpenSearchDashboardsResponseFactory, IOpenSearchDashboardsResponse, } from '../../../../src/core/server'; +import { getClientBasedOnDataSource } from '../utils/helpers'; type SearchParams = { index: string; @@ -39,19 +40,32 @@ export function registerOpenSearchRoutes( opensearchService: OpenSearchService ) { apiRouter.get('/_indices', opensearchService.getIndices); + apiRouter.get('/_indices/{dataSourceId}', opensearchService.getIndices); + apiRouter.get('/_aliases', opensearchService.getAliases); + apiRouter.get('/_aliases/{dataSourceId}', opensearchService.getAliases); + apiRouter.get('/_mappings', opensearchService.getMapping); + apiRouter.get('/_mappings/{dataSourceId}', opensearchService.getMapping); + apiRouter.post('/_search', opensearchService.executeSearch); + apiRouter.put('/create_index', opensearchService.createIndex); + apiRouter.put('/create_index/{dataSourceId}', opensearchService.createIndex); + apiRouter.post('/bulk', opensearchService.bulk); + apiRouter.post('/bulk/{dataSourceId}', opensearchService.bulk); + apiRouter.post('/delete_index', opensearchService.deleteIndex); } export default class OpenSearchService { private client: any; + dataSourceEnabled: boolean; - constructor(client: any) { + constructor(client: any, dataSourceEnabled: boolean) { this.client = client; + this.dataSourceEnabled = dataSourceEnabled; } executeSearch = async ( @@ -112,14 +126,22 @@ export default class OpenSearchService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { const { index } = request.query as { index: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; try { - const response: CatIndex[] = await this.client - .asScoped(request) - .callAsCurrentUser('cat.indices', { - index, - format: 'json', - h: 'health,index', - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response: CatIndex[] = await callWithRequest('cat.indices', { + index, + format: 'json', + h: 'health,index', + }); + return opensearchDashboardsResponse.ok({ body: { ok: true, response: { indices: response } }, }); @@ -149,14 +171,22 @@ export default class OpenSearchService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { const { alias } = request.query as { alias: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + try { - const response: IndexAlias[] = await this.client - .asScoped(request) - .callAsCurrentUser('cat.aliases', { - alias, - format: 'json', - h: 'alias,index', - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response: IndexAlias[] = await callWithRequest('cat.aliases', { + alias, + format: 'json', + h: 'alias,index', + }); return opensearchDashboardsResponse.ok({ body: { ok: true, response: { aliases: response } }, }); @@ -176,12 +206,21 @@ export default class OpenSearchService { request: OpenSearchDashboardsRequest, opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + //@ts-ignore const index = request.body.index; //@ts-ignore const body = request.body.body; + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); try { - await this.client.asScoped(request).callAsCurrentUser('indices.create', { + await callWithRequest('indices.create', { index: index, body: body, }); @@ -195,13 +234,11 @@ export default class OpenSearchService { }); } try { - const response: CatIndex[] = await this.client - .asScoped(request) - .callAsCurrentUser('cat.indices', { - index, - format: 'json', - h: 'health,index', - }); + const response: CatIndex[] = await callWithRequest('cat.indices', { + index, + format: 'json', + h: 'health,index', + }); return opensearchDashboardsResponse.ok({ body: { ok: true, response: { indices: response } }, }); @@ -221,13 +258,20 @@ export default class OpenSearchService { request: OpenSearchDashboardsRequest, opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; const body = request.body; try { - const response: any = await this.client - .asScoped(request) - .callAsCurrentUser('bulk', { - body: body, - }); + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response: any = await callWithRequest('bulk', { + body: body, + }); return opensearchDashboardsResponse.ok({ body: { ok: true, response: { response } }, }); @@ -249,7 +293,7 @@ export default class OpenSearchService { ): Promise> => { const index = request.query as { index: string }; try { - await this.client.asScoped(request).callAsCurrentUser('indices.delete', { + await callWithRequest('indices.delete', { index: index, }); } catch (err) { @@ -295,10 +339,19 @@ export default class OpenSearchService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { const { index } = request.query as { index: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + try { - const response = await this.client - .asScoped(request) - .callAsCurrentUser('indices.getMapping', { + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'indices.getMapping', { index, }); return opensearchDashboardsResponse.ok({ diff --git a/server/routes/sampleData.ts b/server/routes/sampleData.ts index 1874b944..5e8141a3 100644 --- a/server/routes/sampleData.ts +++ b/server/routes/sampleData.ts @@ -31,13 +31,19 @@ export function registerSampleDataRoutes( '/create_sample_data/{type}', sampleDataService.createSampleData ); + apiRouter.post( + '/create_sample_data/{type}/{dataSourceId}', + sampleDataService.createSampleData + ); } export default class SampleDataService { private client: any; + dataSourceEnabled: boolean; - constructor(client: any) { + constructor(client: any, dataSourceEnabled: boolean) { this.client = client; + this.dataSourceEnabled = dataSourceEnabled; } // Get the zip file stored in server, unzip it, and bulk insert it @@ -79,7 +85,14 @@ export default class SampleDataService { } } - await loadSampleData(filePath, indexName, this.client, request); + await loadSampleData( + filePath, + indexName, + this.client, + request, + context, + this.dataSourceEnabled + ); return opensearchDashboardsResponse.ok({ body: { ok: true } }); } catch (err) { diff --git a/server/sampleData/utils/helpers.ts b/server/sampleData/utils/helpers.ts index 86bdd04a..e53f6c8f 100644 --- a/server/sampleData/utils/helpers.ts +++ b/server/sampleData/utils/helpers.ts @@ -19,7 +19,10 @@ import { import fs from 'fs'; import { createUnzip } from 'zlib'; import { isEmpty } from 'lodash'; -import { prettifyErrorMessage } from '../../utils/helpers'; +import { + getClientBasedOnDataSource, + prettifyErrorMessage, +} from '../../utils/helpers'; const BULK_INSERT_SIZE = 500; @@ -27,8 +30,12 @@ export const loadSampleData = ( filePath: string, indexName: string, client: any, - request: OpenSearchDashboardsRequest + request: OpenSearchDashboardsRequest, + context: RequestHandlerContext, + dataSourceEnabled: boolean ) => { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + return new Promise((resolve, reject) => { let count: number = 0; let docs: any[] = []; @@ -95,10 +102,18 @@ export const loadSampleData = ( } }); + const callWithRequest = getClientBasedOnDataSource( + context, + dataSourceEnabled, + request, + dataSourceId, + client + ); + const bulkInsert = async (docs: any[]) => { try { const bulkBody = prepareBody(docs, offset); - const resp = await client.asScoped(request).callAsCurrentUser('bulk', { + const resp = await callWithRequest('bulk', { body: bulkBody, }); if (resp.errors) { diff --git a/server/utils/constants.ts b/server/utils/constants.ts index 4477db0f..ac3c887a 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -46,6 +46,17 @@ export enum SORT_DIRECTION { DESC = 'desc', } +export enum DETECTORS_QUERY_PARAMS { + FROM = 'from', + SIZE = 'size', + SEARCH = 'search', + INDICES = 'indices', + SORT_FIELD = 'sortField', + SORT_DIRECTION = 'sortDirection', + NAME = 'name', + DATASOURCEID = 'dataSourceId', +} + export enum AD_DOC_FIELDS { DATA_START_TIME = 'data_start_time', DATA_END_TIME = 'data_end_time', diff --git a/server/utils/helpers.ts b/server/utils/helpers.ts index 15c80b3e..c9006544 100644 --- a/server/utils/helpers.ts +++ b/server/utils/helpers.ts @@ -20,6 +20,12 @@ import { } from 'lodash'; import { MIN_IN_MILLI_SECS } from './constants'; +import { + ILegacyClusterClient, + LegacyCallAPIOptions, + OpenSearchDashboardsRequest, + RequestHandlerContext, +} from '../../../../src/core/server'; export const SHOW_DECIMAL_NUMBER_THRESHOLD = 0.01; @@ -81,3 +87,23 @@ export const prettifyErrorMessage = (rawErrorMessage: string) => { return `User ${match[2]} has no permissions to [${match[1]}].`; } }; + +export function getClientBasedOnDataSource( + context: RequestHandlerContext, + dataSourceEnabled: boolean, + request: OpenSearchDashboardsRequest, + dataSourceId: string, + client: ILegacyClusterClient +): ( + endpoint: string, + clientParams?: Record, + options?: LegacyCallAPIOptions +) => any { + if (dataSourceEnabled && dataSourceId && dataSourceId.trim().length != 0) { + // client for remote cluster + return context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI; + } else { + // fall back to default local cluster + return client.asScoped(request).callAsCurrentUser; + } +} diff --git a/yarn.lock b/yarn.lock index b7f4ed32..597e3515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -136,10 +136,10 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/redux-mock-store@^1.0.1": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz#895de4a364bc4836661570aec82f2eef5989d1fb" - integrity sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA== +"@types/redux-mock-store@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz#0a03b2655028b7cf62670d41ac1de5ca1b1f5958" + integrity sha512-eg5RDfhJTXuoJjOMyXiJbaDb1B8tfTaJixscmu+jOusj6adGC0Krntz09Tf4gJgXeCqCrM5bBMd+B7ez0izcAQ== dependencies: redux "^4.0.5" @@ -1432,7 +1432,7 @@ readable-stream@^3.6.0, readable-stream@^3.6.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" -redux-mock-store@^1.5.3: +redux-mock-store@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==