diff --git a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts index 581134eae1782..6def7c7bcbd47 100644 --- a/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts +++ b/x-pack/plugins/session_view/common/mocks/constants/session_view_process.mock.ts @@ -160,6 +160,7 @@ export const mockEvents: ProcessEvent[] = [ action: EventAction.fork, category: 'process', kind: EventKind.event, + id: '1', }, host: { architecture: 'x86_64', @@ -317,6 +318,7 @@ export const mockEvents: ProcessEvent[] = [ action: EventAction.exec, category: 'process', kind: EventKind.event, + id: '2', }, }, { @@ -459,6 +461,7 @@ export const mockEvents: ProcessEvent[] = [ action: EventAction.end, category: 'process', kind: EventKind.event, + id: '3', }, host: { architecture: 'x86_64', @@ -621,6 +624,7 @@ export const mockEvents: ProcessEvent[] = [ action: EventAction.end, category: 'process', kind: EventKind.event, + id: '4', }, host: { architecture: 'x86_64', @@ -809,6 +813,7 @@ export const mockAlerts: ProcessEvent[] = [ action: EventAction.exec, category: 'process', kind: EventKind.signal, + id: '5', }, host: { architecture: 'x86_64', @@ -995,6 +1000,7 @@ export const mockAlerts: ProcessEvent[] = [ action: EventAction.end, category: 'process', kind: EventKind.signal, + id: '6', }, host: { architecture: 'x86_64', @@ -1264,6 +1270,7 @@ export const childProcessMock: Process = { getAlerts: () => [], updateAlertsStatus: (_) => undefined, hasExec: () => false, + isVerbose: () => true, getOutput: () => '', getDetails: () => ({ @@ -1272,6 +1279,7 @@ export const childProcessMock: Process = { kind: EventKind.event, category: 'process', action: EventAction.exec, + id: '1', }, host: { architecture: 'x86_64', @@ -1326,6 +1334,7 @@ export const childProcessMock: Process = { isUserEntered: () => false, getMaxAlertLevel: () => null, getEndTime: () => '', + isDescendantOf: () => false, }; export const processMock: Process = { @@ -1346,6 +1355,7 @@ export const processMock: Process = { getAlerts: () => [], updateAlertsStatus: (_) => undefined, hasExec: () => false, + isVerbose: () => true, getOutput: () => '', getDetails: () => ({ @@ -1354,6 +1364,7 @@ export const processMock: Process = { kind: EventKind.event, category: 'process', action: EventAction.exec, + id: '2', }, host: { architecture: 'x86_64', @@ -1390,6 +1401,14 @@ export const processMock: Process = { working_directory: '/home/vagrant', start: '2021-11-23T15:25:04.210Z', pid: 1, + user: { + id: '1000', + name: 'vagrant', + }, + group: { + id: '1000', + name: 'vagrant', + }, parent: { pid: 2442, user: { @@ -1499,6 +1518,7 @@ export const processMock: Process = { isUserEntered: () => false, getMaxAlertLevel: () => null, getEndTime: () => '', + isDescendantOf: () => false, }; export const sessionViewBasicProcessMock: Process = { @@ -1544,7 +1564,9 @@ export const mockProcessMap = mockEvents.reduce( getDetails: () => event, isUserEntered: () => false, getMaxAlertLevel: () => null, + isVerbose: () => true, getEndTime: () => '', + isDescendantOf: () => false, }; return processMap; }, diff --git a/x-pack/plugins/session_view/common/types/process_tree/index.ts b/x-pack/plugins/session_view/common/types/process_tree/index.ts index c4cb85a81dd0c..11f5aeb2ffac2 100644 --- a/x-pack/plugins/session_view/common/types/process_tree/index.ts +++ b/x-pack/plugins/session_view/common/types/process_tree/index.ts @@ -140,6 +140,7 @@ export interface ProcessEvent { kind?: EventKind; category?: string; action?: EventAction; + id?: string; }; user?: User; group?: Group; @@ -178,7 +179,9 @@ export interface Process { isUserEntered(): boolean; getMaxAlertLevel(): number | null; getChildren(verboseMode: boolean): Process[]; + isVerbose(): boolean; getEndTime(): string; + isDescendantOf(process: Process): boolean; } export type ProcessMap = { diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts index dcea2249ac198..9f7ce962dec3f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.test.ts @@ -61,7 +61,7 @@ describe('process tree hook helpers tests', () => { }); it('searchProcessTree works', () => { - const searchResults = searchProcessTree(mockProcessMap, SEARCH_QUERY); + const searchResults = searchProcessTree(mockProcessMap, SEARCH_QUERY, true); // search returns the process with search query in its event args expect(searchResults[0].id).toBe(SEARCH_RESULT_PROCESS_ID); @@ -76,7 +76,7 @@ describe('process tree hook helpers tests', () => { processMap[SESSION_ENTITY_ID].children = childProcesses; expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeFalsy(); - processMap = autoExpandProcessTree(processMap); + processMap = autoExpandProcessTree(processMap, SEARCH_RESULT_PROCESS_ID); // session leader should have autoExpand to be true expect(processMap[SESSION_ENTITY_ID].autoExpand).toBeTruthy(); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts index 2f383271f498d..ee2670dad47d2 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/helpers.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { sortProcesses } from '../../../common/utils/sort_processes'; import { AlertStatusEventEntityIdMap, EventKind, @@ -137,13 +138,25 @@ export const buildProcessTree = ( // this funtion also returns a list of process results which is used by session_view_search_bar to drive // result navigation UX // FYI: this function mutates properties of models contained in processMap -export const searchProcessTree = (processMap: ProcessMap, searchQuery: string | undefined) => { +export const searchProcessTree = ( + processMap: ProcessMap, + searchQuery: string | undefined, + verboseMode: boolean +) => { const results = []; for (const processId of Object.keys(processMap)) { const process = processMap[processId]; if (searchQuery) { + const details = process.getDetails(); + const entryLeader = details?.process?.entry_leader; + + // if this is the entry leader process OR verbose mode is OFF and is a verbose process, don't match. + if (entryLeader?.entity_id === process.id || (!verboseMode && process.isVerbose())) { + continue; + } + const event = process.getDetails(); const { working_directory: workingDirectory, args } = event.process || {}; @@ -162,19 +175,19 @@ export const searchProcessTree = (processMap: ProcessMap, searchQuery: string | } } - return results; + return results.sort(sortProcesses); }; // Iterate over all processes in processMap, and mark each process (and it's ancestors) for auto expansion if: // a) the process was "user entered" (aka an interactive group leader) -// b) matches the plain text search above +// b) we are jumping to a specific process // Returns the processMap with it's processes autoExpand bool set to true or false // process.autoExpand is read by process_tree_node to determine whether to auto expand it's child processes. export const autoExpandProcessTree = (processMap: ProcessMap, jumpToEntityId?: string) => { for (const processId of Object.keys(processMap)) { const process = processMap[processId]; - if (process.searchMatched || process.isUserEntered() || jumpToEntityId === process.id) { + if (process.isUserEntered() || jumpToEntityId === process.id || process.hasAlerts()) { let { parent } = process; const parentIdSet = new Set(); diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index 1725abdcdb5b9..7054bdade9546 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -31,6 +31,7 @@ interface UseProcessTreeDeps { alerts: ProcessEvent[]; searchQuery?: string; updatedAlertsStatus: AlertStatusEventEntityIdMap; + verboseMode: boolean; jumpToEntityId?: string; } @@ -54,10 +55,16 @@ export class ProcessImpl implements Process { this.searchMatched = null; } - addEvent(event: ProcessEvent) { + addEvent(newEvent: ProcessEvent) { // rather than push new events on the array, we return a new one // this helps the below memoizeOne functions to behave correctly. - this.events = this.events.concat(event); + const exists = this.events.find((event) => { + return event.event?.id === newEvent.event?.id; + }); + + if (!exists) { + this.events = this.events.concat(newEvent); + } } addAlert(alert: ProcessEvent) { @@ -66,7 +73,6 @@ export class ProcessImpl implements Process { clearSearch() { this.searchMatched = null; - this.autoExpand = false; } getChildren(verboseMode: boolean) { @@ -74,28 +80,22 @@ export class ProcessImpl implements Process { // if there are orphans, we just render them inline with the other child processes (currently only session leader does this) if (this.orphans.length) { - children = [...children, ...this.orphans].sort(sortProcesses); + children = [...children, ...this.orphans]; } // When verboseMode is false, we filter out noise via a few techniques. // This option is driven by the "verbose mode" toggle in SessionView/index.tsx if (!verboseMode) { - return children.filter((child) => { + children = children.filter((child) => { if (child.events.length === 0) { return false; } - const { group_leader: groupLeader, session_leader: sessionLeader } = - child.getDetails().process ?? {}; - - // search matches or processes with alerts will never be filtered out - if (child.autoExpand || child.searchMatched || child.hasAlerts()) { + // processes with alerts will never be filtered out + if (child.autoExpand || child.hasAlerts()) { return true; } - // Hide processes that have their session leader as their process group leader. - // This accounts for a lot of noise from bash and other shells forking, running auto completion processes and - // other shell startup activities (e.g bashrc .profile etc) - if (!groupLeader || !sessionLeader || groupLeader.pid === sessionLeader.pid) { + if (child.isVerbose()) { return false; } @@ -103,7 +103,27 @@ export class ProcessImpl implements Process { }); } - return children; + return children.sort(sortProcesses); + } + + isVerbose() { + const { + group_leader: groupLeader, + session_leader: sessionLeader, + entry_leader: entryLeader, + } = this.getDetails().process ?? {}; + + // Processes that have their session leader as their process group leader are considered "verbose" + // This accounts for a lot of noise from bash and other shells forking, running auto completion processes and + // other shell startup activities (e.g bashrc .profile etc) + if ( + this.id !== entryLeader?.entity_id && + (!groupLeader || !sessionLeader || groupLeader.pid === sessionLeader.pid) + ) { + return true; + } + + return false; } hasOutput() { @@ -221,6 +241,20 @@ export class ProcessImpl implements Process { // If a process has an 'end' event will always be returned (since it is last and includes details like exit_code and end time) return filtered[filtered.length - 1] ?? {}; }); + + isDescendantOf(process: Process) { + let parent = this.parent; + + while (parent) { + if (parent === process) { + return true; + } + + parent = parent.parent; + } + + return false; + } } export const useProcessTree = ({ @@ -229,6 +263,7 @@ export const useProcessTree = ({ alerts, searchQuery, updatedAlertsStatus, + verboseMode, jumpToEntityId, }: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process @@ -289,8 +324,9 @@ export const useProcessTree = ({ setProcessMap({ ...updatedProcessMap }); setProcessedPages([...processedPages, ...newProcessedPages]); setOrphans(newOrphans); + autoExpandProcessTree(updatedProcessMap, jumpToEntityId); } - }, [data, processMap, orphans, processedPages, sessionEntityId]); + }, [data, processMap, orphans, processedPages, sessionEntityId, jumpToEntityId]); useEffect(() => { // currently we are loading a single page of alerts, with no pagination @@ -303,9 +339,8 @@ export const useProcessTree = ({ }, [processMap, alerts, alertsProcessed]); useEffect(() => { - setSearchResults(searchProcessTree(processMap, searchQuery)); - autoExpandProcessTree(processMap, jumpToEntityId); - }, [searchQuery, processMap, jumpToEntityId]); + setSearchResults(searchProcessTree(processMap, searchQuery, verboseMode)); + }, [searchQuery, processMap, verboseMode]); // set new orphans array on the session leader const sessionLeader = processMap[sessionEntityId]; diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index 4dfb00025f35a..3201d6dfa7e1b 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -14,7 +14,6 @@ import { } from '../../../common/mocks/constants/session_view_process.mock'; import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; -import { ProcessImpl } from './hooks'; import { ProcessTreeDeps, ProcessTree } from './index'; describe('ProcessTree component', () => { @@ -22,7 +21,6 @@ describe('ProcessTree component', () => { let renderResult: ReturnType; let mockedContext: AppContextTestRender; const sessionLeader = mockData[0].events![0]; - const sessionLeaderVerboseTest = mockData[0].events![3]; const props: ProcessTreeDeps = { sessionEntityId: sessionLeader.process!.entity_id!, data: mockData, @@ -104,50 +102,13 @@ describe('ProcessTree component', () => { }); it('When Verbose mode is OFF, it should not show all childrens', () => { - renderResult = mockedContext.render(); + renderResult = mockedContext.render(); expect(renderResult.queryByText('cat')).toBeFalsy(); - - const selectionArea = renderResult.queryAllByTestId('sessionView:processTreeNode'); - const result = selectionArea.map((a) => a?.getAttribute('data-id')); - - expect(result.includes(sessionLeader.process!.entity_id!)).toBeTruthy(); - expect(result.includes(sessionLeaderVerboseTest.process!.entity_id!)).toBeFalsy(); }); it('When Verbose mode is ON, it should show all childrens', () => { - renderResult = mockedContext.render(); + renderResult = mockedContext.render(); expect(renderResult.queryByText('cat')).toBeTruthy(); - - const selectionArea = renderResult.queryAllByTestId('sessionView:processTreeNode'); - const result = selectionArea.map((a) => a?.getAttribute('data-id')); - - expect(result.includes(sessionLeader.process!.entity_id!)).toBeTruthy(); - expect(result.includes(sessionLeaderVerboseTest.process!.entity_id!)).toBeTruthy(); - }); - - it('should insert a DOM element used to highlight a process when selectedProcess is set', () => { - const mockSelectedProcess = new ProcessImpl(mockData[0].events![0].process!.entity_id!); - - renderResult = mockedContext.render( - - ); - - expect( - renderResult - .queryByTestId('sessionView:processTreeSelectionArea') - ?.parentElement?.getAttribute('data-id') - ).toEqual(mockSelectedProcess.id); - - // change the selected process - const mockSelectedProcess2 = new ProcessImpl(mockData[0].events![1].process!.entity_id!); - - renderResult.rerender(); - - expect( - renderResult - .queryByTestId('sessionView:processTreeSelectionArea') - ?.parentElement?.getAttribute('data-id') - ).toEqual(mockSelectedProcess2.id); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 158e5b8faa24a..f2b5fef85002c 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useRef, useEffect, useLayoutEffect, useCallback, useMemo } from 'react'; +import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { ProcessTreeNode } from '../process_tree_node'; import { BackToInvestigatedAlert } from '../back_to_investigated_alert'; @@ -62,8 +62,8 @@ export interface ProcessTreeDeps { // a map for alerts with updated status and process.entity_id updatedAlertsStatus: AlertStatusEventEntityIdMap; onShowAlertDetails: (alertUuid: string) => void; - timeStampOn?: boolean; - verboseModeOn?: boolean; + showTimestamp?: boolean; + verboseMode?: boolean; } export const ProcessTree = ({ @@ -83,8 +83,8 @@ export const ProcessTree = ({ setSearchResults, updatedAlertsStatus, onShowAlertDetails, - timeStampOn, - verboseModeOn, + showTimestamp = true, + verboseMode = false, }: ProcessTreeDeps) => { const [isInvestigatedEventVisible, setIsInvestigatedEventVisible] = useState(true); const [isInvestigatedEventAbove, setIsInvestigatedEventAbove] = useState(false); @@ -96,6 +96,7 @@ export const ProcessTree = ({ alerts, searchQuery, updatedAlertsStatus, + verboseMode, jumpToEntityId, }); @@ -109,7 +110,6 @@ export const ProcessTree = ({ }, [data]); const scrollerRef = useRef(null); - const selectionAreaRef = useRef(null); const onChangeJumpToEventVisibility = useCallback( (isVisible: boolean, isAbove: boolean) => { @@ -143,58 +143,6 @@ export const ProcessTree = ({ }, }); - /** - * highlights a process in the tree - * we do it this way to avoid state changes on potentially thousands of components - */ - const selectProcess = useCallback( - (process: Process) => { - if (!selectionAreaRef?.current || !scrollerRef?.current) { - return; - } - - const selectionAreaEl = selectionAreaRef.current; - selectionAreaEl.style.display = 'block'; - - // TODO: concept of alert level unknown wrt to elastic security - const alertLevel = process.getMaxAlertLevel(); - - if (alertLevel && alertLevel >= 0) { - selectionAreaEl.style.backgroundColor = - alertLevel > 0 ? styles.alertSelected : styles.defaultSelected; - } else { - selectionAreaEl.style.backgroundColor = ''; - } - - // find the DOM element for the command which is selected by id - const processEl = scrollerRef.current.querySelector(`[data-id="${process.id}"]`); - - if (processEl) { - processEl.prepend(selectionAreaEl); - - const { height: elHeight, y: elTop } = processEl.getBoundingClientRect(); - const { y: viewPortElTop, height: viewPortElHeight } = - scrollerRef.current.getBoundingClientRect(); - - const viewPortElBottom = viewPortElTop + viewPortElHeight; - const elBottom = elTop + elHeight; - const isVisible = elBottom >= viewPortElTop && elTop <= viewPortElBottom; - - // jest will die when calling scrollIntoView (perhaps not part of the DOM it executes under) - if (!isVisible && processEl.scrollIntoView) { - processEl.scrollIntoView({ block: 'center' }); - } - } - }, - [styles.alertSelected, styles.defaultSelected] - ); - - useLayoutEffect(() => { - if (selectedProcess) { - selectProcess(selectedProcess); - } - }, [selectedProcess, selectProcess]); - useEffect(() => { if (jumpToEntityId) { const process = processMap[jumpToEntityId]; @@ -206,14 +154,7 @@ export const ProcessTree = ({ } else if (!selectedProcess) { onProcessSelected(sessionLeader); } - }, [ - jumpToEntityId, - processMap, - onProcessSelected, - selectProcess, - selectedProcess, - sessionLeader, - ]); + }, [jumpToEntityId, processMap, onProcessSelected, selectedProcess, sessionLeader]); return ( <> @@ -229,12 +170,12 @@ export const ProcessTree = ({ onProcessSelected={onProcessSelected} jumpToEntityId={jumpToEntityId} investigatedAlertId={investigatedAlertId} - selectedProcessId={selectedProcess?.id} + selectedProcess={selectedProcess} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} onShowAlertDetails={onShowAlertDetails} - timeStampOn={timeStampOn} - verboseModeOn={verboseModeOn} + showTimestamp={showTimestamp} + verboseMode={verboseMode} searchResults={searchResults} loadPreviousButton={ hasPreviousPage ? ( @@ -260,11 +201,6 @@ export const ProcessTree = ({ } /> )} -
{!isInvestigatedEventVisible && ( { } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), onShowAlertDetails: jest.fn(), + showTimestamp: true, + verboseMode: false, }; beforeEach(() => { @@ -123,6 +125,10 @@ describe('ProcessTreeNode component', () => { }, process: { ...processMock.getDetails().process, + user: { + id: '-1', + name: 'root', + }, parent: { ...processMock.getDetails().process!.parent, user: { @@ -176,7 +182,7 @@ describe('ProcessTreeNode component', () => { it('When Timestamp is ON, it shows Timestamp', async () => { // set a mock where Timestamp is turned ON renderResult = mockedContext.render( - + ); expect(renderResult.getByTestId('sessionView:processTreeNodeTimestamp')).toBeTruthy(); @@ -185,7 +191,7 @@ describe('ProcessTreeNode component', () => { it('When Timestamp is OFF, it doesnt show Timestamp', async () => { // set a mock where Timestamp is turned OFF renderResult = mockedContext.render( - + ); expect(renderResult.queryByTestId('sessionView:processTreeNodeTimestamp')).toBeFalsy(); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index 208a0c6fbfd5b..d1b0fded615f6 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -41,9 +41,9 @@ export interface ProcessDeps { onProcessSelected?: (process: Process) => void; jumpToEntityId?: string; investigatedAlertId?: string; - selectedProcessId?: string; - timeStampOn?: boolean; - verboseModeOn?: boolean; + selectedProcess?: Process | null; + showTimestamp: boolean; + verboseMode: boolean; searchResults?: Process[]; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; @@ -62,9 +62,9 @@ export function ProcessTreeNode({ onProcessSelected, jumpToEntityId, investigatedAlertId, - selectedProcessId, - timeStampOn = true, - verboseModeOn = true, + selectedProcess, + showTimestamp, + verboseMode, searchResults, scrollerRef, onChangeJumpToEventVisibility, @@ -78,9 +78,18 @@ export function ProcessTreeNode({ const [alertsExpanded, setAlertsExpanded] = useState(false); const { searchMatched } = process; + // forces nodes to expand if the selected process is a descendant useEffect(() => { - setChildrenExpanded(isSessionLeader || process.autoExpand); - }, [isSessionLeader, process.autoExpand]); + if (!childrenExpanded && selectedProcess) { + if (selectedProcess.isDescendantOf(process)) { + setChildrenExpanded(true); + } + } + }, [selectedProcess, process, childrenExpanded]); + + useEffect(() => { + setChildrenExpanded(process.autoExpand); + }, [process.autoExpand]); const alerts = process.getAlerts(); const hasAlerts = useMemo(() => !!alerts.length, [alerts]); @@ -94,7 +103,7 @@ export function ProcessTreeNode({ ), [hasAlerts, alerts, investigatedAlertId] ); - const isSelected = selectedProcessId === process.id; + const isSelected = selectedProcess?.id === process.id; const styles = useStyles({ depth, hasAlerts, hasInvestigatedAlert, isSelected }); const buttonStyles = useButtonStyles({}); @@ -109,6 +118,12 @@ export function ProcessTreeNode({ shouldAddListener: hasInvestigatedAlert, }); + useEffect(() => { + if (process.id === selectedProcess?.id && nodeRef.current?.scrollIntoView) { + nodeRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }, [selectedProcess, process, nodeRef]); + // Automatically expand alerts list when investigating an alert useEffect(() => { if (hasInvestigatedAlert) { @@ -194,15 +209,14 @@ export function ProcessTreeNode({ // hidden processes. } - return process.getChildren(verboseModeOn); - }, [process, verboseModeOn, searchResults]); + return process.getChildren(verboseMode); + }, [process, verboseMode, searchResults]); if (!processDetails?.process) { return null; } const id = process.id; - const { user } = processDetails; const { args, name, @@ -210,12 +224,13 @@ export function ProcessTreeNode({ parent, working_directory: workingDirectory, start, + user, } = processDetails.process; const shouldRenderChildren = childrenExpanded && children?.length > 0; const childrenTreeDepth = depth + 1; - const showUserEscalation = !isSessionLeader && !!user?.id && user.id !== parent?.user?.id; + const showUserEscalation = !isSessionLeader && !!user?.name && user.name !== parent?.user?.name; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'desktop' : 'gear'; const iconTestSubj = hasExec @@ -248,7 +263,7 @@ export function ProcessTreeNode({ ) : ( - {timeStampOn && ( + {showTimestamp && ( {timeStampsNormal} @@ -273,7 +288,7 @@ export function ProcessTreeNode({ id="xpack.sessionView.execUserChange" defaultMessage="Exec user change: " /> - {user.name} + {user.name} )} {!isSessionLeader && children.length > 0 && ( @@ -293,7 +308,7 @@ export function ProcessTreeNode({ @@ -311,9 +326,9 @@ export function ProcessTreeNode({ onProcessSelected={onProcessSelected} jumpToEntityId={jumpToEntityId} investigatedAlertId={investigatedAlertId} - selectedProcessId={selectedProcessId} - timeStampOn={timeStampOn} - verboseModeOn={verboseModeOn} + selectedProcess={selectedProcess} + showTimestamp={showTimestamp} + verboseMode={verboseMode} searchResults={searchResults} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts index cd514c2087bdc..6503f373240ad 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -53,12 +53,13 @@ export const useStyles = ({ depth, hasAlerts, hasInvestigatedAlert, isSelected } borderColor = colors.danger; } - if (hasInvestigatedAlert) { - bgColor = transparentize(colors.danger, 0.04); - } - if (isSelected) { searchResColor = colors.warning; + bgColor = `${transparentize(colors.primary, 0.1)}!important`; + } + + if (hasInvestigatedAlert) { + bgColor = `${transparentize(colors.danger, 0.04)}!important`; } return { bgColor, borderColor, hoverColor, searchResColor }; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts index 4c713b42a2d7b..67883b12e2bba 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts @@ -75,10 +75,6 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { border: `${border.width.thin} solid ${transparentize(theme.euiColorVis3, 0.48)}`, }; - const userChangedButtonUsername: CSSObject = { - textTransform: 'capitalize', - }; - const buttonSize: CSSObject = { padding: `0px ${euiTheme.size.xs}`, }; @@ -91,7 +87,6 @@ export const useButtonStyles = ({ isExpanded }: ButtonStylesDeps) => { alertButton, alertsCountNumber, userChangedButton, - userChangedButtonUsername, buttonSize, expandedIcon, }; diff --git a/x-pack/plugins/session_view/public/components/session_view/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view/index.test.tsx index e75c09e393911..41865f1adad43 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.test.tsx @@ -84,10 +84,6 @@ describe('SessionView component', () => { await waitForApiCall(); expect(renderResult.getAllByTestId('sessionView:processTreeNode')).toBeTruthy(); - - const selectionArea = renderResult.queryByTestId('sessionView:processTreeSelectionArea'); - - expect(selectionArea?.parentElement?.getAttribute('data-id')).toEqual('test-entity-id'); }); it('should toggle detail panel visibilty when detail button clicked', async () => { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index 1e41878814697..fee4a67c746fc 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { EuiEmptyPrompt, EuiButton, @@ -72,6 +72,11 @@ export const SessionView = ({ const styles = useStyles({ height, isFullScreen }); + // to give an indication to the user that there may be more search results if they turn on verbose mode. + const showVerboseSearchTooltip = useMemo(() => { + return !!(!displayOptions?.verboseMode && searchQuery && searchResults?.length === 0); + }, [displayOptions?.verboseMode, searchResults, searchQuery]); + const onProcessSelected = useCallback((process: Process | null) => { setSelectedProcess(process); }, []); @@ -194,6 +199,7 @@ export const SessionView = ({ @@ -273,8 +279,8 @@ export const SessionView = ({ setSearchResults={setSearchResults} updatedAlertsStatus={updatedAlertsStatus} onShowAlertDetails={onShowAlertDetails} - timeStampOn={displayOptions?.timestamp} - verboseModeOn={displayOptions?.verboseMode} + showTimestamp={displayOptions?.timestamp} + verboseMode={displayOptions?.verboseMode} /> )} diff --git a/x-pack/plugins/session_view/public/components/session_view_display_options/index.tsx b/x-pack/plugins/session_view/public/components/session_view_display_options/index.tsx index 7970a3b523e8b..ab381ca757f9f 100644 --- a/x-pack/plugins/session_view/public/components/session_view_display_options/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_display_options/index.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiToolTip, EuiPopover, EuiSelectable, EuiPopoverTitle, @@ -22,17 +23,50 @@ import { useStyles } from './styles'; const TIMESTAMP_OPTION_KEY = 'Timestamp'; const VERBOSE_MODE_OPTION_KEY = 'Verbose mode'; +const TOOLTIP_SHOW_DELAY = 3000; +const TOOLTIP_HIDE_DELAY = 5000; + +const VERBOSE_TOOLTIP_TITLE = i18n.translate( + 'xpack.sessionView.sessionViewToggle.sessionViewVerboseTipTitle', + { + defaultMessage: 'Some results may be hidden', + } +); + +const VERBOSE_TOOLTIP_CONTENT = i18n.translate( + 'xpack.sessionView.sessionViewToggle.sessionViewVerboseTipContent', + { + defaultMessage: 'For a complete set of results, turn on Verbose mode.', + } +); export const SessionViewDisplayOptions = ({ onChange, displayOptions, + showVerboseSearchTooltip, }: { onChange: (vars: DisplayOptionsState) => void; displayOptions: DisplayOptionsState; + showVerboseSearchTooltip: boolean; }) => { const [isOptionDropdownOpen, setOptionDropdownOpen] = useState(false); - const styles = useStyles(); + const tooltipRef = useRef(null); + + useEffect(() => { + if (tooltipRef.current) { + setTimeout(() => { + if (tooltipRef.current) { + (tooltipRef.current as EuiToolTip).onFocus(); + setTimeout(() => { + if (tooltipRef.current) { + (tooltipRef.current as EuiToolTip).onBlur(); + } + }, TOOLTIP_HIDE_DELAY); + } + }, TOOLTIP_SHOW_DELAY); + } + }, [showVerboseSearchTooltip]); const optionsList: EuiSelectableOption[] = useMemo( () => [ @@ -106,27 +140,38 @@ export const SessionViewDisplayOptions = ({ onChange(updateOptionState); }; - return ( - <> - - - {(list) => ( -
- - - - {list} -
- )} -
-
- + const popOver = ( + + + {(list) => ( +
+ + + + {list} +
+ )} +
+
+ ); + + return !isOptionDropdownOpen && showVerboseSearchTooltip ? ( + + {popOver} + + ) : ( + popOver ); }; diff --git a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx index 05154fca40769..9c24f6b94199c 100644 --- a/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_search_bar/index.tsx @@ -24,6 +24,10 @@ const translatePlaceholder = { }), }; +const NO_RESULTS = i18n.translate('xpack.sessionView.searchBar.searchBarNoResults', { + defaultMessage: 'No results', +}); + /** * The main wrapper component for the session view. */ @@ -33,7 +37,8 @@ export const SessionViewSearchBar = ({ onProcessSelected, searchResults, }: SessionViewSearchBarDeps) => { - const showPagination = !!searchResults?.length; + const showPagination = !!searchQuery && searchResults?.length !== 0; + const noResults = !!searchQuery && searchResults?.length === 0; const styles = useStyles({ hasSearchResults: showPagination }); @@ -62,11 +67,12 @@ export const SessionViewSearchBar = ({ return (
+ {noResults && {NO_RESULTS}} {showPagination && ( { position: 'absolute', top: euiTheme.size.s, right: euiTheme.size.xxl, + 'button[data-test-subj="pagination-button-last"]': { + display: 'none', + }, + 'button[data-test-subj="pagination-button-first"]': { + display: 'none', + }, + }; + + const noResults: CSSObject = { + position: 'absolute', + color: euiTheme.colors.subdued, + top: euiTheme.size.m, + right: euiTheme.size.xxl, }; const searchBarWithResult: CSSObject = { @@ -33,6 +46,7 @@ export const useStyles = ({ hasSearchResults }: StylesDeps) => { return { pagination, searchBarWithResult, + noResults, }; }, [euiTheme, hasSearchResults]);