diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 72a48458..c62acf46 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/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/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx index e2116e30..96aa189e 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,7 @@ import { DETECTOR_STATE, } from '../../../../server/utils/constants'; import { ENTITY_COLORS } from '../../DetectorResults/utils/constants'; +import { useLocation } from 'react-router-dom'; interface AnomalyDetailsChartProps { onDateRangeChange( @@ -118,6 +122,9 @@ interface AnomalyDetailsChartProps { export const AnomalyDetailsChart = React.memo( (props: AnomalyDetailsChartProps) => { const dispatch = useDispatch(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; const [showAlertsFlyout, setShowAlertsFlyout] = useState(false); const [alertAnnotations, setAlertAnnotations] = useState([]); const [isLoadingAlerts, setIsLoadingAlerts] = useState(false); @@ -174,7 +181,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 +202,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 +240,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, diff --git a/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts b/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts index af3cc4e9..3f8d024d 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,7 +44,8 @@ export const useFetchDetectorInfo = ( useEffect(() => { const fetchDetector = async () => { if (!detector) { - await dispatch(getDetector(detectorId)); + // hardcoding the datasource id for now, will update it later when working on create page + await dispatch(getDetector(detectorId, dataSourceId)); } if (selectedIndices) { await dispatch(getMappings(selectedIndices)); diff --git a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx index b02878fe..0d590adc 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 { DATA_SOURCE_ID } from '../../../utils/constants'; export interface AnomaliesDistributionChartProps { selectedDetectors: DetectorListItem[]; } @@ -41,6 +43,9 @@ export const AnomaliesDistributionChart = ( props: AnomaliesDistributionChartProps ) => { const dispatch = useDispatch(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; 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..ed4490c1 100644 --- a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx +++ b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx @@ -51,9 +51,14 @@ 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'; export interface AnomaliesLiveChartProps { selectedDetectors: DetectorListItem[]; @@ -68,6 +73,9 @@ const MAX_LIVE_DETECTORS = 10; export const AnomaliesLiveChart = (props: AnomaliesLiveChartProps) => { const dispatch = useDispatch(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; const [liveTimeRange, setLiveTimeRange] = useState({ startDateTime: moment().subtract(31, 'minutes'), @@ -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/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 67b3d433..88e90eda 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -9,9 +9,10 @@ * 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'; @@ -29,17 +30,26 @@ 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 { + CatIndex, + IndexAlias, + MDSQueryParams, +} from '../../../../server/models/types'; +import { + getAllDetectorsQueryParamsWithDataSourceId, + getVisibleOptions, +} from '../../utils/helpers'; import { BREADCRUMBS } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; -import { getDetectorStateOptions } from '../../DetectorsList/utils/helpers'; +import { + getDetectorStateOptions, + getURLQueryParams, +} from '../../DetectorsList/utils/helpers'; import { DashboardHeader } from '../Components/utils/DashboardHeader'; import { EmptyDashboard } from '../Components/EmptyDashboard/EmptyDashboard'; import { @@ -47,9 +57,26 @@ 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, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { RouteComponentProps } from 'react-router-dom'; + +interface OverviewProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; +} + +interface MDSOverviewState { + queryParams: MDSQueryParams; + selectedDataSourceId: string; +} -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 +85,8 @@ export function DashboardOverview() { const errorGettingDetectors = adState.errorMessage; const isLoadingDetectors = adState.requesting; + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const [currentDetectors, setCurrentDetectors] = useState( Object.values(allDetectorList) ); @@ -65,6 +94,14 @@ export function DashboardOverview() { const [selectedDetectorsName, setSelectedDetectorsName] = useState( [] as string[] ); + const queryParams = getURLQueryParams(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ + queryParams, + selectedDataSourceId: queryParams.dataSourceId + ? queryParams.dataSourceId + : '', + }); + const getDetectorOptions = (detectorsIdMap: { [key: string]: DetectorListItem; }) => { @@ -108,6 +145,20 @@ export function DashboardOverview() { setAllDetectorStatesSelected(isEmpty(selectedStates)); }; + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + if (!dataSourceId) { + 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 +208,28 @@ 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; + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); intializeDetectors(); - }, []); + }, [MDSOverviewState]); useEffect(() => { if (errorGettingDetectors) { @@ -197,9 +262,32 @@ 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/Datasource/DataSource.tsx b/public/pages/DefineDetector/components/Datasource/DataSource.tsx index dd268dde..fdb810e3 100644 --- a/public/pages/DefineDetector/components/Datasource/DataSource.tsx +++ b/public/pages/DefineDetector/components/Datasource/DataSource.tsx @@ -167,4 +167,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/DetectorConfig/containers/DetectorConfig.tsx b/public/pages/DetectorConfig/containers/DetectorConfig.tsx index e829fef4..f2fec2d1 100644 --- a/public/pages/DetectorConfig/containers/DetectorConfig.tsx +++ b/public/pages/DetectorConfig/containers/DetectorConfig.tsx @@ -14,11 +14,12 @@ import { DetectorDefinitionFields } from '../../ReviewAndCreate/components/Detec import { Features } from './Features'; import { DetectorJobs } from './DetectorJobs'; import { EuiSpacer, EuiPage, EuiPageBody } from '@elastic/eui'; -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps, useLocation } from 'react-router'; import { AppState } from '../../../redux/reducers'; import { useSelector, useDispatch } from 'react-redux'; import { getDetector } from '../../../redux/reducers/ad'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { DATA_SOURCE_ID } from '../../../utils/constants'; interface DetectorConfigProps extends RouteComponentProps { detectorId: string; onEditFeatures(): void; @@ -27,12 +28,15 @@ interface DetectorConfigProps extends RouteComponentProps { export function DetectorConfig(props: DetectorConfigProps) { const dispatch = useDispatch(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; const detector = useSelector( (state: AppState) => 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..a523b04c 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, DATA_SOURCE_ID } from '../../../utils/constants'; import { DetectorControls } from '../components/DetectorControls'; import { ConfirmModal } from '../components/ConfirmModal/ConfirmModal'; import { useFetchMonitorInfo } from '../hooks/useFetchMonitorInfo'; @@ -58,12 +64,20 @@ 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, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; export interface DetectorRouterProps { detectorId?: string; } -interface DetectorDetailProps - extends RouteComponentProps {} +interface DetectorDetailProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; +} const tabs = [ { @@ -103,10 +117,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 = getDataSourcePlugin()?.dataSourceEnabled || false; + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + 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 +178,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'); }); @@ -201,7 +223,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.CONFIGURATIONS, }); - props.history.push(`/detectors/${detectorId}/configurations`); + props.history.push( + `/detectors/${detectorId}/configurations?dataSourceId=${dataSourceId}` + ); }, []); const handleSwitchToHistoricalTab = useCallback(() => { @@ -209,7 +233,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.HISTORICAL, }); - props.history.push(`/detectors/${detectorId}/historical`); + props.history.push( + `/detectors/${detectorId}/historical?dataSourceId=${dataSourceId}` + ); }, []); const handleTabChange = (route: DETECTOR_DETAIL_TABS) => { @@ -217,7 +243,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: route, }); - props.history.push(`/detectors/${detectorId}/${route}`); + props.history.push( + `/detectors/${detectorId}/${route}?dataSourceId=${dataSourceId}` + ); }; const hideMonitorCalloutModal = () => { @@ -261,8 +289,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 +306,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,7 +330,7 @@ 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'); @@ -350,6 +378,24 @@ export const DetectorDetail = (props: DetectorDetailProps) => { > ) : null; + let renderDataSourceComponent = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {!isEmpty(detector) && !hasError ? ( @@ -361,6 +407,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/DetectorResults/containers/AnomalyHistory.tsx b/public/pages/DetectorResults/containers/AnomalyHistory.tsx index e9a355df..8cb86217 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,7 @@ 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'; interface AnomalyHistoryProps { detector: Detector; @@ -126,6 +127,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { props.isHistorical && props.detector?.detectionDateRange ? props.detector.detectionDateRange.endTime : moment().valueOf(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; const [dateRange, setDateRange] = useState({ startDate: initialStartDate, endDate: initialEndDate, @@ -223,7 +227,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { taskId.current, modelId ); - return dispatch(searchResults(params, resultIndex, true)); + return dispatch( + searchResults(params, resultIndex, dataSourceId, true) + ); }) : []; @@ -252,7 +258,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 +281,9 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { taskId.current, modelId ); - return dispatch(searchResults(params, resultIndex, true)); + return dispatch( + searchResults(params, resultIndex, dataSourceId, true) + ); } ); @@ -308,7 +316,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 +421,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 +437,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { : await dispatch( getDetectorResults( props.detector.id, + dataSourceId, params, false, resultIndex, @@ -536,6 +557,7 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { const result = await dispatch( getTopAnomalyResults( detectorId, + dataSourceId, get(props, 'isHistorical', false), query ) @@ -553,7 +575,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 +596,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 +670,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 +749,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..d09e26ab 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -28,10 +28,11 @@ 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, MISSING_FEATURE_DATA_SEVERITY, } from '../../../utils/constants'; @@ -83,6 +84,9 @@ export function AnomalyResults(props: AnomalyResultsProps) { const detector = useSelector( (state: AppState) => state.ad.detectors[detectorId] ); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; useEffect(() => { core.chrome.setBreadcrumbs([ @@ -90,11 +94,11 @@ export function AnomalyResults(props: AnomalyResultsProps) { BREADCRUMBS.DETECTORS, { text: detector ? detector.name : '' }, ]); - dispatch(getDetector(detectorId)); + dispatch(getDetector(detectorId, dataSourceId)); }, []); const fetchDetector = async () => { - dispatch(getDetector(detectorId)); + dispatch(getDetector(detectorId, dataSourceId)); }; useEffect(() => { @@ -247,7 +251,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..d38db529 100644 --- a/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx @@ -57,6 +57,8 @@ 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'; +import { DATA_SOURCE_ID } from '../../../utils/constants'; interface AnomalyResultsLiveChartProps { detector: Detector; @@ -66,6 +68,9 @@ export const AnomalyResultsLiveChart = ( props: AnomalyResultsLiveChartProps ) => { const dispatch = useDispatch(); + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; const [firstLoading, setFirstLoading] = useState(true); const [isFullScreen, setIsFullScreen] = useState(false); @@ -133,6 +138,7 @@ export const AnomalyResultsLiveChart = ( await dispatch( getDetectorLiveResults( detectorId, + dataSourceId, queryParams, false, resultIndex, @@ -161,6 +167,7 @@ export const AnomalyResultsLiveChart = ( getLiveAnomalyResults( dispatch, props.detector.id, + dataSourceId, detectionInterval, LIVE_CHART_CONFIG.MONITORING_INTERVALS, resultIndex, diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index 864f9951..fefb8e2a 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 { @@ -44,13 +44,16 @@ import { } from '../../../../redux/reducers/opensearch'; import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants'; import { DETECTOR_STATE } from '../../../../../server/utils/constants'; -import { getVisibleOptions, sanitizeSearchText } from '../../../utils/helpers'; +import { + 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 +68,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 +81,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 { DataSourceSelectableConfig } from '../../../../../../../src/plugins/data_source_management/public'; +import { + getDataSourceManagementPlugin, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../../services'; export interface ListRouterParams { from: string; @@ -88,13 +98,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; } interface ConfirmModalState { isOpen: boolean; @@ -123,6 +137,8 @@ export const DetectorList = (props: ListProps) => { (state: AppState) => state.ad.requesting ); + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const [selectedDetectors, setSelectedDetectors] = useState( [] as DetectorListItem[] ); @@ -152,19 +168,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,6 +205,9 @@ export const DetectorList = (props: ListProps) => { selectedIndices: queryParams.indices ? queryParams.indices.split(',') : ALL_INDICES, + selectedDataSourceId: queryParams.dataSourceId + ? queryParams.dataSourceId + : '', }); // Set breadcrumbs on page initialization @@ -208,6 +218,15 @@ export const DetectorList = (props: ListProps) => { ]); }, []); + // 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; @@ -215,6 +234,7 @@ export const DetectorList = (props: ListProps) => { ...state.queryParams, indices: state.selectedIndices.join(','), from: state.page * state.queryParams.size, + dataSourceId: state.selectedDataSourceId, }; history.replace({ @@ -223,12 +243,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 +295,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 +341,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 +483,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 +510,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 +542,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 +580,21 @@ export const DetectorList = (props: ListProps) => { }); }; + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + if (!dataSourceId) { + 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 +669,34 @@ 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); + return ( + {dataSourceEnabled && renderDataSourceComponent} { monitors list page. */ itemId={getItemId} - columns={staticColumn} + columns={columns} onChange={handleTableChange} isSelectable={true} selection={selection} diff --git a/public/pages/DetectorsList/utils/helpers.ts b/public/pages/DetectorsList/utils/helpers.ts index 89d9075c..01dca578 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,6 +47,7 @@ export const getURLQueryParams = (location: { typeof sortDirection !== 'string' ? DEFAULT_QUERY_PARAMS.sortDirection : (sortDirection as SORT_DIRECTION), + dataSourceId: typeof dataSourceId !== 'string' ? '' : dataSourceId, }; }; 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..f42509d1 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 { DATA_SOURCE_ID } from '../../../utils/constants'; 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 dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; 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..8a61d126 100644 --- a/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx +++ b/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx @@ -21,7 +21,8 @@ import { EuiCard, EuiHorizontalRule, } from '@elastic/eui'; -import { PLUGIN_NAME } from '../../../../utils/constants'; +import { DATA_SOURCE_ID, PLUGIN_NAME } from '../../../../utils/constants'; +import { useLocation } from 'react-router-dom'; interface SampleDataBoxProps { title: string; @@ -37,6 +38,9 @@ interface SampleDataBoxProps { } export const SampleDataBox = (props: SampleDataBoxProps) => { + const location = useLocation(); + const dataSourceId = + new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; return (
{ {props.isDataLoaded ? ( View detector and sample data diff --git a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx index 5d84779e..8af1f06b 100644 --- a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx +++ b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx @@ -17,12 +17,11 @@ 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, @@ -31,10 +30,7 @@ import { BASE_DOCS_LINK, } 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 +50,49 @@ 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, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../../public/services'; +import { MDSQueryParams } from 'server/models/types'; +import { RouteComponentProps } from 'react-router-dom'; +import queryString from 'querystring'; +import { getURLQueryParams } from '../../../../public/pages/DetectorsList/utils/helpers'; +import { getSampleDetectorsQueryParamsWithDataSouceId } from '../../../../public/pages/utils/helpers'; + +interface AnomalyDetectionOverviewProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; +} -interface AnomalyDetectionOverviewProps { - isLoadingDetectors: boolean; +interface MDSOverviewState { + queryParams: MDSQueryParams; + selectedDataSourceId: string; } 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 = getDataSourcePlugin()?.dataSourceEnabled || false; const [isLoadingHttpData, setIsLoadingHttpData] = useState(false); const [isLoadingEcommerceData, setIsLoadingEcommerceData] = useState(false); @@ -87,19 +105,13 @@ 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 = getURLQueryParams(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ + queryParams, + selectedDataSourceId: queryParams.dataSourceId + ? queryParams.dataSourceId + : '', + }); // Set breadcrumbs on page initialization useEffect(() => { @@ -108,9 +120,40 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Getting all initial sample detectors & indices useEffect(() => { - getAllSampleDetectors(); - getAllSampleIndices(); - }, []); + const { history, location } = props; + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); + + if (dataSourceEnabled ? MDSOverviewState.selectedDataSourceId : true) { + 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,51 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { } }; - return props.isLoadingDetectors ? ( + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + + if (!dataSourceId) { + 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]); + } + + return isLoadingSampleDetectors && isLoadingSampleIndices ? (
) : ( + {dataSourceEnabled && renderDataSourceComponent} diff --git a/public/pages/main/Main.tsx b/public/pages/main/Main.tsx index 8f1ef972..4fc9a0be 100644 --- a/public/pages/main/Main.tsx +++ b/public/pages/main/Main.tsx @@ -23,8 +23,9 @@ 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'; enum Navigation { AnomalyDetection = 'Anomaly detection', @@ -32,33 +33,66 @@ 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 ? queryParams.dataSourceId : ''; + const existingParams = + 'from=0&size=20&search=&indices=&sortField=name&sortDirection=asc'; + + const constructHrefWithDataSourceId = ( + basePath: string, + existingParams: string, + dataSourceId: string + ) => { + const searchParams = new URLSearchParams(existingParams); + + if (dataSourceId) { + searchParams.set('dataSourceId', dataSourceId); + } + + return `#${basePath}?${searchParams.toString()}`; + }; + const sideNav = [ { name: Navigation.AnomalyDetection, id: 0, - href: `#${APP_PATH.OVERVIEW}`, + href: constructHrefWithDataSourceId( + APP_PATH.OVERVIEW, + '', + dataSourceId + ), items: [ { name: Navigation.Dashboard, id: 1, - href: `#${APP_PATH.DASHBOARD}`, + href: constructHrefWithDataSourceId( + APP_PATH.DASHBOARD, + '', + dataSourceId + ), isSelected: props.location.pathname === APP_PATH.DASHBOARD, }, { name: Navigation.Detectors, id: 2, - href: `#${APP_PATH.LIST_DETECTORS}`, + href: constructHrefWithDataSourceId( + APP_PATH.LIST_DETECTORS, + existingParams, + dataSourceId + ), isSelected: props.location.pathname === APP_PATH.LIST_DETECTORS, }, ], @@ -77,13 +111,21 @@ 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..f5c0fcfe 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -13,7 +13,7 @@ import { CatIndex, IndexAlias } 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 { ALL_INDICES, ALL_DETECTOR_STATES, MAX_DETECTORS } from './constants'; import { DETECTOR_STATE } from '../../../server/utils/constants'; import { timeFormatter } from '@elastic/charts'; @@ -112,3 +112,27 @@ 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 +}); diff --git a/public/plugin.ts b/public/plugin.ts index 25ea0ebf..07eb6194 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -35,6 +35,9 @@ import { setUiActions, setUISettings, setQueryService, + setSavedObjectsClient, + setDataSourceManagementPlugin, + setDataSourcePlugin, } 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,10 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin // direct server-side calls setClient(core.http); + setDataSourceManagementPlugin(plugins.dataSourceManagement); + + setDataSourcePlugin(plugins.dataSource); + // Create context menu actions const actions = getActions(); @@ -116,6 +126,7 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin setNotifications(core.notifications); setUiActions(uiActions); setQueryService(data.query); + setSavedObjectsClient(core.savedObjects.client); return {}; } } diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index d4d12ae6..67e7b558 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -369,13 +369,22 @@ 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, @@ -388,20 +397,35 @@ export const validateDetector = ( }), }); -export const getDetector = (detectorId: string): APIAction => ({ - type: GET_DETECTOR, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/${detectorId}`), - detectorId, -}); +export const getDetector = ( + detectorId: string, + dataSourceId: string +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}`; + const url = dataSourceId + ? `${baseUrl}/${detectorId}/${dataSourceId}` + : `${baseUrl}/${detectorId}`; + + 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 baseUrl = `..${AD_NODE_API.DETECTOR}/_list`; + const url = queryParams.dataSourceId + ? `${baseUrl}/${queryParams.dataSourceId}` + : baseUrl; + + return { + type: GET_DETECTOR_LIST, + request: (client: HttpSetup) => client.get(url, { query: queryParams }), + }; +}; export const searchDetector = (requestBody: any): APIAction => ({ type: SEARCH_DETECTOR, @@ -423,51 +447,89 @@ export const updateDetector = ( detectorId, }); -export const deleteDetector = (detectorId: string): APIAction => ({ - type: DELETE_DETECTOR, - request: (client: HttpSetup) => - client.delete(`..${AD_NODE_API.DETECTOR}/${detectorId}`), - detectorId, -}); +export const deleteDetector = ( + detectorId: string, + dataSourceId: string +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const startDetector = (detectorId: string): APIAction => ({ - type: START_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/start`), - detectorId, -}); + 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}`; + const url = dataSourceId + ? `${baseUrl}/${dataSourceId}/stop/${true}` + : `${baseUrl}/stop/${true}`; + + return { + type: STOP_HISTORICAL_DETECTOR, + request: (client: HttpSetup) => client.post(url), + detectorId, + }; +}; export const getDetectorProfile = (detectorId: string): APIAction => ({ type: GET_DETECTOR_PROFILE, diff --git a/public/redux/reducers/alerting.ts b/public/redux/reducers/alerting.ts index 04e8895c..79bfb3c9 100644 --- a/public/redux/reducers/alerting.ts +++ b/public/redux/reducers/alerting.ts @@ -94,10 +94,15 @@ 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, diff --git a/public/redux/reducers/anomalyResults.ts b/public/redux/reducers/anomalyResults.ts index 4c5d5f50..5eda008e 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..0c354c45 100644 --- a/public/redux/reducers/opensearch.ts +++ b/public/redux/reducers/opensearch.ts @@ -246,17 +246,30 @@ 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 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 = ''): APIAction => ({ type: GET_MAPPINGS, @@ -274,19 +287,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 +318,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 +334,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/sampleData.ts b/public/redux/reducers/sampleData.ts index 549516a6..b92ce773 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..4cb502f1 100644 --- a/public/services.ts +++ b/public/services.ts @@ -10,10 +10,12 @@ 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 const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = createGetterSetter('savedFeatureAnywhereLoader'); @@ -39,6 +41,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 [getDataSourcePlugin, setDataSourcePlugin] = + createGetterSetter('DataSource'); + // This is primarily used for mocking this module and each of its fns in tests. export default { getSavedFeatureAnywhereLoader, @@ -49,4 +60,5 @@ export default { getOverlays, setUISettings, setQueryService, + getSavedObjectsClient, }; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 6f244704..99988470 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -98,3 +98,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..ab494971 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..2c925ffd 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,20 +57,30 @@ 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; - // Create OpenSearch client w/ relevant plugins and headers - const client: ILegacyClusterClient = core.opensearch.legacy.createClient( - 'anomaly_detection', - { + const dataSourceEnabled = !!dataSource; + + // when MDS is enabled, we leave the client as undefined for now + // it will be defined later with dataSourceId when we have request context + let client: ILegacyClusterClient | undefined = undefined; + + if (!dataSourceEnabled) { + client = core.opensearch.legacy.createClient('anomaly_detection', { plugins: [adPlugin, alertingPlugin], customHeaders: { ...customHeaders, ...DEFAULT_HEADERS }, ...rest, - } - ); + }); + } else { + dataSource.registerCustomApiSchema(adPlugin); + dataSource.registerCustomApiSchema(alertingPlugin); + } // Create router const apiRouter: Router = 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..033365d2 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,42 +60,107 @@ 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); + + // post search anomaly results apiRouter.post('/detectors/results/_search/', adService.searchResults); - 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); + apiRouter.post('/detectors/preview', 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 + ); + apiRouter.get('/detectors/{detectorName}/_match', adService.matchDetector); apiRouter.get('/detectors/_count', adService.getDetectorCount); + + // post get top anomaly results apiRouter.post( '/detectors/{detectorId}/_topAnomalies/{isHistorical}', adService.getTopAnomalyResults ); + apiRouter.post( + '/detectors/{detectorId}/_topAnomalies/{isHistorical}/{dataSourceId}', + adService.getTopAnomalyResults + ); + apiRouter.post( '/detectors/_validate/{validationType}', adService.validateDetector @@ -103,9 +169,11 @@ export function registerADRoutes(apiRouter: Router, adService: AdService) { 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 +183,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, @@ -177,6 +253,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 +271,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, @@ -269,11 +351,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 +379,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 +458,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 +477,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 +514,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 +631,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 +645,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 +697,7 @@ export default class AdService { indices = '', sortDirection = SORT_DIRECTION.DESC, sortField = 'name', + dataSourceId = '', } = request.query as GetDetectorsQueryParams; const mustQueries = []; if (search.trim()) { @@ -621,9 +740,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 +782,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 +795,8 @@ export default class AdService { search, indices, }), - }); + } + ); const aggsDetectors = get( aggregationResult, 'aggregations.unique_detectors.buckets', @@ -720,16 +849,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 +941,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 +1073,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 +1178,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: { diff --git a/server/routes/alerting.ts b/server/routes/alerting.ts index 2616656d..c0728fb5 100644 --- a/server/routes/alerting.ts +++ b/server/routes/alerting.ts @@ -22,20 +22,28 @@ 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); } 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 +52,8 @@ export default class AlertingService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const requestBody = { size: MAX_MONITORS, query: { @@ -71,9 +81,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) => ({ diff --git a/server/routes/opensearch.ts b/server/routes/opensearch.ts index 8a52c46e..622db995 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,30 @@ 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.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 +124,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 +169,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 +204,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 +232,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 +256,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 +291,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) { 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/helpers.ts b/server/utils/helpers.ts index 15c80b3e..4795e701 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,26 @@ 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 | undefined +): ( + 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 { + if (client === undefined) { + throw new Error('Client cannot be undefined.'); + } + // fall back to default local cluster + return client.asScoped(request).callAsCurrentUser; + } +}