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') && (
+
+
+
+ )}
>
) : (
)}
-
+
);