Skip to content

Commit

Permalink
[Cloud Security] Added labels popover (elastic#204954)
Browse files Browse the repository at this point in the history
## Summary

Added expand button for labels with popover:

https://github.com/user-attachments/assets/80950f51-b45b-4174-9be2-267b6aca569b

https://github.com/user-attachments/assets/690ef85b-be48-42df-bf00-02ee7d9303f2

**How to test**

To test this PR you can run

```
yarn storybook cloud_security_posture_packages
```

To test e2e

- Enable the feature flag

`kibana.dev.yml`:

```yml
uiSettings.overrides.securitySolution:enableVisualizationsInFlyout: true
xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled']
```

- Load mocked data:

```
node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601

node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601
```

- Make sure you include data from Oct 13 2024. (in the video I use Last
year)

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: Sean Rathier <[email protected]>
Co-authored-by: Brad White <[email protected]>
Co-authored-by: seanrathier <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit fd47d2e)
  • Loading branch information
kfirpeled committed Dec 23, 2024
1 parent 45f4748 commit 18302cc
Show file tree
Hide file tree
Showing 31 changed files with 376 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export const EVENT_GRAPH_VISUALIZATION_API = '/internal/cloud_security_posture/g
export const RELATED_ENTITY = 'related.entity' as const;
export const ACTOR_ENTITY_ID = 'actor.entity.id' as const;
export const TARGET_ENTITY_ID = 'target.entity.id' as const;
export const EVENT_ACTION = 'event.action' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useEffect, useMemo, useRef } from 'react';
import React, { memo, useEffect, useMemo, useRef } from 'react';
import { ThemeProvider } from '@emotion/react';
import {
ReactFlow,
Expand Down Expand Up @@ -47,7 +47,7 @@ export default {

const nodeTypes = {
// eslint-disable-next-line react/display-name
default: React.memo((props: NodeProps<BuiltInNode>) => {
default: memo<NodeProps<BuiltInNode>>((props: NodeProps<BuiltInNode>) => {
return (
<div>
<Handle
Expand All @@ -67,7 +67,7 @@ const nodeTypes = {
{props.data.label}
</div>
);
}) as React.FC<NodeProps<BuiltInNode>>,
}),
label: LabelNode,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,7 @@ const edgeTypes = {
*
* @returns {JSX.Element} The rendered Graph component.
*/
export const Graph: React.FC<GraphProps> = ({
nodes,
edges,
interactive,
isLocked = false,
...rest
}) => {
export const Graph = ({ nodes, edges, interactive, isLocked = false, ...rest }: GraphProps) => {
const backgroundId = useGeneratedHtmlId();
const fitViewRef = useRef<
((fitViewOptions?: FitViewOptions<Node> | undefined) => Promise<boolean>) | null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
import { ThemeProvider, css } from '@emotion/react';
import { Story } from '@storybook/react';
import { EuiListGroup, EuiHorizontalRule } from '@elastic/eui';
import type { EntityNodeViewModel, NodeProps } from '..';
import type { EntityNodeViewModel, LabelNodeViewModel, NodeProps } from '..';
import { Graph } from '..';
import { GraphPopover } from './graph_popover';
import { ExpandButtonClickCallback } from '../types';
Expand Down Expand Up @@ -179,9 +179,11 @@ const Template: Story = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeClickHandler = (...args: any[]) => popoverOpenWrapper(nodePopover.onNodeClick, ...args);

const nodes: EntityNodeViewModel[] = useMemo(
() =>
(['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const).map((shape, idx) => ({
const nodes: Array<EntityNodeViewModel | LabelNodeViewModel> = useMemo(
() => [
...(
['hexagon', 'ellipse', 'rectangle', 'pentagon', 'diamond'] as const
).map<EntityNodeViewModel>((shape, idx) => ({
id: `${idx}`,
label: `Node ${idx}`,
color: 'primary',
Expand All @@ -191,6 +193,16 @@ const Template: Story = () => {
expandButtonClick: expandButtonClickHandler,
nodeClick: nodeClickHandler,
})),
{
id: 'label',
label: 'Node 5',
color: 'primary',
interactive: true,
shape: 'label',
expandButtonClick: expandButtonClickHandler,
nodeClick: nodeClickHandler,
} as LabelNodeViewModel,
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ export interface GraphPopoverProps
closePopover: () => void;
}

export const GraphPopover: React.FC<GraphPopoverProps> = ({
export const GraphPopover = ({
isOpen,
anchorElement,
closePopover,
children,
...rest
}) => {
}: GraphPopoverProps) => {
const { euiTheme } = useEuiTheme();

if (!anchorElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ import type { Filter, Query, TimeRange, PhraseFilter } from '@kbn/es-query';
import { css } from '@emotion/react';
import { getEsQueryConfig } from '@kbn/data-service';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Graph } from '../../..';
import { Graph, isEntityNode } from '../../..';
import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover';
import { useGraphLabelExpandPopover } from './use_graph_label_expand_popover';
import { useFetchGraphData } from '../../hooks/use_fetch_graph_data';
import { GRAPH_INVESTIGATION_TEST_ID } from '../test_ids';
import { ACTOR_ENTITY_ID, RELATED_ENTITY, TARGET_ENTITY_ID } from '../../common/constants';
import {
ACTOR_ENTITY_ID,
EVENT_ACTION,
RELATED_ENTITY,
TARGET_ENTITY_ID,
} from '../../common/constants';

const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation';

Expand Down Expand Up @@ -112,17 +118,23 @@ const useGraphPopovers = (
},
});

const labelExpandPopover = useGraphLabelExpandPopover({
onShowEventsWithThisActionClick: (node) => {
setSearchFilters((prev) => addFilter(dataViewId, prev, EVENT_ACTION, node.data.label ?? ''));
},
});

const openPopoverCallback = useCallback(
(cb: Function, ...args: unknown[]) => {
[nodeExpandPopover].forEach(({ actions: { closePopover } }) => {
[nodeExpandPopover, labelExpandPopover].forEach(({ actions: { closePopover } }) => {
closePopover();
});
cb(...args);
},
[nodeExpandPopover]
[nodeExpandPopover, labelExpandPopover]
);

return { nodeExpandPopover, openPopoverCallback };
return { nodeExpandPopover, labelExpandPopover, openPopoverCallback };
};

interface GraphInvestigationProps {
Expand Down Expand Up @@ -160,7 +172,7 @@ interface GraphInvestigationProps {
/**
* Graph investigation view allows the user to expand nodes and view related entities.
*/
export const GraphInvestigation: React.FC<GraphInvestigationProps> = memo(
export const GraphInvestigation = memo<GraphInvestigationProps>(
({
initialState: { dataView, originEventIds, timeRange: initialTimeRange },
}: GraphInvestigationProps) => {
Expand All @@ -181,13 +193,17 @@ export const GraphInvestigation: React.FC<GraphInvestigationProps> = memo(
[dataView, searchFilters, uiSettings]
);

const { nodeExpandPopover, openPopoverCallback } = useGraphPopovers(
const { nodeExpandPopover, labelExpandPopover, openPopoverCallback } = useGraphPopovers(
dataView?.id ?? '',
setSearchFilters
);
const expandButtonClickHandler = (...args: unknown[]) =>
const nodeExpandButtonClickHandler = (...args: unknown[]) =>
openPopoverCallback(nodeExpandPopover.onNodeExpandButtonClick, ...args);
const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen);
const labelExpandButtonClickHandler = (...args: unknown[]) =>
openPopoverCallback(labelExpandPopover.onLabelExpandButtonClick, ...args);
const isPopoverOpen = [nodeExpandPopover, labelExpandPopover].some(
({ state: { isOpen } }) => isOpen
);
const { data, refresh, isFetching } = useFetchGraphData({
req: {
query: {
Expand All @@ -206,13 +222,19 @@ export const GraphInvestigation: React.FC<GraphInvestigationProps> = memo(
const nodes = useMemo(() => {
return (
data?.nodes.map((node) => {
const nodeHandlers =
node.shape !== 'label' && node.shape !== 'group'
? {
expandButtonClick: expandButtonClickHandler,
}
: undefined;
return { ...node, ...nodeHandlers };
if (isEntityNode(node)) {
return {
...node,
expandButtonClick: nodeExpandButtonClickHandler,
};
} else if (node.shape === 'label') {
return {
...node,
expandButtonClick: labelExpandButtonClickHandler,
};
}

return { ...node };
}) ?? []
);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -275,6 +297,7 @@ export const GraphInvestigation: React.FC<GraphInvestigationProps> = memo(
</EuiFlexItem>
</EuiFlexGroup>
<nodeExpandPopover.PopoverComponent />
<labelExpandPopover.PopoverComponent />
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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, { memo } from 'react';
import { EuiListGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ExpandPopoverListItem } from '../styles';
import { GraphPopover } from '../../..';
import {
GRAPH_LABEL_EXPAND_POPOVER_TEST_ID,
GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID,
} from '../test_ids';

interface GraphLabelExpandPopoverProps {
isOpen: boolean;
anchorElement: HTMLElement | null;
closePopover: () => void;
onShowEventsWithThisActionClick: () => void;
}

export const GraphLabelExpandPopover = memo<GraphLabelExpandPopoverProps>(
({ isOpen, anchorElement, closePopover, onShowEventsWithThisActionClick }) => {
return (
<GraphPopover
panelPaddingSize="s"
anchorPosition="rightCenter"
isOpen={isOpen}
anchorElement={anchorElement}
closePopover={closePopover}
data-test-subj={GRAPH_LABEL_EXPAND_POPOVER_TEST_ID}
>
<EuiListGroup gutterSize="none" bordered={false} flush={true}>
<ExpandPopoverListItem
iconType="users"
label={i18n.translate(
'securitySolutionPackages.csp.graph.graphLabelExpandPopover.showEventsWithThisAction',
{ defaultMessage: 'Show events with this action' }
)}
onClick={onShowEventsWithThisActionClick}
data-test-subj={GRAPH_LABEL_EXPAND_POPOVER_SHOW_EVENTS_WITH_THIS_ACTION_ITEM_ID}
/>
</EuiListGroup>
</GraphPopover>
);
}
);

GraphLabelExpandPopover.displayName = 'GraphLabelExpandPopover';
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface GraphNodeExpandPopoverProps {
onShowActionsOnEntityClick: () => void;
}

export const GraphNodeExpandPopover: React.FC<GraphNodeExpandPopoverProps> = memo(
export const GraphNodeExpandPopover = memo<GraphNodeExpandPopoverProps>(
({
isOpen,
anchorElement,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useGraphPopover } from '../../..';
import type { ExpandButtonClickCallback, NodeProps } from '../types';
import type { PopoverActions } from '../graph/use_graph_popover';
import { GraphLabelExpandPopover } from './graph_label_expand_popover';

interface UseGraphLabelExpandPopoverArgs {
onShowEventsWithThisActionClick: (node: NodeProps) => void;
}

export const useGraphLabelExpandPopover = ({
onShowEventsWithThisActionClick,
}: UseGraphLabelExpandPopoverArgs) => {
const { id, state, actions } = useGraphPopover('label-expand-popover');
const { openPopover, closePopover } = actions;

const selectedNode = useRef<NodeProps | null>(null);
const unToggleCallbackRef = useRef<(() => void) | null>(null);
const [pendingOpen, setPendingOpen] = useState<{
node: NodeProps;
el: HTMLElement;
unToggleCallback: () => void;
} | null>(null);

const closePopoverHandler = useCallback(() => {
selectedNode.current = null;
unToggleCallbackRef.current?.();
unToggleCallbackRef.current = null;
closePopover();
}, [closePopover]);

const onLabelExpandButtonClick: ExpandButtonClickCallback = useCallback(
(e, node, unToggleCallback) => {
if (selectedNode.current?.id === node.id) {
// If the same node is clicked again, close the popover
closePopoverHandler();
} else {
// Close the current popover if open
closePopoverHandler();

// Set the pending open state
setPendingOpen({ node, el: e.currentTarget, unToggleCallback });
}
},
[closePopoverHandler]
);

useEffect(() => {
// Open pending popover if the popover is not open
if (!state.isOpen && pendingOpen) {
const { node, el, unToggleCallback } = pendingOpen;

selectedNode.current = node;
unToggleCallbackRef.current = unToggleCallback;
openPopover(el);

setPendingOpen(null);
}
}, [state.isOpen, pendingOpen, openPopover]);

const PopoverComponent = memo(() => (
<GraphLabelExpandPopover
isOpen={state.isOpen}
anchorElement={state.anchorElement}
closePopover={closePopoverHandler}
onShowEventsWithThisActionClick={() => {
onShowEventsWithThisActionClick(selectedNode.current as NodeProps);
closePopoverHandler();
}}
/>
));

PopoverComponent.displayName = GraphLabelExpandPopover.displayName;

const actionsWithClose: PopoverActions = useMemo(
() => ({
...actions,
closePopover: closePopoverHandler,
}),
[actions, closePopoverHandler]
);

return useMemo(
() => ({
onLabelExpandButtonClick,
PopoverComponent,
id,
actions: actionsWithClose,
state,
}),
[PopoverComponent, actionsWithClose, id, onLabelExpandButtonClick, state]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { NodeViewModel } from './types';

export { Graph } from './graph/graph';
export { GraphInvestigation } from './graph_investigation/graph_investigation';
export { GraphPopover } from './graph/graph_popover';
Expand All @@ -18,3 +20,10 @@ export type {
EntityNodeViewModel,
NodeProps,
} from './types';

export const isEntityNode = (node: NodeViewModel) =>
node.shape === 'ellipse' ||
node.shape === 'pentagon' ||
node.shape === 'rectangle' ||
node.shape === 'diamond' ||
node.shape === 'hexagon';
Loading

0 comments on commit 18302cc

Please sign in to comment.