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 df16f26730d98..513db37aed6af 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 @@ -1264,6 +1264,7 @@ export const childProcessMock: Process = { getAlerts: () => [], updateAlertsStatus: (_) => undefined, hasExec: () => false, + isVerbose: () => true, getOutput: () => '', getDetails: () => ({ @@ -1345,6 +1346,7 @@ export const processMock: Process = { getAlerts: () => [], updateAlertsStatus: (_) => undefined, hasExec: () => false, + isVerbose: () => true, getOutput: () => '', getDetails: () => ({ @@ -1540,6 +1542,7 @@ export const mockProcessMap = mockEvents.reduce( getDetails: () => event, isUserEntered: () => false, getMaxAlertLevel: () => null, + isVerbose: () => true, }; 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 2020884b141a6..7c628146a7b06 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 @@ -173,6 +173,7 @@ export interface Process { isUserEntered(): boolean; getMaxAlertLevel(): number | null; getChildren(verboseMode: boolean): Process[]; + isVerbose(): 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..ce97a8435ab32 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); 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 064f11c10a736..c195096473b1c 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 @@ -137,13 +137,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 || {}; 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 009b839f6d55c..43dff7703effd 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; } export class ProcessImpl implements Process { @@ -83,18 +84,12 @@ export class ProcessImpl implements Process { 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; } @@ -105,6 +100,26 @@ export class ProcessImpl implements Process { return children; } + 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() { return !!this.findEventByAction(this.events, EventAction.output); } @@ -231,6 +246,7 @@ export const useProcessTree = ({ alerts, searchQuery, updatedAlertsStatus, + verboseMode, }: UseProcessTreeDeps) => { // initialize map, as well as a placeholder for session leader process // we add a fake session leader event, sourced from wide event data. @@ -304,9 +320,9 @@ export const useProcessTree = ({ }, [processMap, alerts, alertsProcessed]); useEffect(() => { - setSearchResults(searchProcessTree(processMap, searchQuery)); + setSearchResults(searchProcessTree(processMap, searchQuery, verboseMode)); autoExpandProcessTree(processMap); - }, [searchQuery, processMap]); + }, [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..4fe312331d33d 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 @@ -104,7 +104,7 @@ 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'); @@ -115,7 +115,7 @@ describe('ProcessTree component', () => { }); 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'); 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 b8c9bbc77c9b1..5b4043a4e2b56 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 @@ -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, }); const eventsRemaining = useMemo(() => { @@ -232,8 +233,8 @@ export const ProcessTree = ({ scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} onShowAlertDetails={onShowAlertDetails} - timeStampOn={timeStampOn} - verboseModeOn={verboseModeOn} + showTimestamp={showTimestamp} + verboseMode={verboseMode} searchResults={searchResults} loadPreviousButton={ hasPreviousPage ? ( diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx index b4a2472f11dab..a5bca881b06d2 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/buttons.tsx @@ -12,9 +12,11 @@ import { useButtonStyles } from './use_button_styles'; export const ChildrenProcessesButton = ({ onToggle, isExpanded, + disabled, }: { onToggle: () => void; isExpanded: boolean; + disabled: boolean; }) => { const { button, buttonArrow, expandedIcon } = useButtonStyles({ isExpanded }); @@ -24,6 +26,7 @@ export const ChildrenProcessesButton = ({ css={button} onClick={onToggle} data-test-subj="sessionView:processTreeNodeChildProcessesButton" + disabled={disabled} > diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 81b3c184baaa4..c013257d15cae 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -39,6 +39,8 @@ describe('ProcessTreeNode component', () => { } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), onShowAlertDetails: jest.fn(), + showTimestamp: true, + verboseMode: false, }; beforeEach(() => { @@ -196,7 +198,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(); @@ -205,7 +207,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 b835d1d0e035d..1234f630a4434 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 @@ -42,8 +42,8 @@ export interface ProcessDeps { jumpToEntityId?: string; investigatedAlertId?: string; selectedProcessId?: string; - timeStampOn?: boolean; - verboseModeOn?: boolean; + showTimestamp: boolean; + verboseMode: boolean; searchResults?: Process[]; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; @@ -63,8 +63,8 @@ export function ProcessTreeNode({ jumpToEntityId, investigatedAlertId, selectedProcessId, - timeStampOn = true, - verboseModeOn = true, + showTimestamp, + verboseMode, searchResults, scrollerRef, onChangeJumpToEventVisibility, @@ -74,14 +74,10 @@ export function ProcessTreeNode({ }: ProcessDeps) { const textRef = useRef(null); - const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader || process.autoExpand); + const [childrenExpanded, setChildrenExpanded] = useState(isSessionLeader); const [alertsExpanded, setAlertsExpanded] = useState(false); const { searchMatched } = process; - useEffect(() => { - setChildrenExpanded(isSessionLeader || process.autoExpand); - }, [isSessionLeader, process.autoExpand]); - const alerts = process.getAlerts(); const hasAlerts = useMemo(() => !!alerts.length, [alerts]); const hasInvestigatedAlert = useMemo( @@ -194,8 +190,8 @@ export function ProcessTreeNode({ // hidden processes. } - return process.getChildren(verboseModeOn); - }, [process, verboseModeOn, searchResults]); + return process.getChildren(verboseMode); + }, [process, verboseMode, searchResults]); if (!processDetails?.process) { return null; @@ -213,7 +209,7 @@ export function ProcessTreeNode({ start, } = processDetails.process; - const shouldRenderChildren = childrenExpanded && children?.length > 0; + const shouldRenderChildren = (process.autoExpand || childrenExpanded) && children?.length > 0; const childrenTreeDepth = depth + 1; const showUserEscalation = !isSessionLeader && !!user?.id && user.id !== parent?.user?.id; @@ -263,7 +259,7 @@ export function ProcessTreeNode({ )} - {timeStampOn && ( + {showTimestamp && ( {timeStampsNormal} @@ -284,7 +280,11 @@ export function ProcessTreeNode({ )} {!isSessionLeader && children.length > 0 && ( - + )} {alerts.length > 0 && ( { + 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]);