From 283fdd9574765ce11b4bba25c8ec5808dbac8097 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 16 May 2024 10:29:21 -0700 Subject: [PATCH] [MDS][Part 3] Wired all UI components to the data source menu (#1029) * wired all UI components to the data source menu Signed-off-by: Amardeepsingh Siglani * fixed UTs Signed-off-by: Amardeepsingh Siglani * fixed code for rule edit, import Signed-off-by: Amardeepsingh Siglani * updated snapshots Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani --- .../components/MDS/DataSourceMenuWrapper.tsx | 55 +- .../pages/Alerts/containers/Alerts/Alerts.tsx | 10 +- .../containers/CorrelationRules.tsx | 10 +- .../containers/CorrelationsContainer.tsx | 21 +- .../containers/CreateCorrelationRule.tsx | 18 +- .../DetectorDataSource/DetectorDataSource.tsx | 13 +- .../containers/DefineDetector.tsx | 3 +- .../containers/CreateDetector.tsx | 38 +- .../UpdateDetectorBasicDetails.test.tsx | 4 +- .../AlertTriggersView.test.tsx | 4 +- .../containers/Detectors/Detectors.tsx | 12 + .../Findings/containers/Findings/Findings.tsx | 14 +- public/pages/LogTypes/containers/LogTypes.tsx | 10 +- public/pages/Main/Main.tsx | 695 +- .../components/Widgets/DetectorsWidget.tsx | 2 +- .../components/Widgets/RecentAlertsWidget.tsx | 8 +- .../Widgets/RecentFindingsWidget.tsx | 8 +- .../Overview/components/Widgets/Summary.tsx | 6 +- .../components/Widgets/TableWidget.tsx | 2 +- .../components/Widgets/TopRulesWidget.tsx | 4 +- .../Overview/containers/Overview/Overview.tsx | 19 +- .../Overview/models/OverviewViewModel.ts | 29 +- public/pages/Overview/models/interfaces.ts | 54 - public/pages/Overview/models/types.ts | 24 - .../RuleEditor/RuleEditorContainer.test.tsx | 20 +- .../RuleEditor/RuleEditorForm.test.tsx | 19 +- .../components/RuleEditor/RuleEditorForm.tsx | 771 +- .../RuleEditorContainer.test.tsx.snap | 6422 +++++++++------- .../RuleEditorForm.test.tsx.snap | 6426 ++++++++++------- public/pages/Rules/containers/Rules/Rules.tsx | 8 +- public/services/AlertsService.ts | 24 +- public/services/CorrelationService.ts | 18 +- public/services/DataSourceContext.ts | 19 + public/services/DetectorService.ts | 22 +- public/services/FieldMappingService.ts | 6 + public/services/FindingsService.ts | 4 +- public/services/IndexService.ts | 19 +- public/services/LogTypeService.ts | 22 +- public/services/NotificationsService.ts | 21 +- public/services/OpenSearchService.ts | 7 +- public/services/RuleService.ts | 12 +- ...ervices.ts => SecurityAnalyticsContext.ts} | 0 public/services/index.ts | 2 +- public/services/utils/constants.ts | 12 + public/store/CorrelationsStore.ts | 74 +- public/utils/constants.ts | 1 + test/mocks/services/index.ts | 4 +- test/mocks/useContext.mock.ts | 5 + 48 files changed, 8608 insertions(+), 6393 deletions(-) delete mode 100644 public/pages/Overview/models/interfaces.ts delete mode 100644 public/pages/Overview/models/types.ts create mode 100644 public/services/DataSourceContext.ts rename public/services/{Services.ts => SecurityAnalyticsContext.ts} (100%) create mode 100644 public/services/utils/constants.ts diff --git a/public/components/MDS/DataSourceMenuWrapper.tsx b/public/components/MDS/DataSourceMenuWrapper.tsx index 52551836c..3c5c40437 100644 --- a/public/components/MDS/DataSourceMenuWrapper.tsx +++ b/public/components/MDS/DataSourceMenuWrapper.tsx @@ -3,33 +3,35 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState } from 'react'; +import React, { useContext } from 'react'; import { Route, Switch } from 'react-router-dom'; import { DataSourceManagementPluginSetup, - DataSourceOption, DataSourceSelectableConfig, DataSourceViewConfig, } from '../../../../../src/plugins/data_source_management/public'; import { AppMountParameters, CoreStart } from 'opensearch-dashboards/public'; import { ROUTES } from '../../utils/constants'; +import { DataSourceContext } from '../../services/DataSourceContext'; export interface DataSourceMenuWrapperProps { core: CoreStart; dataSourceManagement?: DataSourceManagementPluginSetup; + dataSourceMenuReadOnly: boolean; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } export const DataSourceMenuWrapper: React.FC = ({ core, dataSourceManagement, + dataSourceMenuReadOnly, setHeaderActionMenu, }) => { if (!dataSourceManagement) { return null; } - const [activeOption, setActiveOption] = useState({ id: '', label: '', checked: '' }); + const { dataSource, setDataSource } = useContext(DataSourceContext)!; const DataSourceMenuViewComponent = dataSourceManagement.ui?.getDataSourceMenu< DataSourceViewConfig >(); @@ -37,18 +39,6 @@ export const DataSourceMenuWrapper: React.FC = ({ DataSourceSelectableConfig >(); - const onDataSourceSelected = useCallback( - (sources: DataSourceOption[]) => { - if ( - sources[0] && - (activeOption.id !== sources[0].id || activeOption.label !== sources[0].label) - ) { - setActiveOption(sources[0]); - } - }, - [setActiveOption, activeOption] - ); - return ( = ({ = ({ /> = ({ ROUTES.RULES_CREATE, ROUTES.RULES_IMPORT, ROUTES.RULES_DUPLICATE, - ROUTES.DETECTORS_CREATE, ROUTES.LOG_TYPES_CREATE, ROUTES.CORRELATION_RULE_CREATE, ]} @@ -101,9 +91,36 @@ export const DataSourceMenuWrapper: React.FC = ({ setMenuMountPoint={setHeaderActionMenu} componentConfig={{ fullWidth: false, - activeOption: [activeOption], + activeOption: [dataSource], + notifications: core.notifications, + onSelectedDataSources: setDataSource, + savedObjects: core.savedObjects.client, + }} + /> + ); + }} + /> + { + return dataSourceMenuReadOnly ? ( + + ) : ( + diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index 63486d71a..bcb98c571 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -40,7 +40,6 @@ import { import { CoreServicesContext } from '../../../../components/core_services'; import AlertsService from '../../../../services/AlertsService'; import DetectorService from '../../../../services/DetectorService'; -import { AlertItem } from '../../../../../server/models/interfaces'; import { AlertFlyout } from '../../components/AlertFlyout/AlertFlyout'; import { FindingsService, IndexPatternsService, OpenSearchService } from '../../../../services'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; @@ -55,13 +54,12 @@ import { } from '../../../../utils/helpers'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { match, RouteComponentProps, withRouter } from 'react-router-dom'; -import { DateTimeFilter } from '../../../Overview/models/interfaces'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; -import { Detector } from '../../../../../types'; +import { AlertItem, DataSourceProps, DateTimeFilter, Detector } from '../../../../../types'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { DataStore } from '../../../../store/DataStore'; -export interface AlertsProps extends RouteComponentProps { +export interface AlertsProps extends RouteComponentProps, DataSourceProps { alertService: AlertsService; detectorService: DetectorService; findingService: FindingsService; @@ -134,7 +132,9 @@ export class Alerts extends Component { prevState.alerts !== this.state.alerts || prevState.alerts.length !== this.state.alerts.length; - if (alertsChanged) { + if (this.props.dataSource !== prevProps.dataSource) { + this.onRefresh(); + } else if (alertsChanged) { this.filterAlerts(); } else if (this.state.groupBy !== prevState.groupBy) { renderVisualization(this.generateVisualizationSpec(this.state.filteredAlerts), 'alerts-view'); diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index abf398740..f20e5163e 100644 --- a/public/pages/Correlations/containers/CorrelationRules.tsx +++ b/public/pages/Correlations/containers/CorrelationRules.tsx @@ -21,11 +21,13 @@ import { getCorrelationRulesTableColumns, getCorrelationRulesTableSearchConfig, } from '../utils/helpers'; -import { CorrelationRule, CorrelationRuleTableItem } from '../../../../types'; +import { CorrelationRule, CorrelationRuleTableItem, DataSourceProps } from '../../../../types'; import { RouteComponentProps } from 'react-router-dom'; import { DeleteCorrelationRuleModal } from '../components/DeleteModal'; -export const CorrelationRules: React.FC = (props: RouteComponentProps) => { +export interface CorrelationRulesProps extends RouteComponentProps, DataSourceProps {} + +export const CorrelationRules: React.FC = (props: CorrelationRulesProps) => { const context = useContext(CoreServicesContext); const [allRules, setAllRules] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -49,9 +51,11 @@ export const CorrelationRules: React.FC = (props: RouteComp BREADCRUMBS.CORRELATIONS, BREADCRUMBS.CORRELATION_RULES, ]); + }, []); + useEffect(() => { getCorrelationRules(); - }, [getCorrelationRules]); + }, [getCorrelationRules, props.dataSource]); const headerActions = useMemo( () => [ diff --git a/public/pages/Correlations/containers/CorrelationsContainer.tsx b/public/pages/Correlations/containers/CorrelationsContainer.tsx index a80caf1ad..5ab4a897d 100644 --- a/public/pages/Correlations/containers/CorrelationsContainer.tsx +++ b/public/pages/Correlations/containers/CorrelationsContainer.tsx @@ -2,7 +2,12 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { CorrelationFinding, CorrelationGraphData, DateTimeFilter } from '../../../../types'; +import { + CorrelationFinding, + CorrelationGraphData, + DataSourceProps, + DateTimeFilter, +} from '../../../../types'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { @@ -13,7 +18,6 @@ import { getSeverityColor, getSeverityLabel, graphRenderOptions, - getNodeSize, } from '../utils/constants'; import { EuiIcon, @@ -55,10 +59,11 @@ import { getLogTypeLabel } from '../../LogTypes/utils/helpers'; interface CorrelationsProps extends RouteComponentProps< - any, - any, - { finding: FindingItemType; correlatedFindings: CorrelationFinding[] } - > { + any, + any, + { finding: FindingItemType; correlatedFindings: CorrelationFinding[] } + >, + DataSourceProps { setDateTimeFilter?: Function; dateTimeFilter?: DateTimeFilter; onMount: () => void; @@ -123,7 +128,9 @@ export class Correlations extends React.Component, snapshot?: any ): void { - if ( + if (prevProps.dataSource !== this.props.dataSource) { + this.onRefresh(); + } else if ( prevState.logTypeFilterOptions !== this.state.logTypeFilterOptions || prevState.severityFilterOptions !== this.state.severityFilterOptions || prevProps.dateTimeFilter !== this.props.dateTimeFilter diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index f6823a7dc..c45aca062 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { Form, Formik, FormikErrors, FormikTouched } from 'formik'; import { ContentPanel } from '../../../components/ContentPanel'; import { DataStore } from '../../../store/DataStore'; @@ -37,6 +37,7 @@ import { CorrelationRuleAction, CorrelationRuleModel, CorrelationRuleQuery, + DataSourceProps, } from '../../../../types'; import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; import { CoreServicesContext } from '../../../components/core_services'; @@ -44,8 +45,9 @@ import { RouteComponentProps, useParams } from 'react-router-dom'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService } from '../../../services'; import { errorNotificationToast, getDataSources, getLogTypeOptions } from '../../../utils/helpers'; +import _ from 'lodash'; -export interface CreateCorrelationRuleProps { +export interface CreateCorrelationRuleProps extends DataSourceProps { indexService: IndexService; fieldMappingService: FieldMappingService; history: RouteComponentProps< @@ -102,6 +104,7 @@ export const CreateCorrelationRule: React.FC = ( const [period, setPeriod] = useState({ interval: 1, unit: 'MINUTES' }); const [dataFilterEnabled, setDataFilterEnabled] = useState(false); const [groupByEnabled, setGroupByEnabled] = useState(false); + const resetForm = useRef(false); const validateCorrelationRule = useCallback( (rule: CorrelationRuleModel) => { @@ -190,7 +193,9 @@ export const CreateCorrelationRule: React.FC = ( setLogTypeOptions(options); }; setupLogTypeOptions(); - }, []); + + resetForm.current = true; + }, [props.dataSource]); useEffect(() => { setPeriod(parseTime(initialValues.time_window)); @@ -242,7 +247,7 @@ export const CreateCorrelationRule: React.FC = ( useEffect(() => { getIndices(); - }, [props.indexService]); + }, [props.indexService, props.dataSource]); const getLogFields = useCallback( async (indexName: string) => { @@ -723,6 +728,11 @@ export const CreateCorrelationRule: React.FC = ( enableReinitialize={true} > {({ values: { name, queries, time_window }, touched, errors, ...props }) => { + if (resetForm.current) { + resetForm.current = false; + props.resetForm(); + } + return (
, + prevState: Readonly, + snapshot?: any + ): void { + if (prevProps.dataSource !== this.props.dataSource) { + this.getDataSources(); + } + } + getDataSources = async () => { this.setState({ loading: true }); const res = await getDataSources(this.props.indexService, this.props.notifications); diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx index d23821ffb..bad210417 100644 --- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx @@ -24,6 +24,7 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { logTypesWithDashboards } from '../../../../../utils/constants'; import { CreateDetectorSteps, + DataSourceProps, Detector, DetectorCreationStep, FieldMapping, @@ -35,7 +36,7 @@ import { ruleTypes } from '../../../../Rules/utils/constants'; import { ThreatIntelligence } from '../components/ThreatIntelligence/ThreatIntelligence'; import { addDetectionType, removeDetectionType } from '../../../../../utils/helpers'; -interface DefineDetectorProps extends RouteComponentProps { +interface DefineDetectorProps extends RouteComponentProps, DataSourceProps { detector: Detector; isEdit: boolean; indexService: IndexService; diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx index 83546e4ce..d009c0a9b 100644 --- a/public/pages/CreateDetector/containers/CreateDetector.tsx +++ b/public/pages/CreateDetector/containers/CreateDetector.tsx @@ -36,12 +36,18 @@ import { } from '../components/DefineDetector/components/DetectionRules/types/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { getPlugins } from '../../../utils/helpers'; -import { CreateDetectorSteps, Detector, DetectorCreationStep } from '../../../../types'; +import { + CreateDetectorSteps, + DataSourceManagerProps, + DataSourceProps, + Detector, + DetectorCreationStep, +} from '../../../../types'; import { DataStore } from '../../../store/DataStore'; import { errorNotificationToast } from '../../../utils/helpers'; import { MetricsContext } from '../../../metrics/MetricsContext'; -interface CreateDetectorProps extends RouteComponentProps { +interface CreateDetectorProps extends RouteComponentProps, DataSourceProps, DataSourceManagerProps { isEdit: boolean; services: BrowserServices; metrics: MetricsContext; @@ -66,12 +72,15 @@ export default class CreateDetector extends Component, prevState: Readonly, snapshot?: any ): void { - if (prevState.detector.detector_type !== this.state.detector.detector_type) { + if (prevProps.dataSource !== this.props.dataSource) { + this.setState(this.getInitialState()); + this.resetDependencies(); + } else if (prevState.detector.detector_type !== this.state.detector.detector_type) { this.setupRulesState(); } } @@ -172,12 +192,14 @@ export default class CreateDetector extends Component { const { currentStep } = this.state; this.setState({ currentStep: currentStep + 1 }); + this.props.setDataSourceMenuReadOnly(true); this.props.metrics.detectorMetricsManager.sendMetrics(CreateDetectorSteps.stepTwoInitiated); }; onPreviousClick = () => { const { currentStep } = this.state; this.setState({ currentStep: currentStep - 1 }); + this.props.setDataSourceMenuReadOnly(false); }; setCurrentStep = (currentStep: DetectorCreationStep) => { diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx index 8f5ae5afc..6cb5108c2 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateDetectorBasicDetails.test.tsx @@ -9,7 +9,7 @@ import { expect } from '@jest/globals'; import { UpdateDetectorBasicDetails } from './UpdateBasicDetails'; import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; -import { coreContextMock, saContextMock } from '../../../../../test/mocks/useContext.mock'; +import { coreContextMock, mockContexts } from '../../../../../test/mocks/useContext.mock'; describe(' spec', () => { it('renders the component', async () => { @@ -17,7 +17,7 @@ describe(' spec', () => { await act(async () => { jest .spyOn(React, 'useContext') - .mockImplementation(() => ({ ...saContextMock, ...coreContextMock })); + .mockImplementation(() => ({ ...mockContexts, ...coreContextMock })); wrapper = mount(); }); wrapper.update(); diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.test.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.test.tsx index 3ce59b3e3..d9ae693fb 100644 --- a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.test.tsx +++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.test.tsx @@ -8,13 +8,13 @@ import { alertTriggerViewProps } from '../../../../../test/mocks/Detectors/conta import { expect } from '@jest/globals'; import { AlertTriggersView } from './AlertTriggersView'; import { ReactWrapper, mount } from 'enzyme'; -import { saContextMock } from '../../../../../test/mocks/useContext.mock'; +import { mockContexts } from '../../../../../test/mocks/useContext.mock'; import { act } from 'react-dom/test-utils'; describe(' spec', () => { it('renders the component', async () => { let wrapper: ReactWrapper; - jest.spyOn(React, 'useContext').mockImplementation(() => saContextMock); + jest.spyOn(React, 'useContext').mockImplementation(() => mockContexts); await act(async () => { wrapper = mount(); }); diff --git a/public/pages/Detectors/containers/Detectors/Detectors.tsx b/public/pages/Detectors/containers/Detectors/Detectors.tsx index 3533c7897..5d0e3fe32 100644 --- a/public/pages/Detectors/containers/Detectors/Detectors.tsx +++ b/public/pages/Detectors/containers/Detectors/Detectors.tsx @@ -37,10 +37,12 @@ import { DetectorsService } from '../../../../services'; import { DetectorHit } from '../../../../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { Direction } from '@opensearch-project/oui/src/services/sort/sort_direction'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; export interface DetectorsProps extends RouteComponentProps { detectorService: DetectorsService; notifications: NotificationsStart; + dataSource: DataSourceOption; } interface DetectorsState { @@ -71,6 +73,16 @@ export default class Detectors extends Component await this.getDetectors(); } + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any + ): void { + if (this.props.dataSource && prevProps.dataSource !== this.props.dataSource) { + this.getDetectors(); + } + } + getDetectors = async () => { this.setState({ loadingDetectors: true }); const { detectorService, notifications } = this.props; diff --git a/public/pages/Findings/containers/Findings/Findings.tsx b/public/pages/Findings/containers/Findings/Findings.tsx index 529c89ed1..a206dd71e 100644 --- a/public/pages/Findings/containers/Findings/Findings.tsx +++ b/public/pages/Findings/containers/Findings/Findings.tsx @@ -50,13 +50,17 @@ import { } from '../../../../utils/helpers'; import { DetectorHit, RuleSource } from '../../../../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { DateTimeFilter } from '../../../Overview/models/interfaces'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { DataStore } from '../../../../store/DataStore'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; -import { CorrelationFinding, FeatureChannelList } from '../../../../../types'; +import { + CorrelationFinding, + DataSourceProps, + FeatureChannelList, + DateTimeFilter, +} from '../../../../../types'; -interface FindingsProps extends RouteComponentProps { +interface FindingsProps extends RouteComponentProps, DataSourceProps { detectorService: DetectorsService; correlationService: CorrelationService; notificationsService: NotificationsService; @@ -127,7 +131,9 @@ class Findings extends Component { } componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { - if ( + if (this.props.dataSource !== prevProps.dataSource) { + this.onRefresh(); + } else if ( this.state.filteredFindings !== prevState.filteredFindings || this.state.groupBy !== prevState.groupBy ) { diff --git a/public/pages/LogTypes/containers/LogTypes.tsx b/public/pages/LogTypes/containers/LogTypes.tsx index d02ef5cb3..ab302d59d 100644 --- a/public/pages/LogTypes/containers/LogTypes.tsx +++ b/public/pages/LogTypes/containers/LogTypes.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiInMemoryTable } from '@elastic/eui'; import { ContentPanel } from '../../../components/ContentPanel'; import { CoreServicesContext } from '../../../components/core_services'; import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; -import { LogType } from '../../../../types'; +import { DataSourceProps, LogType } from '../../../../types'; import { DataStore } from '../../../store/DataStore'; import { getLogTypesTableColumns, getLogTypesTableSearchConfig } from '../utils/helpers'; import { RouteComponentProps } from 'react-router-dom'; @@ -17,11 +17,11 @@ import { NotificationsStart } from 'opensearch-dashboards/public'; import { successNotificationToast } from '../../../utils/helpers'; import { DeleteLogTypeModal } from '../components/DeleteLogTypeModal'; -export interface LogTypesProps extends RouteComponentProps { +export interface LogTypesProps extends RouteComponentProps, DataSourceProps { notifications: NotificationsStart; } -export const LogTypes: React.FC = ({ history, notifications }) => { +export const LogTypes: React.FC = ({ history, notifications, dataSource }) => { const context = useContext(CoreServicesContext); const [logTypes, setLogTypes] = useState([]); const [logTypeToDelete, setLogTypeItemToDelete] = useState(undefined); @@ -47,9 +47,11 @@ export const LogTypes: React.FC = ({ history, notifications }) => BREADCRUMBS.DETECTORS, BREADCRUMBS.LOG_TYPES, ]); + }, []); + useEffect(() => { getLogTypes(); - }, []); + }, [dataSource]); const showLogTypeDetails = useCallback((id: string) => { history.push(`${ROUTES.LOG_TYPES}/${id}`); diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 9d2b5432c..381c1e4e2 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -37,7 +37,6 @@ import { CreateRule } from '../Rules/containers/CreateRule/CreateRule'; import { EditRule } from '../Rules/containers/EditRule/EditRule'; import { ImportRule } from '../Rules/containers/ImportRule/ImportRule'; import { DuplicateRule } from '../Rules/containers/DuplicateRule/DuplicateRule'; -import { DateTimeFilter } from '../Overview/models/interfaces'; import Callout, { ICalloutProps } from './components/Callout'; import { DataStore } from '../../store/DataStore'; import { CreateCorrelationRule } from '../Correlations/containers/CreateCorrelationRule'; @@ -49,9 +48,16 @@ import FindingDetailsFlyout, { import { LogTypes } from '../LogTypes/containers/LogTypes'; import { LogType } from '../LogTypes/containers/LogType'; import { CreateLogType } from '../LogTypes/containers/CreateLogType'; -import { SecurityAnalyticsContextType } from '../../../types'; +import { + DataSourceContextType, + DateTimeFilter, + SecurityAnalyticsContextType, +} from '../../../types'; import { DataSourceManagementPluginSetup } from '../../../../../src/plugins/data_source_management/public'; import { DataSourceMenuWrapper } from '../../components/MDS/DataSourceMenuWrapper'; +import { DataSourceOption } from 'src/plugins/data_source_management/public/components/data_source_menu/types'; +import { DataSourceContext, DataSourceContextConsumer } from '../../services/DataSourceContext'; +import { dataSourceInfo } from '../../services/utils/constants'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -96,7 +102,13 @@ interface MainState { callout?: ICalloutProps; toasts?: Toast[]; findingFlyout: FindingDetailsFlyoutBaseProps | null; + selectedDataSource: DataSourceOption; + dataSourceLoading: boolean; + dataSourceMenuReadOnly: boolean; } +/** + * + */ const navItemIndexByRoute: { [route: string]: number } = { [ROUTES.OVERVIEW]: 1, @@ -121,6 +133,9 @@ export default class Main extends Component { selectedNavItemId: 1, dateTimeFilter: defaultDateTimeFilter, findingFlyout: null, + dataSourceLoading: props.multiDataSourceEnabled, + selectedDataSource: { id: '' }, + dataSourceMenuReadOnly: false, }; DataStore.detectors.setHandlers(this.showCallout, this.showToast); @@ -200,18 +215,33 @@ export default class Main extends Component { this.setState({ getStartedDismissedOnce: true }); }; - render() { - const { - landingPage, - location: { pathname }, - history, - multiDataSourceEnabled, - dataSourceManagement, - setActionMenu, - } = this.props; + onDataSourceSelected = (sources: DataSourceOption[]) => { + const { selectedDataSource: dataSource, dataSourceLoading } = this.state; + if ( + sources[0] && + (dataSource?.id !== sources[0].id || dataSource?.label !== sources[0].label) + ) { + dataSourceInfo.activeDataSource = sources[0]; + this.setState({ + selectedDataSource: { ...sources[0] }, + }); + } - const { callout, findingFlyout, selectedNavItemId: selectedNavItemIndex } = this.state; - const sideNav: EuiSideNavItemType<{ style: any }>[] = [ + if (dataSourceLoading) { + this.setState({ dataSourceLoading: false }); + } + }; + + setDataSourceMenuReadOnly = (readOnly: boolean) => { + this.setState({ dataSourceMenuReadOnly: readOnly }); + }; + + getSideNavItems = () => { + const { history } = this.props; + + const { selectedNavItemId: selectedNavItemIndex } = this.state; + + return [ { name: Navigation.SecurityAnalytics, id: 0, @@ -290,7 +320,7 @@ export default class Main extends Component { this.setState({ selectedNavItemId: 6 }); history.push(ROUTES.CORRELATIONS); }, - renderItem: (props) => { + renderItem: (props: any) => { return ( @@ -324,6 +354,32 @@ export default class Main extends Component { ], }, ]; + }; + + render() { + const { + landingPage, + location: { pathname }, + history, + multiDataSourceEnabled, + dataSourceManagement, + setActionMenu, + } = this.props; + + const { + callout, + findingFlyout, + selectedDataSource, + dataSourceLoading, + dataSourceMenuReadOnly, + } = this.state; + const sideNav: EuiSideNavItemType<{ style: any }>[] = this.getSideNavItems(); + + const dataSourceContextValue: DataSourceContextType = { + dataSource: selectedDataSource, + setDataSource: this.onDataSourceSelected, + setDataSourceMenuReadOnly: this.setDataSourceMenuReadOnly, + }; return ( @@ -335,288 +391,337 @@ export default class Main extends Component { const metrics = saContext?.metrics!; return ( - <> - {multiDataSourceEnabled && ( - - )} - {services && ( - - {/* Hide side navigation bar when on any HIDDEN_NAV_ROUTES pages. */} - {!HIDDEN_NAV_ROUTES.some((route) => pathname.match(route)) && ( - - - - )} - - {callout ? : null} - {findingFlyout ? ( - - ) : null} - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ) => { - if (!props.location.state?.ruleItem) { - props.history.replace(ROUTES.RULES); - return ; - } - - return ( - - ); - }} - /> - ) => { - if (!props.location.state?.ruleItem) { - props.history.replace(ROUTES.RULES); - return ; - } - - return ( - - ); - }} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - - )} - /> - ) => ( - + + {(_dataSource: DataSourceContextType | null) => + _dataSource && ( + <> + {multiDataSourceEnabled && ( + + )} + {!dataSourceLoading && services && ( + + {/* Hide side navigation bar when on any HIDDEN_NAV_ROUTES pages. */} + {!HIDDEN_NAV_ROUTES.some((route) => pathname.match(route)) && ( + + + + )} + + {callout ? : null} + {findingFlyout ? ( + + ) : null} + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ) => { + if (!props.location.state?.ruleItem) { + props.history.replace(ROUTES.RULES); + return ( + + ); + } + + return ( + + ); + }} + /> + ) => { + if (!props.location.state?.ruleItem) { + props.history.replace(ROUTES.RULES); + return ( + + ); + } + + return ( + + ); + }} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => { + return ( + this.setState({ selectedNavItemId: 6 })} + dateTimeFilter={this.state.dateTimeFilter} + setDateTimeFilter={this.setDateTimeFilter} + dataSource={selectedDataSource} + /> + ); + }} + /> + ) => ( + + )} + /> + ) => { + return ( + + ); + }} + /> + ) => { + return ( + + ); + }} + /> + + + + - )} - /> - ) => { - return ( - this.setState({ selectedNavItemId: 6 })} - dateTimeFilter={this.state.dateTimeFilter} - setDateTimeFilter={this.setDateTimeFilter} - /> - ); - }} - /> - ) => ( - - )} - /> - ) => { - return ; - }} - /> - ) => { - return ( - - ); - }} - /> - - - - - - )} - + + )} + + ) + } + + ); }} diff --git a/public/pages/Overview/components/Widgets/DetectorsWidget.tsx b/public/pages/Overview/components/Widgets/DetectorsWidget.tsx index 262cfb448..624b88691 100644 --- a/public/pages/Overview/components/Widgets/DetectorsWidget.tsx +++ b/public/pages/Overview/components/Widgets/DetectorsWidget.tsx @@ -6,12 +6,12 @@ import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { ROUTES } from '../../../../utils/constants'; import React, { useCallback } from 'react'; -import { DetectorItem } from '../../models/interfaces'; import { TableWidget } from './TableWidget'; import { WidgetContainer } from './WidgetContainer'; import { DetectorHit } from '../../../../../server/models/interfaces'; import { RouteComponentProps } from 'react-router-dom'; import { formatRuleType } from '../../../../utils/helpers'; +import { DetectorItem } from '../../../../../types'; type DetectorIdToHit = { [id: string]: DetectorHit }; diff --git a/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx b/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx index e9733814c..c255f01a4 100644 --- a/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx +++ b/public/pages/Overview/components/Widgets/RecentAlertsWidget.tsx @@ -6,13 +6,13 @@ import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { DEFAULT_EMPTY_DATA, ROUTES, SortDirection } from '../../../../utils/constants'; import React, { useEffect, useState } from 'react'; -import { AlertItem } from '../../models/interfaces'; import { TableWidget } from './TableWidget'; import { WidgetContainer } from './WidgetContainer'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { renderTime } from '../../../../utils/helpers'; +import { OverviewAlertItem } from '../../../../../types'; -const columns: EuiBasicTableColumn[] = [ +const columns: EuiBasicTableColumn[] = [ { field: 'time', name: 'Time', @@ -36,7 +36,7 @@ const columns: EuiBasicTableColumn[] = [ ]; export interface RecentAlertsWidgetProps { - items: AlertItem[]; + items: OverviewAlertItem[]; loading?: boolean; } @@ -44,7 +44,7 @@ export const RecentAlertsWidget: React.FC = ({ items, loading = false, }) => { - const [alertItems, setAlertItems] = useState([]); + const [alertItems, setAlertItems] = useState([]); const [widgetEmptyMessage, setwidgetEmptyMessage] = useState( undefined ); diff --git a/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx b/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx index 85578b8c9..c4d202a17 100644 --- a/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx +++ b/public/pages/Overview/components/Widgets/RecentFindingsWidget.tsx @@ -6,12 +6,12 @@ import { EuiBasicTableColumn, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { ROUTES, SortDirection } from '../../../../utils/constants'; import React, { useEffect, useState } from 'react'; -import { FindingItem } from '../../models/interfaces'; import { TableWidget } from './TableWidget'; import { WidgetContainer } from './WidgetContainer'; import { renderTime, capitalizeFirstLetter } from '../../../../utils/helpers'; +import { OverviewFindingItem } from '../../../../../types'; -const columns: EuiBasicTableColumn[] = [ +const columns: EuiBasicTableColumn[] = [ { field: 'time', name: 'Time', @@ -43,7 +43,7 @@ const columns: EuiBasicTableColumn[] = [ ]; export interface RecentFindingsWidgetProps { - items: FindingItem[]; + items: OverviewFindingItem[]; loading?: boolean; } @@ -51,7 +51,7 @@ export const RecentFindingsWidget: React.FC = ({ items, loading = false, }) => { - const [findingItems, setFindingItems] = useState([]); + const [findingItems, setFindingItems] = useState([]); const [widgetEmptyMessage, setWidgetEmptyMessage] = useState( undefined ); diff --git a/public/pages/Overview/components/Widgets/Summary.tsx b/public/pages/Overview/components/Widgets/Summary.tsx index 1eda65018..baf925b95 100644 --- a/public/pages/Overview/components/Widgets/Summary.tsx +++ b/public/pages/Overview/components/Widgets/Summary.tsx @@ -22,15 +22,15 @@ import { getTimeWithMinPrecision, TimeUnit, } from '../../utils/helpers'; -import { AlertItem, FindingItem } from '../../models/interfaces'; import { createSelectComponent, renderVisualization } from '../../../../utils/helpers'; import { PLUGIN_NAME, ROUTES } from '../../../../utils/constants'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { getLogTypeLabel } from '../../../LogTypes/utils/helpers'; +import { OverviewAlertItem, OverviewFindingItem } from '../../../../../types'; export interface SummaryProps { - findings: FindingItem[]; - alerts: AlertItem[]; + findings: OverviewFindingItem[]; + alerts: OverviewAlertItem[]; loading?: boolean; startTime: string; endTime: string; diff --git a/public/pages/Overview/components/Widgets/TableWidget.tsx b/public/pages/Overview/components/Widgets/TableWidget.tsx index 1d9915258..eb1780ca4 100644 --- a/public/pages/Overview/components/Widgets/TableWidget.tsx +++ b/public/pages/Overview/components/Widgets/TableWidget.tsx @@ -4,8 +4,8 @@ */ import { EuiInMemoryTable } from '@elastic/eui'; +import { TableWidgetItem, TableWidgetProps } from '../../../../../types'; import React from 'react'; -import { TableWidgetItem, TableWidgetProps } from '../../models/types'; export class TableWidget extends React.Component> { render() { diff --git a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx index feb1f8775..d6c017609 100644 --- a/public/pages/Overview/components/Widgets/TopRulesWidget.tsx +++ b/public/pages/Overview/components/Widgets/TopRulesWidget.tsx @@ -5,14 +5,14 @@ import { renderVisualization } from '../../../../utils/helpers'; import React, { useEffect } from 'react'; -import { FindingItem } from '../../models/interfaces'; import { WidgetContainer } from './WidgetContainer'; import { getTopRulesVisualizationSpec } from '../../utils/helpers'; import { ChartContainer } from '../../../../components/Charts/ChartContainer'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { OverviewFindingItem } from '../../../../../types'; export interface TopRulesWidgetProps { - findings: FindingItem[]; + findings: OverviewFindingItem[]; loading?: boolean; } diff --git a/public/pages/Overview/containers/Overview/Overview.tsx b/public/pages/Overview/containers/Overview/Overview.tsx index 92a4635b1..863833e2e 100644 --- a/public/pages/Overview/containers/Overview/Overview.tsx +++ b/public/pages/Overview/containers/Overview/Overview.tsx @@ -22,17 +22,17 @@ import { PLUGIN_NAME, ROUTES, } from '../../../../utils/constants'; -import { OverviewProps, OverviewState } from '../../models/interfaces'; import { CoreServicesContext } from '../../../../../public/components/core_services'; import { RecentAlertsWidget } from '../../components/Widgets/RecentAlertsWidget'; import { RecentFindingsWidget } from '../../components/Widgets/RecentFindingsWidget'; import { DetectorsWidget } from '../../components/Widgets/DetectorsWidget'; -import { OverviewViewModel, OverviewViewModelActor } from '../../models/OverviewViewModel'; +import { OverviewViewModelActor } from '../../models/OverviewViewModel'; import { SecurityAnalyticsContext } from '../../../../services'; import { Summary } from '../../components/Widgets/Summary'; import { TopRulesWidget } from '../../components/Widgets/TopRulesWidget'; import { GettingStartedPopup } from '../../components/GettingStarted/GettingStartedPopup'; import { getChartTimeUnit, TimeUnit } from '../../utils/helpers'; +import { OverviewProps, OverviewState, OverviewViewModel } from '../../../../../types'; export const Overview: React.FC = (props) => { const { @@ -78,10 +78,15 @@ export const Overview: React.FC = (props) => { useEffect(() => { context?.chrome.setBreadcrumbs([BREADCRUMBS.SECURITY_ANALYTICS, BREADCRUMBS.OVERVIEW]); overviewViewModelActor.registerRefreshHandler(updateState); + }, []); + useEffect(() => { const updateModel = async () => { await overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); - setInitialLoadingFinished(true); + + if (!initialLoadingFinished) { + setInitialLoadingFinished(true); + } }; updateModel(); @@ -117,15 +122,15 @@ export const Overview: React.FC = (props) => { setRecentlyUsedRanges(usedRanges); }; - useEffect(() => { - overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); - }, [dateTimeFilter.startTime, dateTimeFilter.endTime]); - const onRefresh = async () => { setLoading(true); await overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); }; + useEffect(() => { + onRefresh(); + }, [props.dataSource]); + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); const closePopover = () => { diff --git a/public/pages/Overview/models/OverviewViewModel.ts b/public/pages/Overview/models/OverviewViewModel.ts index 7f47f8e94..15be20040 100644 --- a/public/pages/Overview/models/OverviewViewModel.ts +++ b/public/pages/Overview/models/OverviewViewModel.ts @@ -4,23 +4,20 @@ */ import { BrowserServices } from '../../../models/interfaces'; -import { DetectorHit, RuleSource } from '../../../../server/models/interfaces'; -import { AlertItem, FindingItem } from './interfaces'; +import { RuleSource } from '../../../../server/models/interfaces'; import { DEFAULT_DATE_RANGE, DEFAULT_EMPTY_DATA } from '../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast, isThreatIntelQuery } from '../../../utils/helpers'; import dateMath from '@elastic/datemath'; import moment from 'moment'; import { DataStore } from '../../../store/DataStore'; -import { Finding } from '../../../../types'; - -export interface OverviewViewModel { - detectors: DetectorHit[]; - findings: FindingItem[]; - alerts: AlertItem[]; -} - -export type OverviewViewModelRefreshHandler = (overviewState: OverviewViewModel) => void; +import { + Finding, + OverviewAlertItem, + OverviewFindingItem, + OverviewViewModel, + OverviewViewModelRefreshHandler, +} from '../../../../types'; export class OverviewViewModelActor { private overviewViewModel: OverviewViewModel = { @@ -74,7 +71,7 @@ export class OverviewViewModelActor { }); }); const detectorIds = detectorInfo.keys(); - let findingItems: FindingItem[] = []; + let findingItems: OverviewFindingItem[] = []; const ruleIds = new Set(); try { @@ -82,7 +79,7 @@ export class OverviewViewModelActor { let detectorFindings: Finding[] = await DataStore.findings.getFindingsPerDetector(id); const logType = detectorInfo.get(id)?.logType; const detectorName = detectorInfo.get(id)?.name || ''; - const detectorFindingItems: FindingItem[] = detectorFindings.map((finding) => { + const detectorFindingItems: OverviewFindingItem[] = detectorFindings.map((finding) => { const ruleQueries = finding.queries.filter(({ id }) => !isThreatIntelQuery(id)); const ids = ruleQueries.map((query) => query.id); ids.forEach((id) => ruleIds.add(id)); @@ -94,7 +91,7 @@ export class OverviewViewModelActor { detector: detectorName, findingName: finding.id, id: finding.id, - time: findingTime, + time: findingTime.getTime(), logType: logType || '', ruleId: ruleQueries[0]?.id || finding.queries[0].id, ruleName: '', @@ -127,7 +124,7 @@ export class OverviewViewModelActor { } private async updateAlerts() { - let alertItems: AlertItem[] = []; + let alertItems: OverviewAlertItem[] = []; try { for (let detector of this.overviewViewModel.detectors) { @@ -136,7 +133,7 @@ export class OverviewViewModelActor { id, detector._source.name ); - const detectorAlertItems: AlertItem[] = detectorAlerts.map((alert) => ({ + const detectorAlertItems: OverviewAlertItem[] = detectorAlerts.map((alert) => ({ id: alert.id, severity: alert.severity, time: alert.last_notification_time, diff --git a/public/pages/Overview/models/interfaces.ts b/public/pages/Overview/models/interfaces.ts deleted file mode 100644 index 690c7c43c..000000000 --- a/public/pages/Overview/models/interfaces.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { NotificationsStart } from 'opensearch-dashboards/public'; -import { RouteComponentProps } from 'react-router-dom'; -import { OverviewViewModel } from './OverviewViewModel'; - -export interface DateTimeFilter { - startTime: string; - endTime: string; -} - -export interface OverviewProps extends RouteComponentProps { - getStartedDismissedOnce: boolean; - onGetStartedDismissed: () => void; - notifications?: NotificationsStart; - setDateTimeFilter?: Function; - dateTimeFilter?: DateTimeFilter; -} - -export interface OverviewState { - groupBy: string; - overviewViewModel: OverviewViewModel; -} - -export interface FindingItem { - id: string; - time: Date; - findingName: string; - detector: string; - logType: string; - ruleId: string; - ruleName: string; - ruleSeverity: string; - isThreatIntelOnlyFinding: boolean; -} - -export interface AlertItem { - id: string; - time: string; - triggerName: string; - severity: string; - logType: string; - acknowledged: boolean; -} - -export interface DetectorItem { - id: string; - detectorName: string; - status: string; - logTypes: string; -} diff --git a/public/pages/Overview/models/types.ts b/public/pages/Overview/models/types.ts deleted file mode 100644 index 83ed06681..000000000 --- a/public/pages/Overview/models/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiBasicTableColumn } from '@elastic/eui'; -import { SortDirection } from '../../../utils/constants'; -import { AlertItem, DetectorItem, FindingItem } from './interfaces'; - -export type TableWidgetItem = FindingItem | AlertItem | DetectorItem; - -export type TableWidgetProps = { - columns: EuiBasicTableColumn[]; - items: T[]; - sorting?: { - sort: { - field: string; - direction: SortDirection; - }; - }; - className?: string; - loading?: boolean; - message?: React.ReactNode; -}; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.test.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.test.tsx index 23ecfd39b..9f3649070 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.test.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.test.tsx @@ -4,13 +4,25 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { act } from '@testing-library/react'; import { RuleEditorContainer } from './RuleEditorContainer'; import RuleEditorContainerMock from '../../../../../test/mocks/Rules/components/RuleEditor/RuleEditorContainer.mock'; +import { DataStore } from '../../../../store/DataStore'; +import { mockContexts } from '../../../../../test/mocks/useContext.mock'; +import { notificationsMock } from '../../../../../test/mocks/services'; +import { ReactWrapper, mount } from 'enzyme'; +import { expect } from '@jest/globals'; describe(' spec', () => { - it('renders the component', () => { - const tree = render(); - expect(tree).toMatchSnapshot(); + it('renders the component', async () => { + DataStore.init(mockContexts.services as any, notificationsMock.NotificationsStart); + jest.spyOn(DataStore.logTypes, 'getLogTypes'); + + const treeObj: { wrapper?: ReactWrapper } = { wrapper: undefined }; + await act(async () => { + treeObj.wrapper = await mount(); + }); + treeObj.wrapper?.update(); + expect(treeObj.wrapper).toMatchSnapshot(); }); }); diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.test.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.test.tsx index 76a5350f5..94f587f02 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorForm.test.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.test.tsx @@ -4,13 +4,24 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { act } from '@testing-library/react'; import { RuleEditorForm } from './RuleEditorForm'; import RuleEditorFormMock from '../../../../../test/mocks/Rules/components/RuleEditor/RuleEditorForm.mock'; +import { DataStore } from '../../../../store/DataStore'; +import { mockContexts } from '../../../../../test/mocks/useContext.mock'; +import { notificationsMock } from '../../../../../test/mocks/services'; +import { ReactWrapper, mount } from 'enzyme'; +import { expect } from '@jest/globals'; describe(' spec', () => { - it('renders the component', () => { - const tree = render(); - expect(tree).toMatchSnapshot(); + it('renders the component', async () => { + DataStore.init(mockContexts.services as any, notificationsMock.NotificationsStart); + jest.spyOn(DataStore.logTypes, 'getLogTypes'); + const treeObj: { wrapper?: ReactWrapper } = { wrapper: undefined }; + await act(async () => { + treeObj.wrapper = await mount(); + }); + treeObj.wrapper?.update(); + expect(treeObj.wrapper).toMatchSnapshot(); }); }); diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx index 31c508ec2..997f86911 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; import { Formik, Form, FormikErrors } from 'formik'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { @@ -42,6 +42,7 @@ import { useCallback } from 'react'; import { getLogTypeOptions } from '../../../../utils/helpers'; import { getLogTypeLabel } from '../../../LogTypes/utils/helpers'; import { getSeverityLabel } from '../../../Correlations/utils/constants'; +import { DataSourceContext } from '../../../../services/DataSourceContext'; export interface VisualRuleEditorProps { initialValue: RuleEditorFormModel; @@ -77,7 +78,13 @@ export const RuleEditorForm: React.FC = ({ }) => { const [selectedEditorType, setSelectedEditorType] = useState('visual'); const [isDetectionInvalid, setIsDetectionInvalid] = useState(false); + const resetLogType = useRef(false); const [logTypeOptions, setLogTypeOptions] = useState([]); + const dataSourceContext = useContext(DataSourceContext); + + // This is used to avoid refreshing the log type options on first render since that clears the + // selected log type when importing log types. + const firstUpdate = useRef(true); const onEditorTypeChange = (optionId: string) => { setSelectedEditorType(optionId); @@ -88,6 +95,17 @@ export const RuleEditorForm: React.FC = ({ setLogTypeOptions(logTypeOptions); }, []); + useEffect(() => { + const shouldResetLogTypeOptions = mode !== 'edit' && !firstUpdate.current; + if (!shouldResetLogTypeOptions) { + return; + } + + refreshLogTypeOptions(); + resetLogType.current = true; + firstUpdate.current = false; + }, [dataSourceContext.dataSource]); + const validateTags = (fields: string[]) => { let isValid = true; let tag; @@ -165,396 +183,407 @@ export const RuleEditorForm: React.FC = ({ submit(values); }} > - {(props) => ( - - - onEditorTypeChange(id)} - /> - - - - {selectedEditorType === 'yaml' && ( - 0} - errors={Object.keys(props.errors).map( - (key) => props.errors[key as keyof RuleEditorFormModel] as string - )} - change={(e) => { - const formState = mapRuleToForm(e); - props.setValues(formState); - }} - > - )} - - {selectedEditorType === 'visual' && ( - <> - - -

Rule overview

-
-
- - - - - Rule name - - } - isInvalid={(validateOnMount || props.touched.name) && !!props.errors?.name} - error={props.errors.name} - helpText={detectionRuleNameError} - > - { - props.handleChange('name')(e); - }} - onBlur={props.handleBlur('name')} - value={props.values.name} - /> - - - + {(props) => { + if (resetLogType.current) { + resetLogType.current = false; + props.setFieldValue('logType', ''); + } - - Description - - optional + return ( + + + onEditorTypeChange(id)} + /> + + + + {selectedEditorType === 'yaml' && ( + 0} + errors={Object.keys(props.errors).map( + (key) => props.errors[key as keyof RuleEditorFormModel] as string + )} + change={(e) => { + const formState = mapRuleToForm(e); + props.setValues(formState); + }} + > + )} + + {selectedEditorType === 'visual' && ( + <> + + +

Rule overview

- } - isInvalid={ - (validateOnMount || props.touched.description) && !!props.errors?.description - } - error={props.errors.description} - > - { - props.handleChange('description')(e.target.value); - }} - onBlur={props.handleBlur('description')} - value={props.values.description} - placeholder={'Detects ...'} - /> -
+ - + - - Author - - } - helpText="Combine multiple authors separated with a comma" - isInvalid={(validateOnMount || props.touched.author) && !!props.errors?.author} - error={props.errors.author} - > - + Rule name + + } + isInvalid={(validateOnMount || props.touched.name) && !!props.errors?.name} + error={props.errors.name} + helpText={detectionRuleNameError} + > + { + props.handleChange('name')(e); + }} + onBlur={props.handleBlur('name')} + value={props.values.name} + /> + + + + + + Description + - optional + + } + isInvalid={ + (validateOnMount || props.touched.description) && !!props.errors?.description + } + error={props.errors.description} + > + { + props.handleChange('description')(e.target.value); + }} + onBlur={props.handleBlur('description')} + value={props.values.description} + placeholder={'Detects ...'} + /> + + + + + + Author + + } + helpText="Combine multiple authors separated with a comma" isInvalid={(validateOnMount || props.touched.author) && !!props.errors?.author} - placeholder="Enter author name" - data-test-subj={'rule_author_field'} - onChange={(e) => { - props.handleChange('author')(e); - }} - onBlur={props.handleBlur('author')} - value={props.values.author} - /> - - - - - - -

Details

-
-
- - - - - - - Log type - - } + error={props.errors.author} + > + - { + props.handleChange('author')(e); + }} + onBlur={props.handleBlur('author')} + value={props.values.author} + /> + + + + + + +

Details

+
+
+ + + + + + + Log type + + } isInvalid={ (validateOnMount || props.touched.logType) && !!props.errors?.logType } - placeholder="Select a log type" - data-test-subj={'rule_type_dropdown'} - options={logTypeOptions} - singleSelection={{ asPlainText: true }} - onChange={(e) => { - props.handleChange('logType')(e[0]?.value ? e[0].value : ''); - }} - onFocus={refreshLogTypeOptions} - onBlur={props.handleBlur('logType')} - selectedOptions={ - props.values.logType - ? [ - { - value: props.values.logType, - label: getLogTypeLabel(props.values.logType), - }, - ] - : [] - } - /> - - - - - Manage - - - + error={props.errors.logType} + > + { + props.handleChange('logType')(e[0]?.value ? e[0].value : ''); + }} + onFocus={refreshLogTypeOptions} + onBlur={props.handleBlur('logType')} + selectedOptions={ + props.values.logType + ? [ + { + value: props.values.logType, + label: getLogTypeLabel(props.values.logType), + }, + ] + : [] + } + /> + +
+ + + Manage + + +
+ + + + + Rule level (severity) + + } + isInvalid={(validateOnMount || props.touched.level) && !!props.errors?.level} + error={props.errors.level} + > + ({ label: name, value }))} + singleSelection={{ asPlainText: true }} + onChange={(e) => { + props.handleChange('level')(e[0]?.value ? e[0].value : ''); + }} + onBlur={props.handleBlur('level')} + selectedOptions={ + props.values.level + ? [ + { + value: props.values.level, + label: getSeverityLabel(props.values.level), + }, + ] + : [] + } + /> + - + - - Rule level (severity) - - } - isInvalid={(validateOnMount || props.touched.level) && !!props.errors?.level} - error={props.errors.level} - > - ({ label: name, value }))} - singleSelection={{ asPlainText: true }} - onChange={(e) => { - props.handleChange('level')(e[0]?.value ? e[0].value : ''); - }} - onBlur={props.handleBlur('level')} - selectedOptions={ - props.values.level - ? [ - { - value: props.values.level, - label: getSeverityLabel(props.values.level), - }, - ] - : [] + + Rule Status + } - /> - + isInvalid={(validateOnMount || props.touched.status) && !!props.errors?.status} + error={props.errors.status} + > + ({ value: type, label: type }))} + singleSelection={{ asPlainText: true }} + onChange={(e) => { + props.handleChange('status')(e[0]?.value ? e[0].value : ''); + }} + onBlur={props.handleBlur('status')} + selectedOptions={ + props.values.status + ? [{ value: props.values.status, label: props.values.status }] + : [] + } + /> + - + - - Rule Status + + +

Detection

- } - isInvalid={(validateOnMount || props.touched.status) && !!props.errors?.status} - error={props.errors.status} - > - ({ value: type, label: type }))} - singleSelection={{ asPlainText: true }} - onChange={(e) => { - props.handleChange('status')(e[0]?.value ? e[0].value : ''); - }} - onBlur={props.handleBlur('status')} - selectedOptions={ - props.values.status - ? [{ value: props.values.status, label: props.values.status }] - : [] - } - /> -
+ + +

Define the detection criteria for the rule

+
- + + + { + if (isInvalid) { + props.errors.detection = 'Invalid detection entries'; + } else { + delete props.errors.detection; + } - - -

Detection

-
-
- -

Define the detection criteria for the rule

-
- - - - { - if (isInvalid) { - props.errors.detection = 'Invalid detection entries'; - } else { - delete props.errors.detection; - } + setIsDetectionInvalid(isInvalid); + }} + onChange={(detection: string) => { + props.handleChange('detection')(detection); + }} + /> - setIsDetectionInvalid(isInvalid); - }} - onChange={(detection: string) => { - props.handleChange('detection')(detection); - }} - /> - - - - - - Additional details - optional - - } - > -
- + - - - Tags - - optional - - - - - - Tag - - - } - addButtonName="Add tag" - fields={props.values.tags} - error={props.errors.tags} - isInvalid={(validateOnMount || props.touched.tags) && !!props.errors.tags} - onChange={(tags) => { - props.touched.tags = true; - props.setFieldValue('tags', tags); - }} - data-test-subj={'rule_tags_field'} - /> - - - - References - - optional - - - - - - URL - - - } - addButtonName="Add URL" - fields={props.values.references} - error={props.errors.references} - isInvalid={ - (validateOnMount || props.touched.references) && - !!props.errors?.references - } - onChange={(references) => { - props.touched.references = true; - props.setFieldValue('references', references); - }} - data-test-subj={'rule_references_field'} - /> - - - - False positive cases - - optional - - - - - - Description - - - } - addButtonName="Add false positive" - fields={props.values.falsePositives} - error={props.errors.falsePositives} - isInvalid={ - (validateOnMount || props.touched.falsePositives) && - !!props.errors?.falsePositives - } - onChange={(falsePositives) => { - props.touched.falsePositives = true; - props.setFieldValue('falsePositives', falsePositives); - }} - data-test-subj={'rule_falsePositives_field'} - /> -
-
-
- - )} -
- - - - - - Cancel - - - props.handleSubmit()} - data-test-subj={'submit_rule_form_button'} - fill - > - {mode === 'create' ? 'Create detection rule' : 'Save changes'} - - - - - )} + + + Additional details - optional + + } + > +
+ + + + + Tags + - optional + + + + + + Tag + + + } + addButtonName="Add tag" + fields={props.values.tags} + error={props.errors.tags} + isInvalid={(validateOnMount || props.touched.tags) && !!props.errors.tags} + onChange={(tags) => { + props.touched.tags = true; + props.setFieldValue('tags', tags); + }} + data-test-subj={'rule_tags_field'} + /> + + + + References + - optional + + + + + + URL + + + } + addButtonName="Add URL" + fields={props.values.references} + error={props.errors.references} + isInvalid={ + (validateOnMount || props.touched.references) && + !!props.errors?.references + } + onChange={(references) => { + props.touched.references = true; + props.setFieldValue('references', references); + }} + data-test-subj={'rule_references_field'} + /> + + + + False positive cases + - optional + + + + + + Description + + + } + addButtonName="Add false positive" + fields={props.values.falsePositives} + error={props.errors.falsePositives} + isInvalid={ + (validateOnMount || props.touched.falsePositives) && + !!props.errors?.falsePositives + } + onChange={(falsePositives) => { + props.touched.falsePositives = true; + props.setFieldValue('falsePositives', falsePositives); + }} + data-test-subj={'rule_falsePositives_field'} + /> +
+
+
+ + )} +
+ + + + + + Cancel + + + props.handleSubmit()} + data-test-subj={'submit_rule_form_button'} + fill + > + {mode === 'create' ? 'Create detection rule' : 'Save changes'} + + + + + ); + }} ); }; diff --git a/public/pages/Rules/components/RuleEditor/__snapshots__/RuleEditorContainer.test.tsx.snap b/public/pages/Rules/components/RuleEditor/__snapshots__/RuleEditorContainer.test.tsx.snap index d555e6da6..b41c5c72f 100644 --- a/public/pages/Rules/components/RuleEditor/__snapshots__/RuleEditorContainer.test.tsx.snap +++ b/public/pages/Rules/components/RuleEditor/__snapshots__/RuleEditorContainer.test.tsx.snap @@ -1,2970 +1,3946 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` spec renders the component 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
-
-
+ + + + -
-
-

- Rule title -

-
-
-
-
-
- - This is editor type selector - -
- - -
-
-
-
-

- Rule overview -

-
-
-
- -
-
-
- + +
+ +

+ Rule title +

+
+
+
-
-
- Rule name can be max 256 characters. -
-
-
-
-
-
- -
-
+
+
-
- -
-
-
-
-
-
-
- -
-
-
-
+ + + This is editor type selector + + +
+ + + + + + + + + + +
+ + + - -
-
-
- Combine multiple authors separated with a comma -
-
-
-
-
-

- Details -

-
-
-
-
-
-
- -
-
+ } + labelType="label" > -
-
-
- -
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-

- Detection -

-
-
-

- Define the detection criteria for the rule -

-
-
-
-
-
-
-
-
-
- +
+ + + + +
+ + + +
+ Combine multiple authors separated with a comma
-
+
+
+ + +
+ + +
-

- Define the search identifier in your data the rule will be applied to. -

+

+ Details +

-
+ + +
-
-
-
+
-
- -
-
-
-
+ + Log type + + + } + labelType="label" >
-
-
-
- -
-
-
- + + Log type +
-
-
-
+ + +
-
- -
-
- -
-
-
-
-
- -
- -
-
- -
- +
-
-
+
+ + +
+ + -
-
-
- -
-
-
-
-
-
+ + Manage + + EuiIconMock + + + + + + +
-
+
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-