Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][POC] Analyzer in flyout POC #183094

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
analyzerDatePickersAndSourcererDisabled: false,

/**
* Enables visualization: session viewer and analyzer in expandable flyout
*/
visualizationInFlyoutEnabled: false,

/**
* Enables per-field rule diffs tab in the prebuilt rule upgrade flyout
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = useLeftPanelContext();
const scopeId = 'flyout'; // Different scope Id to distinguish flyout and data table analyzers
const { eventId, scopeId } = useLeftPanelContext();
const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters(
isActiveTimeline(scopeId)
);
Expand All @@ -34,7 +33,7 @@ export const AnalyzeGraph: FC = () => {
<div data-test-subj={ANALYZER_GRAPH_TEST_ID}>
<Resolver
databaseDocumentID={eventId}
resolverComponentInstanceID={scopeId}
resolverComponentInstanceID={`flyout-${scopeId}`}
indices={selectedPatterns}
shouldUpdate={shouldUpdate}
filters={filters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getField } from '../shared/utils';
import { EventKind } from '../shared/constants/event_kinds';
import { useLeftPanelContext } from './context';
import { LeftPanelTour } from './components/tour';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';

export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation' | 'response';
export const LeftPanelVisualizeTab: LeftPanelPaths = 'visualize';
Expand All @@ -35,21 +36,27 @@ export interface LeftPanelProps extends FlyoutPanelProps {
scopeId: string;
};
}
const EVENT_TABS = [tabs.insightsTab];
const ALERT_TABS = [tabs.insightsTab, tabs.investigationTab, tabs.responseTab];

export const LeftPanel: FC<Partial<LeftPanelProps>> = memo(({ path }) => {
const { telemetry } = useKibana().services;
const { openLeftPanel } = useExpandableFlyoutApi();
const { eventId, indexName, scopeId, getFieldsData } = useLeftPanelContext();
const eventKind = getField(getFieldsData('event.kind'));

const tabsDisplayed = useMemo(
() =>
eventKind === EventKind.signal
? [tabs.insightsTab, tabs.investigationTab, tabs.responseTab]
: [tabs.insightsTab],
[eventKind]
const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled(
'visualizationInFlyoutEnabled'
);

const tabsDisplayed = useMemo(() => {
return eventKind !== EventKind.signal
? EVENT_TABS
: visualizationInFlyoutEnabled
? [tabs.visualizeTab, ...ALERT_TABS]
: ALERT_TABS;
}, [eventKind, visualizationInFlyoutEnabled]);

const selectedTabId = useMemo(() => {
const defaultTab = tabsDisplayed[0].id;
if (!path) return defaultTab;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { usePreviewPanelContext } from '../context';

/**
* Analyzer side panel on a preview panel
*/
export const AnalyzerPanel: React.FC = () => {
const { scopeId } = usePreviewPanelContext();
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 ? (
<div data-test-subj="resolver:graph:loading" className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
) : hasError ? (
<div data-test-subj="resolver:graph:error" className="loading-container">
<div>
{' '}
<FormattedMessage
id="xpack.securitySolution.endpoint.resolver.loadingError"
defaultMessage="Error loading data."
/>
</div>
</div>
) : resolverTreeHasNodes ? (
<PanelRouter id={resolverComponentInstanceID} />
) : (
<div data-test-subj="resolver:graph:error" className="loading-container">
<div>
{' '}
<FormattedMessage
id="xpack.securitySolution.endpoint.resolver.loadingError"
defaultMessage="Error loading data."
/>
</div>
</div>
);
};

AnalyzerPanel.displayName = 'AnalyzerPanel';
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { DocumentDetailsPreviewPanelKey } from '../shared/constants/panel_keys';
import { panels } from './panels';

export type PreviewPanelPaths = 'rule-preview' | 'alert-reason-preview';
export type PreviewPanelPaths = 'rule-preview' | 'alert-reason-preview' | 'analyzer-panel';
export const RulePreviewPanel: PreviewPanelPaths = 'rule-preview';
export const AlertReasonPreviewPanel: PreviewPanelPaths = 'alert-reason-preview';
export const AnalyzerPanel: PreviewPanelPaths = 'analyzer-panel';

export interface PreviewPanelProps extends FlyoutPanelProps {
key: typeof DocumentDetailsPreviewPanelKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AlertReasonPreview } from './components/alert_reason_preview';
import type { PreviewPanelPaths } from '.';
import { RulePreview } from './components/rule_preview';
import { RulePreviewFooter } from './components/rule_preview_footer';
import { AnalyzerPanel } from './components/analyzer_panel';

export type PreviewPanelType = Array<{
/**
Expand Down Expand Up @@ -39,4 +40,8 @@ export const panels: PreviewPanelType = [
id: 'alert-reason-preview',
content: <AlertReasonPreview />,
},
{
id: 'analyzer-panel',
content: <AnalyzerPanel />,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import { DateSelectionButton } from './date_picker';
import { StyledGraphControls, StyledGraphControlsColumn, StyledEuiRange } from './styles';
import { NodeLegend } from './legend';
import { SchemaInformation } from './schema';
import { PanelButton } from './panel';

export const GraphControls = React.memo(
({
id,
className,
databaseDocumentID,
}: {
/**
* Id that identify the scope of analyzer
Expand All @@ -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) {
Expand Down Expand Up @@ -148,6 +158,9 @@ export const GraphControls = React.memo(
/>
</>
) : null}
{visualizationInFlyoutEnabled && inFlyout && (
<PanelButton id={id} eventId={databaseDocumentID} indexName={eventIndices[0]} />
)}
</StyledGraphControlsColumn>
<StyledGraphControlsColumn>
<EuiPanel className="panning-controls" paddingSize="none" hasBorder>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { StyledEuiButtonIcon } from './styles';
import { useColors } from '../use_colors';
import { DocumentDetailsPreviewPanelKey } from '../../../flyout/document_details/shared/constants/panel_keys';
import { type PreviewPanelProps, AnalyzerPanel } from '../../../flyout/document_details/preview';

export const PanelButton = memo(
({ id, eventId, indexName }: { id: string; eventId: string; indexName: string }) => {
const { openPreviewPanel } = useExpandableFlyoutApi();

// If in flyout, scope Id is "flyout-scopeId"
const scopeId = id.startsWith('flyout') ? id.substring(7) : id;

const onClick = useCallback(() => {
const PreviewPanelAnalyzerPanel: PreviewPanelProps['path'] = { tab: AnalyzerPanel };
openPreviewPanel({
id: DocumentDetailsPreviewPanelKey,
path: PreviewPanelAnalyzerPanel,
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [openPreviewPanel, eventId, indexName, scopeId]);
const colorMap = useColors();

return (
<StyledEuiButtonIcon
data-test-subj="resolver:graph-controls:show-panel-button"
size="m"
title={'panel'}
aria-label={'open panel'}
onClick={onClick}
iconType={'eye'}
$backgroundColor={colorMap.graphControlsBackground}
$iconColor={colorMap.graphControls}
$borderColor={colorMap.graphControlsBorderColor}
/>
);
}
);

PanelButton.displayName = 'PanelButton';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EuiBreadcrumb, EuiBasicTableColumn, EuiSearchBarProps } from '@elastic/eui';
import { EuiSpacer, EuiText, EuiInMemoryTable } from '@elastic/eui';
import { EuiSpacer, EuiText, EuiInMemoryTable, EuiPanel } from '@elastic/eui';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import { StyledPanel } from '../styles';
Expand Down Expand Up @@ -72,10 +72,12 @@ export const EventDetail = memo(function EventDetail({
selectors.currentRelatedEventData(state.analyzer[id])
);

const PanelWrapper = id.startsWith('flyout') ? EuiPanel : StyledPanel;

return isLoading ? (
<StyledPanel hasBorder>
<PanelWrapper hasBorder>
<PanelLoading id={id} />
</StyledPanel>
</PanelWrapper>
) : event ? (
<EventDetailContents
id={id}
Expand All @@ -85,9 +87,9 @@ export const EventDetail = memo(function EventDetail({
eventType={eventType}
/>
) : (
<StyledPanel hasBorder>
<PanelWrapper hasBorder>
<PanelContentError id={id} translatedErrorMessage={eventDetailRequestError} />
</StyledPanel>
</PanelWrapper>
);
});

Expand Down Expand Up @@ -120,9 +122,10 @@ const EventDetailContents = memo(function ({
});

const nodeName = processEvent ? eventModel.processNameSafeVersion(processEvent) : null;
const PanelWrapper = id.startsWith('flyout') ? EuiPanel : StyledPanel;

return (
<StyledPanel hasBorder data-test-subj="resolver:panel:event-detail">
<PanelWrapper hasBorder={!id.startsWith('flyout')} data-test-subj="resolver:panel:event-detail">
<EventDetailBreadcrumbs
id={id}
nodeID={nodeID}
Expand Down Expand Up @@ -158,7 +161,7 @@ const EventDetailContents = memo(function ({
</StyledDescriptiveName>
<EuiSpacer size="l" />
<EventDetailFields event={event} id={id} />
</StyledPanel>
</PanelWrapper>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
EuiTextColor,
EuiLink,
EuiInMemoryTable,
EuiPanel,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
Expand Down Expand Up @@ -59,19 +60,20 @@ export const NodeDetail = memo(function ({ id, nodeID }: { id: string; nodeID: s
const nodeStatus = useSelector((state: State) =>
selectors.nodeDataStatus(state.analyzer[id])(nodeID)
);
const PanelWrapper = id.startsWith('flyout') ? EuiPanel : StyledPanel;

return nodeStatus === 'loading' ? (
<StyledPanel hasBorder>
<PanelWrapper hasBorder={!id.startsWith('flyout')}>
<PanelLoading id={id} />
</StyledPanel>
</PanelWrapper>
) : processEvent ? (
<StyledPanel hasBorder data-test-subj="resolver:panel:node-detail">
<PanelWrapper hasBorder={!id.startsWith('flyout')} data-test-subj="resolver:panel:node-detail">
<NodeDetailView id={id} nodeID={nodeID} processEvent={processEvent} />
</StyledPanel>
</PanelWrapper>
) : (
<StyledPanel hasBorder>
<PanelWrapper hasBorder={!id.startsWith('flyout')}>
<PanelContentError id={id} translatedErrorMessage={nodeDetailError} />
</StyledPanel>
</PanelWrapper>
);
});

Expand Down