diff --git a/x-pack/plugins/session_view/common/test/mock_data.ts b/x-pack/plugins/session_view/common/test/mock_data.ts index 8426aa26023122..df10d0d85bb3ba 100644 --- a/x-pack/plugins/session_view/common/test/mock_data.ts +++ b/x-pack/plugins/session_view/common/test/mock_data.ts @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import uuid from 'uuid'; -import { EventAction, ProcessEvent } from '../../public/hooks/use_process_tree'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { EventAction, ProcessEvent, EventKind } from '../../public/hooks/use_process_tree'; export const getStart = () => { return [ { - '@timestamp': 'Thu Oct 14 2021 12: 06: 48 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 06: 48 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', @@ -110,7 +110,7 @@ export const getEvent = () => { return [ { - '@timestamp': 'Thu Oct 14 2021 12: 06: 52 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 06: 52 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', @@ -187,7 +187,7 @@ export const getEvent = () => { export const getEnd = () => { return [ { - '@timestamp': 'Thu Oct 14 2021 12: 07: 52 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 52 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', @@ -259,7 +259,7 @@ export const getEnd = () => { }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', @@ -333,7 +333,7 @@ export const getEnd = () => { }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', @@ -407,11 +407,11 @@ export const getEnd = () => { }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 08: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 08: 56 GMT-0700 (Pacific Daylight Time)'), event: { kind: 'event', category: 'process', - action: EventAction.end, + action: EventAction.exit, }, process: { args: ['df', 'nested'], @@ -487,9 +487,9 @@ export const getEnd = () => { export const mockData: ProcessEvent[] = [ { - '@timestamp': 'Thu Oct 14 2021 12: 06: 48 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 06: 48 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', action: EventAction.exec, }, @@ -561,9 +561,9 @@ export const mockData: ProcessEvent[] = [ }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 06: 52 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 06: 52 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', action: EventAction.exec, }, @@ -633,9 +633,9 @@ export const mockData: ProcessEvent[] = [ }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 07: 52 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 52 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', action: EventAction.exec, }, @@ -705,9 +705,9 @@ export const mockData: ProcessEvent[] = [ }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', action: EventAction.fork, }, @@ -779,9 +779,9 @@ export const mockData: ProcessEvent[] = [ }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 07: 56 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', action: EventAction.exec, }, @@ -853,11 +853,11 @@ export const mockData: ProcessEvent[] = [ }, }, { - '@timestamp': 'Thu Oct 14 2021 12: 08: 56 GMT-0700 (Pacific Daylight Time)', + '@timestamp': new Date('Thu Oct 14 2021 12: 08: 56 GMT-0700 (Pacific Daylight Time)'), event: { - kind: 'event', + kind: EventKind.event, category: 'process', - action: EventAction.end, + action: EventAction.exit, }, process: { args: ['df', 'nested'], diff --git a/x-pack/plugins/session_view/public/components/ProcessTree/styles.ts b/x-pack/plugins/session_view/public/components/ProcessTree/styles.ts index 2a8a76d3b5ce01..cf98e161077d9d 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTree/styles.ts +++ b/x-pack/plugins/session_view/public/components/ProcessTree/styles.ts @@ -13,16 +13,12 @@ export const useStyles = () => { const cached = useMemo(() => { const defaultSelectionColor = euiTheme.colors.accent; - const padding = euiTheme.size.s; const scroller = ` font-family: ${euiTheme.font.familyCode}; overflow: auto; height: 100%; background-color: ${euiTheme.colors.lightestShade}; - padding-top: ${padding}; - padding-left: ${padding}; - padding-bottom: ${padding}; display: flex; flex-direction: column; `; diff --git a/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx b/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx index a0435a3e8d7b62..097dcd58b9670c 100644 --- a/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx +++ b/x-pack/plugins/session_view/public/components/ProcessTreeNode/index.tsx @@ -49,20 +49,19 @@ export function ProcessTreeNode({ const processDetails = useMemo(() => { return process.getDetails(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [process.events.length]); const hasExec = useMemo(() => { return process.hasExec(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [process.events.length]); const alerts = useMemo(() => { return process.getAlerts(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [process.events.length]); - if (!processDetails) { - return null; - } - const styles = useStyles({ depth, hasAlerts: !!alerts.length }); useLayoutEffect(() => { @@ -74,9 +73,14 @@ export function ProcessTreeNode({ return `${match}`; }); + // eslint-disable-next-line no-unsanitized/property textRef.current.innerHTML = html; } - }, [searchMatched]); + }, [searchMatched, styles.searchHighlight]); + + if (!processDetails) { + return null; + } const { interactive } = processDetails.process; diff --git a/x-pack/plugins/session_view/public/components/SessionView/index.tsx b/x-pack/plugins/session_view/public/components/SessionView/index.tsx index 949a2dc7f0afae..3a9ee2a0dfc2a6 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/index.tsx +++ b/x-pack/plugins/session_view/public/components/SessionView/index.tsx @@ -6,13 +6,22 @@ */ import React, { useState, useEffect } from 'react'; import { useQuery } from 'react-query'; -import { EuiSearchBar, EuiSearchBarOnChangeArgs, EuiEmptyPrompt } from '@elastic/eui'; +import { + EuiSearchBar, + EuiSearchBarOnChangeArgs, + EuiEmptyPrompt, + EuiButton, + EuiSplitPanel, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { CoreStart } from '../../../../../../src/core/public'; import { SectionLoading } from '../../shared_imports'; import { ProcessTree } from '../ProcessTree'; import { Process, ProcessEvent } from '../../hooks/use_process_tree'; +import { SessionViewDetailPanel } from '../SessionViewDetailPanel'; import { useStyles } from './styles'; import { PROCESS_EVENTS_ROUTE } from '../../../common/constants'; @@ -44,6 +53,8 @@ interface ProcessEventResults { export const SessionView = ({ sessionEntityId, height }: SessionViewDeps) => { const [searchQuery, setSearchQuery] = useState(''); const [data, setData] = useState([]); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [isDetailMounted, setIsDetailMounted] = useState(false); const [selectedProcess, setSelectedProcess] = useState(null); const { http } = useKibana().services; @@ -91,10 +102,10 @@ export const SessionView = ({ sessionEntityId, height }: SessionViewDeps) => { return; } - const events: ProcessEvent[] = getData.events.hits.map( + const events: ProcessEvent[] = (getData.events?.hits || []).map( (event: any) => event._source as ProcessEvent ); - const alerts: ProcessEvent[] = getData.alerts.hits.map((event: any) => { + const alerts: ProcessEvent[] = (getData.alerts?.hits || []).map((event: any) => { return event._source as ProcessEvent; }); const all: ProcessEvent[] = events.concat(alerts).sort(sortEvents); @@ -146,14 +157,57 @@ export const SessionView = ({ sessionEntityId, height }: SessionViewDeps) => { } }; + const renderSessionViewDetailPanel = () => { + if (selectedProcess && isDetailOpen) { + return ( + + ); + } + }; + + const toggleDetailPanel = () => { + setIsDetailMounted(!isDetailMounted); + if (!isDetailOpen) { + setIsDetailOpen(true); + } + }; + if (!(isLoading || isError || data.length)) { return renderNoData(); } return ( <> - - {renderProcessTree()} + + + + + + + + + + + + + {renderProcessTree()} + + {renderSessionViewDetailPanel()} + ); }; diff --git a/x-pack/plugins/session_view/public/components/SessionView/styles.ts b/x-pack/plugins/session_view/public/components/SessionView/styles.ts index 063d4f81768689..5212a4e4812829 100644 --- a/x-pack/plugins/session_view/public/components/SessionView/styles.ts +++ b/x-pack/plugins/session_view/public/components/SessionView/styles.ts @@ -6,27 +6,83 @@ */ import { useMemo } from 'react'; -// import { useEuiTheme } from '@elastic/eui'; -import { CSSObject } from '@emotion/react'; +import { useEuiTheme } from '@elastic/eui'; +import { keyframes, CSSObject } from '@emotion/react'; interface StylesDeps { height: number | undefined; } export const useStyles = ({ height = 500 }: StylesDeps) => { - // const { euiTheme } = useEuiTheme(); + const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { // const { colors, border, font, size } = euiTheme; + const padding = euiTheme.size.s; const processTree: CSSObject = { - height: height + 'px', + height: `${height}px`, }; + const outerPanel: CSSObject = { + fontFamily: euiTheme.font.familyCode, + position: 'relative', + overflowX: 'hidden', + }; + + const treePanel: CSSObject = { + paddingTop: padding, + paddingLeft: padding, + }; + + const slideIn = keyframes({ + to: { + right: '0', + }, + }); + + const slideOut = keyframes({ + from: { + right: '0', + }, + to: { + right: '-100%', + }, + }); + + const detailPanel: CSSObject = { + width: '424px', + height: `${height}px`, + overflowY: 'auto', + position: 'absolute', + top: '8px', + right: '-100%', + }; + + const detailPanelIn: Array = [ + slideIn.styles, + { + ...detailPanel, + animation: `${slideIn.name} 200ms ease forwards`, + }, + ]; + + const detailPanelOut: Array = [ + slideOut.styles, + { + ...detailPanel, + animation: `${slideOut.name} 150ms ease`, + }, + ]; + return { processTree, + outerPanel, + treePanel, + detailPanelIn, + detailPanelOut, }; - }, []); + }, [height, euiTheme]); return cached; }; diff --git a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx new file mode 100644 index 00000000000000..9e3ed1a1493db3 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/index.tsx @@ -0,0 +1,133 @@ +/* + * 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, { useState, useEffect, ReactNode } from 'react'; +import MonacoEditor from 'react-monaco-editor'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiSplitPanel, EuiTitle, EuiTabs, EuiTab } from '@elastic/eui'; +import { Process } from '../../hooks/use_process_tree'; +import { useStyles } from './styles'; + +interface SessionViewDetailPanelDeps { + isDetailMounted: boolean; + height?: number; + selectedProcess: Process | null; + setIsDetailOpen(isDetailOpen: boolean): void; + session?: any; +} + +interface ProcessEventTabData { + id: string | number; + name: string; + content: ReactNode; +} + +/** + * Detail panel in the session view. + */ +export const SessionViewDetailPanel = ({ + isDetailMounted, + height, + selectedProcess, + setIsDetailOpen, +}: SessionViewDetailPanelDeps) => { + const [selectedDetailTab, setSelectedDetailTab] = useState(''); + const [processEventsTabs, setProcessEventsTabs] = useState([]); + + const styles = useStyles({ height }); + + useEffect(() => { + const selectedProcessEvents = (selectedProcess?.events || []).map((processEvent, idx) => ({ + id: `${processEvent?.event.action}-${idx + 1}` || `event-${idx + 1}`, + name: `${processEvent?.event.action}-${idx + 1}` || `event-${idx + 1}`, + content: ( +
+ + +
+ ), + })); + + setProcessEventsTabs(selectedProcessEvents); + setSelectedDetailTab(selectedProcessEvents?.[0]?.id || ''); + }, [selectedProcess]); + + const handleAnimationEnd = () => { + if (!isDetailMounted) { + setIsDetailOpen(false); + } + }; + + const renderSelectedProcessEvents = () => { + if (selectedProcess) { + return ( +
+ + + + + + + + {processEventsTabs.map((tab, idx) => ( + setSelectedDetailTab(tab.id)} + isSelected={tab.id === selectedDetailTab} + > + {tab.name} + + ))} + + + {processEventsTabs.find((tab) => tab.id === selectedDetailTab)?.content} +
+ ); + } + }; + + return ( + + {renderSelectedProcessEvents()} + + + + + + {/* Add session detail */} + + + + + + + {/* Add server detail */} + + + + + + + {/* Add alert detail conditionally */} + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/styles.ts b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/styles.ts new file mode 100644 index 00000000000000..bb9959cffc0a5b --- /dev/null +++ b/x-pack/plugins/session_view/public/components/SessionViewDetailPanel/styles.ts @@ -0,0 +1,64 @@ +/* + * 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 { useMemo } from 'react'; +import { keyframes, CSSObject } from '@emotion/react'; + +interface StylesDeps { + height: number | undefined; +} + +export const useStyles = ({ height = 500 }: StylesDeps) => { + const cached = useMemo(() => { + const slideIn = keyframes({ + to: { + right: '0', + }, + }); + + const slideOut = keyframes({ + from: { + right: '0', + }, + to: { + right: '-100%', + }, + }); + + const detailPanel: CSSObject = { + width: '424px', + height: `${height}px`, + overflowY: 'auto', + position: 'absolute', + top: '8px', + right: '-100%', + }; + + const detailPanelIn: Array = [ + slideIn.styles, + { + ...detailPanel, + animation: `${slideIn.name} 200ms ease forwards`, + }, + ]; + + const detailPanelOut: Array = [ + slideOut.styles, + { + ...detailPanel, + animation: `${slideOut.name} 150ms ease`, + }, + ]; + + return { + detailPanelIn, + detailPanelOut, + }; + }, [height]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/hooks/use_process_tree.ts b/x-pack/plugins/session_view/public/hooks/use_process_tree.ts index 4414f4728d8657..75e39f17078cdd 100644 --- a/x-pack/plugins/session_view/public/hooks/use_process_tree.ts +++ b/x-pack/plugins/session_view/public/hooks/use_process_tree.ts @@ -47,7 +47,7 @@ interface ProcessFields { exit_code?: number; } -interface ProcessSelf extends ProcessFields { +export interface ProcessSelf extends ProcessFields { parent: ProcessFields; session: ProcessFields; entry: ProcessFields; @@ -165,6 +165,7 @@ class ProcessImpl implements Process { ); if (execsForks.length === 0) { + // eslint-disable-next-line no-debugger debugger; } @@ -183,7 +184,7 @@ class ProcessImpl implements Process { isUserEntered() { const event = this.getDetails(); - const { interactive, pgid, parent } = event.process; + const { interactive, pgid, parent } = event?.process || {}; return interactive && pgid !== parent.pgid; } @@ -298,7 +299,7 @@ export const useProcessTree = ({ if (process.searchMatched || process.isUserEntered()) { let { parent } = process; - while (parent) { + while (parent && parent.id !== parent.parent?.id) { parent.autoExpand = true; parent = parent.parent; }