From 4ea8698b8da991eb699d7edd62f659f2b869ea32 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Thu, 18 Apr 2024 09:46:19 -0700 Subject: [PATCH 1/5] Support MDS on List, Detail, Dashboard, Overview pages (#722) * update snapshots Signed-off-by: Jackie Han * add snpshot Signed-off-by: Jackie Han * add data source client Signed-off-by: Jackie Han * test * neo List Detectors page Signed-off-by: Jackie Han * add opensearch_dashboards.json file Signed-off-by: Jackie Han * neo List Detectors page Signed-off-by: Jackie Han * test Signed-off-by: Jackie Han * Support MDS on DetectorDetails page Signed-off-by: Jackie Han * change 4/7 Signed-off-by: Jackie Han * version change Signed-off-by: Jackie Han * list detector list change Signed-off-by: Jackie Han * Support MDS on List, Detail, Dashboard, Overview pages Signed-off-by: Jackie Han * change version back to 3.0 Signed-off-by: Jackie Han * revert version change Signed-off-by: Jackie Han * make dataSourceId optional in DetectorListItem type Signed-off-by: Jackie Han * update imports Signed-off-by: Jackie Han * remove used function Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * add getter and setter for dataSource plugin Signed-off-by: Jackie Han * read dataSourceId from the url instead of passing props Signed-off-by: Jackie Han * addressing comments and run prettier Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * make getDataSourceManagementPlugin() optional Signed-off-by: Jackie Han * add comment Signed-off-by: Jackie Han * make dataSourceId type safe Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- opensearch_dashboards.json | 1 + public/anomaly_detection_app.tsx | 6 +- .../containers/AnomalyDetailsChart.tsx | 21 +- .../hooks/useFetchDetectorInfo.ts | 6 +- .../Components/AnomaliesDistribution.tsx | 6 + .../Components/AnomaliesLiveChart.tsx | 16 +- .../Dashboard/Container/DashboardOverview.tsx | 110 ++++++- public/pages/Dashboard/utils/utils.tsx | 14 +- .../components/Datasource/DataSource.tsx | 2 +- .../NameAndDescription/NameAndDescription.tsx | 2 +- .../containers/DetectorConfig.tsx | 8 +- .../containers/DetectorDetail.tsx | 81 ++++- .../hooks/useFetchMonitorInfo.ts | 9 +- .../containers/AnomalyHistory.tsx | 47 ++- .../containers/AnomalyResults.tsx | 19 +- .../containers/AnomalyResultsLiveChart.tsx | 7 + .../DetectorsList/containers/List/List.tsx | 112 +++++-- public/pages/DetectorsList/utils/helpers.ts | 12 +- .../pages/DetectorsList/utils/tableUtils.tsx | 226 ++++++------- .../containers/HistoricalDetectorResults.tsx | 21 +- .../SampleDataBox/SampleDataBox.tsx | 8 +- .../containers/AnomalyDetectionOverview.tsx | 157 +++++++-- public/pages/main/Main.tsx | 96 ++++-- public/pages/utils/anomalyResultUtils.ts | 2 + public/pages/utils/constants.ts | 1 + public/pages/utils/helpers.ts | 26 +- public/plugin.ts | 13 +- public/redux/reducers/ad.ts | 172 ++++++---- public/redux/reducers/alerting.ts | 13 +- public/redux/reducers/anomalyResults.ts | 97 +++--- public/redux/reducers/liveAnomalyResults.ts | 39 +-- public/redux/reducers/opensearch.ts | 77 +++-- public/redux/reducers/sampleData.ts | 18 +- public/services.ts | 12 + public/utils/constants.ts | 2 + server/models/types.ts | 5 + server/plugin.ts | 39 ++- server/routes/ad.ts | 310 +++++++++++++----- server/routes/alerting.ts | 28 +- server/routes/opensearch.ts | 100 ++++-- server/routes/sampleData.ts | 17 +- server/sampleData/utils/helpers.ts | 21 +- server/utils/helpers.ts | 29 ++ 43 files changed, 1447 insertions(+), 561 deletions(-) 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; + } +} From 2643c14d78fe86b4e37baf6693fbe0cc4a5943ee Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Fri, 19 Apr 2024 16:45:38 -0700 Subject: [PATCH 2/5] add MDS support for detector creation&Update flow (#728) * update snapshots Signed-off-by: Jackie Han * add snpshot Signed-off-by: Jackie Han * add data source client Signed-off-by: Jackie Han * test * neo List Detectors page Signed-off-by: Jackie Han * add opensearch_dashboards.json file Signed-off-by: Jackie Han * neo List Detectors page Signed-off-by: Jackie Han * test Signed-off-by: Jackie Han * Support MDS on DetectorDetails page Signed-off-by: Jackie Han * change 4/7 Signed-off-by: Jackie Han * version change Signed-off-by: Jackie Han * list detector list change Signed-off-by: Jackie Han * Support MDS on List, Detail, Dashboard, Overview pages Signed-off-by: Jackie Han * change version back to 3.0 Signed-off-by: Jackie Han * revert version change Signed-off-by: Jackie Han * make dataSourceId optional in DetectorListItem type Signed-off-by: Jackie Han * update imports Signed-off-by: Jackie Han * remove used function Signed-off-by: Jackie Han * cleanup Signed-off-by: Jackie Han * add getter and setter for dataSource plugin Signed-off-by: Jackie Han * read dataSourceId from the url instead of passing props Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * addressing comments and run prettier Signed-off-by: Jackie Han * addressing comments in neo1 branch Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * make getDataSourceManagementPlugin() optional Signed-off-by: Jackie Han * add comment Signed-off-by: Jackie Han * make dataSourceId type safe Signed-off-by: Jackie Han * add MDS support on creation page Signed-off-by: Jackie Han * updated methods to get data source query param from url Signed-off-by: Jackie Han * using helper to construct url with dataSourceId Signed-off-by: Jackie Han * add update page, and update urls on detector detail page Signed-off-by: Jackie Han * update update call router Signed-off-by: Jackie Han * update reducer and router Signed-off-by: Jackie Han * clean up Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * update enums Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- .../CreateDetectorButtons.tsx | 14 +- public/models/interfaces.ts | 6 + .../containers/AnomalyDetailsChart.tsx | 7 +- .../containers/ConfigureModel.tsx | 76 ++++++++-- .../containers/SampleAnomalies.tsx | 6 + .../containers/CreateDetectorSteps.tsx | 5 +- .../hooks/useFetchDetectorInfo.ts | 3 +- .../Components/AnomaliesDistribution.tsx | 6 +- .../Components/AnomaliesLiveChart.tsx | 6 +- .../Components/utils/DashboardHeader.tsx | 14 +- .../Dashboard/Container/DashboardOverview.tsx | 14 +- .../components/Datasource/DataSource.tsx | 18 ++- .../components/Timestamp/Timestamp.tsx | 11 +- .../containers/DefineDetector.tsx | 130 ++++++++++++++++-- .../containers/DetectorConfig.tsx | 7 +- .../containers/DetectorDetail.tsx | 29 ++-- .../DetectorJobs/containers/DetectorJobs.tsx | 49 ++++++- .../containers/AnomalyHistory.tsx | 5 +- .../containers/AnomalyResults.tsx | 5 +- .../containers/AnomalyResultsLiveChart.tsx | 7 +- .../DetectorsList/containers/List/List.tsx | 5 +- .../containers/HistoricalDetectorResults.tsx | 6 +- .../SampleDataBox/SampleDataBox.tsx | 8 +- .../containers/AnomalyDetectionOverview.tsx | 26 ++-- .../containers/ReviewAndCreate.tsx | 72 ++++++++-- public/pages/main/Main.tsx | 51 +++---- public/pages/utils/helpers.ts | 57 +++++++- public/redux/reducers/ad.ts | 89 +++++++----- public/redux/reducers/alerting.ts | 30 ++-- public/redux/reducers/opensearch.ts | 18 ++- public/redux/reducers/previewAnomalies.ts | 19 ++- public/utils/constants.ts | 1 - server/models/types.ts | 2 +- server/routes/ad.ts | 72 ++++++++-- server/routes/alerting.ts | 16 ++- server/routes/opensearch.ts | 17 ++- server/utils/constants.ts | 10 ++ 37 files changed, 680 insertions(+), 237 deletions(-) diff --git a/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx b/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx index 087789f4..551bb709 100644 --- a/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx +++ b/public/components/CreateDetectorButtons/CreateDetectorButtons.tsx @@ -12,14 +12,24 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import React from 'react'; import { APP_PATH, PLUGIN_NAME } from '../../utils/constants'; +import { useLocation } from 'react-router-dom'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../pages/utils/helpers'; export const CreateDetectorButtons = () => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + + const createDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(`${APP_PATH.CREATE_DETECTOR}`, dataSourceId, false); + + const sampleDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(`${APP_PATH.OVERVIEW}`, dataSourceId, false); + return ( Try a sample detector @@ -29,7 +39,7 @@ export const CreateDetectorButtons = () => { Create detector diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index eff5ead5..c2fc4590 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -14,6 +14,7 @@ import { DATA_TYPES } from '../utils/constants'; import { DETECTOR_STATE } from '../../server/utils/constants'; import { Duration } from 'moment'; import moment from 'moment'; +import { MDSQueryParams } from '../../server/models/types'; export type FieldInfo = { label: string; @@ -326,3 +327,8 @@ export interface ValidationSettingResponse { message: string; validationType: string; } + +export interface MDSStates { + queryParams: MDSQueryParams; + selectedDataSourceId: string; +} diff --git a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx index 96aa189e..74661b67 100644 --- a/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx +++ b/public/pages/AnomalyCharts/containers/AnomalyDetailsChart.tsx @@ -93,6 +93,7 @@ import { } from '../../../../server/utils/constants'; import { ENTITY_COLORS } from '../../DetectorResults/utils/constants'; import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface AnomalyDetailsChartProps { onDateRangeChange( @@ -123,8 +124,8 @@ export const AnomalyDetailsChart = React.memo( (props: AnomalyDetailsChartProps) => { const dispatch = useDispatch(); const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [showAlertsFlyout, setShowAlertsFlyout] = useState(false); const [alertAnnotations, setAlertAnnotations] = useState([]); const [isLoadingAlerts, setIsLoadingAlerts] = useState(false); @@ -331,7 +332,7 @@ export const AnomalyDetailsChart = React.memo( try { setIsLoadingAlerts(true); const result = await dispatch( - searchAlerts(monitorId, startDateTime, endDateTime) + searchAlerts(monitorId, startDateTime, endDateTime, dataSourceId) ); setIsLoadingAlerts(false); setTotalAlerts(get(result, 'response.totalAlerts')); diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index b2a21696..7a43f6bd 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -26,9 +26,9 @@ import { } from '@elastic/eui'; import { FormikProps, Formik } from 'formik'; import { get, isEmpty } from 'lodash'; -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, ReactElement } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; import { AppState } from '../../../redux/reducers'; import { getMappings } from '../../../redux/reducers/opensearch'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; @@ -49,7 +49,7 @@ import { Features } from '../components/Features'; import { CategoryField } from '../components/CategoryField'; import { AdvancedSettings } from '../components/AdvancedSettings'; import { SampleAnomalies } from './SampleAnomalies'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { Detector } from '../../../models/interfaces'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; @@ -58,6 +58,17 @@ import { ModelConfigurationFormikValues } from '../models/interfaces'; import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/interfaces'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { getErrorMessage } from '../../../utils/utils'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface ConfigureModelRouterProps { detectorId?: string; @@ -70,14 +81,20 @@ interface ConfigureModelProps initialValues?: ModelConfigurationFormikValues; setInitialValues?(initialValues: ModelConfigurationFormikValues): void; detectorDefinitionValues?: DetectorDefinitionFormikValues; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function ConfigureModel(props: ConfigureModelProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceId = MDSQueryParams.dataSourceId; + useHideSideNavBar(true, false); const detectorId = get(props, 'match.params.detectorId', ''); - const { detector, hasError } = useFetchDetectorInfo(detectorId); + const { detector, hasError } = useFetchDetectorInfo(detectorId, dataSourceId); const indexDataTypes = useSelector( (state: AppState) => state.opensearch.dataTypes ); @@ -100,7 +117,7 @@ export function ConfigureModel(props: ConfigureModelProps) { setIsHCDetector(true); } if (detector?.indices) { - dispatch(getMappings(detector.indices[0])); + dispatch(getMappings(detector.indices[0], dataSourceId)); } }, [detector]); @@ -111,7 +128,7 @@ export function ConfigureModel(props: ConfigureModelProps) { BREADCRUMBS.DETECTORS, { text: detector && detector.name ? detector.name : '', - href: `#/detectors/${detectorId}`, + href: constructHrefWithDataSourceId(`#/detectors/${detectorId}`, dataSourceId, false) }, BREADCRUMBS.EDIT_MODEL_CONFIGURATION, ]); @@ -126,7 +143,9 @@ export function ConfigureModel(props: ConfigureModelProps) { useEffect(() => { if (hasError) { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId('/detectors', dataSourceId, false) + ); } }, [hasError]); @@ -178,12 +197,18 @@ export function ConfigureModel(props: ConfigureModelProps) { }; const handleUpdateDetector = async (detectorToUpdate: Detector) => { - dispatch(updateDetector(detectorId, detectorToUpdate)) + dispatch(updateDetector(detectorId, detectorToUpdate, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector updated: ${response.response.name}` ); - props.history.push(`/detectors/${detectorId}/configurations/`); + props.history.push( + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + dataSourceId, + false + ) + ); }) .catch((err: any) => { core.notifications.toasts.addDanger( @@ -207,6 +232,24 @@ export function ConfigureModel(props: ConfigureModelProps) { ...props.initialValues, } as CreateDetectorFormikValues); + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { if (props.isEdit) { props.history.push( - `/detectors/${detectorId}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + dataSourceId, + false + ) ); } else { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); } }} > diff --git a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx index a6ad30ad..096316cd 100644 --- a/public/pages/ConfigureModel/containers/SampleAnomalies.tsx +++ b/public/pages/ConfigureModel/containers/SampleAnomalies.tsx @@ -49,6 +49,8 @@ import { BASE_DOCS_LINK } from '../../../utils/constants'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; +import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface SampleAnomaliesProps { detector: Detector; @@ -62,6 +64,9 @@ interface SampleAnomaliesProps { export function SampleAnomalies(props: SampleAnomaliesProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); const [isLoading, setIsLoading] = useState(false); @@ -150,6 +155,7 @@ export function SampleAnomalies(props: SampleAnomaliesProps) { periodStart: dateRange.startDate.valueOf(), periodEnd: dateRange.endDate.valueOf(), detector: detector, + dataSourceId: dataSourceId, }) ); setIsLoading(false); diff --git a/public/pages/CreateDetectorSteps/containers/CreateDetectorSteps.tsx b/public/pages/CreateDetectorSteps/containers/CreateDetectorSteps.tsx index 4a2a8759..924b0c0e 100644 --- a/public/pages/CreateDetectorSteps/containers/CreateDetectorSteps.tsx +++ b/public/pages/CreateDetectorSteps/containers/CreateDetectorSteps.tsx @@ -25,8 +25,11 @@ import { DetectorDefinitionFormikValues } from '../../DefineDetector/models/inte import { ModelConfigurationFormikValues } from '../../ConfigureModel/models/interfaces'; import { DetectorJobsFormikValues } from '../../DetectorJobs/models/interfaces'; import { CreateDetectorFormikValues } from '../models/interfaces'; +import { MountPoint } from '../../../../../../src/core/public'; -interface CreateDetectorStepsProps extends RouteComponentProps {} +interface CreateDetectorStepsProps extends RouteComponentProps { + setActionMenu: (menuMount: MountPoint | undefined) => void; +} export const CreateDetectorSteps = (props: CreateDetectorStepsProps) => { useHideSideNavBar(true, false); diff --git a/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts b/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts index 3f8d024d..a686775b 100644 --- a/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts +++ b/public/pages/CreateDetectorSteps/hooks/useFetchDetectorInfo.ts @@ -44,11 +44,10 @@ export const useFetchDetectorInfo = ( useEffect(() => { const fetchDetector = async () => { if (!detector) { - // 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)); + await dispatch(getMappings(selectedIndices, dataSourceId)); } }; if (detectorId) { diff --git a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx index 0d590adc..d9749777 100644 --- a/public/pages/Dashboard/Components/AnomaliesDistribution.tsx +++ b/public/pages/Dashboard/Components/AnomaliesDistribution.tsx @@ -34,7 +34,7 @@ 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'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface AnomaliesDistributionChartProps { selectedDetectors: DetectorListItem[]; } @@ -44,8 +44,8 @@ export const AnomaliesDistributionChart = ( ) => { const dispatch = useDispatch(); const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [anomalyDistribution, setAnomalyDistribution] = useState( [] as object[] diff --git a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx index ed4490c1..5e6e5f72 100644 --- a/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx +++ b/public/pages/Dashboard/Components/AnomaliesLiveChart.tsx @@ -59,6 +59,7 @@ import { import { ALL_CUSTOM_AD_RESULT_INDICES } from '../../utils/constants'; import { searchResults } from '../../../redux/reducers/anomalyResults'; import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface AnomaliesLiveChartProps { selectedDetectors: DetectorListItem[]; @@ -74,9 +75,8 @@ 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 MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [liveTimeRange, setLiveTimeRange] = useState({ startDateTime: moment().subtract(31, 'minutes'), endDateTime: moment(), diff --git a/public/pages/Dashboard/Components/utils/DashboardHeader.tsx b/public/pages/Dashboard/Components/utils/DashboardHeader.tsx index ed4fa87e..f39cf74f 100644 --- a/public/pages/Dashboard/Components/utils/DashboardHeader.tsx +++ b/public/pages/Dashboard/Components/utils/DashboardHeader.tsx @@ -17,12 +17,22 @@ import { EuiPageHeader, EuiTitle, } from '@elastic/eui'; -import { PLUGIN_NAME, APP_PATH } from '../../../../utils/constants'; +import { + PLUGIN_NAME, + APP_PATH, +} from '../../../../utils/constants'; +import { useLocation } from 'react-router-dom'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../../../pages/utils/helpers'; export interface DashboardHeaderProps { hasDetectors: boolean; } export const DashboardHeader = (props: DashboardHeaderProps) => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const createDetectorUrl = `${PLUGIN_NAME}#` + constructHrefWithDataSourceId(APP_PATH.CREATE_DETECTOR, dataSourceId, false); + return ( @@ -35,7 +45,7 @@ export const DashboardHeader = (props: DashboardHeaderProps) => { Create detector diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 88e90eda..93f0a983 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -17,7 +17,7 @@ import queryString from 'querystring'; import { useDispatch, useSelector } from 'react-redux'; import { get, isEmpty, cloneDeep } from 'lodash'; -import { DetectorListItem } from '../../../models/interfaces'; +import { DetectorListItem, MDSStates } from '../../../models/interfaces'; import { getIndices, getAliases } from '../../../redux/reducers/opensearch'; import { getDetectorList } from '../../../redux/reducers/ad'; import { @@ -38,17 +38,16 @@ import { AppState } from '../../../redux/reducers'; import { CatIndex, IndexAlias, - MDSQueryParams, } from '../../../../server/models/types'; import { getAllDetectorsQueryParamsWithDataSourceId, + getDataSourceFromURL, getVisibleOptions, } from '../../utils/helpers'; import { BREADCRUMBS } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { getDetectorStateOptions, - getURLQueryParams, } from '../../DetectorsList/utils/helpers'; import { DashboardHeader } from '../Components/utils/DashboardHeader'; import { EmptyDashboard } from '../Components/EmptyDashboard/EmptyDashboard'; @@ -71,11 +70,6 @@ interface OverviewProps extends RouteComponentProps { setActionMenu: (menuMount: MountPoint | undefined) => void; } -interface MDSOverviewState { - queryParams: MDSQueryParams; - selectedDataSourceId: string; -} - export function DashboardOverview(props: OverviewProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); @@ -94,8 +88,8 @@ export function DashboardOverview(props: OverviewProps) { const [selectedDetectorsName, setSelectedDetectorsName] = useState( [] as string[] ); - const queryParams = getURLQueryParams(props.location); - const [MDSOverviewState, setMDSOverviewState] = useState({ + const queryParams = getDataSourceFromURL(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ queryParams, selectedDataSourceId: queryParams.dataSourceId ? queryParams.dataSourceId diff --git a/public/pages/DefineDetector/components/Datasource/DataSource.tsx b/public/pages/DefineDetector/components/Datasource/DataSource.tsx index fdb810e3..4fbc882d 100644 --- a/public/pages/DefineDetector/components/Datasource/DataSource.tsx +++ b/public/pages/DefineDetector/components/Datasource/DataSource.tsx @@ -24,7 +24,11 @@ import { } from '../../../../redux/reducers/opensearch'; import { getError, isInvalid } from '../../../../utils/utils'; import { IndexOption } from './IndexOption'; -import { getVisibleOptions, sanitizeSearchText } from '../../../utils/helpers'; +import { + getDataSourceFromURL, + getVisibleOptions, + sanitizeSearchText, +} from '../../../utils/helpers'; import { validateIndex } from '../../../utils/validate'; import { DataFilterList } from '../DataFilterList/DataFilterList'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; @@ -32,6 +36,7 @@ import { DetectorDefinitionFormikValues } from '../../models/interfaces'; import { ModelConfigurationFormikValues } from '../../../ConfigureModel/models/interfaces'; import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../../ConfigureModel/utils/constants'; import { FILTER_TYPES } from '../../../../models/interfaces'; +import { useLocation } from 'react-router-dom'; interface DataSourceProps { formikProps: FormikProps; @@ -45,6 +50,9 @@ interface DataSourceProps { export function DataSource(props: DataSourceProps) { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [indexName, setIndexName] = useState( props.formikProps.values.index[0]?.label ); @@ -52,10 +60,10 @@ export function DataSource(props: DataSourceProps) { const opensearchState = useSelector((state: AppState) => state.opensearch); useEffect(() => { const getInitialIndices = async () => { - await dispatch(getIndices(queryText)); + await dispatch(getIndices(queryText, dataSourceId)); }; getInitialIndices(); - }, []); + }, [dataSourceId]); useEffect(() => { setIndexName(props.formikProps.values.index[0]?.label); @@ -65,7 +73,7 @@ export function DataSource(props: DataSourceProps) { if (searchValue !== queryText) { const sanitizedQuery = sanitizeSearchText(searchValue); setQueryText(sanitizedQuery); - await dispatch(getPrioritizedIndices(sanitizedQuery)); + await dispatch(getPrioritizedIndices(sanitizedQuery, dataSourceId)); } }, 300); @@ -73,7 +81,7 @@ export function DataSource(props: DataSourceProps) { const indexName = get(selectedOptions, '0.label', ''); setIndexName(indexName); if (indexName !== '') { - dispatch(getMappings(indexName)); + dispatch(getMappings(indexName, dataSourceId)); } if (indexName !== props.origIndex) { if (props.setNewIndexSelected) { diff --git a/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx b/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx index 16cf8594..48431fe8 100644 --- a/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx +++ b/public/pages/DefineDetector/components/Timestamp/Timestamp.tsx @@ -18,9 +18,13 @@ import ContentPanel from '../../../../components/ContentPanel/ContentPanel'; import { AppState } from '../../../../redux/reducers'; import { getPrioritizedIndices } from '../../../../redux/reducers/opensearch'; import { FormattedFormRow } from '../../../../components/FormattedFormRow/FormattedFormRow'; -import { sanitizeSearchText } from '../../../utils/helpers'; +import { + getDataSourceFromURL, + sanitizeSearchText, +} from '../../../utils/helpers'; import { getError, isInvalid, required } from '../../../../utils/utils'; import { DetectorDefinitionFormikValues } from '../../models/interfaces'; +import { useLocation } from 'react-router-dom'; interface TimestampProps { formikProps: FormikProps; @@ -28,6 +32,9 @@ interface TimestampProps { export function Timestamp(props: TimestampProps) { const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const opensearchState = useSelector((state: AppState) => state.opensearch); const selectedIndex = get(props, 'formikProps.values.index.0.label', ''); const isRemoteIndex = selectedIndex.includes(':'); @@ -37,7 +44,7 @@ export function Timestamp(props: TimestampProps) { if (searchValue !== queryText) { const sanitizedQuery = sanitizeSearchText(searchValue); setQueryText(sanitizedQuery); - await dispatch(getPrioritizedIndices(sanitizedQuery)); + await dispatch(getPrioritizedIndices(sanitizedQuery, dataSourceId)); } }, 300); diff --git a/public/pages/DefineDetector/containers/DefineDetector.tsx b/public/pages/DefineDetector/containers/DefineDetector.tsx index 459553c0..220b4526 100644 --- a/public/pages/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/DefineDetector/containers/DefineDetector.tsx @@ -9,8 +9,14 @@ * GitHub history for details. */ -import React, { Fragment, useEffect, useState } from 'react'; -import { RouteComponentProps } from 'react-router'; +import React, { + Fragment, + ReactElement, + useEffect, + useMemo, + useState, +} from 'react'; +import { RouteComponentProps, useLocation } from 'react-router'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { FormikProps, Formik } from 'formik'; @@ -30,10 +36,10 @@ import { import { updateDetector, matchDetector } from '../../../redux/reducers/ad'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { APIAction } from '../../../redux/middleware/types'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; -import { BREADCRUMBS } from '../../../utils/constants'; +import { BREADCRUMBS, DATA_SOURCE_ID } from '../../../utils/constants'; import { getErrorMessage, validateDetectorName } from '../../../utils/utils'; import { NameAndDescription } from '../components/NameAndDescription'; import { DataSource } from '../components/Datasource/DataSource'; @@ -46,10 +52,22 @@ import { clearModelConfiguration, } from '../utils/helpers'; import { DetectorDefinitionFormikValues } from '../models/interfaces'; -import { Detector, FILTER_TYPES } from '../../../models/interfaces'; +import { Detector, FILTER_TYPES, MDSStates } from '../../../models/interfaces'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { ModelConfigurationFormikValues } from 'public/pages/ConfigureModel/models/interfaces'; +import { + getDataSourceManagementPlugin, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceSelectableConfig, DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import queryString from 'querystring'; interface DefineDetectorRouterProps { detectorId?: string; @@ -62,16 +80,27 @@ interface DefineDetectorProps initialValues?: DetectorDefinitionFormikValues; setInitialValues?(initialValues: DetectorDefinitionFormikValues): void; setModelConfigValues?(initialValues: ModelConfigurationFormikValues): void; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export const DefineDetector = (props: DefineDetectorProps) => { + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch>(); useHideSideNavBar(true, false); const detectorId: string = get(props, 'match.params.detectorId', ''); - const { detector, hasError } = useFetchDetectorInfo(detectorId); + const { detector, hasError } = useFetchDetectorInfo(detectorId, dataSourceId); const [newIndexSelected, setNewIndexSelected] = useState(false); + const [MDSCreateState, setMDSCreateState] = useState({ + queryParams: MDSQueryParams, + selectedDataSourceId: dataSourceId ? dataSourceId : '', + }); + // To handle backward compatibility, we need to pass some fields via // props to the subcomponents so they can render correctly // @@ -117,13 +146,21 @@ export const DefineDetector = (props: DefineDetectorProps) => { // If no detector found with ID, redirect it to list useEffect(() => { + const { history, location } = props; + const updatedParams = { + dataSourceId: MDSCreateState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); if (props.isEdit && hasError) { core.notifications.toasts.addDanger( 'Unable to find the detector for editing' ); - props.history.push(`/detectors`); + props.history.push(constructHrefWithDataSourceId(`/detectors`, dataSourceId, false)); } - }, [props.isEdit]); + }, [props.isEdit, MDSCreateState]); const handleValidateName = async (detectorName: string) => { if (isEmpty(detectorName)) { @@ -134,7 +171,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { return error; } //TODO::Avoid making call if value is same - const resp = await dispatch(matchDetector(detectorName)); + const resp = await dispatch(matchDetector(detectorName, dataSourceId)); const match = get(resp, 'response.match', false); if (!match) { return undefined; @@ -196,12 +233,13 @@ export const DefineDetector = (props: DefineDetectorProps) => { const preparedDetector = newIndexSelected ? clearModelConfiguration(detectorToUpdate) : detectorToUpdate; - dispatch(updateDetector(detectorId, preparedDetector)) + dispatch(updateDetector(detectorId, preparedDetector, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector updated: ${response.response.name}` ); - props.history.push(`/detectors/${detectorId}/configurations/`); + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, dataSourceId, false)); }) .catch((err: any) => { core.notifications.toasts.addDanger( @@ -218,6 +256,61 @@ export const DefineDetector = (props: DefineDetectorProps) => { } }; + const handleDataSourceChange = ([event]) => { + const dataSourceId = event?.id; + if (!dataSourceId) { + getNotifications().toasts.addDanger( + prettifyErrorMessage('Unable to set data source.') + ); + } else { + setMDSCreateState({ + queryParams: dataSourceId, + selectedDataSourceId: dataSourceId, + }); + } + }; + + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + if (props.isEdit) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } else { + const DataSourceMenu = + getDataSourceManagementPlugin().ui.getDataSourceMenu(); + renderDataSourceComponent = useMemo(() => { + return ( + + handleDataSourceChange(dataSources), + }} + /> + ); + }, [getSavedObjectsClient(), getNotifications(), props.setActionMenu]); + } + } + + + return ( { > {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { onClick={() => { if (props.isEdit) { props.history.push( - `/detectors/${detectorId}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations/`, + MDSCreateState.selectedDataSourceId, + false + ) ); } else { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + MDSCreateState.selectedDataSourceId, + false + ) + ); } }} > diff --git a/public/pages/DetectorConfig/containers/DetectorConfig.tsx b/public/pages/DetectorConfig/containers/DetectorConfig.tsx index f2fec2d1..c78459da 100644 --- a/public/pages/DetectorConfig/containers/DetectorConfig.tsx +++ b/public/pages/DetectorConfig/containers/DetectorConfig.tsx @@ -19,7 +19,8 @@ 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'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; + interface DetectorConfigProps extends RouteComponentProps { detectorId: string; onEditFeatures(): void; @@ -29,8 +30,8 @@ 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 MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const detector = useSelector( (state: AppState) => state.ad.detectors[props.detectorId] ); diff --git a/public/pages/DetectorDetail/containers/DetectorDetail.tsx b/public/pages/DetectorDetail/containers/DetectorDetail.tsx index a523b04c..7145be2c 100644 --- a/public/pages/DetectorDetail/containers/DetectorDetail.tsx +++ b/public/pages/DetectorDetail/containers/DetectorDetail.tsx @@ -71,6 +71,7 @@ import { getNotifications, getSavedObjectsClient, } from '../../../services'; +import { constructHrefWithDataSourceId, getDataSourceFromURL } from '../../../pages/utils/helpers'; export interface DetectorRouterProps { detectorId?: string; @@ -119,8 +120,8 @@ export const DetectorDetail = (props: DetectorDetailProps) => { 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 MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const { detector, hasError, isLoadingDetector, errorMessage } = useFetchDetectorInfo(detectorId, dataSourceId); @@ -196,7 +197,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ? prettifyErrorMessage(errorMessage) : 'Unable to find the detector' ); - props.history.push('/detectors'); + props.history.push(constructHrefWithDataSourceId('/detectors', dataSourceId, false)); } }, [hasError]); @@ -223,8 +224,8 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.CONFIGURATIONS, }); - props.history.push( - `/detectors/${detectorId}/configurations?dataSourceId=${dataSourceId}` + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/configurations`, dataSourceId, false) ); }, []); @@ -233,8 +234,8 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.HISTORICAL, }); - props.history.push( - `/detectors/${detectorId}/historical?dataSourceId=${dataSourceId}` + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/historical`, dataSourceId, false) ); }, []); @@ -243,8 +244,8 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, selectedTab: route, }); - props.history.push( - `/detectors/${detectorId}/${route}?dataSourceId=${dataSourceId}` + props.history.push(constructHrefWithDataSourceId( + `/detectors/${detectorId}/${route}`, dataSourceId, false) ); }; @@ -273,9 +274,9 @@ export const DetectorDetail = (props: DetectorDetailProps) => { const listener: Listener = { onSuccess: () => { if (detectorDetailModel.showStopDetectorModalFor === 'detector') { - props.history.push(`/detectors/${detectorId}/edit`); + props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/edit`, dataSourceId, false)); } else { - props.history.push(`/detectors/${detectorId}/features`); + props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/features`, dataSourceId, false)); } hideStopDetectorModal(); }, @@ -333,7 +334,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { await dispatch(deleteDetector(detectorId, dataSourceId)); core.notifications.toasts.addSuccess(`Successfully deleted the detector`); hideDeleteDetectorModal(); - props.history.push('/detectors'); + props.history.push(constructHrefWithDataSourceId('/detectors', dataSourceId, false)); } catch (err) { core.notifications.toasts.addDanger( prettifyErrorMessage( @@ -350,7 +351,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, showStopDetectorModalFor: 'detector', }) - : props.history.push(`/detectors/${detectorId}/edit`); + : props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/edit`, dataSourceId, false)); }; const handleEditFeature = () => { @@ -359,7 +360,7 @@ export const DetectorDetail = (props: DetectorDetailProps) => { ...detectorDetailModel, showStopDetectorModalFor: 'features', }) - : props.history.push(`/detectors/${detectorId}/features`); + : props.history.push(constructHrefWithDataSourceId(`/detectors/${detectorId}/features`, dataSourceId, false)); }; const lightStyles = { diff --git a/public/pages/DetectorJobs/containers/DetectorJobs.tsx b/public/pages/DetectorJobs/containers/DetectorJobs.tsx index 22599da4..e919ae53 100644 --- a/public/pages/DetectorJobs/containers/DetectorJobs.tsx +++ b/public/pages/DetectorJobs/containers/DetectorJobs.tsx @@ -23,25 +23,41 @@ import { } from '@elastic/eui'; import { FormikProps, Formik } from 'formik'; import { isEmpty } from 'lodash'; -import React, { Fragment, useState, useEffect } from 'react'; +import React, { Fragment, useState, useEffect, ReactElement } from 'react'; import { BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { DetectorJobsFormikValues } from '../models/interfaces'; import { RealTimeJob } from '../components/RealTimeJob'; import { HistoricalJob } from '../components/HistoricalJob'; import { convertTimestampToNumber } from '../../../utils/utils'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface DetectorJobsProps extends RouteComponentProps { setStep?(stepNumber: number): void; initialValues: DetectorJobsFormikValues; setInitialValues(initialValues: DetectorJobsFormikValues): void; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function DetectorJobs(props: DetectorJobsProps) { const core = React.useContext(CoreServicesContext) as CoreStart; + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); const [realTime, setRealTime] = useState( @@ -103,6 +119,24 @@ export function DetectorJobs(props: DetectorJobsProps) { props.setInitialValues(values); }; + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); }} > Cancel diff --git a/public/pages/DetectorResults/containers/AnomalyHistory.tsx b/public/pages/DetectorResults/containers/AnomalyHistory.tsx index 8cb86217..4030340b 100644 --- a/public/pages/DetectorResults/containers/AnomalyHistory.tsx +++ b/public/pages/DetectorResults/containers/AnomalyHistory.tsx @@ -96,6 +96,7 @@ import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { prettifyErrorMessage } from '../../../../server/utils/helpers'; import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface AnomalyHistoryProps { detector: Detector; @@ -128,8 +129,8 @@ export const AnomalyHistory = (props: AnomalyHistoryProps) => { ? props.detector.detectionDateRange.endTime : moment().valueOf(); const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [dateRange, setDateRange] = useState({ startDate: initialStartDate, endDate: initialEndDate, diff --git a/public/pages/DetectorResults/containers/AnomalyResults.tsx b/public/pages/DetectorResults/containers/AnomalyResults.tsx index d09e26ab..4d869961 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -68,6 +68,7 @@ import { SampleIndexDetailsCallout } from '../../Overview/components/SampleIndex import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { DEFAULT_SHINGLE_SIZE } from '../../../utils/constants'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface AnomalyResultsProps extends RouteComponentProps { detectorId: string; @@ -85,8 +86,8 @@ export function AnomalyResults(props: AnomalyResultsProps) { (state: AppState) => state.ad.detectors[detectorId] ); const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; useEffect(() => { core.chrome.setBreadcrumbs([ diff --git a/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx b/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx index d38db529..8d9449f2 100644 --- a/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResultsLiveChart.tsx @@ -52,13 +52,12 @@ import { } from '../../AnomalyCharts/utils/constants'; import { LIVE_ANOMALY_CHART_THEME } from '../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; -import { dateFormatter } from '../../utils/helpers'; +import { dateFormatter, getDataSourceFromURL } from '../../utils/helpers'; import { darkModeEnabled } from '../../../utils/opensearchDashboardsUtils'; import { EuiIcon } from '@elastic/eui'; import { formatAnomalyNumber } from '../../../../server/utils/helpers'; import { getDetectorLiveResults } from '../../../redux/reducers/liveAnomalyResults'; import { useLocation } from 'react-router-dom'; -import { DATA_SOURCE_ID } from '../../../utils/constants'; interface AnomalyResultsLiveChartProps { detector: Detector; @@ -69,8 +68,8 @@ export const AnomalyResultsLiveChart = ( ) => { const dispatch = useDispatch(); const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const [firstLoading, setFirstLoading] = useState(true); const [isFullScreen, setIsFullScreen] = useState(false); diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index fefb8e2a..933d6d0a 100644 --- a/public/pages/DetectorsList/containers/List/List.tsx +++ b/public/pages/DetectorsList/containers/List/List.tsx @@ -45,6 +45,7 @@ import { import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants'; import { DETECTOR_STATE } from '../../../../../server/utils/constants'; import { + constructHrefWithDataSourceId, getAllDetectorsQueryParamsWithDataSourceId, getVisibleOptions, sanitizeSearchText, @@ -693,6 +694,8 @@ export const DetectorList = (props: ListProps) => { const columns = getColumns(state.selectedDataSourceId); + const createDetectorUrl =`${PLUGIN_NAME}#` + constructHrefWithDataSourceId(APP_PATH.CREATE_DETECTOR, state.selectedDataSourceId, false); + return ( @@ -716,7 +719,7 @@ export const DetectorList = (props: ListProps) => { Create detector , diff --git a/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx b/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx index f42509d1..b295bfd7 100644 --- a/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx +++ b/public/pages/HistoricalDetectorResults/containers/HistoricalDetectorResults.tsx @@ -50,7 +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'; +import { getDataSourceFromURL } from '../../../pages/utils/helpers'; interface HistoricalDetectorResultsProps extends RouteComponentProps { detectorId: string; @@ -64,8 +64,8 @@ export function HistoricalDetectorResults( 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 MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; const adState = useSelector((state: AppState) => state.ad); const allDetectors = adState.detectors; diff --git a/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx b/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx index 8a61d126..98d8a469 100644 --- a/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx +++ b/public/pages/Overview/components/SampleDataBox/SampleDataBox.tsx @@ -21,8 +21,9 @@ import { EuiCard, EuiHorizontalRule, } from '@elastic/eui'; -import { DATA_SOURCE_ID, PLUGIN_NAME } from '../../../../utils/constants'; +import { PLUGIN_NAME } from '../../../../utils/constants'; import { useLocation } from 'react-router-dom'; +import { getDataSourceFromURL } from '../../../../pages/utils/helpers'; interface SampleDataBoxProps { title: string; @@ -39,8 +40,9 @@ interface SampleDataBoxProps { export const SampleDataBox = (props: SampleDataBoxProps) => { const location = useLocation(); - const dataSourceId = - new URLSearchParams(location.search).get(DATA_SOURCE_ID) || ''; + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + return (
void; } -interface MDSOverviewState { - queryParams: MDSQueryParams; - selectedDataSourceId: string; -} - export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const isLoadingSampleDetectors = useSelector( @@ -105,8 +99,8 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const [showHostHealthDetailsFlyout, setShowHostHealthDetailsFlyout] = useState(false); - const queryParams = getURLQueryParams(props.location); - const [MDSOverviewState, setMDSOverviewState] = useState({ + const queryParams = getDataSourceFromURL(props.location); + const [MDSOverviewState, setMDSOverviewState] = useState({ queryParams, selectedDataSourceId: queryParams.dataSourceId ? queryParams.dataSourceId @@ -129,9 +123,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { search: queryString.stringify(updatedParams), }); - if (dataSourceEnabled ? MDSOverviewState.selectedDataSourceId : true) { - fetchData(); - } + fetchData(); }, [MDSOverviewState]); // fetch smaple detectors and sample indices @@ -261,6 +253,12 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { }, [getSavedObjectsClient, getNotifications, props.setActionMenu]); } + const createDetectorUrl = + `${PLUGIN_NAME}#` + + (dataSourceEnabled + ? `${APP_PATH.CREATE_DETECTOR}?dataSourceId=${MDSOverviewState.selectedDataSourceId}` + : `${APP_PATH.CREATE_DETECTOR}`); + return isLoadingSampleDetectors && isLoadingSampleIndices ? (
@@ -278,7 +276,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { Create detector diff --git a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx index 1055c2d4..0f7335e7 100644 --- a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx +++ b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx @@ -30,13 +30,13 @@ import { } from '../../../redux/reducers/ad'; import { Formik, FormikHelpers } from 'formik'; import { get, isEmpty } from 'lodash'; -import React, { Fragment, useEffect, useState } from 'react'; -import { RouteComponentProps } from 'react-router'; +import React, { Fragment, ReactElement, useEffect, useState } from 'react'; +import { RouteComponentProps, useLocation } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../redux/reducers'; import { BREADCRUMBS, MAX_DETECTORS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; -import { CoreStart } from '../../../../../../src/core/public'; +import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { CreateDetectorFormikValues } from '../../CreateDetectorSteps/models/interfaces'; import { DetectorDefinitionFields } from '../components/DetectorDefinitionFields'; @@ -53,15 +53,31 @@ import { ValidationSettingResponse, VALIDATION_ISSUE_TYPES, } from '../../../models/interfaces'; +import { + constructHrefWithDataSourceId, + getDataSourceFromURL, +} from '../../../pages/utils/helpers'; +import { + getDataSourceManagementPlugin, + getDataSourcePlugin, + getNotifications, + getSavedObjectsClient, +} from '../../../services'; +import { DataSourceViewConfig } from '../../../../../../src/plugins/data_source_management/public'; interface ReviewAndCreateProps extends RouteComponentProps { setStep(stepNumber: number): void; values: CreateDetectorFormikValues; + setActionMenu: (menuMount: MountPoint | undefined) => void; } export function ReviewAndCreate(props: ReviewAndCreateProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch(); + const location = useLocation(); + const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceId = MDSQueryParams.dataSourceId; + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; useHideSideNavBar(true, false); // This variable indicates if validate API declared detector settings as valid for AD creation @@ -90,7 +106,9 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { // meaning validation has passed and succesful callout will display or validation has failed // and callouts displaying what the issue is will be displayed instead. useEffect(() => { - dispatch(validateDetector(formikToDetector(props.values), 'model')) + dispatch( + validateDetector(formikToDetector(props.values), 'model', dataSourceId) + ) .then((resp: any) => { if (isEmpty(Object.keys(resp.response))) { setValidDetectorSettings(true); @@ -174,14 +192,14 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { setIsCreatingDetector(true); formikHelpers.setSubmitting(true); const detectorToCreate = formikToDetector(values); - dispatch(createDetector(detectorToCreate)) + dispatch(createDetector(detectorToCreate, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Detector created: ${detectorToCreate.name}` ); // Optionally start RT job if (get(props, 'values.realTime', true)) { - dispatch(startDetector(response.response.id)) + dispatch(startDetector(response.response.id, dataSourceId)) .then((response: any) => { core.notifications.toasts.addSuccess( `Successfully started the real-time detector` @@ -209,7 +227,12 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { ); dispatch( //@ts-ignore - startHistoricalDetector(response.response.id, startTime, endTime) + startHistoricalDetector( + response.response.id, + dataSourceId, + startTime, + endTime + ) ) .then((response: any) => { core.notifications.toasts.addSuccess( @@ -229,11 +252,15 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { } props.history.push( - `/detectors/${response.response.id}/configurations/` + constructHrefWithDataSourceId( + `/detectors/${response.response.id}/configurations/`, + dataSourceId, + false + ) ); }) .catch((err: any) => { - dispatch(getDetectorCount()).then((response: any) => { + dispatch(getDetectorCount(dataSourceId)).then((response: any) => { const totalDetectors = get(response, 'response.count', 0); if (totalDetectors === MAX_DETECTORS) { core.notifications.toasts.addDanger( @@ -262,6 +289,24 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { // Converting to detector for passing to the fields const detectorToCreate = formikToDetector(props.values); + let renderDataSourceComponent: ReactElement | null = null; + if (dataSourceEnabled) { + const DataSourceMenu = + getDataSourceManagementPlugin()?.ui.getDataSourceMenu(); + renderDataSourceComponent = ( + + ); + } + return ( {(formikProps) => ( + {dataSourceEnabled && renderDataSourceComponent} { - props.history.push('/detectors'); + props.history.push( + constructHrefWithDataSourceId( + '/detectors', + dataSourceId, + false + ) + ); }} > Cancel diff --git a/public/pages/main/Main.tsx b/public/pages/main/Main.tsx index 4fc9a0be..6dfa2951 100644 --- a/public/pages/main/Main.tsx +++ b/public/pages/main/Main.tsx @@ -26,6 +26,7 @@ import { CoreServicesConsumer } from '../../components/CoreServices/CoreServices import { CoreStart, MountPoint } from '../../../../../src/core/public'; import { AnomalyDetectionOverview } from '../Overview'; import { getURLQueryParams } from '../DetectorsList/utils/helpers'; +import { constructHrefWithDataSourceId } from '../utils/helpers'; enum Navigation { AnomalyDetection = 'Anomaly detection', @@ -48,31 +49,15 @@ export function Main(props: MainProps) { const totalDetectors = adState.totalDetectors; 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: constructHrefWithDataSourceId( + href: constructHrefWithDataSourceId( APP_PATH.OVERVIEW, - '', - dataSourceId + dataSourceId, + true ), items: [ { @@ -80,8 +65,8 @@ export function Main(props: MainProps) { id: 1, href: constructHrefWithDataSourceId( APP_PATH.DASHBOARD, - '', - dataSourceId + dataSourceId, + true ), isSelected: props.location.pathname === APP_PATH.DASHBOARD, }, @@ -90,8 +75,8 @@ export function Main(props: MainProps) { id: 2, href: constructHrefWithDataSourceId( APP_PATH.LIST_DETECTORS, - existingParams, - dataSourceId + dataSourceId, + true ), isSelected: props.location.pathname === APP_PATH.LIST_DETECTORS, }, @@ -132,21 +117,28 @@ export function Main(props: MainProps) { exact path={APP_PATH.CREATE_DETECTOR} render={(props: RouteComponentProps) => ( - + )} /> ( - + )} /> ( - + )} /> )} /> - ( - - )} - /> ( diff --git a/public/pages/utils/helpers.ts b/public/pages/utils/helpers.ts index f5c0fcfe..99f98168 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -8,14 +8,19 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ - -import { CatIndex, IndexAlias } from '../../../server/models/types'; +import queryString from 'query-string'; +import { + CatIndex, + IndexAlias, + MDSQueryParams, +} from '../../../server/models/types'; import sortBy from 'lodash/sortBy'; import { DetectorListItem } from '../../models/interfaces'; -import { SORT_DIRECTION } from '../../../server/utils/constants'; -import { ALL_INDICES, ALL_DETECTOR_STATES, MAX_DETECTORS } from './constants'; +import { DETECTORS_QUERY_PARAMS, SORT_DIRECTION } from '../../../server/utils/constants'; +import { ALL_INDICES, ALL_DETECTOR_STATES, MAX_DETECTORS, DEFAULT_QUERY_PARAMS } from './constants'; import { DETECTOR_STATE } from '../../../server/utils/constants'; import { timeFormatter } from '@elastic/charts'; +import { getDataSourcePlugin } from '../../services'; export function sanitizeSearchText(searchValue: string): string { if (!searchValue || searchValue == '*') { @@ -122,7 +127,7 @@ export const getAllDetectorsQueryParamsWithDataSourceId = ( size: MAX_DETECTORS, sortDirection: SORT_DIRECTION.ASC, sortField: 'name', - dataSourceId + dataSourceId, }); export const getSampleDetectorsQueryParamsWithDataSouceId = ( @@ -134,5 +139,45 @@ export const getSampleDetectorsQueryParamsWithDataSouceId = ( size: MAX_DETECTORS, sortDirection: SORT_DIRECTION.ASC, sortField: 'name', - dataSourceId + dataSourceId, }); + +export const getDataSourceFromURL = (location: { + search: string; +}): MDSQueryParams => { + const queryParams = queryString.parse(location.search); + const dataSourceId = queryParams.dataSourceId; + return { dataSourceId: typeof dataSourceId === 'string' ? dataSourceId : '' }; +}; + +export const constructHrefWithDataSourceId = ( + basePath: string, + dataSourceId: string, + withHash: Boolean +): string => { + const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const url = new URLSearchParams(); + + // Set up base parameters for '/detectors' + if (basePath.startsWith('/detectors')) { + url.set(DETECTORS_QUERY_PARAMS.FROM, DEFAULT_QUERY_PARAMS.from.toString()); + url.set(DETECTORS_QUERY_PARAMS.SIZE, DEFAULT_QUERY_PARAMS.size.toString()); + url.set(DETECTORS_QUERY_PARAMS.SEARCH, DEFAULT_QUERY_PARAMS.search); + url.set(DETECTORS_QUERY_PARAMS.INDICES, DEFAULT_QUERY_PARAMS.indices); + url.set(DETECTORS_QUERY_PARAMS.SORT_FIELD, DEFAULT_QUERY_PARAMS.sortField); + url.set(DETECTORS_QUERY_PARAMS.SORT_DIRECTION, SORT_DIRECTION.ASC); + } + + if (dataSourceEnabled && dataSourceId) { + url.set('dataSourceId', dataSourceId); + } + + // we share this helper function to construct the href with dataSourceId + // some places we need to return the url with hash, some places we don't need to + // so adding this flag to indicate if we want to return the url with hash + if (withHash) { + return `#${basePath}?${url.toString()}`; + } + + return `${basePath}?${url.toString()}`; +}; diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index 67e7b558..545396d0 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -388,23 +388,27 @@ export const createDetector = ( export const validateDetector = ( requestBody: Detector, - validationType: string -): APIAction => ({ - type: VALIDATE_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/_validate/${validationType}`, { - body: JSON.stringify(requestBody), - }), -}); + validationType: string, + dataSourceId: string +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/_validate/${validationType}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: VALIDATE_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify(requestBody), + }), + }; +} export const getDetector = ( detectorId: string, dataSourceId: string ): APIAction => { - const baseUrl = `..${AD_NODE_API.DETECTOR}`; - const url = dataSourceId - ? `${baseUrl}/${detectorId}/${dataSourceId}` - : `${baseUrl}/${detectorId}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { type: GET_DETECTOR, @@ -437,15 +441,21 @@ export const searchDetector = (requestBody: any): APIAction => ({ export const updateDetector = ( detectorId: string, - requestBody: Detector -): APIAction => ({ - type: UPDATE_DETECTOR, - request: (client: HttpSetup) => - client.put(`..${AD_NODE_API.DETECTOR}/${detectorId}`, { - body: JSON.stringify(requestBody), - }), - detectorId, -}); + requestBody: Detector, + dataSourceId: string +): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: UPDATE_DETECTOR, + request: (client: HttpSetup) => + client.put(url, { + body: JSON.stringify(requestBody), + }), + detectorId, + }; +} export const deleteDetector = ( detectorId: string, @@ -519,10 +529,8 @@ 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}`; + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; return { type: STOP_HISTORICAL_DETECTOR, @@ -538,16 +546,27 @@ export const getDetectorProfile = (detectorId: string): APIAction => ({ detectorId, }); -export const matchDetector = (detectorName: string): APIAction => ({ - type: MATCH_DETECTOR, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/${detectorName}/_match`), -}); +export const matchDetector = ( + detectorName: string, + dataSourceId: string): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorName}/_match`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; -export const getDetectorCount = (): APIAction => ({ - type: GET_DETECTOR_COUNT, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API.DETECTOR}/_count`, {}), -}); + return { + type: MATCH_DETECTOR, + request: (client: HttpSetup) => client.get(url), + }; +}; + +export const getDetectorCount = (dataSourceId: string): APIAction => { + const url = dataSourceId ? + `..${AD_NODE_API.DETECTOR}/_count/${dataSourceId}` : + `..${AD_NODE_API.DETECTOR}/_count`; + + return { + type: GET_DETECTOR_COUNT, + request: (client: HttpSetup) => client.get(url), + }; +}; export default reducer; diff --git a/public/redux/reducers/alerting.ts b/public/redux/reducers/alerting.ts index 79bfb3c9..47fed3ea 100644 --- a/public/redux/reducers/alerting.ts +++ b/public/redux/reducers/alerting.ts @@ -107,17 +107,23 @@ export const searchMonitors = (dataSourceId: string): APIAction => { export const searchAlerts = ( monitorId: string, startTime: number, - endTime: number -): APIAction => ({ - type: SEARCH_ALERTS, - request: (client: HttpSetup) => - client.get(`..${ALERTING_NODE_API.ALERTS}`, { - query: { - monitorId: monitorId, - startTime: startTime, - endTime: endTime, - }, - }), -}); + endTime: number, + dataSourceId: string +): APIAction => { + const baseUrl = `..${ALERTING_NODE_API.ALERTS}`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: SEARCH_ALERTS, + request: (client: HttpSetup) => + client.get(url, { + query: { + monitorId: monitorId, + startTime: startTime, + endTime: endTime, + }, + }), + }; +} export default reducer; diff --git a/public/redux/reducers/opensearch.ts b/public/redux/reducers/opensearch.ts index 0c354c45..b80be8da 100644 --- a/public/redux/reducers/opensearch.ts +++ b/public/redux/reducers/opensearch.ts @@ -271,13 +271,17 @@ export const getAliases = ( }; }; -export const getMappings = (searchKey: string = ''): APIAction => ({ - type: GET_MAPPINGS, - request: (client: HttpSetup) => - client.get(`..${AD_NODE_API._MAPPINGS}`, { - query: { index: searchKey }, - }), -}); +export const getMappings = (searchKey: string = '', dataSourceId: string): APIAction => { + const url = dataSourceId ? `${AD_NODE_API._MAPPINGS}/${dataSourceId}` : AD_NODE_API._MAPPINGS; + + return { + type: GET_MAPPINGS, + request: (client: HttpSetup) => + client.get(`..${url}`, { + query: { index: searchKey }, + }), + }; +}; export const searchOpenSearch = (requestData: any): APIAction => ({ type: SEARCH_OPENSEARCH, diff --git a/public/redux/reducers/previewAnomalies.ts b/public/redux/reducers/previewAnomalies.ts index 9ee231f7..753cafb8 100644 --- a/public/redux/reducers/previewAnomalies.ts +++ b/public/redux/reducers/previewAnomalies.ts @@ -59,12 +59,17 @@ const reducer = handleActions( initialDetectorsState ); -export const previewDetector = (requestBody: any): APIAction => ({ - type: PREVIEW_DETECTOR, - request: (client: HttpSetup) => - client.post(`..${AD_NODE_API.DETECTOR}/preview`, { - body: JSON.stringify(requestBody), - }), -}); +export const previewDetector = (requestBody: any, dataSourceId: string): APIAction => { + const baseUrl = `..${AD_NODE_API.DETECTOR}/preview`; + const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; + + return { + type: PREVIEW_DETECTOR, + request: (client: HttpSetup) => + client.post(url, { + body: JSON.stringify(requestBody), + }), + }; +} export default reducer; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 99988470..ac7907a7 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -33,7 +33,6 @@ export const APP_PATH = { EDIT_DETECTOR: '/detectors/:detectorId/edit', EDIT_FEATURES: '/detectors/:detectorId/features/', DETECTOR_DETAIL: '/detectors/:detectorId/', - CREATE_DETECTOR_STEPS: '/create-detector-steps', OVERVIEW: '/overview', }; diff --git a/server/models/types.ts b/server/models/types.ts index ab494971..c1d679b0 100644 --- a/server/models/types.ts +++ b/server/models/types.ts @@ -110,7 +110,7 @@ export type DetectorResultsQueryParams = { }; export type MDSQueryParams = { - dataSourceId?: string; + dataSourceId: string; }; export type Entity = { diff --git a/server/routes/ad.ts b/server/routes/ad.ts index 033365d2..1acbcc18 100644 --- a/server/routes/ad.ts +++ b/server/routes/ad.ts @@ -92,7 +92,9 @@ export function registerADRoutes(apiRouter: Router, adService: AdService) { apiRouter.get('/detectors/_list', adService.getDetectors); apiRouter.get('/detectors/_list/{dataSourceId}', adService.getDetectors); + // preview detector apiRouter.post('/detectors/preview', adService.previewDetector); + apiRouter.post('/detectors/preview/{dataSourceId}', adService.previewDetector); // get detector anomaly results apiRouter.get( @@ -148,8 +150,13 @@ export function registerADRoutes(apiRouter: Router, adService: AdService) { adService.getDetector ); + // match detector apiRouter.get('/detectors/{detectorName}/_match', adService.matchDetector); + apiRouter.get('/detectors/{detectorName}/_match/{dataSourceId}', adService.matchDetector); + + // get detector count apiRouter.get('/detectors/_count', adService.getDetectorCount); + apiRouter.get('/detectors/_count/{dataSourceId}', adService.getDetectorCount); // post get top anomaly results apiRouter.post( @@ -161,10 +168,15 @@ export function registerADRoutes(apiRouter: Router, adService: AdService) { adService.getTopAnomalyResults ); + // validate detector apiRouter.post( '/detectors/_validate/{validationType}', adService.validateDetector ); + apiRouter.post( + '/detectors/_validate/{validationType}/{dataSourceId}', + adService.validateDetector + ); } export default class AdService { @@ -219,12 +231,21 @@ export default class AdService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + const requestBody = JSON.stringify( convertPreviewInputKeysToSnakeCase(request.body) ); - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.previewDetector', { + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const response = await callWithRequest( + 'ad.previewDetector', { body: requestBody, }); const transformedKeys = mapKeysDeep(response, toCamel); @@ -318,12 +339,21 @@ export default class AdService { let { validationType } = request.params as { validationType: string; }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + const requestBody = JSON.stringify( convertPreviewInputKeysToSnakeCase(request.body) ); - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.validateDetector', { + const response = await callWithRequest( + 'ad.validateDetector', { body: requestBody, validationType: validationType, }); @@ -1222,9 +1252,18 @@ export default class AdService { ): Promise> => { try { const { detectorName } = request.params as { detectorName: string }; - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.matchDetector', { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'ad.matchDetector', { detectorName, }); return opensearchDashboardsResponse.ok({ @@ -1247,9 +1286,18 @@ export default class AdService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { try { - const response = await this.client - .asScoped(request) - .callAsCurrentUser('ad.detectorCount'); + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'ad.detectorCount'); return opensearchDashboardsResponse.ok({ body: { ok: true, diff --git a/server/routes/alerting.ts b/server/routes/alerting.ts index c0728fb5..59fce0ae 100644 --- a/server/routes/alerting.ts +++ b/server/routes/alerting.ts @@ -35,6 +35,7 @@ export function registerAlertingRoutes( ); apiRouter.get('/monitors/alerts', alertingService.searchAlerts); + apiRouter.get('/monitors/alerts/{dataSourceId}', alertingService.searchAlerts); } export default class AlertingService { @@ -143,9 +144,18 @@ export default class AlertingService { startTime?: number; endTime?: number; }; - const response = await this.client - .asScoped(request) - .callAsCurrentUser('alerting.searchAlerts', { + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = callWithRequest( + 'alerting.searchAlerts', { monitorId: monitorId, startTime: startTime, endTime: endTime, diff --git a/server/routes/opensearch.ts b/server/routes/opensearch.ts index 622db995..3a8444d9 100644 --- a/server/routes/opensearch.ts +++ b/server/routes/opensearch.ts @@ -46,6 +46,8 @@ export function registerOpenSearchRoutes( apiRouter.get('/_aliases/{dataSourceId}', opensearchService.getAliases); apiRouter.get('/_mappings', opensearchService.getMapping); + apiRouter.get('/_mappings/{dataSourceId}', opensearchService.getMapping); + apiRouter.post('/_search', opensearchService.executeSearch); apiRouter.put('/create_index', opensearchService.createIndex); @@ -337,10 +339,19 @@ export default class OpenSearchService { opensearchDashboardsResponse: OpenSearchDashboardsResponseFactory ): Promise> => { const { index } = request.query as { index: string }; + const { dataSourceId = '' } = request.params as { dataSourceId?: string }; + try { - const response = await this.client - .asScoped(request) - .callAsCurrentUser('indices.getMapping', { + const callWithRequest = getClientBasedOnDataSource( + context, + this.dataSourceEnabled, + request, + dataSourceId, + this.client + ); + + const response = await callWithRequest( + 'indices.getMapping', { index, }); return opensearchDashboardsResponse.ok({ diff --git a/server/utils/constants.ts b/server/utils/constants.ts index 4477db0f..f0ac63f9 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -46,6 +46,16 @@ export enum SORT_DIRECTION { DESC = 'desc', } +export enum DETECTORS_QUERY_PARAMS { + FROM = 'from', + SIZE = 'size', + SEARCH = 'search', + INDICES = 'indices', + SORT_FIELD = 'sortField', + SORT_DIRECTION = 'sortDirection', + NAME = 'name', +} + export enum AD_DOC_FIELDS { DATA_START_TIME = 'data_start_time', DATA_END_TIME = 'data_end_time', From 348a29c9a9381a60da68d8c37859dad3d31bc80e Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Tue, 23 Apr 2024 11:39:53 -0700 Subject: [PATCH 3/5] update actionOption attribution for all data soure components (#732) Signed-off-by: Jackie Han --- public/models/interfaces.ts | 2 +- .../Dashboard/Container/DashboardOverview.tsx | 6 ++-- .../containers/DefineDetector.tsx | 6 ++-- .../DetectorsList/containers/List/List.tsx | 8 +++-- .../containers/AnomalyDetectionOverview.tsx | 6 ++-- public/pages/utils/helpers.ts | 6 ++-- public/redux/reducers/ad.ts | 29 ++++++++++--------- public/redux/reducers/alerting.ts | 4 +-- public/redux/reducers/anomalyResults.ts | 4 +-- public/redux/reducers/opensearch.ts | 13 +++++---- public/redux/reducers/previewAnomalies.ts | 2 +- public/redux/reducers/sampleData.ts | 2 +- 12 files changed, 50 insertions(+), 38 deletions(-) diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index c2fc4590..3d836207 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -330,5 +330,5 @@ export interface ValidationSettingResponse { export interface MDSStates { queryParams: MDSQueryParams; - selectedDataSourceId: string; + selectedDataSourceId: string | undefined; } diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 93f0a983..6478f0ad 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -93,7 +93,7 @@ export function DashboardOverview(props: OverviewProps) { queryParams, selectedDataSourceId: queryParams.dataSourceId ? queryParams.dataSourceId - : '', + : undefined, }); const getDetectorOptions = (detectorsIdMap: { @@ -267,7 +267,9 @@ export function DashboardOverview(props: OverviewProps) { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: [{ id: MDSOverviewState.selectedDataSourceId }], + activeOption: MDSOverviewState.selectedDataSourceId !== undefined + ? [{ id: MDSOverviewState.selectedDataSourceId }] + : undefined, savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/DefineDetector/containers/DefineDetector.tsx b/public/pages/DefineDetector/containers/DefineDetector.tsx index 220b4526..ee4e795d 100644 --- a/public/pages/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/DefineDetector/containers/DefineDetector.tsx @@ -98,7 +98,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { const [MDSCreateState, setMDSCreateState] = useState({ queryParams: MDSQueryParams, - selectedDataSourceId: dataSourceId ? dataSourceId : '', + selectedDataSourceId: dataSourceId ? dataSourceId : undefined, }); // To handle backward compatibility, we need to pass some fields via @@ -297,7 +297,9 @@ export const DefineDetector = (props: DefineDetectorProps) => { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: [{ id: MDSCreateState.selectedDataSourceId }], + activeOption: MDSCreateState.selectedDataSourceId !== undefined + ? [{ id: MDSCreateState.selectedDataSourceId }] + : undefined, savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index 933d6d0a..e98d10eb 100644 --- a/public/pages/DetectorsList/containers/List/List.tsx +++ b/public/pages/DetectorsList/containers/List/List.tsx @@ -109,7 +109,7 @@ interface ListState { queryParams: GetDetectorsQueryParams; selectedDetectorStates: DETECTOR_STATE[]; selectedIndices: string[]; - selectedDataSourceId: string; + selectedDataSourceId: string | undefined; } interface ConfirmModalState { isOpen: boolean; @@ -208,7 +208,7 @@ export const DetectorList = (props: ListProps) => { : ALL_INDICES, selectedDataSourceId: queryParams.dataSourceId ? queryParams.dataSourceId - : '', + : undefined, }); // Set breadcrumbs on page initialization @@ -681,7 +681,9 @@ export const DetectorList = (props: ListProps) => { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: [{ id: state.selectedDataSourceId }], + activeOption: state.selectedDataSourceId !== undefined + ? [{ id: state.selectedDataSourceId }] + : undefined, savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx index cb8bcd63..d6c00927 100644 --- a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx +++ b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx @@ -104,7 +104,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { queryParams, selectedDataSourceId: queryParams.dataSourceId ? queryParams.dataSourceId - : '', + : undefined, }); // Set breadcrumbs on page initialization @@ -242,7 +242,9 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: [{ id: MDSOverviewState.selectedDataSourceId }], + activeOption: MDSOverviewState.selectedDataSourceId !== undefined + ? [{ id: MDSOverviewState.selectedDataSourceId }] + : undefined, savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/utils/helpers.ts b/public/pages/utils/helpers.ts index 99f98168..321f6fc4 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -119,7 +119,7 @@ export const formatNumber = (data: any) => { }; export const getAllDetectorsQueryParamsWithDataSourceId = ( - dataSourceId: string + dataSourceId: string = '' ) => ({ from: 0, search: '', @@ -131,7 +131,7 @@ export const getAllDetectorsQueryParamsWithDataSourceId = ( }); export const getSampleDetectorsQueryParamsWithDataSouceId = ( - dataSourceId: string + dataSourceId: string = '' ) => ({ from: 0, search: 'sample', @@ -152,7 +152,7 @@ export const getDataSourceFromURL = (location: { export const constructHrefWithDataSourceId = ( basePath: string, - dataSourceId: string, + dataSourceId: string = '', withHash: Boolean ): string => { const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index 545396d0..3fa06ad3 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -371,7 +371,7 @@ const reducer = handleActions( export const createDetector = ( requestBody: Detector, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const url = dataSourceId ? `..${AD_NODE_API.DETECTOR}/${dataSourceId}` @@ -389,7 +389,7 @@ export const createDetector = ( export const validateDetector = ( requestBody: Detector, validationType: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/_validate/${validationType}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -405,7 +405,7 @@ export const validateDetector = ( export const getDetector = ( detectorId: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -420,9 +420,11 @@ export const getDetector = ( export const getDetectorList = ( queryParams: GetDetectorsQueryParams ): APIAction => { + const dataSourceId = queryParams.dataSourceId || ''; + const baseUrl = `..${AD_NODE_API.DETECTOR}/_list`; - const url = queryParams.dataSourceId - ? `${baseUrl}/${queryParams.dataSourceId}` + const url = dataSourceId + ? `${baseUrl}/${dataSourceId}` : baseUrl; return { @@ -442,7 +444,7 @@ export const searchDetector = (requestBody: any): APIAction => ({ export const updateDetector = ( detectorId: string, requestBody: Detector, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -459,7 +461,7 @@ export const updateDetector = ( export const deleteDetector = ( detectorId: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -473,7 +475,7 @@ export const deleteDetector = ( export const startDetector = ( detectorId: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/start`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -487,7 +489,7 @@ export const startDetector = ( export const startHistoricalDetector = ( detectorId: string, - dataSourceId: string, + dataSourceId: string = '', startTime: number, endTime: number ): APIAction => { @@ -513,7 +515,7 @@ export const startHistoricalDetector = ( export const stopDetector = ( detectorId: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${false}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -527,7 +529,7 @@ export const stopDetector = ( export const stopHistoricalDetector = ( detectorId: string, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorId}/stop/${true}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -548,7 +550,8 @@ export const getDetectorProfile = (detectorId: string): APIAction => ({ export const matchDetector = ( detectorName: string, - dataSourceId: string): APIAction => { + dataSourceId: string = '' +): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/${detectorName}/_match`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -558,7 +561,7 @@ export const matchDetector = ( }; }; -export const getDetectorCount = (dataSourceId: string): APIAction => { +export const getDetectorCount = (dataSourceId: string = ''): APIAction => { const url = dataSourceId ? `..${AD_NODE_API.DETECTOR}/_count/${dataSourceId}` : `..${AD_NODE_API.DETECTOR}/_count`; diff --git a/public/redux/reducers/alerting.ts b/public/redux/reducers/alerting.ts index 47fed3ea..788e6f83 100644 --- a/public/redux/reducers/alerting.ts +++ b/public/redux/reducers/alerting.ts @@ -94,7 +94,7 @@ const reducer = handleActions( initialDetectorsState ); -export const searchMonitors = (dataSourceId: string): APIAction => { +export const searchMonitors = (dataSourceId: string = ''): APIAction => { const baseUrl = `..${ALERTING_NODE_API._SEARCH}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -108,7 +108,7 @@ export const searchAlerts = ( monitorId: string, startTime: number, endTime: number, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${ALERTING_NODE_API.ALERTS}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; diff --git a/public/redux/reducers/anomalyResults.ts b/public/redux/reducers/anomalyResults.ts index 5eda008e..bf643a3c 100644 --- a/public/redux/reducers/anomalyResults.ts +++ b/public/redux/reducers/anomalyResults.ts @@ -94,7 +94,7 @@ const reducer = handleActions( export const getDetectorResults = ( id: string, - dataSourceId: string, + dataSourceId: string = '', queryParams: any, isHistorical: boolean, resultIndex: string, @@ -119,7 +119,7 @@ export const getDetectorResults = ( export const searchResults = ( requestBody: any, resultIndex: string, - dataSourceId: string, + dataSourceId: string = '', onlyQueryCustomResultIndex: boolean ): APIAction => { let baseUrl = `..${AD_NODE_API.DETECTOR}/results/_search`; diff --git a/public/redux/reducers/opensearch.ts b/public/redux/reducers/opensearch.ts index b80be8da..fd153bca 100644 --- a/public/redux/reducers/opensearch.ts +++ b/public/redux/reducers/opensearch.ts @@ -21,6 +21,7 @@ import { getPathsPerDataType } from './mapper'; import { CatIndex, IndexAlias } from '../../../server/models/types'; import { AD_NODE_API } from '../../../utils/constants'; import { get } from 'lodash'; +import { data } from 'jquery'; const GET_INDICES = 'opensearch/GET_INDICES'; const GET_ALIASES = 'opensearch/GET_ALIASES'; @@ -246,7 +247,7 @@ const reducer = handleActions( initialState ); -export const getIndices = (searchKey = '', dataSourceId: string) => { +export const getIndices = (searchKey = '', dataSourceId: string = '') => { const baseUrl = `..${AD_NODE_API._INDICES}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -259,7 +260,7 @@ export const getIndices = (searchKey = '', dataSourceId: string) => { export const getAliases = ( searchKey: string = '', - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const baseUrl = `..${AD_NODE_API._ALIASES}`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; @@ -271,7 +272,7 @@ export const getAliases = ( }; }; -export const getMappings = (searchKey: string = '', dataSourceId: string): APIAction => { +export const getMappings = (searchKey: string = '', dataSourceId: string = ''): APIAction => { const url = dataSourceId ? `${AD_NODE_API._MAPPINGS}/${dataSourceId}` : AD_NODE_API._MAPPINGS; return { @@ -291,7 +292,7 @@ export const searchOpenSearch = (requestData: any): APIAction => ({ }), }); -export const createIndex = (indexConfig: any, dataSourceId: string): APIAction => { +export const createIndex = (indexConfig: any, dataSourceId: string = ''): APIAction => { const url = dataSourceId ? `${AD_NODE_API.CREATE_INDEX}/${dataSourceId}` : AD_NODE_API.CREATE_INDEX; @@ -304,7 +305,7 @@ export const createIndex = (indexConfig: any, dataSourceId: string): APIAction = }; }; -export const bulk = (body: any, dataSourceId: string): APIAction => { +export const bulk = (body: any, dataSourceId: string = ''): APIAction => { const url = dataSourceId ? `${AD_NODE_API.BULK}/${dataSourceId}` : AD_NODE_API.BULK; @@ -322,7 +323,7 @@ export const deleteIndex = (index: string): APIAction => ({ }); export const getPrioritizedIndices = - (searchKey: string, dataSourceId: string): ThunkAction => + (searchKey: string, dataSourceId: string = ''): ThunkAction => async (dispatch, getState) => { //Fetch Indices and Aliases with text provided await dispatch(getIndices(searchKey, dataSourceId)); diff --git a/public/redux/reducers/previewAnomalies.ts b/public/redux/reducers/previewAnomalies.ts index 753cafb8..01e36f29 100644 --- a/public/redux/reducers/previewAnomalies.ts +++ b/public/redux/reducers/previewAnomalies.ts @@ -59,7 +59,7 @@ const reducer = handleActions( initialDetectorsState ); -export const previewDetector = (requestBody: any, dataSourceId: string): APIAction => { +export const previewDetector = (requestBody: any, dataSourceId: string = ''): APIAction => { const baseUrl = `..${AD_NODE_API.DETECTOR}/preview`; const url = dataSourceId ? `${baseUrl}/${dataSourceId}` : baseUrl; diff --git a/public/redux/reducers/sampleData.ts b/public/redux/reducers/sampleData.ts index b92ce773..576c515e 100644 --- a/public/redux/reducers/sampleData.ts +++ b/public/redux/reducers/sampleData.ts @@ -56,7 +56,7 @@ const reducer = handleActions( export const createSampleData = ( sampleDataType: SAMPLE_TYPE, - dataSourceId: string + dataSourceId: string = '' ): APIAction => { const url = dataSourceId ? `..${AD_NODE_API.CREATE_SAMPLE_DATA}/${sampleDataType}/${dataSourceId}` From 1bed6ab6c9def2271fcde8981e25bfea35445c05 Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Thu, 25 Apr 2024 17:56:29 -0700 Subject: [PATCH 4/5] Add MDS UTs and bug fixes (#734) * working on ut Signed-off-by: Jackie Han * update ut Signed-off-by: Jackie Han * update UTs and add bug fixes Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- package.json | 2 +- .../__tests__/AnomaliesChart.test.tsx | 19 +++++- .../__tests__/AnomalyDetailsChart.test.tsx | 56 +++++++++++----- .../__tests__/AnomalyOccurrenceChart.test.tsx | 57 +++++++++++----- .../containers/ConfigureModel.tsx | 67 +++++++++++++------ .../__tests__/ConfigureModel.test.tsx | 9 +++ .../__tests__/EmptyDashboard.test.tsx | 17 ++++- .../EmptyDashboard.test.tsx.snap | 4 +- .../__tests__/AnomaliesLiveCharts.test.tsx | 28 +++++++- .../Dashboard/Container/DashboardOverview.tsx | 23 ++++--- .../components/__tests__/DataFilter.test.tsx | 4 +- .../containers/DefineDetector.tsx | 57 ++++++++++------ .../__tests__/DefineDetector.test.tsx | 10 +++ .../containers/DetectorDetail.tsx | 24 ++++--- .../DetectorJobs/containers/DetectorJobs.tsx | 22 ++++-- .../__tests__/DetectorJobs.test.tsx | 8 +++ .../containers/AnomalyResults.tsx | 21 ++++-- .../__tests__/EmptyMessage.test.tsx | 33 +++++++-- .../__snapshots__/EmptyMessage.test.tsx.snap | 4 +- .../ConfirmDeleteDetectorsModal.test.tsx | 4 +- .../DetectorsList/containers/List/List.tsx | 24 ++++--- .../containers/List/__tests__/List.test.tsx | 12 +++- .../utils/__tests__/helpers.test.ts | 3 + .../utils/__tests__/tableUtils.test.tsx | 16 ++--- .../__tests__/SampleDataBox.test.tsx | 41 +++++++++++- .../__snapshots__/SampleDataBox.test.tsx.snap | 2 +- .../containers/AnomalyDetectionOverview.tsx | 13 ++-- .../AnomalyDetectionOverview.test.tsx | 33 ++++++--- .../AnomalyDetectionOverview.test.tsx.snap | 11 ++- .../containers/ReviewAndCreate.tsx | 24 ++++--- .../__tests__/ReviewAndCreate.test.tsx | 8 +++ public/pages/utils/helpers.ts | 4 +- public/plugin.ts | 6 +- .../reducers/__tests__/anomalyResults.test.ts | 11 ++- public/services.ts | 8 ++- public/utils/constants.ts | 9 +++ server/plugin.ts | 20 +++--- server/routes/ad.ts | 2 +- server/utils/helpers.ts | 5 +- 39 files changed, 536 insertions(+), 185 deletions(-) diff --git a/package.json b/package.json index 97b79f4d..c5b88cd0 100644 --- a/package.json +++ b/package.json @@ -56,4 +56,4 @@ "browserify-sign": "^4.2.2", "axios": "^1.6.1" } -} \ No newline at end of file +} diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx index aebc1ea2..02f63fcd 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomaliesChart.test.tsx @@ -22,6 +22,8 @@ import { FAKE_ANOMALIES_RESULT, FAKE_DATE_RANGE, } from '../../../../pages/utils/__tests__/constants'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const DEFAULT_PROPS = { onDateRangeChange: jest.fn(), @@ -47,17 +49,32 @@ const DEFAULT_PROPS = { entityAnomalySummaries: [], } as AnomaliesChartProps; +const history = createMemoryHistory(); + const renderDataFilter = (chartProps: AnomaliesChartProps) => ({ ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('renders the component for sample / preview', () => { console.error = jest.fn(); const { getByText, getAllByText } = renderDataFilter({ diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx index fbf1e66d..5bed2db9 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomalyDetailsChart.test.tsx @@ -22,6 +22,18 @@ import { } from '../../../../pages/utils/__tests__/constants'; import { INITIAL_ANOMALY_SUMMARY } from '../../utils/constants'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); const renderAnomalyOccurenceChart = ( isNotSample: boolean, @@ -30,27 +42,41 @@ const renderAnomalyOccurenceChart = ( ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); + test('renders the component in case of Sample Anomaly', () => { console.error = jest.fn(); const { getByText } = renderAnomalyOccurenceChart(false, false); diff --git a/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx b/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx index 802d7d6c..94b4ffd4 100644 --- a/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx +++ b/public/pages/AnomalyCharts/containers/__tests__/AnomalyOccurrenceChart.test.tsx @@ -16,6 +16,8 @@ import { mockedStore } from '../../../../redux/utils/testUtils'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { coreServicesMock } from '../../../../../test/mocks'; import { AnomalyOccurrenceChart } from '../AnomalyOccurrenceChart'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; import { FAKE_ANOMALY_DATA, FAKE_DATE_RANGE, @@ -23,6 +25,16 @@ import { import { INITIAL_ANOMALY_SUMMARY } from '../../utils/constants'; import { getRandomDetector } from '../../../../redux/reducers/__tests__/utils'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); + const renderAnomalyOccurenceChart = ( isNotSample: boolean, isHCDetector: boolean @@ -30,28 +42,41 @@ const renderAnomalyOccurenceChart = ( ...render( - + + + ), }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('renders the component in case of Sample Anomaly', () => { console.error = jest.fn(); const { getByText } = renderAnomalyOccurenceChart(false, false); diff --git a/public/pages/ConfigureModel/containers/ConfigureModel.tsx b/public/pages/ConfigureModel/containers/ConfigureModel.tsx index 7a43f6bd..6d2996e8 100644 --- a/public/pages/ConfigureModel/containers/ConfigureModel.tsx +++ b/public/pages/ConfigureModel/containers/ConfigureModel.tsx @@ -32,7 +32,7 @@ import { RouteComponentProps, useLocation } from 'react-router-dom'; import { AppState } from '../../../redux/reducers'; import { getMappings } from '../../../redux/reducers/opensearch'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; -import { BREADCRUMBS, BASE_DOCS_LINK } from '../../../utils/constants'; +import { BREADCRUMBS, BASE_DOCS_LINK, MDS_BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { updateDetector } from '../../../redux/reducers/ad'; import { @@ -64,7 +64,7 @@ import { } from '../../../pages/utils/helpers'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../services'; @@ -89,7 +89,7 @@ export function ConfigureModel(props: ConfigureModelProps) { const dispatch = useDispatch(); const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); @@ -122,30 +122,55 @@ export function ConfigureModel(props: ConfigureModelProps) { }, [detector]); useEffect(() => { - if (props.isEdit) { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { - text: detector && detector.name ? detector.name : '', - href: constructHrefWithDataSourceId(`#/detectors/${detectorId}`, dataSourceId, false) - }, - BREADCRUMBS.EDIT_MODEL_CONFIGURATION, - ]); + if (dataSourceEnabled) { + if (props.isEdit) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { + text: detector && detector.name ? detector.name : '', + href: constructHrefWithDataSourceId(`#/detectors/${detectorId}`, dataSourceId, false) + }, + MDS_BREADCRUMBS.EDIT_MODEL_CONFIGURATION, + ]); + } else { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + MDS_BREADCRUMBS.CREATE_DETECTOR, + ]); + } } else { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (props.isEdit) { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { + text: detector && detector.name ? detector.name : '', + href: `#/detectors/${detectorId}`, + }, + BREADCRUMBS.EDIT_MODEL_CONFIGURATION, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } } }, [detector]); useEffect(() => { if (hasError) { - props.history.push( - constructHrefWithDataSourceId('/detectors', dataSourceId, false) - ); + if(dataSourceEnabled) { + props.history.push( + constructHrefWithDataSourceId('/detectors', dataSourceId, false) + ); + } + else { + props.history.push('/detectors'); + } } }, [hasError]); diff --git a/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx b/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx index 77a9e370..6cd845bd 100644 --- a/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx +++ b/public/pages/ConfigureModel/containers/__tests__/ConfigureModel.test.tsx @@ -25,6 +25,14 @@ import { CoreServicesContext } from '../../../../components/CoreServices/CoreSer import { INITIAL_DETECTOR_DEFINITION_VALUES } from '../../../DefineDetector/utils/constants'; import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( @@ -34,6 +42,7 @@ const renderWithRouter = (isEdit: boolean = false) => ({ render={(props: RouteComponentProps) => ( ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + +const history = createMemoryHistory(); describe(' spec', () => { describe('Empty results', () => { test('renders component with empty message', async () => { - const { container } = render(); + const { container } = render( + + + ); expect(container.firstChild).toMatchSnapshot(); }); }); diff --git a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap index 67762b0d..e71ff3ba 100644 --- a/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap +++ b/public/pages/Dashboard/Components/EmptyDashboard/__tests__/__snapshots__/EmptyDashboard.test.tsx.snap @@ -72,7 +72,7 @@ exports[` spec Empty results renders component with empty messa @@ -93,7 +93,7 @@ exports[` spec Empty results renders component with empty messa diff --git a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx index 23f65912..fd043c49 100644 --- a/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx +++ b/public/pages/Dashboard/Components/__tests__/AnomaliesLiveCharts.test.tsx @@ -11,6 +11,17 @@ import { Provider } from 'react-redux'; import { coreServicesMock } from '../../../../../test/mocks'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { mockedStore } from '../../../../redux/utils/testUtils'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; + +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const anomalyResponse = [ { ok: true, @@ -41,11 +52,26 @@ jest.mock('../../utils/utils', () => ({ visualizeAnomalyResultForXYChart: jest.fn(), })); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); test('AnomaliesLiveChart with Sample anomaly data', async () => { + const history = createMemoryHistory(); + const { container, getByTestId, getAllByText, getByText } = render( - + + + ); diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index 6478f0ad..d1728536 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -44,7 +44,7 @@ import { getDataSourceFromURL, getVisibleOptions, } from '../../utils/helpers'; -import { BREADCRUMBS } from '../../../utils/constants'; +import { BREADCRUMBS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { getDetectorStateOptions, @@ -60,7 +60,7 @@ import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { DataSourceSelectableConfig } from '../../../../../../src/plugins/data_source_management/public'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../services'; @@ -79,7 +79,7 @@ export function DashboardOverview(props: OverviewProps) { const errorGettingDetectors = adState.errorMessage; const isLoadingDetectors = adState.requesting; - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const [currentDetectors, setCurrentDetectors] = useState( Object.values(allDetectorList) @@ -141,7 +141,7 @@ export function DashboardOverview(props: OverviewProps) { const handleDataSourceChange = ([event]) => { const dataSourceId = event?.id; - if (!dataSourceId) { + if (dataSourceEnabled && dataSourceId === undefined) { getNotifications().toasts.addDanger( prettifyErrorMessage('Unable to set data source.') ); @@ -238,10 +238,17 @@ export function DashboardOverview(props: OverviewProps) { }, [errorGettingDetectors]); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DASHBOARD, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(MDSOverviewState.selectedDataSourceId), + MDS_BREADCRUMBS.DASHBOARD(MDSOverviewState.selectedDataSourceId), + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DASHBOARD, + ]); + } }); useEffect(() => { diff --git a/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx b/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx index 4eb5f4cd..2b42c5e1 100644 --- a/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx +++ b/public/pages/DefineDetector/components/DataFilterList/components/__tests__/DataFilter.test.tsx @@ -150,7 +150,7 @@ describe('dataFilter', () => { getAllByText('cpu'); }); userEvent.click(getByTestId('cancelFilter0Button')); - }, 10000); + }, 30000); test('renders data filter, click on custom', async () => { const { container, getByText, getByTestId } = renderWithProvider(); getByText('Create custom label?'); @@ -164,5 +164,5 @@ describe('dataFilter', () => { getByText('Use query DSL'); }); userEvent.click(getByTestId('cancelFilter0Button')); - }, 10000); + }, 30000); }); diff --git a/public/pages/DefineDetector/containers/DefineDetector.tsx b/public/pages/DefineDetector/containers/DefineDetector.tsx index ee4e795d..21d6ccba 100644 --- a/public/pages/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/DefineDetector/containers/DefineDetector.tsx @@ -39,7 +39,7 @@ import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDe import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { APIAction } from '../../../redux/middleware/types'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; -import { BREADCRUMBS, DATA_SOURCE_ID } from '../../../utils/constants'; +import { BREADCRUMBS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { getErrorMessage, validateDetectorName } from '../../../utils/utils'; import { NameAndDescription } from '../components/NameAndDescription'; import { DataSource } from '../components/Datasource/DataSource'; @@ -58,7 +58,7 @@ import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { ModelConfigurationFormikValues } from 'public/pages/ConfigureModel/models/interfaces'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../services'; @@ -87,7 +87,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); const dataSourceId = MDSQueryParams.dataSourceId; - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const core = React.useContext(CoreServicesContext) as CoreStart; const dispatch = useDispatch>(); @@ -126,22 +126,41 @@ export const DefineDetector = (props: DefineDetectorProps) => { // Set breadcrumbs based on create / update useEffect(() => { - const createOrEditBreadcrumb = props.isEdit - ? BREADCRUMBS.EDIT_DETECTOR - : BREADCRUMBS.CREATE_DETECTOR; - let breadCrumbs = [ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - createOrEditBreadcrumb, - ]; - if (detector && detector.name) { - breadCrumbs.splice(2, 0, { - text: detector.name, - //@ts-ignore - href: `#/detectors/${detectorId}`, - }); + if (dataSourceEnabled) { + const createOrEditBreadcrumb = props.isEdit + ? MDS_BREADCRUMBS.EDIT_DETECTOR + : MDS_BREADCRUMBS.CREATE_DETECTOR; + let breadCrumbs = [ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + createOrEditBreadcrumb, + ]; + if (detector && detector.name) { + breadCrumbs.splice(2, 0, { + text: detector.name, + //@ts-ignore + href: `#/detectors/${detectorId}/?dataSourceId=${dataSourceId}`, + }); + } + core.chrome.setBreadcrumbs(breadCrumbs); + } else { + const createOrEditBreadcrumb = props.isEdit + ? BREADCRUMBS.EDIT_DETECTOR + : BREADCRUMBS.CREATE_DETECTOR; + let breadCrumbs = [ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + createOrEditBreadcrumb, + ]; + if (detector && detector.name) { + breadCrumbs.splice(2, 0, { + text: detector.name, + //@ts-ignore + href: `#/detectors/${detectorId}`, + }); + } + core.chrome.setBreadcrumbs(breadCrumbs); } - core.chrome.setBreadcrumbs(breadCrumbs); }); // If no detector found with ID, redirect it to list @@ -258,7 +277,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { const handleDataSourceChange = ([event]) => { const dataSourceId = event?.id; - if (!dataSourceId) { + if (dataSourceEnabled && dataSourceId === undefined) { getNotifications().toasts.addDanger( prettifyErrorMessage('Unable to set data source.') ); diff --git a/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx b/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx index 84f9a212..0ba9421f 100644 --- a/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx +++ b/public/pages/DefineDetector/containers/__tests__/DefineDetector.test.tsx @@ -27,6 +27,14 @@ import { testDetectorDefinitionValues, } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouterEmpty = (isEdit: boolean = false) => ({ ...render( @@ -36,6 +44,7 @@ const renderWithRouterEmpty = (isEdit: boolean = false) => ({ render={(props: RouteComponentProps) => ( ({ render={(props: RouteComponentProps) => ( { 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 dataSourceEnabled = getDataSourceEnabled().enabled; const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); const dataSourceId = MDSQueryParams.dataSourceId; @@ -203,11 +203,19 @@ export const DetectorDetail = (props: DetectorDetailProps) => { useEffect(() => { if (detector) { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { text: detector ? detector.name : '' }, - ]); + if(dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { text: detector ? detector.name : '' }, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { text: detector ? detector.name : '' }, + ]); + } } }, [detector]); diff --git a/public/pages/DetectorJobs/containers/DetectorJobs.tsx b/public/pages/DetectorJobs/containers/DetectorJobs.tsx index e919ae53..435b4dc8 100644 --- a/public/pages/DetectorJobs/containers/DetectorJobs.tsx +++ b/public/pages/DetectorJobs/containers/DetectorJobs.tsx @@ -39,7 +39,7 @@ import { } from '../../../pages/utils/helpers'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../services'; @@ -56,7 +56,7 @@ export function DetectorJobs(props: DetectorJobsProps) { const core = React.useContext(CoreServicesContext) as CoreStart; const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const dataSourceId = MDSQueryParams.dataSourceId; useHideSideNavBar(true, false); @@ -73,11 +73,19 @@ export function DetectorJobs(props: DetectorJobsProps) { }, []); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } }, []); const handleFormValidation = async ( diff --git a/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx b/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx index ac8e9b7b..cea842df 100644 --- a/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx +++ b/public/pages/DetectorJobs/containers/__tests__/DetectorJobs.test.tsx @@ -24,6 +24,14 @@ import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; import { INITIAL_DETECTOR_JOB_VALUES } from '../../utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( diff --git a/public/pages/DetectorResults/containers/AnomalyResults.tsx b/public/pages/DetectorResults/containers/AnomalyResults.tsx index 4d869961..b09b18b2 100644 --- a/public/pages/DetectorResults/containers/AnomalyResults.tsx +++ b/public/pages/DetectorResults/containers/AnomalyResults.tsx @@ -34,6 +34,7 @@ import { BREADCRUMBS, DATA_SOURCE_ID, FEATURE_DATA_POINTS_WINDOW, + MDS_BREADCRUMBS, MISSING_FEATURE_DATA_SEVERITY, } from '../../../utils/constants'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; @@ -69,6 +70,7 @@ import { CoreStart } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; import { DEFAULT_SHINGLE_SIZE } from '../../../utils/constants'; import { getDataSourceFromURL } from '../../../pages/utils/helpers'; +import { getDataSourceEnabled } from '../../../services'; interface AnomalyResultsProps extends RouteComponentProps { detectorId: string; @@ -87,14 +89,23 @@ export function AnomalyResults(props: AnomalyResultsProps) { ); const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); + const dataSourceEnabled = getDataSourceEnabled().enabled; const dataSourceId = MDSQueryParams.dataSourceId; useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - { text: detector ? detector.name : '' }, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + { text: detector ? detector.name : '' }, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + { text: detector ? detector.name : '' }, + ]); + } dispatch(getDetector(detectorId, dataSourceId)); }, []); diff --git a/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx b/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx index db9cfebf..af8c0feb 100644 --- a/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx +++ b/public/pages/DetectorsList/components/EmptyMessage/__tests__/EmptyMessage.test.tsx @@ -11,16 +11,41 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { EmptyDetectorMessage } from '../EmptyMessage'; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); describe('Empty results', () => { test('renders component with empty message', () => { + const history = createMemoryHistory(); + const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Create detector'); diff --git a/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap b/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap index ed30a454..a429c5f9 100644 --- a/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap +++ b/public/pages/DetectorsList/components/EmptyMessage/__tests__/__snapshots__/EmptyMessage.test.tsx.snap @@ -33,7 +33,7 @@ exports[` spec Empty results renders component with empt @@ -54,7 +54,7 @@ exports[` spec Empty results renders component with empt diff --git a/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx b/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx index fa9987e4..1af99b47 100644 --- a/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx +++ b/public/pages/DetectorsList/containers/ConfirmActionModals/__tests__/ConfirmDeleteDetectorsModal.test.tsx @@ -82,7 +82,7 @@ describe(' spec', () => { expect(defaultDeleteProps.onStopDetectors).not.toHaveBeenCalled(); expect(defaultDeleteProps.onDeleteDetectors).not.toHaveBeenCalled(); expect(defaultDeleteProps.onConfirm).not.toHaveBeenCalled(); - }, 10000); + }, 5000); test('should have delete button enabled if delete typed', async () => { const { getByTestId, getByPlaceholderText } = render( @@ -93,7 +93,7 @@ describe(' spec', () => { userEvent.click(getByTestId('confirmButton')); await waitFor(() => {}); expect(defaultDeleteProps.onConfirm).toHaveBeenCalled(); - }, 10000); + }, 5000); test('should not show callout if no detectors are running', async () => { const { queryByText } = render( diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index e98d10eb..366ac3fe 100644 --- a/public/pages/DetectorsList/containers/List/List.tsx +++ b/public/pages/DetectorsList/containers/List/List.tsx @@ -42,7 +42,7 @@ import { getIndices, getPrioritizedIndices, } from '../../../../redux/reducers/opensearch'; -import { APP_PATH, PLUGIN_NAME } from '../../../../utils/constants'; +import { APP_PATH, MDS_BREADCRUMBS, PLUGIN_NAME } from '../../../../utils/constants'; import { DETECTOR_STATE } from '../../../../../server/utils/constants'; import { constructHrefWithDataSourceId, @@ -87,7 +87,7 @@ import { CoreServicesContext } from '../../../../components/CoreServices/CoreSer import { DataSourceSelectableConfig } from '../../../../../../../src/plugins/data_source_management/public'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../../services'; @@ -138,7 +138,7 @@ export const DetectorList = (props: ListProps) => { (state: AppState) => state.ad.requesting ); - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const [selectedDetectors, setSelectedDetectors] = useState( [] as DetectorListItem[] @@ -213,10 +213,17 @@ export const DetectorList = (props: ListProps) => { // Set breadcrumbs on page initialization useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + ]); + } else { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(state.selectedDataSourceId), + MDS_BREADCRUMBS.DETECTORS(state.selectedDataSourceId), + ]); + } }, []); // Getting all initial indices @@ -583,7 +590,8 @@ export const DetectorList = (props: ListProps) => { const handleDataSourceChange = ([event]) => { const dataSourceId = event?.id; - if (!dataSourceId) { + + if (dataSourceEnabled && dataSourceId === undefined) { getNotifications().toasts.addDanger( prettifyErrorMessage('Unable to set data source.') ); diff --git a/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx b/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx index e932f4b2..3bc8c268 100644 --- a/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx +++ b/public/pages/DetectorsList/containers/List/__tests__/List.test.tsx @@ -31,6 +31,14 @@ import { DetectorList, ListRouterParams } from '../List'; import { DETECTOR_STATE } from '../../../../../../server/utils/constants'; import { CoreServicesContext } from '../../../../../components/CoreServices/CoreServices'; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = ( initialAdState: Detectors = initialDetectorsState ) => ({ @@ -43,7 +51,9 @@ const renderWithRouter = ( path="/detectors" render={(props: RouteComponentProps) => ( - + )} /> diff --git a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts index 0cf68c84..a0c8f07d 100644 --- a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts +++ b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts @@ -30,6 +30,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, + dataSourceId: '', }); }); test('should default values if missing from queryParams', () => { @@ -42,6 +43,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, + dataSourceId:'', }); }); test('should return queryParams from location', () => { @@ -57,6 +59,7 @@ describe('helpers spec', () => { indices: 'someIndex', sortField: 'name', sortDirection: SORT_DIRECTION.DESC, + dataSourceId:'', }); }); }); diff --git a/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx b/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx index 327767dc..9496cd2a 100644 --- a/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx +++ b/public/pages/DetectorsList/utils/__tests__/tableUtils.test.tsx @@ -9,43 +9,43 @@ * GitHub history for details. */ -import { staticColumn } from '../../utils/tableUtils'; +import { getColumns } from '../../utils/tableUtils'; import { render } from '@testing-library/react'; describe('tableUtils spec', () => { describe('should render the column titles', () => { test('detector name column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[0].name); getByText('Detector'); }); test('indices column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[1].name); getByText('Indices'); }); test('detector state column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[2].name); getByText('Real-time state'); }); test('historical analysis column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[3].name); getByText('Historical analysis'); }); test('anomalies last 24 hrs column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[4].name); getByText('Anomalies last 24 hours'); }); test('last RT occurrence column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[5].name); getByText('Last real-time occurrence'); }); test('last started time column', () => { - const result = staticColumn; + const result = getColumns(''); const { getByText } = render(result[6].name); getByText('Last started'); }); diff --git a/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx b/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx index ef32c66b..d5afb538 100644 --- a/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx +++ b/public/pages/Overview/components/SampleDataBox/__tests__/SampleDataBox.test.tsx @@ -13,6 +13,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { EuiIcon } from '@elastic/eui'; import { SampleDataBox } from '../SampleDataBox'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const defaultProps = { title: 'Sample title', @@ -25,11 +27,33 @@ const defaultProps = { detectorId: 'sample-detector-id', }; +jest.mock('../../../../../services', () => ({ + ...jest.requireActual('../../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); describe('Data not loaded', () => { test('renders component', () => { + const history = createMemoryHistory(); const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); @@ -39,8 +63,13 @@ describe(' spec', () => { }); describe('Data is loading', () => { test('renders component', () => { + const history = createMemoryHistory(); const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); @@ -50,8 +79,14 @@ describe(' spec', () => { }); describe('Data is loaded', () => { test('renders component', () => { + const history = createMemoryHistory(); + const { container, getByText } = render( - + + + ); expect(container.firstChild).toMatchSnapshot(); getByText('Sample title'); diff --git a/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap b/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap index fbec5767..4cdf707a 100644 --- a/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap +++ b/public/pages/Overview/components/SampleDataBox/__tests__/__snapshots__/SampleDataBox.test.tsx.snap @@ -98,7 +98,7 @@ exports[` spec Data is loaded renders component 1`] = ` View detector and sample data diff --git a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx index d6c00927..78aab2fc 100644 --- a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx +++ b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx @@ -28,6 +28,7 @@ import { BREADCRUMBS, PLUGIN_NAME, BASE_DOCS_LINK, + MDS_BREADCRUMBS, } from '../../../utils/constants'; import { SAMPLE_TYPE } from '../../../../server/utils/constants'; import { GET_SAMPLE_INDICES_QUERY } from '../../utils/constants'; @@ -58,7 +59,7 @@ import { CreateWorkflowStepSeparator } from '../components/CreateWorkflowStepSep import { DataSourceSelectableConfig } from '../../../../../../src/plugins/data_source_management/public'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../../public/services'; @@ -86,7 +87,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const allSampleDetectors = Object.values( useSelector((state: AppState) => state.ad.detectorList) ); - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const [isLoadingHttpData, setIsLoadingHttpData] = useState(false); const [isLoadingEcommerceData, setIsLoadingEcommerceData] = useState(false); @@ -109,7 +110,11 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Set breadcrumbs on page initialization useEffect(() => { - core.chrome.setBreadcrumbs([BREADCRUMBS.ANOMALY_DETECTOR]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([MDS_BREADCRUMBS.ANOMALY_DETECTOR(MDSOverviewState.selectedDataSourceId)]); + } else { + core.chrome.setBreadcrumbs([BREADCRUMBS.ANOMALY_DETECTOR]); + } }, []); // Getting all initial sample detectors & indices @@ -219,7 +224,7 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const handleDataSourceChange = ([event]) => { const dataSourceId = event?.id; - if (!dataSourceId) { + if (dataSourceEnabled && dataSourceId === undefined) { getNotifications().toasts.addDanger( prettifyErrorMessage('Unable to set data source.') ); diff --git a/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx b/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx index 60536abc..88411861 100644 --- a/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx +++ b/public/pages/Overview/containers/__tests__/AnomalyDetectionOverview.test.tsx @@ -18,17 +18,23 @@ import { Redirect, Route, Switch, + RouteComponentProps, } from 'react-router-dom'; import { httpClientMock, coreServicesMock } from '../../../../../test/mocks'; import configureStore from '../../../../redux/configureStore'; -import { - Detectors, - initialDetectorsState, -} from '../../../../redux/reducers/ad'; import { sampleHttpResponses } from '../../../Overview/utils/constants'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; -const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + + +const renderWithRouter = () => ({ ...render( @@ -36,11 +42,11 @@ const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ ( + render={(props: RouteComponentProps) => ( {' '} + setActionMenu={jest.fn()} + {...props}/> )} /> @@ -52,6 +58,17 @@ const renderWithRouter = (isLoadingDetectors: boolean = false) => ({ }); describe(' spec', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + href: 'http://test.com', + pathname: '/', + search: '', + hash: '', + }, + writable: true + }); + }); jest.clearAllMocks(); describe('No sample detectors created', () => { test('renders component', async () => { diff --git a/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap b/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap index 1cdefe53..0182660d 100644 --- a/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap +++ b/public/pages/Overview/containers/__tests__/__snapshots__/AnomalyDetectionOverview.test.tsx.snap @@ -24,7 +24,7 @@ exports[` spec No sample detectors created renders c spec No sample detectors created renders c
-
`; @@ -785,7 +784,7 @@ exports[` spec Some detectors created renders compon spec Some detectors created renders compon - `; @@ -1541,7 +1539,7 @@ exports[` spec Some detectors created renders compon spec Some detectors created renders compon View detector and sample data @@ -2290,6 +2288,5 @@ exports[` spec Some detectors created renders compon - `; diff --git a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx index 0f7335e7..81ca40de 100644 --- a/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx +++ b/public/pages/ReviewAndCreate/containers/ReviewAndCreate.tsx @@ -34,7 +34,7 @@ import React, { Fragment, ReactElement, useEffect, useState } from 'react'; import { RouteComponentProps, useLocation } from 'react-router'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../../../redux/reducers'; -import { BREADCRUMBS, MAX_DETECTORS } from '../../../utils/constants'; +import { BREADCRUMBS, MAX_DETECTORS, MDS_BREADCRUMBS } from '../../../utils/constants'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { CoreStart, MountPoint } from '../../../../../../src/core/public'; import { CoreServicesContext } from '../../../components/CoreServices/CoreServices'; @@ -59,7 +59,7 @@ import { } from '../../../pages/utils/helpers'; import { getDataSourceManagementPlugin, - getDataSourcePlugin, + getDataSourceEnabled, getNotifications, getSavedObjectsClient, } from '../../../services'; @@ -77,7 +77,7 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { const location = useLocation(); const MDSQueryParams = getDataSourceFromURL(location); const dataSourceId = MDSQueryParams.dataSourceId; - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; useHideSideNavBar(true, false); // This variable indicates if validate API declared detector settings as valid for AD creation @@ -177,11 +177,19 @@ export function ReviewAndCreate(props: ReviewAndCreateProps) { }, []); useEffect(() => { - core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, - BREADCRUMBS.CREATE_DETECTOR, - ]); + if (dataSourceEnabled) { + core.chrome.setBreadcrumbs([ + MDS_BREADCRUMBS.ANOMALY_DETECTOR(dataSourceId), + MDS_BREADCRUMBS.DETECTORS(dataSourceId), + MDS_BREADCRUMBS.CREATE_DETECTOR, + ]); + } else { + core.chrome.setBreadcrumbs([ + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.CREATE_DETECTOR, + ]); + } }, []); const handleSubmit = async ( diff --git a/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx b/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx index f3209fff..35cac0f0 100644 --- a/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx +++ b/public/pages/ReviewAndCreate/containers/__tests__/ReviewAndCreate.test.tsx @@ -27,6 +27,14 @@ import { INITIAL_DETECTOR_JOB_VALUES } from '../../../DetectorJobs/utils/constan import { INITIAL_MODEL_CONFIGURATION_VALUES } from '../../../ConfigureModel/utils/constants'; import { INITIAL_DETECTOR_DEFINITION_VALUES } from '../../../DefineDetector/utils/constants'; +jest.mock('../../../../services', () => ({ + ...jest.requireActual('../../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + const renderWithRouter = (isEdit: boolean = false) => ({ ...render( diff --git a/public/pages/utils/helpers.ts b/public/pages/utils/helpers.ts index 321f6fc4..2467f7dc 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -20,7 +20,7 @@ import { DETECTORS_QUERY_PARAMS, SORT_DIRECTION } from '../../../server/utils/co import { ALL_INDICES, ALL_DETECTOR_STATES, MAX_DETECTORS, DEFAULT_QUERY_PARAMS } from './constants'; import { DETECTOR_STATE } from '../../../server/utils/constants'; import { timeFormatter } from '@elastic/charts'; -import { getDataSourcePlugin } from '../../services'; +import { getDataSourceEnabled, getDataSourcePlugin } from '../../services'; export function sanitizeSearchText(searchValue: string): string { if (!searchValue || searchValue == '*') { @@ -155,7 +155,7 @@ export const constructHrefWithDataSourceId = ( dataSourceId: string = '', withHash: Boolean ): string => { - const dataSourceEnabled = getDataSourcePlugin()?.dataSourceEnabled || false; + const dataSourceEnabled = getDataSourceEnabled().enabled; const url = new URLSearchParams(); // Set up base parameters for '/detectors' diff --git a/public/plugin.ts b/public/plugin.ts index 07eb6194..ed771c26 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -37,7 +37,7 @@ import { setQueryService, setSavedObjectsClient, setDataSourceManagementPlugin, - setDataSourcePlugin, + setDataSourceEnabled, } from './services'; import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; import { @@ -100,7 +100,9 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin setDataSourceManagementPlugin(plugins.dataSourceManagement); - setDataSourcePlugin(plugins.dataSource); + const enabled = !!plugins.dataSource; + + setDataSourceEnabled({ enabled }); // Create context menu actions const actions = getActions(); diff --git a/public/redux/reducers/__tests__/anomalyResults.test.ts b/public/redux/reducers/__tests__/anomalyResults.test.ts index bc873208..e668e2d1 100644 --- a/public/redux/reducers/__tests__/anomalyResults.test.ts +++ b/public/redux/reducers/__tests__/anomalyResults.test.ts @@ -20,6 +20,14 @@ import reducer, { initialDetectorsState, } from '../anomalyResults'; +jest.mock('../../../services', () => ({ + ...jest.requireActual('../../../services'), + + getDataSourceEnabled: () => ({ + enabled: false + }) +})); + describe('anomaly results reducer actions', () => { let store: MockStore; beforeEach(() => { @@ -45,6 +53,7 @@ describe('anomaly results reducer actions', () => { await store.dispatch( getDetectorResults( tempDetectorId, + '', queryParams, false, resultIndex, @@ -87,7 +96,7 @@ describe('anomaly results reducer actions', () => { }; try { await store.dispatch( - getDetectorResults(tempDetectorId, queryParams, false, '', false) + getDetectorResults(tempDetectorId, '', queryParams, false, '', false) ); } catch (e) { const actions = store.getActions(); diff --git a/public/services.ts b/public/services.ts index 4cb502f1..dbe060e6 100644 --- a/public/services.ts +++ b/public/services.ts @@ -17,6 +17,10 @@ import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { SavedAugmentVisLoader } from '../../../src/plugins/vis_augmenter/public'; import { DataSourcePluginSetup } from '../../../src/plugins/data_source/public'; +export interface DataSourceEnabled { + enabled: boolean; +} + export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = createGetterSetter('savedFeatureAnywhereLoader'); @@ -47,8 +51,8 @@ export const [getSavedObjectsClient, setSavedObjectsClient] = export const [getDataSourceManagementPlugin, setDataSourceManagementPlugin] = createGetterSetter('DataSourceManagement'); -export const [getDataSourcePlugin, setDataSourcePlugin] = - createGetterSetter('DataSource'); +export const [getDataSourceEnabled, setDataSourceEnabled] = + createGetterSetter('DataSourceEnabled'); // This is primarily used for mocking this module and each of its fns in tests. export default { diff --git a/public/utils/constants.ts b/public/utils/constants.ts index ac7907a7..fa3022d6 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -26,6 +26,15 @@ export const BREADCRUMBS = Object.freeze({ EDIT_MODEL_CONFIGURATION: { text: 'Edit model configuration' }, }); +export const MDS_BREADCRUMBS = Object.freeze({ + ANOMALY_DETECTOR: (dataSourceId?: string) => ({ text: 'Anomaly detection', href: `#/?dataSourceId=${dataSourceId}` }), + DETECTORS: (dataSourceId?: string) => ({ text: 'Detectors', href: `#/detectors?dataSourceId=${dataSourceId}` }), + CREATE_DETECTOR: { text: 'Create detector' }, + EDIT_DETECTOR: { text: 'Edit detector' }, + DASHBOARD: (dataSourceId?: string) => ({ text: 'Dashboard', href: `#/dashboard?dataSourceId=${dataSourceId}` }), + EDIT_MODEL_CONFIGURATION: { text: 'Edit model configuration' }, +}); + export const APP_PATH = { DASHBOARD: '/dashboard', LIST_DETECTORS: '/detectors', diff --git a/server/plugin.ts b/server/plugin.ts index 2c925ffd..a6dfc3b0 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -65,19 +65,19 @@ export class AnomalyDetectionOpenSearchDashboardsPlugin const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); const { customHeaders, ...rest } = globalConfig.opensearch; - 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', { + // Create OpenSearch client w/ relevant plugins and headers + const client: ILegacyClusterClient = core.opensearch.legacy.createClient( + 'anomaly_detection', + { plugins: [adPlugin, alertingPlugin], customHeaders: { ...customHeaders, ...DEFAULT_HEADERS }, ...rest, - }); - } else { + } + ); + + const dataSourceEnabled = !!dataSource; + + if (dataSourceEnabled) { dataSource.registerCustomApiSchema(adPlugin); dataSource.registerCustomApiSchema(alertingPlugin); } diff --git a/server/routes/ad.ts b/server/routes/ad.ts index 1acbcc18..ed42e7e9 100644 --- a/server/routes/ad.ts +++ b/server/routes/ad.ts @@ -74,7 +74,7 @@ export function registerADRoutes(apiRouter: Router, adService: AdService) { 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 diff --git a/server/utils/helpers.ts b/server/utils/helpers.ts index 4795e701..c9006544 100644 --- a/server/utils/helpers.ts +++ b/server/utils/helpers.ts @@ -93,7 +93,7 @@ export function getClientBasedOnDataSource( dataSourceEnabled: boolean, request: OpenSearchDashboardsRequest, dataSourceId: string, - client: ILegacyClusterClient | undefined + client: ILegacyClusterClient ): ( endpoint: string, clientParams?: Record, @@ -103,9 +103,6 @@ export function getClientBasedOnDataSource( // 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; } From 9c2d58430834ebd3d395a91c0872b5f95c8a07da Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Mon, 29 Apr 2024 09:11:40 -0700 Subject: [PATCH 5/5] bug fixes (#737) Signed-off-by: Jackie Han --- package.json | 4 +- .../Dashboard/Container/DashboardOverview.tsx | 32 ++++++++------- .../containers/DefineDetector.tsx | 2 +- .../DetectorsList/containers/List/List.tsx | 41 +++++++++++-------- .../utils/__tests__/helpers.test.ts | 6 +-- public/pages/DetectorsList/utils/helpers.ts | 4 +- .../containers/AnomalyDetectionOverview.tsx | 30 ++++++++------ public/pages/main/Main.tsx | 8 +++- public/pages/utils/helpers.ts | 7 +++- server/utils/constants.ts | 1 + yarn.lock | 10 ++--- 11 files changed, 86 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index c5b88cd0..8ac2f912 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,13 @@ "devDependencies": { "@testing-library/user-event": "^12.1.6", "@types/react-plotly.js": "^2.6.0", - "@types/redux-mock-store": "^1.0.1", + "@types/redux-mock-store": "^1.0.6", "babel-polyfill": "^6.26.0", "eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-prefer-object-spread": "^1.2.1", "lint-staged": "^9.2.0", "moment": "^2.24.0", - "redux-mock-store": "^1.5.3", + "redux-mock-store": "^1.5.4", "start-server-and-test": "^1.11.7" }, "dependencies": { diff --git a/public/pages/Dashboard/Container/DashboardOverview.tsx b/public/pages/Dashboard/Container/DashboardOverview.tsx index d1728536..ff57d99e 100644 --- a/public/pages/Dashboard/Container/DashboardOverview.tsx +++ b/public/pages/Dashboard/Container/DashboardOverview.tsx @@ -68,6 +68,7 @@ import { RouteComponentProps } from 'react-router-dom'; interface OverviewProps extends RouteComponentProps { setActionMenu: (menuMount: MountPoint | undefined) => void; + landingDataSourceId: string | undefined; } export function DashboardOverview(props: OverviewProps) { @@ -91,11 +92,11 @@ export function DashboardOverview(props: OverviewProps) { const queryParams = getDataSourceFromURL(props.location); const [MDSOverviewState, setMDSOverviewState] = useState({ queryParams, - selectedDataSourceId: queryParams.dataSourceId - ? queryParams.dataSourceId - : undefined, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, }); - + const getDetectorOptions = (detectorsIdMap: { [key: string]: DetectorListItem; }) => { @@ -215,13 +216,15 @@ export function DashboardOverview(props: OverviewProps) { useEffect(() => { const { history, location } = props; - const updatedParams = { - dataSourceId: MDSOverviewState.selectedDataSourceId, - }; - history.replace({ - ...location, - search: queryString.stringify(updatedParams), - }); + if (dataSourceEnabled) { + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); + } intializeDetectors(); }, [MDSOverviewState]); @@ -274,9 +277,10 @@ export function DashboardOverview(props: OverviewProps) { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: MDSOverviewState.selectedDataSourceId !== undefined - ? [{ id: MDSOverviewState.selectedDataSourceId }] - : undefined, + activeOption: props.landingDataSourceId === undefined + || MDSOverviewState.selectedDataSourceId === undefined + ? undefined + : [{ id: MDSOverviewState.selectedDataSourceId }], savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/DefineDetector/containers/DefineDetector.tsx b/public/pages/DefineDetector/containers/DefineDetector.tsx index 21d6ccba..13de30cc 100644 --- a/public/pages/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/DefineDetector/containers/DefineDetector.tsx @@ -98,7 +98,7 @@ export const DefineDetector = (props: DefineDetectorProps) => { const [MDSCreateState, setMDSCreateState] = useState({ queryParams: MDSQueryParams, - selectedDataSourceId: dataSourceId ? dataSourceId : undefined, + selectedDataSourceId: dataSourceId === undefined? undefined : dataSourceId, }); // To handle backward compatibility, we need to pass some fields via diff --git a/public/pages/DetectorsList/containers/List/List.tsx b/public/pages/DetectorsList/containers/List/List.tsx index 366ac3fe..a54f14c9 100644 --- a/public/pages/DetectorsList/containers/List/List.tsx +++ b/public/pages/DetectorsList/containers/List/List.tsx @@ -84,7 +84,7 @@ import { } from '../../../../../server/utils/helpers'; import { CoreStart, MountPoint } from '../../../../../../../src/core/public'; import { CoreServicesContext } from '../../../../components/CoreServices/CoreServices'; -import { DataSourceSelectableConfig } from '../../../../../../../src/plugins/data_source_management/public'; +import { DataSourceOption, DataSourceSelectableConfig } from '../../../../../../../src/plugins/data_source_management/public'; import { getDataSourceManagementPlugin, getDataSourceEnabled, @@ -206,25 +206,25 @@ export const DetectorList = (props: ListProps) => { selectedIndices: queryParams.indices ? queryParams.indices.split(',') : ALL_INDICES, - selectedDataSourceId: queryParams.dataSourceId - ? queryParams.dataSourceId - : undefined, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, }); // Set breadcrumbs on page initialization useEffect(() => { if (dataSourceEnabled) { core.chrome.setBreadcrumbs([ - BREADCRUMBS.ANOMALY_DETECTOR, - BREADCRUMBS.DETECTORS, + MDS_BREADCRUMBS.ANOMALY_DETECTOR(state.selectedDataSourceId), + MDS_BREADCRUMBS.DETECTORS(state.selectedDataSourceId), ]); } else { core.chrome.setBreadcrumbs([ - MDS_BREADCRUMBS.ANOMALY_DETECTOR(state.selectedDataSourceId), - MDS_BREADCRUMBS.DETECTORS(state.selectedDataSourceId), + BREADCRUMBS.ANOMALY_DETECTOR, + BREADCRUMBS.DETECTORS, ]); } - }, []); + }, [state.selectedDataSourceId]); // Getting all initial indices const [indexQuery, setIndexQuery] = useState(''); @@ -238,12 +238,21 @@ export const DetectorList = (props: ListProps) => { // Refresh data if user change any parameters / filter / sort useEffect(() => { const { history, location } = props; - const updatedParams = { - ...state.queryParams, - indices: state.selectedIndices.join(','), + let updatedParams = { from: state.page * state.queryParams.size, - dataSourceId: state.selectedDataSourceId, - }; + size: state.queryParams.size, + search: state.queryParams.search, + indices: state.selectedIndices.join(','), + sortDirection: state.queryParams.sortDirection, + sortField: state.queryParams.sortField, + } as GetDetectorsQueryParams; + + if (dataSourceEnabled) { + updatedParams = { + ...updatedParams, + dataSourceId: state.selectedDataSourceId, + } + } history.replace({ ...location, @@ -588,8 +597,8 @@ export const DetectorList = (props: ListProps) => { }); }; - const handleDataSourceChange = ([event]) => { - const dataSourceId = event?.id; + const handleDataSourceChange = (dataSources: DataSourceOption[]) => { + const dataSourceId = dataSources[0].id; if (dataSourceEnabled && dataSourceId === undefined) { getNotifications().toasts.addDanger( diff --git a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts index a0c8f07d..979396e6 100644 --- a/public/pages/DetectorsList/utils/__tests__/helpers.test.ts +++ b/public/pages/DetectorsList/utils/__tests__/helpers.test.ts @@ -30,7 +30,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, - dataSourceId: '', + dataSourceId: undefined, }); }); test('should default values if missing from queryParams', () => { @@ -43,7 +43,7 @@ describe('helpers spec', () => { indices: '', sortField: 'name', sortDirection: SORT_DIRECTION.ASC, - dataSourceId:'', + dataSourceId: undefined, }); }); test('should return queryParams from location', () => { @@ -59,7 +59,7 @@ describe('helpers spec', () => { indices: 'someIndex', sortField: 'name', sortDirection: SORT_DIRECTION.DESC, - dataSourceId:'', + dataSourceId: undefined, }); }); }); diff --git a/public/pages/DetectorsList/utils/helpers.ts b/public/pages/DetectorsList/utils/helpers.ts index 01dca578..7e59bdb5 100644 --- a/public/pages/DetectorsList/utils/helpers.ts +++ b/public/pages/DetectorsList/utils/helpers.ts @@ -47,10 +47,12 @@ export const getURLQueryParams = (location: { typeof sortDirection !== 'string' ? DEFAULT_QUERY_PARAMS.sortDirection : (sortDirection as SORT_DIRECTION), - dataSourceId: typeof dataSourceId !== 'string' ? '' : dataSourceId, + dataSourceId: dataSourceId === undefined ? undefined : dataSourceId, }; }; + + // For realtime detectors: cannot have 'Finished' state export const getDetectorStateOptions = () => { return Object.values(DETECTOR_STATE) diff --git a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx index 78aab2fc..f1082976 100644 --- a/public/pages/Overview/containers/AnomalyDetectionOverview.tsx +++ b/public/pages/Overview/containers/AnomalyDetectionOverview.tsx @@ -70,6 +70,7 @@ import { MDSStates } from '../../../models/interfaces'; interface AnomalyDetectionOverviewProps extends RouteComponentProps { setActionMenu: (menuMount: MountPoint | undefined) => void; + landingDataSourceId: string | undefined; } export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { @@ -103,9 +104,9 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { const queryParams = getDataSourceFromURL(props.location); const [MDSOverviewState, setMDSOverviewState] = useState({ queryParams, - selectedDataSourceId: queryParams.dataSourceId - ? queryParams.dataSourceId - : undefined, + selectedDataSourceId: queryParams.dataSourceId === undefined + ? undefined + : queryParams.dataSourceId, }); // Set breadcrumbs on page initialization @@ -120,14 +121,16 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { // Getting all initial sample detectors & indices useEffect(() => { const { history, location } = props; - const updatedParams = { - dataSourceId: MDSOverviewState.selectedDataSourceId, - }; - history.replace({ - ...location, - search: queryString.stringify(updatedParams), - }); + if (dataSourceEnabled && props.landingDataSourceId !== undefined) { + const updatedParams = { + dataSourceId: MDSOverviewState.selectedDataSourceId, + }; + history.replace({ + ...location, + search: queryString.stringify(updatedParams), + }); + } fetchData(); }, [MDSOverviewState]); @@ -247,9 +250,10 @@ export function AnomalyDetectionOverview(props: AnomalyDetectionOverviewProps) { componentType={'DataSourceSelectable'} componentConfig={{ fullWidth: false, - activeOption: MDSOverviewState.selectedDataSourceId !== undefined - ? [{ id: MDSOverviewState.selectedDataSourceId }] - : undefined, + activeOption: props.landingDataSourceId === undefined + || MDSOverviewState.selectedDataSourceId === undefined + ? undefined + : [{ id: MDSOverviewState.selectedDataSourceId }], savedObjects: getSavedObjectsClient(), notifications: getNotifications(), onSelectedDataSources: (dataSources) => diff --git a/public/pages/main/Main.tsx b/public/pages/main/Main.tsx index 6dfa2951..728bdcc6 100644 --- a/public/pages/main/Main.tsx +++ b/public/pages/main/Main.tsx @@ -48,8 +48,8 @@ export function Main(props: MainProps) { const adState = useSelector((state: AppState) => state.ad); const totalDetectors = adState.totalDetectors; const queryParams = getURLQueryParams(props.location); - const dataSourceId = queryParams.dataSourceId ? queryParams.dataSourceId : ''; - + const dataSourceId = queryParams.dataSourceId === undefined ? undefined : queryParams.dataSourceId; + const sideNav = [ { name: Navigation.AnomalyDetection, @@ -99,6 +99,7 @@ export function Main(props: MainProps) { render={(props: RouteComponentProps) => ( )} @@ -155,6 +156,7 @@ export function Main(props: MainProps) { render={(props: RouteComponentProps) => ( )} @@ -165,11 +167,13 @@ export function Main(props: MainProps) { totalDetectors > 0 ? ( ) : ( ) diff --git a/public/pages/utils/helpers.ts b/public/pages/utils/helpers.ts index 2467f7dc..7a581c97 100644 --- a/public/pages/utils/helpers.ts +++ b/public/pages/utils/helpers.ts @@ -165,10 +165,13 @@ export const constructHrefWithDataSourceId = ( url.set(DETECTORS_QUERY_PARAMS.SEARCH, DEFAULT_QUERY_PARAMS.search); url.set(DETECTORS_QUERY_PARAMS.INDICES, DEFAULT_QUERY_PARAMS.indices); url.set(DETECTORS_QUERY_PARAMS.SORT_FIELD, DEFAULT_QUERY_PARAMS.sortField); - url.set(DETECTORS_QUERY_PARAMS.SORT_DIRECTION, SORT_DIRECTION.ASC); + url.set(DETECTORS_QUERY_PARAMS.SORT_DIRECTION, SORT_DIRECTION.ASC) + if (dataSourceEnabled) { + url.set(DETECTORS_QUERY_PARAMS.DATASOURCEID, ''); + } } - if (dataSourceEnabled && dataSourceId) { + if (dataSourceEnabled && dataSourceId !== undefined) { url.set('dataSourceId', dataSourceId); } diff --git a/server/utils/constants.ts b/server/utils/constants.ts index f0ac63f9..ac3c887a 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -54,6 +54,7 @@ export enum DETECTORS_QUERY_PARAMS { SORT_FIELD = 'sortField', SORT_DIRECTION = 'sortDirection', NAME = 'name', + DATASOURCEID = 'dataSourceId', } export enum AD_DOC_FIELDS { diff --git a/yarn.lock b/yarn.lock index b7f4ed32..597e3515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -136,10 +136,10 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/redux-mock-store@^1.0.1": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz#895de4a364bc4836661570aec82f2eef5989d1fb" - integrity sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA== +"@types/redux-mock-store@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/redux-mock-store/-/redux-mock-store-1.0.6.tgz#0a03b2655028b7cf62670d41ac1de5ca1b1f5958" + integrity sha512-eg5RDfhJTXuoJjOMyXiJbaDb1B8tfTaJixscmu+jOusj6adGC0Krntz09Tf4gJgXeCqCrM5bBMd+B7ez0izcAQ== dependencies: redux "^4.0.5" @@ -1432,7 +1432,7 @@ readable-stream@^3.6.0, readable-stream@^3.6.2: string_decoder "^1.1.1" util-deprecate "^1.0.1" -redux-mock-store@^1.5.3: +redux-mock-store@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.4.tgz#90d02495fd918ddbaa96b83aef626287c9ab5872" integrity sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==