diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx index ff4cc2dc64757..ded6cc868fd3d 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import { size, isEmpty, isEqual, xorWith } from 'lodash'; import { Background, @@ -15,7 +15,7 @@ import { useEdgesState, useNodesState, } from '@xyflow/react'; -import type { Edge, Node } from '@xyflow/react'; +import type { Edge, FitViewOptions, Node } from '@xyflow/react'; import type { CommonProps } from '@elastic/eui'; import { SvgDefsMarker } from '../edge/styles'; import { @@ -89,6 +89,9 @@ export const Graph: React.FC = ({ ...rest }) => { const backgroundId = Math.random().toFixed(4); // TODO: use useId(); when available (react >=18) + const fitViewRef = useRef< + ((fitViewOptions?: FitViewOptions | undefined) => Promise) | null + >(null); const [prevNodes, setPrevNodes] = useState([]); const [prevEdges, setPrevEdges] = useState([]); const [isGraphLocked, setIsGraphLocked] = useState(!interactive); @@ -105,6 +108,9 @@ export const Graph: React.FC = ({ setEdges(initialEdges); setPrevNodes(nodes); setPrevEdges(edges); + setTimeout(() => { + fitViewRef.current?.(); + }, 0); } // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect when nodes or edges change }, [nodes, edges, setNodes, setEdges]); @@ -132,6 +138,7 @@ export const Graph: React.FC = ({ fitView={true} onInit={(xyflow) => { window.requestAnimationFrame(() => xyflow.fitView()); + fitViewRef.current = xyflow.fitView; // When the graph is not initialized as interactive, we need to fit the view on resize if (!interactive) { diff --git a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx index d06b1b76f80ce..d1963458102a3 100644 --- a/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx +++ b/x-pack/packages/kbn-cloud-security-posture/graph/src/components/graph/graph_popover.tsx @@ -44,7 +44,11 @@ export const GraphPopover: React.FC = ({ disabled: false, crossFrame: true, noIsolation: false, - returnFocus: false, + returnFocus: (el) => { + anchorElement.focus(); + return false; + }, + preventScrollOnFocus: true, onClickOutside: () => { closePopover(); }, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx index 480e6bdfa3bc2..fed990c3572ad 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/graph_visualization.tsx @@ -9,8 +9,10 @@ import React, { memo, useMemo, useState } from 'react'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { NodeViewModel } from '@kbn/cloud-security-posture-graph'; -import type { Query } from '@kbn/es-query'; +import type { Filter, Query } from '@kbn/es-query'; import { css } from '@emotion/css'; +import { useSourcererDataView } from '../../../../sourcerer/containers'; +import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useDocumentDetailsContext } from '../../shared/context'; import { GRAPH_VISUALIZATION_TEST_ID } from './test_ids'; import { useFetchGraphData } from '../../right/hooks/use_fetch_graph_data'; @@ -61,7 +63,10 @@ const useGraphData = ( return { data, refresh, isFetching }; }; -const useGraphPopovers = (setActorIds: React.Dispatch>>) => { +const useGraphPopovers = ( + setActorIds: React.Dispatch>>, + setSearchFilters: React.Dispatch> +) => { const { services: { notifications }, } = useKibana(); @@ -72,6 +77,29 @@ const useGraphPopovers = (setActorIds: React.Dispatch { setActorIds((prev) => new Set([...prev, node.id])); + setSearchFilters((prev) => [ + ...prev, + { + meta: { + disabled: false, + key: 'actor.entity.id', + field: 'actor.entity.id', + negate: false, + params: { + query: node.id, + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'actor.entity.id': node.id, + }, + }, + $state: { + store: 'appState', + } as Filter['$state'], + }, + ]); }, onShowActionsOnEntityClick: (node) => { notifications?.toasts.addInfo('Show actions on entity is not implemented yet'); @@ -114,6 +142,8 @@ const useGraphNodes = ( * Graph visualization view displayed in the document details expandable flyout left section under the Visualize tab */ export const GraphVisualization: React.FC = memo(() => { + const { indexPattern } = useSourcererDataView(SourcererScopeName.default); + const [searchFilters, setSearchFilters] = useState(() => []); const { filters, updateFilters } = useGraphFilters(); const { getFieldsData, dataAsNestedObject } = useDocumentDetailsContext(); const { eventIds } = useGraphPreview({ @@ -122,7 +152,7 @@ export const GraphVisualization: React.FC = memo(() => { }); const [actorIds, setActorIds] = useState(() => new Set()); - const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds); + const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(setActorIds, setSearchFilters); const expandButtonClickHandler = (...args: unknown[]) => popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args); const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); @@ -135,7 +165,7 @@ export const GraphVisualization: React.FC = memo(() => { {...{ appName: 'graph-visualization', intl: null, - showFilterBar: false, + showFilterBar: true, showDatePicker: true, showAutoRefreshOnly: false, showSaveQuery: false, @@ -145,12 +175,24 @@ export const GraphVisualization: React.FC = memo(() => { dateRangeFrom: filters.from.split('/')[0], dateRangeTo: filters.to.split('/')[0], query: { query: '', language: 'kuery' }, - filters: [], + indexPatterns: [indexPattern], + filters: searchFilters, submitButtonStyle: 'iconOnly', + onFiltersUpdated: (newFilters) => { + setSearchFilters(newFilters); + + setActorIds( + new Set( + newFilters + .filter((filter) => filter.meta.key === 'actor.entity.id') + .map((filter) => (filter.meta.params as { query: string })?.query) + .filter((query) => typeof query === 'string') + ) + ); + }, onQuerySubmit: (payload, isUpdate) => { if (isUpdate) { updateFilters({ from: payload.dateRange.from, to: payload.dateRange.to }); - setActorIds(new Set()); } else { refresh(); }