diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 7b8b1a3e47d392..e00fe87d39a552 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -208,6 +208,11 @@ export const allowedExperimentalValues = Object.freeze({ */ analyzerDatePickersAndSourcererDisabled: false, + /** + * Enables visualization: session viewer and analyzer in expandable flyout + */ + visualizationInFlyoutEnabled: false, + /** * Enables an ability to customize Elastic prebuilt rules. * diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/analyzer_panel.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/analyzer_panel.tsx new file mode 100644 index 00000000000000..0b6bac4d9379a9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/analyzer_panel.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n-react'; +import * as selectors from '../../../resolver/store/selectors'; +import type { State } from '../../../common/store/types'; +import { PanelRouter } from '../../../resolver/view/panels'; +import { useAnalyzerPanelContext } from './context'; + +/** + * Analyzer side panel on a preview panel + */ +export const AnalyzerPanels: React.FC = () => { + const { scopeId } = useAnalyzerPanelContext(); + const resolverComponentInstanceID = `flyout-${scopeId}`; + + const isLoading = useSelector((state: State) => + selectors.isTreeLoading(state.analyzer[resolverComponentInstanceID]) + ); + + const hasError = useSelector((state: State) => + selectors.hadErrorLoadingTree(state.analyzer[resolverComponentInstanceID]) + ); + + const resolverTreeHasNodes = useSelector((state: State) => + selectors.resolverTreeHasNodes(state.analyzer[resolverComponentInstanceID]) + ); + + return isLoading ? ( +
+ +
+ ) : hasError ? ( +
+
+ {' '} + +
+
+ ) : resolverTreeHasNodes ? ( + + ) : ( +
+
+ {' '} + +
+
+ ); +}; + +AnalyzerPanels.displayName = 'AnalyzerPanels'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/context.tsx new file mode 100644 index 00000000000000..18f9b357a13e62 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/context.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, memo, useContext, useMemo } from 'react'; +import { FlyoutError } from '../../shared/components/flyout_error'; +import type { AnalyzerPanelProps } from '.'; + +export interface AnalyzerPanelContext { + /** + * Scope id + */ + scopeId: string; +} + +export const AnalyzerPanelContext = createContext(undefined); + +export type AnalyzerPanelProviderProps = { + /** + * React components to render + */ + children: React.ReactNode; +} & Partial; + +export const AnalyzerPanelProvider = memo(({ scopeId, children }: AnalyzerPanelProviderProps) => { + const contextValue = useMemo(() => (scopeId ? { scopeId } : undefined), [scopeId]); + + if (!contextValue) { + return ; + } + + return ( + {children} + ); +}); + +AnalyzerPanelProvider.displayName = 'AnalyzerPanelProvider'; + +export const useAnalyzerPanelContext = (): AnalyzerPanelContext => { + const contextValue = useContext(AnalyzerPanelContext); + + if (!contextValue) { + throw new Error( + 'RuleOverviewPanelContext can only be used within RuleOverviewPanelContext provider' + ); + } + + return contextValue; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx new file mode 100644 index 00000000000000..7fa53bf75de8e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/analyzer_panels/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import type { FlyoutPanelProps } from '@kbn/expandable-flyout'; +import { FlyoutBody } from '../../shared/components/flyout_body'; +import type { DocumentDetailsAnalyzerPanelKey } from '../shared/constants/panel_keys'; +import { AnalyzerPanels } from './analyzer_panel'; + +export interface AnalyzerPanelProps extends FlyoutPanelProps { + key: typeof DocumentDetailsAnalyzerPanelKey; + params: { + scopeId: string; + }; +} + +/** + * Displays analyzer panel + */ +export const AnalyzerPanel: React.FC = memo(() => { + return ( + <> + +
+ +
+
+ + ); +}); + +AnalyzerPanel.displayName = 'AnalyzerPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx index faefd92e9b6897..fa97c579e56c92 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/analyze_graph.tsx @@ -20,8 +20,7 @@ export const ANALYZE_GRAPH_ID = 'analyze_graph'; * Analyzer graph view displayed in the document details expandable flyout left section under the Visualize tab */ export const AnalyzeGraph: FC = () => { - const { eventId } = useDocumentDetailsContext(); - const scopeId = 'flyout'; // Different scope Id to distinguish flyout and data table analyzers + const { eventId, scopeId } = useDocumentDetailsContext(); const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters( isActiveTimeline(scopeId) ); @@ -34,7 +33,7 @@ export const AnalyzeGraph: FC = () => {
> = memo(({ path }) => { 'securitySolutionNotesEnabled' ); + const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'visualizationInFlyoutEnabled' + ); + const tabsDisplayed = useMemo(() => { const tabList = eventKind === EventKind.signal @@ -46,8 +49,11 @@ export const LeftPanel: FC> = memo(({ path }) => { if (securitySolutionNotesEnabled && !isPreview) { tabList.push(tabs.notesTab); } + if (visualizationInFlyoutEnabled) { + return [tabs.visualizeTab, ...tabList]; + } return tabList; - }, [eventKind, isPreview, securitySolutionNotesEnabled]); + }, [eventKind, isPreview, securitySolutionNotesEnabled, visualizationInFlyoutEnabled]); const selectedTabId = useMemo(() => { const defaultTab = tabsDisplayed[0].id; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts index a57cbf85fa784e..6039c0c184802d 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/constants/panel_keys.ts @@ -12,3 +12,4 @@ export const DocumentDetailsPreviewPanelKey = 'document-details-preview' as cons export const DocumentDetailsIsolateHostPanelKey = 'document-details-isolate-host' as const; export const DocumentDetailsAlertReasonPanelKey = 'document-details-alert-reason' as const; export const DocumentDetailsRuleOverviewPanelKey = 'document-details-rule-overview' as const; +export const DocumentDetailsAnalyzerPanelKey = 'document-details-analyzer-details' as const; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index 1197e39ad86cb4..f934538aee135f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -91,6 +91,7 @@ export const DocumentDetailsProvider = memo( loading, refetchFlyoutData, searchHit, + index, } = useEventDetails({ eventId: id, indexName }); const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); @@ -99,14 +100,14 @@ export const DocumentDetailsProvider = memo( const contextValue = useMemo( () => id && - indexName && + // indexName && scopeId && dataAsNestedObject && dataFormattedForFieldBrowser && searchHit ? { eventId: id, - indexName, + indexName: index, scopeId, browserFields, dataAsNestedObject, @@ -122,7 +123,6 @@ export const DocumentDetailsProvider = memo( [ id, maybeRule, - indexName, scopeId, browserFields, dataAsNestedObject, @@ -131,6 +131,7 @@ export const DocumentDetailsProvider = memo( refetchFlyoutData, getFieldsData, isPreviewMode, + index, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts index a75453d4d2f4d9..9f344630d1ba28 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_event_details.ts @@ -64,6 +64,7 @@ export interface UseEventDetailsResult { * The actual raw document object */ searchHit: SearchHit | undefined; + index: string; } /** @@ -101,5 +102,6 @@ export const useEventDetails = ({ loading, refetchFlyoutData, searchHit, + index: eventIndex, }; }; diff --git a/x-pack/plugins/security_solution/public/flyout/index.tsx b/x-pack/plugins/security_solution/public/flyout/index.tsx index e3f2bb8c82d8c0..a8fab8fb75abbb 100644 --- a/x-pack/plugins/security_solution/public/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/index.tsx @@ -18,6 +18,7 @@ import { DocumentDetailsPreviewPanelKey, DocumentDetailsAlertReasonPanelKey, DocumentDetailsRuleOverviewPanelKey, + DocumentDetailsAnalyzerPanelKey, } from './document_details/shared/constants/panel_keys'; import type { IsolateHostPanelProps } from './document_details/isolate_host'; import { IsolateHostPanel } from './document_details/isolate_host'; @@ -41,6 +42,9 @@ import type { HostPanelExpandableFlyoutProps } from './entity_details/host_right import { HostPanel, HostPanelKey, HostPreviewPanelKey } from './entity_details/host_right'; import type { HostDetailsExpandableFlyoutProps } from './entity_details/host_details_left'; import { HostDetailsPanel, HostDetailsPanelKey } from './entity_details/host_details_left'; +import type { AnalyzerPanelProps } from './document_details/analyzer_panels'; +import { AnalyzerPanel } from './document_details/analyzer_panels'; +import { AnalyzerPanelProvider } from './document_details/analyzer_panels/context'; /** * List of all panels that will be used within the document details expandable flyout. @@ -95,6 +99,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels'] ), }, + { + key: DocumentDetailsAnalyzerPanelKey, + component: (props) => ( + + + + ), + }, { key: UserPanelKey, component: (props) => , diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx index 6829f9ece1fe03..eff90acaf5bec4 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/index.tsx @@ -28,11 +28,13 @@ import { DateSelectionButton } from './date_picker'; import { StyledGraphControls, StyledGraphControlsColumn, StyledEuiRange } from './styles'; import { NodeLegend } from './legend'; import { SchemaInformation } from './schema'; +import { ShowPanelButton } from './show_panel'; export const GraphControls = React.memo( ({ id, className, + databaseDocumentID, }: { /** * Id that identify the scope of analyzer @@ -42,20 +44,28 @@ export const GraphControls = React.memo( * A className string provided by `styled` */ className?: string; + /** + * The `_id` for an ES document. Used to select a process that we'll show the graph for. + */ + databaseDocumentID: string; }) => { const dispatch = useDispatch(); const scalingFactor = useSelector((state: State) => selectors.scalingFactor(state.analyzer[id]) ); + const eventIndices = useSelector((state: State) => selectors.eventIndices(state.analyzer[id])); const { timestamp } = useContext(SideEffectContext); const isDatePickerAndSourcererDisabled = useIsExperimentalFeatureEnabled( 'analyzerDatePickersAndSourcererDisabled' ); + const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled( + 'visualizationInFlyoutEnabled' + ); const [activePopover, setPopover] = useState< null | 'schemaInfo' | 'nodeLegend' | 'sourcererSelection' | 'datePicker' >(null); const colorMap = useColors(); - + const inFlyout = id.startsWith('flyout'); const setActivePopover = useCallback( (value) => { if (value === activePopover) { @@ -148,6 +158,9 @@ export const GraphControls = React.memo( /> ) : null} + {visualizationInFlyoutEnabled && inFlyout && ( + + )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx new file mode 100644 index 00000000000000..4f54b50c4458ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/controls/show_panel.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, memo, useState } from 'react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { i18n } from '@kbn/i18n'; +import { StyledEuiButtonIcon } from './styles'; +import { useColors } from '../use_colors'; +import { DocumentDetailsAnalyzerPanelKey } from '../../../flyout/document_details/shared/constants/panel_keys'; + +const ANALYZER_PREVIEW_BANNER = { + title: i18n.translate( + 'xpack.securitySolution.flyout.left.visualizations.analyzer.panelPreviewTitle', + { + defaultMessage: 'Preview analyzer panels', + } + ), + backgroundColor: 'warning', + textColor: 'warning', +}; + +export const ShowPanelButton = memo( + ({ id, eventId, indexName }: { id: string; eventId: string; indexName: string }) => { + const { openPreviewPanel, closePreviewPanel } = useExpandableFlyoutApi(); + const [isVisible, setIsPanelVisible] = useState(true); + + // If in flyout, scope Id is "flyout-scopeId" + const scopeId = id.startsWith('flyout') ? id.substring(7) : id; + + const onClick = useCallback(() => { + setIsPanelVisible(!isVisible); + if (isVisible) { + openPreviewPanel({ + id: DocumentDetailsAnalyzerPanelKey, + params: { + scopeId, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); + } else { + closePreviewPanel(); + } + }, [openPreviewPanel, closePreviewPanel, scopeId, isVisible]); + + const colorMap = useColors(); + + return ( + + ); + } +); + +ShowPanelButton.displayName = 'ShowPanelButton'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index 3367de214ab0ee..1d7c7e75728188 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -20,7 +20,7 @@ import type { State } from '../../../common/store/types'; */ // eslint-disable-next-line react/display-name -export const PanelRouter = memo(function ({ id }: { id: string }) { +export const PanelRouter = memo(function ({ id, inFlyout }: { id: string; inFlyout: boolean }) { const params: PanelViewAndParameters = useSelector((state: State) => selectors.panelViewAndParameters(state.analyzer[id]) ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 5409eaede0a769..66e7dc362cf8a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { BoldCode, StyledTime } from './styles'; import { Breadcrumbs } from './breadcrumbs'; import * as eventModel from '../../../../common/endpoint/models/event'; @@ -30,7 +31,18 @@ import { useFormattedDate } from './use_formatted_date'; import { expandDottedObject } from '../../../../common/utils/expand_dotted'; import type { State } from '../../../common/store/types'; import { userRequestedAdditionalRelatedEvents } from '../../store/data/action'; +import { DocumentDetailsPreviewPanelKey } from '../../../flyout/document_details/shared/constants/panel_keys'; +const ANALYZER_PREVIEW_BANNER = { + title: i18n.translate( + 'xpack.securitySolution.flyout.left.visualizations.analyzer.panelPreviewTitle', + { + defaultMessage: 'Preview analyzer panels', + } + ), + backgroundColor: 'warning', + textColor: 'warning', +}; /** * Render a list of events that are related to `nodeID` and that have a category of `eventType`. */ @@ -125,6 +137,23 @@ const NodeEventsListItem = memo(function ({ winlogRecordID: String(winlogRecordID), }, }); + const { openPreviewPanel } = useExpandableFlyoutApi(); + const inFlyout = id.startsWith('flyout'); + const scopeId = id.startsWith('flyout') ? id.substring(7) : id; + + const openPreview = useCallback(() => { + openPreviewPanel({ + id: DocumentDetailsPreviewPanelKey, + params: { + id: 'ogE95pABw84TvHu32PI0', + indexName: '', + scopeId, + isPreviewMode: true, + banner: ANALYZER_PREVIEW_BANNER, + }, + }); + }, [openPreviewPanel, scopeId]); + return ( <> @@ -147,12 +176,21 @@ const NodeEventsListItem = memo(function ({ - - - + {!inFlyout ? ( + + + + ) : ( + + + + )} ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index f9dafcf717ad3c..ed05b53a6c145f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -162,14 +162,16 @@ export const ResolverWithoutProviders = React.memo( ); })} - - - + {!resolverComponentInstanceID.startsWith('flyout') && ( + + + + )} ) : ( )} - + );