From 068d03ed5cd4b40ea51030a814402b63c7de5225 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 23 Mar 2023 19:13:00 +0100 Subject: [PATCH] Communicate to users when the detector is initializing (#487) * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * Common data store for the rules #474 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * [FEATURE] Communicate to users when detector is initializing #227 Signed-off-by: Jovan Cvetkovic * Communicate to users when the detector is initializing #487 Signed-off-by: Jovan Cvetkovic * Code review Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic --- models/interfaces.ts | 12 - .../containers/CreateDetector.tsx | 101 +++---- .../pages/CreateDetector/utils/constants.ts | 2 + .../DetectorBasicDetailsView.tsx | 18 +- .../DetectorRulesView/DetectorRulesView.tsx | 21 +- .../DetectorRulesView.test.tsx.snap | 1 + .../FieldMappingsView/FieldMappingsView.tsx | 18 +- .../UpdateBasicDetails/UpdateBasicDetails.tsx | 3 +- .../UpdateFieldMappings.tsx | 3 +- .../components/UpdateRules/UpdateRules.tsx | 2 +- .../AlertTriggersView/AlertTriggersView.tsx | 7 +- .../containers/Detector/DetectorDetails.tsx | 261 +++++++++++++++--- .../DetectorDetails.test.tsx.snap | 59 +--- .../DetectorDetailsView.tsx | 6 +- .../DetectorDetailsView.test.tsx.snap | 2 + .../FieldMappings/EditFieldMapping.tsx | 34 ++- .../EditFieldMappings.test.tsx.snap | 39 +++ public/pages/Main/Main.tsx | 1 + public/store/DataStore.ts | 5 +- public/store/DetectorsStore.ts | 79 ++++++ .../Rules => }/store/RulesStore.test.ts | 8 +- public/{pages/Rules => }/store/RulesStore.ts | 10 +- public/utils/constants.ts | 4 +- .../DetectorBasicDetailsView.mock.ts | 1 + .../DetectorRulesView.mock.ts | 5 +- .../FieldMappingsView.mock.ts | 5 +- .../UpdateFieldMappings.mock.ts | 9 +- .../DetectorDetails/DetectorDetails.mock.ts | 10 +- types/Detector.ts | 2 +- 29 files changed, 480 insertions(+), 248 deletions(-) create mode 100644 public/store/DetectorsStore.ts rename public/{pages/Rules => }/store/RulesStore.test.ts (81%) rename public/{pages/Rules => }/store/RulesStore.ts (96%) diff --git a/models/interfaces.ts b/models/interfaces.ts index a92554ca3..b55303ad6 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -18,18 +18,6 @@ export interface Rule { detection: string; } -export interface Detector { - id?: string; - type: string; - detector_type: string; - name: string; - enabled: boolean; - createdBy: string; - schedule: PeriodSchedule; - inputs: DetectorInput[]; - triggers: AlertCondition[]; -} - export interface PeriodSchedule { period: { interval: number; diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx index 5ddfebde5..e71a2b1d5 100644 --- a/public/pages/CreateDetector/containers/CreateDetector.tsx +++ b/public/pages/CreateDetector/containers/CreateDetector.tsx @@ -7,19 +7,17 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSteps } from '@elastic/eui'; import DefineDetector from '../components/DefineDetector/containers/DefineDetector'; -import { createDetectorSteps } from '../utils/constants'; +import { createDetectorSteps, PENDING_DETECTOR_ID } from '../utils/constants'; import { BREADCRUMBS, EMPTY_DEFAULT_DETECTOR, OS_NOTIFICATION_PLUGIN, PLUGIN_NAME, ROUTES, - logTypesWithDashboards, - pendingDashboardCreations, } from '../../../utils/constants'; import ConfigureFieldMapping from '../components/ConfigureFieldMapping'; import ConfigureAlerts from '../components/ConfigureAlerts'; -import { Detector, FieldMapping } from '../../../../models/interfaces'; +import { FieldMapping } from '../../../../models/interfaces'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { CoreServicesContext } from '../../../components/core_services'; import { DetectorCreationStep } from '../models/types'; @@ -32,20 +30,18 @@ import { RuleItemInfo, } from '../components/DefineDetector/components/DetectionRules/types/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { - errorNotificationToast, - getPlugins, - successNotificationToast, -} from '../../../utils/helpers'; +import { getPlugins } from '../../../utils/helpers'; +import { Detector } from '../../../../types'; import { DataStore } from '../../../store/DataStore'; interface CreateDetectorProps extends RouteComponentProps { isEdit: boolean; services: BrowserServices; + history: RouteComponentProps['history']; notifications: NotificationsStart; } -interface CreateDetectorState { +export interface CreateDetectorState { currentStep: DetectorCreationStep; detector: Detector; fieldMappings: FieldMapping[]; @@ -61,6 +57,11 @@ export default class CreateDetector extends Component { + onCreateClick = () => { const { creatingDetector, detector, fieldMappings } = this.state; if (creatingDetector) { return; } + this.setState({ creatingDetector: true }); - try { - const createMappingsRes = await this.props.services.fieldMappingService.createMappings( - detector.inputs[0].detector_input.indices[0], - detector.detector_type, - fieldMappings - ); - if (!createMappingsRes.ok) { - errorNotificationToast( - this.props.notifications, - 'create', - 'detector', - 'Double check the field mappings and try again.' - ); - } else { - const createDetectorRes = await this.props.services.detectorsService.createDetector( - detector - ); - if (createDetectorRes.ok) { - successNotificationToast( - this.props.notifications, - 'created', - `detector, "${detector.name}"` - ); - // Create the dashboard - let createDashboardPromise; - if (logTypesWithDashboards.has(detector.detector_type)) { - createDashboardPromise = this.createDashboard( - detector.name, - detector.detector_type, - createDetectorRes.response._id, - detector.inputs[0].detector_input.indices - ); - pendingDashboardCreations[createDetectorRes.response._id] = createDashboardPromise; - } - this.props.history.push(`${ROUTES.DETECTOR_DETAILS}/${createDetectorRes.response._id}`); - } else { - errorNotificationToast( - this.props.notifications, - 'create', - 'detector', - createDetectorRes.error - ); - } - } - } catch (error: any) { - errorNotificationToast(this.props.notifications, 'create', 'detector', error); - } + + const fieldsMappingPromise = this.props.services.fieldMappingService.createMappings( + detector.inputs[0].detector_input.indices[0], + detector.detector_type, + fieldMappings + ); + + const createDetectorPromise = this.props.services.detectorsService.createDetector(detector); + + // set detector pending state, this will be used in detector details page + DataStore.detectors.setPendingState({ + pendingRequests: [fieldsMappingPromise, createDetectorPromise], + detectorState: { ...this.state }, + }); + this.setState({ creatingDetector: false }); - }; - private createDashboard = ( - detectorName: string, - logType: string, - detectorId: string, - inputIndices: string[] - ) => { - return this.props.services.savedObjectsService - .createSavedObject(detectorName, logType, detectorId, inputIndices) - .catch((error: any) => { - console.error(error); - }); + // navigate to detector details + this.props.history.push(`${ROUTES.DETECTOR_DETAILS}/${PENDING_DETECTOR_ID}`); }; onNextClick = () => { diff --git a/public/pages/CreateDetector/utils/constants.ts b/public/pages/CreateDetector/utils/constants.ts index 4b06a468b..e3d09b4c4 100644 --- a/public/pages/CreateDetector/utils/constants.ts +++ b/public/pages/CreateDetector/utils/constants.ts @@ -24,3 +24,5 @@ export const createDetectorSteps: Record void; + isEditable: boolean; } export const DetectorBasicDetailsView: React.FC = ({ @@ -28,6 +29,7 @@ export const DetectorBasicDetailsView: React.FC = children, dashboardId, onEditClicked, + isEditable = true, }) => { const { name, detector_type, inputs, schedule } = detector; const detectorSchedule = parseSchedule(schedule); @@ -55,11 +57,15 @@ export const DetectorBasicDetailsView: React.FC = return ( - Edit - , - ]} + actions={ + isEditable + ? [ + + Edit + , + ] + : null + } > {createTextDetailsGroup(firstTextDetailsGroupEntries, 4)} diff --git a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx index ec0c0e677..a8ec85ac6 100644 --- a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx +++ b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx @@ -8,7 +8,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiAccordion, EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; import { ServicesContext } from '../../../../services'; -import { Detector } from '../../../../../models/interfaces'; import { RuleInfo } from '../../../../../server/models/interfaces'; import { errorNotificationToast, translateToRuleItems } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -16,12 +15,14 @@ import { RulesTable } from '../../../Rules/components/RulesTable/RulesTable'; import { RuleTableItem } from '../../../Rules/utils/helpers'; import { RuleViewerFlyout } from '../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; import { DataStore } from '../../../../store/DataStore'; +import { Detector } from '../../../../../types'; export interface DetectorRulesViewProps { detector: Detector; rulesCanFold?: boolean; onEditClicked: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void; notifications: NotificationsStart; + isEditable: boolean; } const mapRuleItemToRuleTableItem = (ruleItem: RuleItem): RuleTableItem => { @@ -49,14 +50,16 @@ export const DetectorRulesView: React.FC = (props) => { const [enabledRuleItems, setEnabledRuleItems] = useState([]); const [allRuleItems, setAllRuleItems] = useState([]); const [loading, setLoading] = useState(false); - const actions = [ - props.onEditClicked(enabledRuleItems, allRuleItems)} - data-test-subj={'edit-detector-rules'} - > - Edit - , - ]; + const actions = props.isEditable + ? [ + props.onEditClicked(enabledRuleItems, allRuleItems)} + data-test-subj={'edit-detector-rules'} + > + Edit + , + ] + : null; const services = useContext(ServicesContext); useEffect(() => { diff --git a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap index cedeea49f..6598f2bc1 100644 --- a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap +++ b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap @@ -178,6 +178,7 @@ exports[` spec renders the component 1`] = ` "type": "detector", } } + isEditable={true} notifications={ Object { "toasts": Object { diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx index 06c782874..6dff33d84 100644 --- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx +++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx @@ -8,15 +8,17 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're import { EuiBasicTableColumn, EuiButton, EuiInMemoryTable } from '@elastic/eui'; import { FieldMappingsTableItem } from '../../../CreateDetector/models/interfaces'; import { ServicesContext } from '../../../../services'; -import { Detector, FieldMapping } from '../../../../../models/interfaces'; +import { FieldMapping } from '../../../../../models/interfaces'; import { errorNotificationToast } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { Detector } from '../../../../../types'; export interface FieldMappingsViewProps { detector: Detector; existingMappings?: FieldMapping[]; editFieldMappings: () => void; notifications: NotificationsStart; + isEditable: boolean; } const columns: EuiBasicTableColumn[] = [ @@ -36,13 +38,17 @@ export const FieldMappingsView: React.FC = ({ existingMappings, editFieldMappings, notifications, + isEditable = true, }) => { const actions = useMemo( - () => [ - - Edit - , - ], + () => + isEditable + ? [ + + Edit + , + ] + : null, [] ); const [fieldMappingItems, setFieldMappingItems] = useState([]); diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx index edd77e191..6ff215498 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx @@ -11,7 +11,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { Detector, PeriodSchedule } from '../../../../../models/interfaces'; +import { PeriodSchedule } from '../../../../../models/interfaces'; import React, { useContext, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import DetectorBasicDetailsForm from '../../../CreateDetector/components/DefineDetector/components/DetectorDetails'; @@ -25,6 +25,7 @@ import { ServerResponse } from '../../../../../server/models/types'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; +import { Detector } from '../../../../../types'; export interface UpdateDetectorBasicDetailsProps extends RouteComponentProps { diff --git a/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx index f135efb03..d9ff1a9c1 100644 --- a/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx +++ b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx @@ -6,7 +6,7 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { Detector, FieldMapping } from '../../../../../models/interfaces'; +import { FieldMapping } from '../../../../../models/interfaces'; import FieldMappingService from '../../../../services/FieldMappingService'; import { DetectorHit, SearchDetectorsResponse } from '../../../../../server/models/interfaces'; import { BREADCRUMBS, EMPTY_DEFAULT_DETECTOR, ROUTES } from '../../../../utils/constants'; @@ -16,6 +16,7 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers'; import EditFieldMappings from '../../containers/FieldMappings/EditFieldMapping'; import { CoreServicesContext } from '../../../../components/core_services'; +import { Detector } from '../../../../../types'; export interface UpdateFieldMappingsProps extends RouteComponentProps { diff --git a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx index a193b3238..b53748e6d 100644 --- a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx +++ b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx @@ -12,7 +12,6 @@ import { import React, { useCallback, useContext, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; -import { Detector } from '../../../../../models/interfaces'; import { DetectionRulesTable } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRulesTable'; import { BREADCRUMBS, EMPTY_DEFAULT_DETECTOR, ROUTES } from '../../../../utils/constants'; import { ServicesContext } from '../../../../services'; @@ -24,6 +23,7 @@ import { RuleTableItem } from '../../../Rules/utils/helpers'; import { RuleViewerFlyout } from '../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; import { ContentPanel } from '../../../../components/ContentPanel'; import { DataStore } from '../../../../store/DataStore'; +import { Detector } from '../../../../../types'; export interface UpdateDetectorRulesProps extends RouteComponentProps< diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx index 92e1d721b..1322eb1b9 100644 --- a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx +++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx @@ -23,17 +23,22 @@ export interface AlertTriggersViewProps { detector: Detector; editAlertTriggers: () => void; notifications: NotificationsStart; + isEditable: boolean; } export const AlertTriggersView: React.FC = ({ detector, editAlertTriggers, notifications, + isEditable = true, }) => { const services = useContext(ServicesContext); const [channels, setChannels] = useState([]); const [rules, setRules] = useState<{ [key: string]: RuleInfo }>({}); - const actions = useMemo(() => [Edit], []); + const actions = useMemo( + () => (isEditable ? [Edit] : null), + [] + ); useEffect(() => { const getNotificationChannels = async () => { diff --git a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx index c6810211f..46e90b969 100644 --- a/public/pages/Detectors/containers/Detector/DetectorDetails.tsx +++ b/public/pages/Detectors/containers/Detector/DetectorDetails.tsx @@ -16,6 +16,9 @@ import { EuiTabs, EuiTitle, EuiHealth, + EuiCallOut, + EuiLoadingSpinner, + EuiPanel, } from '@elastic/eui'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; @@ -23,18 +26,20 @@ import { CoreServicesContext } from '../../../../components/core_services'; import { BREADCRUMBS, EMPTY_DEFAULT_DETECTOR_HIT, - pendingDashboardCreations, + logTypesWithDashboards, ROUTES, } from '../../../../utils/constants'; -import { DetectorHit } from '../../../../../server/models/interfaces'; +import { CreateMappingsResponse, DetectorHit } from '../../../../../server/models/interfaces'; import { DetectorDetailsView } from '../DetectorDetailsView/DetectorDetailsView'; import { FieldMappingsView } from '../../components/FieldMappingsView/FieldMappingsView'; import { AlertTriggersView } from '../AlertTriggersView/AlertTriggersView'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; import { DetectorsService } from '../../../../services'; -import { errorNotificationToast } from '../../../../utils/helpers'; +import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers'; import { NotificationsStart, SimpleSavedObject } from 'opensearch-dashboards/public'; -import { ISavedObjectsService, ServerResponse } from '../../../../../types'; +import { CreateDetectorResponse, ISavedObjectsService, ServerResponse } from '../../../../../types'; +import { PENDING_DETECTOR_ID } from '../../../CreateDetector/utils/constants'; +import { DataStore } from '../../../../store/DataStore'; export interface DetectorDetailsProps extends RouteComponentProps< @@ -61,6 +66,7 @@ export interface DetectorDetailsState { tabs: any[]; loading: boolean; dashboardId?: string; + createFailed: boolean; } enum TabId { @@ -109,6 +115,9 @@ export class DetectorDetails extends React.Component ), }, @@ -133,6 +143,7 @@ export class DetectorDetails extends React.Component ), }, @@ -144,6 +155,7 @@ export class DetectorDetails extends React.Component ), }, @@ -154,60 +166,166 @@ export class DetectorDetails extends React.Component { - if (savedObject && savedObject.ok) { - this.setState({ dashboardId: savedObject.response.id }); - this.getTabs(); - } - delete pendingDashboardCreations[this.state.detectorId]; + private createDashboard = ( + detectorName: string, + logType: string, + detectorId: string, + inputIndices: string[] + ) => { + return this.props.savedObjectsService + .createSavedObject(detectorName, logType, detectorId, inputIndices) + .catch((error: any) => { + console.error(error); }); - } else { - const dashboards = await this.props.savedObjectsService.getDashboards(); - let detectorDashboardId; - dashboards.some((dashboard) => { - if ( - dashboard.references.findIndex((reference) => reference.id === this.state.detectorId) > -1 - ) { - detectorDashboardId = dashboard.id; - return true; - } + }; + + getPendingDetector = async () => { + const pendingState = DataStore.detectors.getPendingState(); + const detector = pendingState?.detectorState?.detector; + const pendingRequests = pendingState?.pendingRequests; + this.getTabs(); - return false; + if (pendingRequests && detector) { + this.detectorHit = Object.assign({}, EMPTY_DEFAULT_DETECTOR_HIT, { + ...detector, + _source: { + ...detector, + }, }); - if (detectorDashboardId) { - this.setState({ dashboardId: detectorDashboardId }); - this.getTabs(); + this.context.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.DETECTORS_DETAILS(detector.name, PENDING_DETECTOR_ID), + ]); + + const [mappingsResponse, detectorResponse] = (await Promise.all(pendingRequests)) as [ + ServerResponse, + ServerResponse + ]; + + if (mappingsResponse.ok) { + if (detectorResponse.ok) { + let dashboardId; + const detectorId = detectorResponse.response._id; + if (logTypesWithDashboards.has(detector.detector_type)) { + const dashboardResponse = await this.createDashboard( + detector.name, + detector.detector_type, + detectorResponse.response._id, + detector.inputs[0].detector_input.indices + ); + if (dashboardResponse && dashboardResponse.ok) { + dashboardId = dashboardResponse.response.id; + } else { + const dashboards = await this.props.savedObjectsService.getDashboards(); + dashboards.some((dashboard) => { + if ( + dashboard.references.findIndex( + (reference) => reference.id === this.state.detectorId + ) > -1 + ) { + dashboardId = dashboard.id; + return true; + } + + return false; + }); + } + } + + this.setState( + { + detectorId, + dashboardId, + }, + () => { + DataStore.detectors.deletePendingState(); + this.props.history.push(`${ROUTES.DETECTOR_DETAILS}/${detectorId}`); + this.getDetector(); + } + ); + + successNotificationToast( + this.props.notifications, + 'created', + `detector, "${detector.name}"` + ); + } else { + this.setState( + { + createFailed: true, + }, + () => + errorNotificationToast( + this.props.notifications, + 'create', + 'detector', + detectorResponse.error + ) + ); + } + } else { + this.setState( + { + createFailed: true, + }, + () => + errorNotificationToast( + this.props.notifications, + 'create', + 'detector', + 'Double check the field mappings and try again.' + ) + ); } } + this.setState({ loading: false }); + }; + + async componentDidMount() { + const pendingState = DataStore.detectors.getPendingState(); + pendingState ? this.getPendingDetector() : this.getDetector(); } getDetector = async () => { this.setState({ loading: true }); const { detectorService, notifications } = this.props; try { + const { detectorId } = this.state; const response = await detectorService.getDetectors(); if (response.ok) { - const { detectorId } = this.state; const detector = response.response.hits.hits.find( (detectorHit) => detectorHit._id === detectorId ) as DetectorHit; + this.detectorHit = { ...detector, _source: { @@ -226,6 +344,7 @@ export class DetectorDetails extends React.Component { + const pendingState = DataStore.detectors.getPendingState(); + const detectorState = pendingState?.detectorState; + this.props.history.push({ + pathname: `${ROUTES.DETECTORS_CREATE}`, + // @ts-ignore + state: { detectorState }, + }); + DataStore.detectors.deletePendingState(); + }; + render() { const { _source: detector } = this.detectorHit; - const { selectedTabContent } = this.state; + const { selectedTabContent, detectorId, createFailed } = this.state; + const creatingDetector: boolean = detectorId === PENDING_DETECTOR_ID; + + let statusColor = 'primary'; + let statusText = 'Initializing'; + + if (creatingDetector) { + if (createFailed) { + statusColor = 'danger'; + statusText = 'Failed'; + } + } else { + if (detector.enabled) { + statusColor = 'success'; + statusText = 'Active'; + } else { + statusColor = 'subdued'; + statusText = 'Inactive'; + } + } return ( <> + {creatingDetector ? ( + <> + + {!createFailed && ( + + + + )} + + {createFailed + ? 'Detector creation failed. Please review detector configuration and try again.' + : 'Attempting to create the detector.'} + + + } + color={createFailed ? 'danger' : 'primary'} + > + {createFailed && ( + + Review detector configuration + + )} + + + + ) : null} @@ -408,21 +585,21 @@ export class DetectorDetails extends React.Component - - {detector.enabled ? 'Active' : 'Inactive'} - + {statusText} - {this.createHeaderActions().map( - (action: React.ReactNode, idx: number): React.ReactNode => ( - - {action} - - ) - )} + {!creatingDetector && !createFailed + ? this.createHeaderActions().map( + (action: React.ReactNode, idx: number): React.ReactNode => ( + + {action} + + ) + ) + : null} diff --git a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap index 29921169b..e492bc285 100644 --- a/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap +++ b/public/pages/Detectors/containers/Detector/__snapshots__/DetectorDetails.test.tsx.snap @@ -185,20 +185,8 @@ exports[` spec renders the component 1`] = ` } } detectorService={ - DetectorService { - "createDetector": [Function], - "deleteDetector": [Function], - "getDetector": [Function], + Object { "getDetectors": [Function], - "osDriver": Object { - "delete": [MockFunction], - "get": [MockFunction], - "head": [MockFunction], - "post": [MockFunction], - "put": [MockFunction], - }, - "searchDetectors": [Function], - "updateDetector": [Function], } } history={ @@ -886,20 +874,8 @@ exports[` spec renders the component 1`] = ` } } detectorService={ - DetectorService { - "createDetector": [Function], - "deleteDetector": [Function], - "getDetector": [Function], + Object { "getDetectors": [Function], - "osDriver": Object { - "delete": [MockFunction], - "get": [MockFunction], - "head": [MockFunction], - "post": [MockFunction], - "put": [MockFunction], - }, - "searchDetectors": [Function], - "updateDetector": [Function], } } editBasicDetails={[Function]} @@ -914,6 +890,7 @@ exports[` spec renders the component 1`] = ` "replace": [MockFunction], } } + isEditable={true} last_update_time={1} location={ Object { @@ -1303,20 +1280,8 @@ exports[` spec renders the component 1`] = ` } } detectorService={ - DetectorService { - "createDetector": [Function], - "deleteDetector": [Function], - "getDetector": [Function], + Object { "getDetectors": [Function], - "osDriver": Object { - "delete": [MockFunction], - "get": [MockFunction], - "head": [MockFunction], - "post": [MockFunction], - "put": [MockFunction], - }, - "searchDetectors": [Function], - "updateDetector": [Function], } } editBasicDetails={[Function]} @@ -1331,6 +1296,7 @@ exports[` spec renders the component 1`] = ` "replace": [MockFunction], } } + isEditable={true} last_update_time={1} location={ Object { @@ -2669,20 +2635,8 @@ exports[` spec renders the component 1`] = ` } } detectorService={ - DetectorService { - "createDetector": [Function], - "deleteDetector": [Function], - "getDetector": [Function], + Object { "getDetectors": [Function], - "osDriver": Object { - "delete": [MockFunction], - "get": [MockFunction], - "head": [MockFunction], - "post": [MockFunction], - "put": [MockFunction], - }, - "searchDetectors": [Function], - "updateDetector": [Function], } } editBasicDetails={[Function]} @@ -2697,6 +2651,7 @@ exports[` spec renders the component 1`] = ` "replace": [MockFunction], } } + isEditable={true} last_update_time={1} location={ Object { diff --git a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx index 73d610223..58d8dcde2 100644 --- a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx +++ b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { DetectorBasicDetailsView } from '../../components/DetectorBasicDetailsView/DetectorBasicDetailsView'; import { DetectorRulesView } from '../../components/DetectorRulesView/DetectorRulesView'; import { EuiSpacer } from '@elastic/eui'; -import { Detector } from '../../../../../models/interfaces'; import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { Detector } from '../../../../../types'; export interface DetectorDetailsViewProps { detector: Detector; @@ -20,6 +20,7 @@ export interface DetectorDetailsViewProps { dashboardId?: string; editBasicDetails: () => void; editDetectorRules: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void; + isEditable: boolean; } export interface DetectorDetailsViewState {} @@ -37,6 +38,7 @@ export class DetectorDetailsView extends React.Component< dashboardId, editBasicDetails, editDetectorRules, + isEditable = true, } = this.props; const detectorRules = ( ); @@ -57,6 +60,7 @@ export class DetectorDetailsView extends React.Component< rulesCanFold={rulesCanFold} dashboardId={dashboardId} onEditClicked={editBasicDetails} + isEditable={isEditable} > {rulesCanFold ? detectorRules : null} diff --git a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap index 437227411..3f9cac30e 100644 --- a/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap +++ b/public/pages/Detectors/containers/DetectorDetailsView/__snapshots__/DetectorDetailsView.test.tsx.snap @@ -371,6 +371,7 @@ exports[` spec renders the component 1`] = ` editBasicDetails={[MockFunction]} editDetectorRules={[MockFunction]} enabled_time={1} + isEditable={true} last_update_time={1} notifications={ Object { @@ -1510,6 +1511,7 @@ exports[` spec renders the component 1`] = ` editBasicDetails={[MockFunction]} editDetectorRules={[MockFunction]} enabled_time={1} + isEditable={true} last_update_time={1} notifications={ Object { diff --git a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx index 03944e0f8..515cec45c 100644 --- a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx +++ b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx @@ -15,9 +15,10 @@ import { } from '@elastic/eui'; import FieldMappingsTable from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { Detector, FieldMapping } from '../../../../../models/interfaces'; +import { FieldMapping } from '../../../../../models/interfaces'; import FieldMappingService from '../../../../services/FieldMappingService'; import { MappingViewType } from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable'; +import { Detector } from '../../../../../types'; export interface ruleFieldToIndexFieldMap { [fieldName: string]: string; @@ -196,23 +197,17 @@ export default class EditFieldMappings extends Component< - {unmappedRuleFields.length > 0 && ( + {unmappedRuleFields.length > 0 ? ( <> - {unmappedRuleFields.length > 0 ? ( - -

- To generate accurate findings, we recommend mapping the following security rules - fields with the log fields in your data source. -

-
- ) : ( - -

Your data source have been mapped with all security rule fields.

-
- )} + +

+ To generate accurate findings, we recommend mapping the following security rules + fields with the log fields in your data source. +

+
@@ -229,8 +224,11 @@ export default class EditFieldMappings extends Component< }} /> - + ) : ( + +

Your data source have been mapped with all security rule fields.

+
)} diff --git a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap index 59bffb581..e44d1149f 100644 --- a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap +++ b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap @@ -1103,6 +1103,45 @@ exports[` spec renders the component 1`] = ` className="euiSpacer euiSpacer--m" /> + +
+
+ + All rule fields have been mapped + +
+ +
+ +
+

+ Your data source have been mapped with all security rule fields. +

+
+
+
+
+
+
diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 6029454de..9c3cad9d0 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -265,6 +265,7 @@ export default class Main extends Component { {...props} isEdit={false} services={services} + history={props.history} notifications={core?.notifications} /> )} diff --git a/public/store/DataStore.ts b/public/store/DataStore.ts index 3a751b65c..9a0687cfd 100644 --- a/public/store/DataStore.ts +++ b/public/store/DataStore.ts @@ -3,14 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RulesStore } from '../pages/Rules/store/RulesStore'; +import { RulesStore } from './RulesStore'; import { BrowserServices } from '../models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import { DetectorsStore } from './DetectorsStore'; export class DataStore { public static rules: RulesStore; + public static detectors: DetectorsStore; public static init = (services: BrowserServices, notifications: NotificationsStart) => { DataStore.rules = new RulesStore(services.ruleService, notifications); + DataStore.detectors = new DetectorsStore(services.detectorsService, notifications); }; } diff --git a/public/store/DetectorsStore.ts b/public/store/DetectorsStore.ts new file mode 100644 index 000000000..3a39224dd --- /dev/null +++ b/public/store/DetectorsStore.ts @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DetectorsService } from '../services'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { CreateDetectorState } from '../pages/CreateDetector/containers/CreateDetector'; + +export interface IDetectorsStore {} +export interface IDetectorsCache {} + +export interface IDetectorsState { + pendingRequests: Promise[]; + detectorState: CreateDetectorState; +} + +/** + * Class is used to make detector's API calls and cache the detectors. + * If there is a cache data requests are skipped and result is returned from the cache. + * If cache is invalidated then the request is made to get a new set of data. + * + * @class DetectorsStore + * @implements IDetectorsStore + * @param {BrowserServices} services Uses services to make API requests + */ +export class DetectorsStore implements IDetectorsStore { + /** + * Rule service instance + * + * @property {DetectorsService} service + * @readonly + */ + readonly service: DetectorsService; + + /** + * Notifications + * @property {NotificationsStart} + * @readonly + */ + readonly notifications: NotificationsStart; + + /** + * Keeps detector's data cached + * + * @property {IDetectorsCache} cache + */ + private cache: IDetectorsCache = {}; + + private state: IDetectorsState | undefined; + + constructor(service: DetectorsService, notifications: NotificationsStart) { + this.service = service; + this.notifications = notifications; + } + + /** + * Invalidates all detectors data + */ + private invalidateCache = () => { + this.cache = {}; + return this; + }; + + public setPendingState = (state: IDetectorsState) => { + this['state'] = state; + }; + + public getPendingState = () => { + if (!this.state) return undefined; + return { + ...this.state, + }; + }; + + public deletePendingState = () => { + delete this.state; + }; +} diff --git a/public/pages/Rules/store/RulesStore.test.ts b/public/store/RulesStore.test.ts similarity index 81% rename from public/pages/Rules/store/RulesStore.test.ts rename to public/store/RulesStore.test.ts index 24a6584d4..cbdcb0802 100644 --- a/public/pages/Rules/store/RulesStore.test.ts +++ b/public/store/RulesStore.test.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataStore } from '../../../store/DataStore'; -import notificationsStartMock from '../../../../test/mocks/services/notifications/NotificationsStart.mock'; -import services from '../../../../test/mocks/services'; +import { DataStore } from './DataStore'; +import notificationsStartMock from '../../test/mocks/services/notifications/NotificationsStart.mock'; +import services from '../../test/mocks/services'; import { RulesStore } from './RulesStore'; import { expect } from '@jest/globals'; -import * as rulesResponseMock from '../../../../cypress/fixtures/sample_rule.json'; +import * as rulesResponseMock from '../../cypress/fixtures/sample_rule.json'; describe('Rules store specs', () => { Object.assign(services, { ruleService: { diff --git a/public/pages/Rules/store/RulesStore.ts b/public/store/RulesStore.ts similarity index 96% rename from public/pages/Rules/store/RulesStore.ts rename to public/store/RulesStore.ts index 774174b2a..ba735767d 100644 --- a/public/pages/Rules/store/RulesStore.ts +++ b/public/store/RulesStore.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RuleService } from '../../../services'; +import { RuleService } from '../services'; import { load, safeDump } from 'js-yaml'; -import { RuleItemInfoBase, IRulesStore, IRulesCache } from '../../../../types'; -import { Rule } from '../../../../models/interfaces'; +import { RuleItemInfoBase, IRulesStore, IRulesCache } from '../../types'; +import { Rule } from '../../models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { errorNotificationToast } from '../../../utils/helpers'; -import { ruleTypes } from '../utils/constants'; +import { errorNotificationToast } from '../utils/helpers'; +import { ruleTypes } from '../pages/Rules/utils/constants'; import _ from 'lodash'; /** diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 3e90f8a8d..89bc2d431 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -4,8 +4,8 @@ */ import { SimpleSavedObject } from 'opensearch-dashboards/public'; -import { ServerResponse } from '../../types'; -import { Detector, DetectorInput, PeriodSchedule } from '../../models/interfaces'; +import { Detector, ServerResponse } from '../../types'; +import { DetectorInput, PeriodSchedule } from '../../models/interfaces'; import { DetectorHit } from '../../server/models/interfaces'; import { DETECTOR_TYPES } from '../pages/Detectors/utils/constants'; diff --git a/test/mocks/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.mock.ts b/test/mocks/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.mock.ts index 6c30e29ae..61d89e49f 100644 --- a/test/mocks/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.mock.ts +++ b/test/mocks/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.mock.ts @@ -12,4 +12,5 @@ export default ({ enabled_time: 1, last_update_time: 1, onEditClicked: () => jest.fn(), + isEditable: true, } as unknown) as typeof DetectorBasicDetailsView; diff --git a/test/mocks/Detectors/components/DetectorRulesView/DetectorRulesView.mock.ts b/test/mocks/Detectors/components/DetectorRulesView/DetectorRulesView.mock.ts index 1c1d8c4c9..721ea5fd4 100644 --- a/test/mocks/Detectors/components/DetectorRulesView/DetectorRulesView.mock.ts +++ b/test/mocks/Detectors/components/DetectorRulesView/DetectorRulesView.mock.ts @@ -5,11 +5,12 @@ import detectorMock from '../../containers/Detectors/Detector.mock'; import notificationsStartMock from '../../../services/notifications/NotificationsStart.mock'; -import { DetectorRulesView } from '../../../../../public/pages/Detectors/components/DetectorRulesView/DetectorRulesView'; +import { DetectorRulesViewProps } from '../../../../../public/pages/Detectors/components/DetectorRulesView/DetectorRulesView'; export default ({ detector: detectorMock, rulesCanFold: false, onEditClicked: jest.fn(), notifications: notificationsStartMock, -} as unknown) as typeof DetectorRulesView; + isEditable: true, +} as unknown) as DetectorRulesViewProps; diff --git a/test/mocks/Detectors/components/FieldMappingsView/FieldMappingsView.mock.ts b/test/mocks/Detectors/components/FieldMappingsView/FieldMappingsView.mock.ts index de09dbdc3..546b9d75f 100644 --- a/test/mocks/Detectors/components/FieldMappingsView/FieldMappingsView.mock.ts +++ b/test/mocks/Detectors/components/FieldMappingsView/FieldMappingsView.mock.ts @@ -1,11 +1,12 @@ import notificationsStartMock from '../../../services/notifications/NotificationsStart.mock'; import detectorMock from '../../containers/Detectors/Detector.mock'; import fieldMappingMock from './FieldMapping.mock'; -import { FieldMappingsView } from '../../../../../public/pages/Detectors/components/FieldMappingsView/FieldMappingsView'; +import { FieldMappingsViewProps } from '../../../../../public/pages/Detectors/components/FieldMappingsView/FieldMappingsView'; export default ({ detector: detectorMock, existingMappings: [fieldMappingMock, fieldMappingMock], editFieldMappings: jest.fn(), notifications: notificationsStartMock, -} as unknown) as typeof FieldMappingsView; + isEditable: true, +} as unknown) as FieldMappingsViewProps; diff --git a/test/mocks/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.mock.ts b/test/mocks/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.mock.ts index a21121229..1c6891874 100644 --- a/test/mocks/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.mock.ts +++ b/test/mocks/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.mock.ts @@ -1,16 +1,15 @@ import detectorHitMock from '../../containers/Detectors/DetectorHit.mock'; -import services from '../../../services'; import notificationsStartMock from '../../../services/notifications/NotificationsStart.mock'; import browserHistoryMock from '../../../services/browserHistory.mock'; import UpdateFieldMappings from '../../../../../public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings'; - -const { detectorService, fieldMappingService } = services; +import { detectorServiceMock } from '../../../services/detectorService.mock'; +import fieldMappingServiceMock from '../../../services/fieldMappingService.mock'; export default ({ detectorHit: detectorHitMock, - detectorService: detectorService, + detectorService: detectorServiceMock, notifications: notificationsStartMock, - filedMappingService: fieldMappingService, + filedMappingService: fieldMappingServiceMock, location: { state: { detectorHit: detectorHitMock, diff --git a/test/mocks/Detectors/containers/DetectorDetails/DetectorDetails.mock.ts b/test/mocks/Detectors/containers/DetectorDetails/DetectorDetails.mock.ts index 4dc665222..87e5d4c7b 100644 --- a/test/mocks/Detectors/containers/DetectorDetails/DetectorDetails.mock.ts +++ b/test/mocks/Detectors/containers/DetectorDetails/DetectorDetails.mock.ts @@ -4,21 +4,19 @@ */ import detectorHitMock from '../Detectors/DetectorHit.mock'; -import services from '../../../services'; import notificationsStartMock from '../../../services/notifications/NotificationsStart.mock'; import browserHistoryMock from '../../../services/browserHistory.mock'; import savedObjectsServiceMock from '../../../services/savedObjectService.mock'; -import { DetectorDetails } from '../../../../../public/pages/Detectors/containers/Detector/DetectorDetails'; - -const { detectorService } = services; +import { DetectorDetailsProps } from '../../../../../public/pages/Detectors/containers/Detector/DetectorDetails'; +import { detectorServiceMock } from '../../../services/detectorService.mock'; export default ({ detectorHit: detectorHitMock, - detectorService: detectorService, + detectorService: detectorServiceMock, notifications: notificationsStartMock, location: { pathname: '/detector-details/detector_id_1', }, history: browserHistoryMock, savedObjectsService: savedObjectsServiceMock, -} as unknown) as typeof DetectorDetails; +} as unknown) as DetectorDetailsProps; diff --git a/types/Detector.ts b/types/Detector.ts index 3632dcde8..1e7857f74 100644 --- a/types/Detector.ts +++ b/types/Detector.ts @@ -24,7 +24,7 @@ export interface DetectorSchedule { export interface Detector { id?: string; - type: 'detector'; + type: string; detector_type: string; name: string; enabled: boolean;