diff --git a/common/helpers.ts b/common/helpers.ts new file mode 100644 index 000000000..af8677885 --- /dev/null +++ b/common/helpers.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import { DEFAULT_METRICS_COUNTER } from '../server/utils/constants'; +import { MetricsCounter, PartialMetricsCounter } from '../types'; +import { SecurityAnalyticsPluginConfigType } from '../config'; + +export function aggregateMetrics( + metrics: PartialMetricsCounter, + currentMetricsCounter: PartialMetricsCounter +): MetricsCounter { + const partialMetrics: PartialMetricsCounter = { + ...currentMetricsCounter, + }; + Object.keys(metrics).forEach((w) => { + const workflow = w as keyof MetricsCounter; + const workFlowMetrics = metrics[workflow]; + + if (workFlowMetrics) { + const counterToUpdate: any = + partialMetrics[workflow] || _.cloneDeep(DEFAULT_METRICS_COUNTER[workflow]); + Object.entries(workFlowMetrics).forEach(([metric, count]) => { + if (!counterToUpdate[metric]) { + counterToUpdate[metric] = 0; + } + counterToUpdate[metric] += count; + }); + + partialMetrics[workflow] = counterToUpdate; + } + }); + + return partialMetrics as MetricsCounter; +} + +let securityAnalyticsPluginConfig: SecurityAnalyticsPluginConfigType; +export const setSecurityAnalyticsPluginConfig = (config: SecurityAnalyticsPluginConfigType) => { + securityAnalyticsPluginConfig = config; +}; + +export const getSecurityAnalyticsPluginConfig = (): SecurityAnalyticsPluginConfigType | undefined => + securityAnalyticsPluginConfig; diff --git a/config.ts b/config.ts new file mode 100644 index 000000000..7fd489269 --- /dev/null +++ b/config.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema, TypeOf } from '@osd/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + // Interval in minutes at which the browser should emit the metrics to the Kibana server + // Setting this to "0" will disable the metrics + uxTelemetryInterval: schema.number({ defaultValue: 2 }), +}); + +export type SecurityAnalyticsPluginConfigType = TypeOf; diff --git a/public/components/Modal/Modal.test.tsx b/public/components/Modal/Modal.test.tsx index 04de2cecf..017e0ebc4 100644 --- a/public/components/Modal/Modal.test.tsx +++ b/public/components/Modal/Modal.test.tsx @@ -8,19 +8,24 @@ import { EuiButton, EuiOverlayMask, EuiModal } from '@elastic/eui'; import { render, fireEvent } from '@testing-library/react'; import ModalRoot from './ModalRoot'; import { ModalConsumer, ModalProvider } from './Modal'; -import { ServicesConsumer, ServicesContext } from '../../services'; +import { SecurityAnalyticsContext, SaContextConsumer } from '../../services'; import services from '../../../test/mocks/services'; +import { MetricsContext } from '../../metrics/MetricsContext'; +import MetricsService from '../../services/MetricsService'; +import httpClientMock from '../../../test/mocks/services/httpClient.mock'; describe(' spec', () => { it('renders nothing when not used', () => { const { container } = render( - + - - {(services) => services && } - + + {(context) => context?.services && } + - + ); expect(container.firstChild).toBeNull(); @@ -34,11 +39,13 @@ describe(' spec', () => { ); const { queryByText, getByTestId, getByLabelText } = render(
- + - - {(services) => services && } - + + {(context) => context?.services && } + {({ onShow }) => ( spec', () => { )} - +
); diff --git a/public/metrics/DetectorMetricsManager.ts b/public/metrics/DetectorMetricsManager.ts new file mode 100644 index 000000000..edb17f01e --- /dev/null +++ b/public/metrics/DetectorMetricsManager.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CreateDetectorSteps, CreateDetectorStepValue } from '../../types'; +import MetricsService from '../services/MetricsService'; + +export class DetectorMetricsManager { + private static initialCheckpoint = CreateDetectorStepValue[CreateDetectorSteps.notStarted]; + private stepsLogged: number = DetectorMetricsManager.initialCheckpoint; + private creationStartedStepValue = CreateDetectorStepValue[CreateDetectorSteps.started]; + + constructor(private readonly metricsService: MetricsService) {} + + public sendMetrics(step: CreateDetectorSteps, stepNameForCounter?: string) { + const stepValue = CreateDetectorStepValue[step]; + + // If we are not in detection creation flow, we should not emit any metric + if ( + stepValue !== this.creationStartedStepValue && + !this.metricEmittedForStep(this.creationStartedStepValue) + ) { + return; + } + + // Checks if we have already emitted metrics for this step + if (!this.metricEmittedForStep(stepValue)) { + this.metricsService.updateMetrics({ + CreateDetector: { + [stepNameForCounter || step]: 1, + }, + }); + this.stepsLogged |= stepValue; + } + } + + public resetMetrics() { + this.stepsLogged = DetectorMetricsManager.initialCheckpoint; + } + + private metricEmittedForStep(stepValue: number): boolean { + return (this.stepsLogged & stepValue) === stepValue; + } +} diff --git a/public/metrics/MetricsContext.ts b/public/metrics/MetricsContext.ts new file mode 100644 index 000000000..3718118b0 --- /dev/null +++ b/public/metrics/MetricsContext.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import MetricsService from '../services/MetricsService'; +import { DetectorMetricsManager } from './DetectorMetricsManager'; + +export class MetricsContext { + public detectorMetricsManager: DetectorMetricsManager; + + constructor(metricsService: MetricsService) { + this.detectorMetricsManager = new DetectorMetricsManager(metricsService); + } +} diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index b547fd486..0601efe44 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -17,6 +17,7 @@ import { LogTypeService, } from '../services'; import CorrelationService from '../services/CorrelationService'; +import MetricsService from '../services/MetricsService'; export interface BrowserServices { detectorsService: DetectorsService; @@ -31,6 +32,7 @@ export interface BrowserServices { savedObjectsService: ISavedObjectsService; indexPatternsService: IndexPatternsService; logTypeService: LogTypeService; + metricsService: MetricsService; } export interface RuleOptions { diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx index 9e8087586..a14a8e8f1 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/components/AlertCondition/AlertConditionPanel.tsx @@ -44,7 +44,7 @@ interface AlertConditionPanelProps extends RouteComponentProps { isEdit: boolean; hasNotificationPlugin: boolean; loadingNotifications: boolean; - onAlertTriggerChanged: (newDetector: Detector) => void; + onAlertTriggerChanged: (newDetector: Detector, emitMetrics?: boolean) => void; refreshNotificationChannels: () => void; } @@ -76,7 +76,7 @@ export default class AlertConditionPanel extends Component< } componentDidMount() { - this.prepareMessage(); + this.prepareMessage(false /* updateMessage */, true /* onMount */); } onDetectionTypeChange(detectionType: 'rules' | 'threat_intel', enabled: boolean) { @@ -87,7 +87,10 @@ export default class AlertConditionPanel extends Component< }); } - prepareMessage = (updateMessage: boolean = false) => { + // When component mounts, we prepare message but at this point we don't want to emit the + // trigger changed metric since it is not user initiated. So we use the onMount flag to determine that + // and pass it downstream accordingly. + prepareMessage = (updateMessage: boolean = false, onMount: boolean = false) => { const { alertCondition, detector } = this.props; const detectorInput = detector.inputs[0].detector_input; const lineBreak = '\n'; @@ -101,7 +104,7 @@ export default class AlertConditionPanel extends Component< const defaultSubject = [alertConditionName, alertConditionSeverity, detectorName].join(' - '); if (updateMessage || !alertCondition.actions[0]?.subject_template.source) - this.onMessageSubjectChange(defaultSubject); + this.onMessageSubjectChange(defaultSubject, !onMount); if (updateMessage || !alertCondition.actions[0]?.message_template.source) { const selectedNames = this.setSelectedNames(alertCondition.ids); @@ -142,11 +145,11 @@ export default class AlertConditionPanel extends Component< if (alertConditionSelections.length) defaultMessageBody = defaultMessageBody + lineBreak + lineBreak + alertConditionSelections.join(lineBreak); - this.onMessageBodyChange(defaultMessageBody); + this.onMessageBodyChange(defaultMessageBody, !onMount); } }; - updateTrigger(trigger: Partial) { + updateTrigger(trigger: Partial, emitMetrics: boolean = true) { const { alertCondition, onAlertTriggerChanged, @@ -157,7 +160,7 @@ export default class AlertConditionPanel extends Component< trigger.types = [detector.detector_type.toLowerCase()]; const newTriggers = [...triggers]; newTriggers.splice(indexNum, 1, { ...alertCondition, ...trigger }); - onAlertTriggerChanged({ ...detector, triggers: newTriggers }); + onAlertTriggerChanged({ ...detector, triggers: newTriggers }, emitMetrics); } onNameBlur = (event: React.ChangeEvent) => { @@ -220,21 +223,21 @@ export default class AlertConditionPanel extends Component< onAlertTriggerChanged({ ...detector, triggers: triggers }); }; - onMessageSubjectChange = (subject: string) => { + onMessageSubjectChange = (subject: string, emitMetrics: boolean = true) => { const { alertCondition: { actions }, } = this.props; actions[0].name = subject; actions[0].subject_template.source = subject; - this.updateTrigger({ actions: actions }); + this.updateTrigger({ actions: actions }, emitMetrics); }; - onMessageBodyChange = (message: string) => { + onMessageBodyChange = (message: string, emitMetrics: boolean = true) => { const { alertCondition: { actions }, } = this.props; actions[0].message_template.source = message; - this.updateTrigger({ actions: actions }); + this.updateTrigger({ actions: actions }, emitMetrics); }; onDelete = () => { diff --git a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx index 6864c911b..5f36a817e 100644 --- a/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx +++ b/public/pages/CreateDetector/components/ConfigureAlerts/containers/ConfigureAlerts.tsx @@ -27,7 +27,13 @@ import { NotificationsService } from '../../../../../services'; import { validateName } from '../../../../../utils/validation'; import { CoreServicesContext } from '../../../../../components/core_services'; import { BREADCRUMBS } from '../../../../../utils/constants'; -import { AlertCondition, Detector, DetectorCreationStep } from '../../../../../../types'; +import { + AlertCondition, + CreateDetectorSteps, + Detector, + DetectorCreationStep, +} from '../../../../../../types'; +import { MetricsContext } from '../../../../../metrics/MetricsContext'; interface ConfigureAlertsProps extends RouteComponentProps { detector: Detector; @@ -38,6 +44,7 @@ interface ConfigureAlertsProps extends RouteComponentProps { notificationsService: NotificationsService; hasNotificationPlugin: boolean; getTriggerName: () => string; + metricsContext?: MetricsContext; } interface ConfigureAlertsState { @@ -114,7 +121,8 @@ export default class ConfigureAlerts extends Component { this.setState({ loading: true }); const channels = await getNotificationChannels(this.props.notificationsService); - this.setState({ notificationChannels: parseNotificationChannelsToOptions(channels) }); + const parsedChannels = parseNotificationChannelsToOptions(channels); + this.setState({ notificationChannels: parsedChannels }); this.setState({ loading: false }); }; @@ -134,13 +142,18 @@ export default class ConfigureAlerts extends Component { + onAlertTriggerChanged = (newDetector: Detector, emitMetrics: boolean = true): void => { const isTriggerDataValid = isTriggerValid( newDetector.triggers, this.props.hasNotificationPlugin ); this.props.changeDetector(newDetector); this.props.updateDataValidState(DetectorCreationStep.CONFIGURE_ALERTS, isTriggerDataValid); + if (emitMetrics) { + this.props.metricsContext?.detectorMetricsManager.sendMetrics( + CreateDetectorSteps.triggerConfigured + ); + } }; onDelete = (index: number) => { diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx index 3e0e407ca..df7070c91 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx @@ -21,7 +21,12 @@ import { GetFieldMappingViewResponse } from '../../../../../../server/models/int import FieldMappingService from '../../../../../services/FieldMappingService'; import { MappingViewType } from '../components/RequiredFieldMapping/FieldMappingsTable'; import { CreateDetectorRulesState } from '../../DefineDetector/components/DetectionRules/DetectionRules'; -import { Detector } from '../../../../../../types'; +import { + CreateDetectorSteps, + Detector, + SecurityAnalyticsContextType, +} from '../../../../../../types'; +import { SecurityAnalyticsContext } from '../../../../../services'; export interface ruleFieldToIndexFieldMap { [fieldName: string]: string; @@ -59,6 +64,10 @@ export default class ConfigureFieldMapping extends Component< ConfigureFieldMappingProps, ConfigureFieldMappingState > { + public static contextType?: + | React.Context + | undefined = SecurityAnalyticsContext; + constructor(props: ConfigureFieldMappingProps) { super(props); const createdMappings: ruleFieldToIndexFieldMap = {}; @@ -310,6 +319,9 @@ export default class ConfigureFieldMapping extends Component< invalidMappingFieldNames: invalidMappingFieldNames, }); this.updateMappingSharedState(newMappings); + this.context.metrics.detectorMetricsManager.sendMetrics( + CreateDetectorSteps.fieldMappingsConfigured + ); }; updateMappingSharedState = (createdMappings: ruleFieldToIndexFieldMap) => { diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx index 33e2c0f3f..d23821ffb 100644 --- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx @@ -11,14 +11,24 @@ import DetectorBasicDetailsForm from '../components/DetectorDetails'; import DetectorDataSource from '../components/DetectorDataSource'; import DetectorType from '../components/DetectorType'; import { EuiComboBoxOptionOption } from '@opensearch-project/oui'; -import { FieldMappingService, IndexService } from '../../../../../services'; +import { + FieldMappingService, + IndexService, + SecurityAnalyticsContext, +} from '../../../../../services'; import { MIN_NUM_DATA_SOURCES } from '../../../../Detectors/utils/constants'; import { DetectorSchedule } from '../components/DetectorSchedule/DetectorSchedule'; import { RuleItem } from '../components/DetectionRules/types/interfaces'; import { CreateDetectorRulesState } from '../components/DetectionRules/DetectionRules'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { logTypesWithDashboards } from '../../../../../utils/constants'; -import { Detector, DetectorCreationStep, FieldMapping } from '../../../../../../types'; +import { + CreateDetectorSteps, + Detector, + DetectorCreationStep, + FieldMapping, + SecurityAnalyticsContextType, +} from '../../../../../../types'; import { ConfigureFieldMappingProps } from '../../ConfigureFieldMapping/containers/ConfigureFieldMapping'; import { ContentPanel } from '../../../../../components/ContentPanel'; import { ruleTypes } from '../../../../Rules/utils/constants'; @@ -47,6 +57,9 @@ interface DefineDetectorState { } export default class DefineDetector extends Component { + public static contextType?: + | React.Context + | undefined = SecurityAnalyticsContext; private standardLogTypes = new Set( ruleTypes.filter((ruleType) => ruleType.isStandard).map(({ value }) => value) ); @@ -125,6 +138,7 @@ export default class DefineDetector extends Component { @@ -135,6 +149,10 @@ export default class DefineDetector extends Component { @@ -152,46 +170,9 @@ export default class DefineDetector extends Component { - const { inputs } = this.state.detector; - const newDetector: Detector = { - ...this.state.detector, - inputs: [ - { - detector_input: { - ...inputs[0].detector_input, - pre_packaged_rules: enabledRuleIds.map((id) => { - return { id }; - }), - }, - }, - ...inputs.slice(1), - ], - }; - - this.updateDetectorCreationState(newDetector); - }; - - onCustomRulesChanged = (enabledRuleIds: string[]) => { - const { inputs } = this.state.detector; - const newDetector: Detector = { - ...this.state.detector, - inputs: [ - { - detector_input: { - ...inputs[0].detector_input, - custom_rules: enabledRuleIds.map((id) => { - return { id }; - }), - }, - }, - ...inputs.slice(1), - ], - }; - - this.updateDetectorCreationState(newDetector); + this.context.metrics.detectorMetricsManager.sendMetrics( + CreateDetectorSteps.threatIntelConfigured + ); }; onDetectorScheduleChange = (schedule: PeriodSchedule) => { diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx index 1aa98fb3f..90339ad79 100644 --- a/public/pages/CreateDetector/containers/CreateDetector.tsx +++ b/public/pages/CreateDetector/containers/CreateDetector.tsx @@ -36,13 +36,15 @@ import { } from '../components/DefineDetector/components/DetectionRules/types/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { getPlugins } from '../../../utils/helpers'; -import { Detector, DetectorCreationStep } from '../../../../types'; +import { CreateDetectorSteps, Detector, DetectorCreationStep } from '../../../../types'; import { DataStore } from '../../../store/DataStore'; import { errorNotificationToast } from '../../../utils/helpers'; +import { MetricsContext } from '../../../metrics/MetricsContext'; interface CreateDetectorProps extends RouteComponentProps { isEdit: boolean; services: BrowserServices; + metrics: MetricsContext; history: RouteComponentProps['history']; notifications: NotificationsStart; } @@ -97,6 +99,8 @@ export default class CreateDetector extends Component { const { currentStep } = this.state; this.setState({ currentStep: currentStep + 1 }); + this.props.metrics.detectorMetricsManager.sendMetrics(CreateDetectorSteps.stepTwoInitiated); }; onPreviousClick = () => { @@ -291,6 +297,7 @@ export default class CreateDetector extends Component { @@ -341,6 +349,7 @@ export default class CreateDetector extends Component ); } diff --git a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx index 3bb719c14..7fb910418 100644 --- a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx +++ b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx @@ -7,7 +7,7 @@ import { ContentPanel } from '../../../../components/ContentPanel'; 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 { SecurityAnalyticsContext } from '../../../../services'; import { RuleInfo } from '../../../../../server/models/interfaces'; import { errorNotificationToast, translateToRuleItems } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -60,7 +60,7 @@ export const DetectorRulesView: React.FC = (props) => { , ] : null; - const services = useContext(ServicesContext); + const saContext = useContext(SecurityAnalyticsContext); useEffect(() => { const updateRulesState = async () => { @@ -108,7 +108,7 @@ export const DetectorRulesView: React.FC = (props) => { updateRulesState().catch((e) => { errorNotificationToast(props.notifications, 'retrieve', 'rules', e); }); - }, [services, props.detector]); + }, [saContext?.services, props.detector]); const getDetectionRulesTitle = () => `View detection rules`; diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx index e85131194..494300116 100644 --- a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx +++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx @@ -7,7 +7,7 @@ import { ContentPanel } from '../../../../components/ContentPanel'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { EuiBasicTableColumn, EuiButton, EuiInMemoryTable } from '@elastic/eui'; import { FieldMappingsTableItem } from '../../../CreateDetector/models/interfaces'; -import { ServicesContext } from '../../../../services'; +import { SecurityAnalyticsContext } from '../../../../services'; import { FieldMapping } from '../../../../../models/interfaces'; import { errorNotificationToast } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -53,11 +53,11 @@ export const FieldMappingsView: React.FC = ({ [] ); const [fieldMappingItems, setFieldMappingItems] = useState([]); - const services = useContext(ServicesContext); + const saContext = useContext(SecurityAnalyticsContext); const fetchFieldMappings = useCallback( async (indexName: string) => { - const getMappingRes = await services?.fieldMappingService.getMappings(indexName); + const getMappingRes = await saContext?.services.fieldMappingService.getMappings(indexName); if (getMappingRes?.ok) { const mappingsData = getMappingRes.response[indexName]; if (mappingsData) { @@ -73,7 +73,7 @@ export const FieldMappingsView: React.FC = ({ errorNotificationToast(notifications, 'retrieve', 'field mappings', getMappingRes?.error); } }, - [services, detector] + [saContext?.services, detector] ); useEffect(() => { diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx index 9dc550978..3ba0c4aa1 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx @@ -17,7 +17,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import DetectorBasicDetailsForm from '../../../CreateDetector/components/DefineDetector/components/DetectorDetails'; import DetectorDataSource from '../../../CreateDetector/components/DefineDetector/components/DetectorDataSource'; -import { FieldMappingService, IndexService, ServicesContext } from '../../../../services'; +import { FieldMappingService, IndexService, SecurityAnalyticsContext } from '../../../../services'; import { DetectorSchedule } from '../../../CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule'; import { useCallback } from 'react'; import { DetectorHit, SearchDetectorsResponse } from '../../../../../server/models/interfaces'; @@ -36,7 +36,7 @@ export interface UpdateDetectorBasicDetailsProps } export const UpdateDetectorBasicDetails: React.FC = (props) => { - const services = useContext(ServicesContext); + const saContext = useContext(SecurityAnalyticsContext); const [detector, setDetector] = useState( (props.location.state?.detectorHit?._source || EMPTY_DEFAULT_DETECTOR) as Detector ); @@ -57,7 +57,7 @@ export const UpdateDetectorBasicDetails: React.FC { const getDetector = async () => { - const response = (await services?.detectorsService.getDetectors()) as ServerResponse< + const response = (await saContext?.services.detectorsService.getDetectors()) as ServerResponse< SearchDetectorsResponse >; if (response.ok) { @@ -99,7 +99,7 @@ export const UpdateDetectorBasicDetails: React.FC { @@ -217,7 +217,7 @@ export const UpdateDetectorBasicDetails: React.FC { const detectorHit = props.location.state.detectorHit; - const updateDetectorRes = await services?.detectorsService?.updateDetector( + const updateDetectorRes = await saContext?.services.detectorsService?.updateDetector( detectorHit._id, detector ); @@ -242,7 +242,7 @@ export const UpdateDetectorBasicDetails: React.FC @@ -306,7 +306,7 @@ export const UpdateDetectorBasicDetails: React.FC diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx index 004c3fa34..08105aef9 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx @@ -17,7 +17,7 @@ jest.mock( ); describe(' spec', () => { it('renders the component', async () => { - let wrapper; + let wrapper: any; await act(async () => { wrapper = await mount(); }); diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap index 8ea4a3525..d5c4de811 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap @@ -197,393 +197,7 @@ exports[` spec renders the component 1`] = ` "pathname": "", }, "push": [MockFunction], - "replace": [MockFunction] { - "calls": Array [ - Array [ - Object { - "pathname": "/edit-detector-details/detector_id_1", - "state": Object { - "detectorHit": Object { - "_id": "detector_id_1", - "_index": ".windows", - "_source": Object { - "_id": "detector_id_1", - "_index": ".windows", - "_source": Object { - "createdBy": "someone", - "detector_type": "detector_type", - "enabled": true, - "enabled_time": 1, - "id": "detector_id_1", - "inputs": Array [ - Object { - "detector_input": Object { - "custom_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - "prePackaged": true, - }, - ], - "description": "detectorDescription", - "indices": Array [ - ".windows", - ], - "pre_packaged_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - "prePackaged": true, - }, - ], - }, - }, - ], - "last_update_time": 1, - "name": "detector_name", - "schedule": Object { - "period": Object { - "interval": 1, - "unit": "MINUTES", - }, - }, - "triggers": Array [ - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "detection_types": Array [ - "rules", - ], - "id": "trigger_id_0", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "detection_types": Array [ - "rules", - ], - "id": "trigger_id_1", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - ], - "type": "detector", - }, - "createdBy": "someone", - "detector_type": "detector_type", - "enabled": true, - "enabled_time": 1, - "id": "detector_id_1", - "inputs": Array [ - Object { - "detector_input": Object { - "custom_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - "prePackaged": true, - }, - ], - "description": "detectorDescription", - "indices": Array [ - ".windows", - ], - "pre_packaged_rules": Array [ - Object { - "_id": "rule_id_1", - "_index": ".windows", - "_primary_term": 1, - "_source": Object { - "last_update_time": "12/12/2022", - "queries": Array [ - Object { - "value": ".windows", - }, - ], - "rule": "rule_name", - }, - "_version": 1, - "id": "rule_id_1", - "prePackaged": true, - }, - ], - }, - }, - ], - "last_update_time": 1, - "name": "detector_name", - "schedule": Object { - "period": Object { - "interval": 1, - "unit": "MINUTES", - }, - }, - "triggers": Array [ - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "detection_types": Array [ - "rules", - ], - "id": "trigger_id_0", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - Object { - "actions": Array [ - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_0", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - Object { - "destination_id": "some_destination_id_1", - "id": "trigger_id_1_1", - "message_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "name": "some_name", - "subject_template": Object { - "lang": "some_lang", - "source": "some_source", - }, - "throttle": Object { - "unit": "minutes", - "value": 1, - }, - "throttle_enabled": true, - }, - ], - "detection_types": Array [ - "rules", - ], - "id": "trigger_id_1", - "ids": Array [ - "rule_id_1", - ], - "name": "alert_name", - "sev_levels": Array [ - "severity_level_low", - ], - "severity": "1", - "tags": Array [ - "any.tag", - ], - "types": Array [ - "detector_type_1", - ], - }, - ], - "type": "detector", - }, - }, - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, + "replace": [MockFunction], } } location={ @@ -594,7 +208,23 @@ exports[` spec renders the component 1`] = ` notifications={ Object { "toasts": Object { - "addDanger": [MockFunction], + "addDanger": [MockFunction] { + "calls": Array [ + Array [ + Object { + "text": [TypeError: Cannot read properties of undefined (reading 'detectorsService')], + "title": "Failed to retrieve detector:", + "toastLifeTimeMs": 5000, + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, "addInfo": [MockFunction], "addSuccess": [MockFunction], "addWarning": [MockFunction], @@ -623,8 +253,8 @@ exports[` spec renders the component 1`] = ` className="euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" > spec renders the component 1`] = ` spec renders the component 1`] = ` placeholder="Enter a name for the detector." readOnly={false} required={true} - value="detector_name" + value="" > spec renders the component 1`] = ` readOnly={false} required={true} type="text" - value="detector_name" + value="" /> @@ -820,7 +451,7 @@ exports[` spec renders the component 1`] = ` onChange={[Function]} onFocus={[Function]} placeholder="Enter a description for the detector." - value="detectorDescription" + value="" >