/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ import React, { useState, useEffect, useCallback, Fragment } from 'react'; import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiOverlayMask, EuiCallOut, EuiSpacer, EuiText, EuiFieldText, EuiLoadingSpinner, EuiButton, } from '@elastic/eui'; import { CoreStart } 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 { useDispatch, useSelector } from 'react-redux'; import { useFetchDetectorInfo } from '../../CreateDetectorSteps/hooks/useFetchDetectorInfo'; import { useHideSideNavBar } from '../../main/hooks/useHideSideNavBar'; import { AppState } from '../../../redux/reducers'; import { deleteDetector, startDetector, stopDetector, getDetector, stopHistoricalDetector, } from '../../../redux/reducers/ad'; import { getIndices } from '../../../redux/reducers/opensearch'; import { getErrorMessage, Listener } from '../../../utils/utils'; import { darkModeEnabled } from '../../../utils/opensearchDashboardsUtils'; import { BREADCRUMBS } from '../../../utils/constants'; import { DetectorControls } from '../components/DetectorControls'; import { ConfirmModal } from '../components/ConfirmModal/ConfirmModal'; import { useFetchMonitorInfo } from '../hooks/useFetchMonitorInfo'; import { MonitorCallout } from '../components/MonitorCallout/MonitorCallout'; import { DETECTOR_DETAIL_TABS } from '../utils/constants'; import { DetectorConfig } from '../../DetectorConfig/containers/DetectorConfig'; import { AnomalyResults } from '../../DetectorResults/containers/AnomalyResults'; import { HistoricalDetectorResults } from '../../HistoricalDetectorResults/containers/HistoricalDetectorResults'; import { NO_PERMISSIONS_KEY_WORD, prettifyErrorMessage, } from '../../../../server/utils/helpers'; import { DETECTOR_STATE } from '../../../../server/utils/constants'; import { CatIndex } from '../../../../server/models/types'; import { containsIndex } from '../utils/helpers'; export interface DetectorRouterProps { detectorId?: string; } interface DetectorDetailProps extends RouteComponentProps {} const tabs = [ { id: DETECTOR_DETAIL_TABS.RESULTS, name: 'Real-time results', route: DETECTOR_DETAIL_TABS.RESULTS, }, { id: DETECTOR_DETAIL_TABS.HISTORICAL, name: 'Historical analysis', route: DETECTOR_DETAIL_TABS.HISTORICAL, }, { id: DETECTOR_DETAIL_TABS.CONFIGURATIONS, name: 'Detector configuration', route: DETECTOR_DETAIL_TABS.CONFIGURATIONS, }, ]; const getSelectedTabId = (pathname: string) => { return pathname.includes(DETECTOR_DETAIL_TABS.CONFIGURATIONS) ? DETECTOR_DETAIL_TABS.CONFIGURATIONS : pathname.includes(DETECTOR_DETAIL_TABS.HISTORICAL) ? DETECTOR_DETAIL_TABS.HISTORICAL : DETECTOR_DETAIL_TABS.RESULTS; }; interface DetectorDetailModel { selectedTab: DETECTOR_DETAIL_TABS; showDeleteDetectorModal: boolean; showStopDetectorModalFor: string | undefined; showMonitorCalloutModal: boolean; deleteTyped: boolean; } 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 { detector, hasError, isLoadingDetector, errorMessage } = useFetchDetectorInfo(detectorId); const { monitor, fetchMonitorError, isLoadingMonitor } = useFetchMonitorInfo(detectorId); const visibleIndices = useSelector( (state: AppState) => state.opensearch.indices ) as CatIndex[]; const isResultIndexMissing = isLoadingDetector ? false : isEmpty(get(detector, 'resultIndex', '')) ? false : !containsIndex(get(detector, 'resultIndex', ''), visibleIndices); // String to set in the modal if the realtime detector and/or historical analysis // are running when the user tries to edit the detector details or model config const isRTJobRunning = get(detector, 'enabled'); const isHistoricalJobRunning = get(detector, 'taskState') === DETECTOR_STATE.RUNNING || get(detector, 'taskState') === DETECTOR_STATE.INIT; const runningJobsAsString = isRTJobRunning && isHistoricalJobRunning ? 'detector and historical analysis' : isRTJobRunning ? 'detector' : isHistoricalJobRunning ? 'historical analysis' : ''; //TODO: test dark mode once detector configuration and AD result page merged const isDark = darkModeEnabled(); const [detectorDetailModel, setDetectorDetailModel] = useState({ selectedTab: getSelectedTabId( props.location.pathname ) as DETECTOR_DETAIL_TABS, showDeleteDetectorModal: false, showStopDetectorModalFor: undefined, showMonitorCalloutModal: false, deleteTyped: false, }); useHideSideNavBar(true, false); // Jump to top of page on first load useEffect(() => { scroll(0, 0); }, []); // Getting all visible indices. Will re-fetch if changes to the detector (e.g., // detector starts, result index recreated or user switches tabs to re-fetch detector) useEffect(() => { const getInitialIndices = async () => { await dispatch(getIndices('')).catch((error: any) => { console.error(error); core.notifications.toasts.addDanger('Error getting all indices'); }); }; // only need to check if indices exist after detector finishes loading if (!isLoadingDetector) { getInitialIndices(); } }, [detector]); useEffect(() => { if (hasError) { core.notifications.toasts.addDanger( errorMessage.includes(NO_PERMISSIONS_KEY_WORD) ? prettifyErrorMessage(errorMessage) : 'Unable to find the detector' ); props.history.push('/detectors'); } }, [hasError]); useEffect(() => { if (detector) { core.chrome.setBreadcrumbs([ BREADCRUMBS.ANOMALY_DETECTOR, BREADCRUMBS.DETECTORS, { text: detector ? detector.name : '' }, ]); } }, [detector]); // If the detector state was changed after opening the stop detector modal, // re-check if any jobs are running, and close the modal if it's not needed anymore useEffect(() => { if (!isRTJobRunning && !isHistoricalJobRunning && !isEmpty(detector)) { hideStopDetectorModal(); } }, [detector]); const handleSwitchToConfigurationTab = useCallback(() => { setDetectorDetailModel({ ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.CONFIGURATIONS, }); props.history.push(`/detectors/${detectorId}/configurations`); }, []); const handleSwitchToHistoricalTab = useCallback(() => { setDetectorDetailModel({ ...detectorDetailModel, selectedTab: DETECTOR_DETAIL_TABS.HISTORICAL, }); props.history.push(`/detectors/${detectorId}/historical`); }, []); const handleTabChange = (route: DETECTOR_DETAIL_TABS) => { setDetectorDetailModel({ ...detectorDetailModel, selectedTab: route, }); props.history.push(`/detectors/${detectorId}/${route}`); }; const hideMonitorCalloutModal = () => { setDetectorDetailModel({ ...detectorDetailModel, showMonitorCalloutModal: false, }); }; const hideStopDetectorModal = () => { setDetectorDetailModel({ ...detectorDetailModel, showStopDetectorModalFor: undefined, }); }; const hideDeleteDetectorModal = () => { setDetectorDetailModel({ ...detectorDetailModel, showDeleteDetectorModal: false, }); }; const handleStopDetectorForEditing = (detectorId: string) => { const listener: Listener = { onSuccess: () => { if (detectorDetailModel.showStopDetectorModalFor === 'detector') { props.history.push(`/detectors/${detectorId}/edit`); } else { props.history.push(`/detectors/${detectorId}/features`); } hideStopDetectorModal(); }, onException: hideStopDetectorModal, }; handleStopAdJob(detectorId, listener); }; const handleStartAdJob = async (detectorId: string) => { try { // 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)); core.notifications.toasts.addSuccess( `Successfully started the detector job` ); } catch (err) { core.notifications.toasts.addDanger( prettifyErrorMessage( getErrorMessage(err, 'There was a problem starting the detector job') ) ); } }; const handleStopAdJob = async (detectorId: string, listener?: Listener) => { try { if (isRTJobRunning) { await dispatch(stopDetector(detectorId)); } if (isHistoricalJobRunning) { await dispatch(stopHistoricalDetector(detectorId)); } core.notifications.toasts.addSuccess( `Successfully stopped the ${runningJobsAsString}` ); if (listener) listener.onSuccess(); } catch (err) { core.notifications.toasts.addDanger( prettifyErrorMessage( getErrorMessage( err, `There was a problem stopping the ${runningJobsAsString}` ) ) ); if (listener) listener.onException(); } }; const handleDelete = useCallback(async (detectorId: string) => { try { await dispatch(deleteDetector(detectorId)); core.notifications.toasts.addSuccess(`Successfully deleted the detector`); hideDeleteDetectorModal(); props.history.push('/detectors'); } catch (err) { core.notifications.toasts.addDanger( prettifyErrorMessage( getErrorMessage(err, 'There was a problem deleting the detector') ) ); hideDeleteDetectorModal(); } }, []); const handleEditDetector = () => { isRTJobRunning || isHistoricalJobRunning ? setDetectorDetailModel({ ...detectorDetailModel, showStopDetectorModalFor: 'detector', }) : props.history.push(`/detectors/${detectorId}/edit`); }; const handleEditFeature = () => { isRTJobRunning || isHistoricalJobRunning ? setDetectorDetailModel({ ...detectorDetailModel, showStopDetectorModalFor: 'features', }) : props.history.push(`/detectors/${detectorId}/features`); }; const lightStyles = { backgroundColor: '#FFF', }; const monitorCallout = monitor ? ( ) : null; const deleteDetectorCallout = isRTJobRunning || isHistoricalJobRunning ? ( ) : null; return ( {!isEmpty(detector) && !hasError ? ( {

{detector && detector.name}

}
setDetectorDetailModel({ ...detectorDetailModel, showDeleteDetectorModal: true, }) } onStartDetector={() => handleStartAdJob(detectorId)} onStopDetector={() => monitor ? setDetectorDetailModel({ ...detectorDetailModel, showMonitorCalloutModal: true, }) : handleStopAdJob(detectorId) } onEditFeatures={handleEditFeature} detector={detector} />
{isResultIndexMissing ? ( ) : null} {tabs.map((tab) => ( { handleTabChange(tab.route); }} isSelected={tab.id === detectorDetailModel.selectedTab} key={tab.id} data-test-subj={`${tab.id}Tab`} > {tab.name} ))}
) : (
)} {detectorDetailModel.showDeleteDetectorModal ? (

Detector and feature configuration will be permanently removed. This action is irreversible. To confirm deletion, type delete in the field.

{ if (e.target.value === 'delete') { setDetectorDetailModel({ ...detectorDetailModel, deleteTyped: true, }); } else { setDetectorDetailModel({ ...detectorDetailModel, deleteTyped: false, }); } }} /> } callout={ {deleteDetectorCallout} {monitorCallout ? : null} {monitorCallout} } confirmButtonText="Delete" confirmButtonColor="danger" confirmButtonDisabled={!detectorDetailModel.deleteTyped} onClose={hideDeleteDetectorModal} onCancel={hideDeleteDetectorModal} onConfirm={() => { if (detector.enabled) { const listener: Listener = { onSuccess: () => { handleDelete(detectorId); }, onException: hideDeleteDetectorModal, }; handleStopAdJob(detectorId, listener); } else { handleDelete(detectorId); } }} />
) : null} {detectorDetailModel.showStopDetectorModalFor ? ( handleStopDetectorForEditing(detectorId)} /> ) : null} {detectorDetailModel.showMonitorCalloutModal ? ( { handleStopAdJob(detectorId); hideMonitorCalloutModal(); }} /> ) : null} ( handleStartAdJob(detectorId)} onStopDetector={() => handleStopAdJob(detectorId)} onSwitchToConfiguration={handleSwitchToConfigurationTab} onSwitchToHistorical={handleSwitchToHistoricalTab} /> )} /> ( )} /> ( )} />
); };