mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Terminal Output] Truncated process output msg (#143304)
* xterm updated to v5. initial work done for max kb warning msg * max bytes exceeded msg implemented, along with authz gated link to security policies page. * marker indication on max bytes exceeded fixed * tests written * tests written * fixed a bug where an active search would prevent normal playback/seeking * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * updates per review comments * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fixed text * fixed ts error * fix to scroll hack Co-authored-by: Karl Godard <karlgodard@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
73d2b0da09
commit
97a68d8558
21 changed files with 244 additions and 74 deletions
|
@ -666,7 +666,7 @@
|
|||
"vinyl": "^2.2.0",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"xml2js": "^0.4.22",
|
||||
"xterm": "^4.18.0",
|
||||
"xterm": "^5.0.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
|
|
|
@ -162,6 +162,7 @@ describe('useSessionView with active timeline and a session id and graph event i
|
|||
height: 1000,
|
||||
sessionEntityId: 'test',
|
||||
loadAlertDetails: mockDetails,
|
||||
canAccessEndpointManagement: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
useGlobalFullScreen,
|
||||
} from '../../../../common/containers/use_full_screen';
|
||||
import { detectionsTimelineIds } from '../../../containers/helpers';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import { timelineActions, timelineSelectors } from '../../../store/timeline';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
|
@ -266,6 +267,7 @@ export const useSessionView = ({
|
|||
}, [scopeId]);
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const { timelineFullScreen } = useTimelineFullScreen();
|
||||
const { canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults;
|
||||
const { sessionViewConfig, activeTab } = useDeepEqualSelector((state) => ({
|
||||
|
@ -310,9 +312,17 @@ export const useSessionView = ({
|
|||
loadAlertDetails: openDetailsPanel,
|
||||
isFullScreen: fullScreen,
|
||||
height: heightMinusSearchBar,
|
||||
canAccessEndpointManagement,
|
||||
})
|
||||
: null;
|
||||
}, [fullScreen, openDetailsPanel, sessionView, sessionViewConfig, height]);
|
||||
}, [
|
||||
height,
|
||||
sessionViewConfig,
|
||||
sessionView,
|
||||
openDetailsPanel,
|
||||
fullScreen,
|
||||
canAccessEndpointManagement,
|
||||
]);
|
||||
|
||||
return {
|
||||
openDetailsPanel,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const SESSION_VIEW_APP_ID = 'sessionView';
|
||||
|
||||
// routes
|
||||
export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events';
|
||||
export const ALERTS_ROUTE = '/internal/session_view/alerts';
|
||||
|
@ -12,6 +14,9 @@ export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status';
|
|||
export const IO_EVENTS_ROUTE = '/internal/session_view/io_events';
|
||||
export const GET_TOTAL_IO_BYTES_ROUTE = '/internal/session_view/get_total_io_bytes';
|
||||
|
||||
export const SECURITY_APP_ID = 'security';
|
||||
export const POLICIES_PAGE_PATH = '/administration/policy';
|
||||
|
||||
// index patterns
|
||||
export const PROCESS_EVENTS_INDEX = '*:logs-endpoint.events.process*,logs-endpoint.events.process*'; // match on both cross cluster and local indices
|
||||
export const PREVIEW_ALERTS_INDEX = '.preview.alerts-security.alerts-default';
|
||||
|
|
|
@ -282,6 +282,7 @@ export const sessionViewIOEventsMock: ProcessEventResults = {
|
|||
total_bytes_captured: 1024,
|
||||
total_bytes_skipped: 0,
|
||||
bytes_skipped: [],
|
||||
max_bytes_per_process_exceeded: true,
|
||||
text: '\u001b[38;5;130m 84 \n 85 \u001b[mif [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 86 \u001b[m echo "WARNING: Not waiting for connections to close gracefully"\n\u001b[38;5;130m 87 \u001b[m echo "Press any key to continue... wsrep_reject_queries will be set to \'ALL_KILL\'"\n\u001b[38;5;130m 88 \u001b[m read a\n\u001b[38;5;130m 89 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL_KILL\'"\n\u001b[38;5;130m 90 \u001b[melse\n\u001b[38;5;130m 91 \u001b[m # Stop accepting queries in mariadb, do not kill opened connections\n\u001b[38;5;130m 92 \u001b[m mysql -h127.0.0.1 -P3306 -uroot -e "set global wsrep_reject_queries=\'ALL\'"\n\u001b[38;5;130m 93 \u001b[mfi\n\u001b[38;5;130m 94 \n 95 \u001b[mexit_code=$?\n\u001b[38;5;130m 96 \u001b[mif [[ $exit_code != 0 ]]; then\n\u001b[38;5;130m 97 \u001b[m >&2 echo "Failed to set the reject of queries on Mysql node, exiting."\n\u001b[38;5;130m 98 \u001b[m exit $exit_code\n\u001b[38;5;130m 99 \u001b[melse\n\u001b[38;5;130m 100 \u001b[m echo "Successfully stopped accepting queries."\n\u001b[38;5;130m 101 \u001b[m if [[ $KILL_IMMEDIATELY == 1 ]]; then\n\u001b[38;5;130m 102 \u001b[m\u001b[8Cexit\n\u001b[38;5;130m 103 \u001b[m fi\n\u001b[38;5;130m 104 \u001b[mfi\n\u001b[38;5;130m 105 \n 106 \u001b[mif [[ $GRACE_PERIOD == -1 ]]; then\n\u001b[38;5;130m 107 \u001b[m set_number_grace_seconds\n\u001b[38;5;130m 108 \u001b[mfi\n\u001b[38;5;130m 109 \n 110 \u001b[mwait_for_connections\n\u001b[38;5;130m 111 \u001b[mif [[ $DB_CONNECTIONS_NUMBER != 0 ]]; then\n\u001b[38;5;130m 112 \u001b[m get_number_db_connections\n\u001b[38;5;130m 113 \u001b[m >&2 echo "ERROR: There are still $DB_CONNECTIONS_NUMBER opened DB connections."\n\u001b[38;5;130m 114 \u001b[m exit 3\n\u001b[38;5;130m 115 \u001b[mfi\b\b\u001b[?25h\u001b[?25l\nType :qa! and press <Enter> to abandon all changes and exit Vim\u0007\u001b[58;9H\u001b[?25h\u0007\u001b[?25l\u001b[59;1H\u001b[K\u001b[59;1H:\u001b[?2004h\u001b[?25hqa!\r\u001b[?25l\u001b[?2004l\u001b[59;1H\u001b[K\u001b[59;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[?1049l\u001b[23;0;0t,\u001bkroot@staging-host:~\u001b\\\n',
|
||||
},
|
||||
tty: {
|
||||
|
|
|
@ -72,6 +72,7 @@ export interface IOLine {
|
|||
export interface ProcessStartMarker {
|
||||
event: ProcessEvent;
|
||||
line: number;
|
||||
maxBytesExceeded?: boolean;
|
||||
}
|
||||
|
||||
export interface IOFields {
|
||||
|
|
|
@ -53,6 +53,7 @@ export const SessionView = ({
|
|||
jumpToCursor,
|
||||
investigatedAlertId,
|
||||
loadAlertDetails,
|
||||
canAccessEndpointManagement,
|
||||
}: SessionViewDeps) => {
|
||||
// don't engage jumpTo if jumping to session leader.
|
||||
if (jumpToEntityId === sessionEntityId) {
|
||||
|
@ -422,6 +423,7 @@ export const SessionView = ({
|
|||
isFullscreen={isFullScreen}
|
||||
onJumpToEvent={onJumpToEvent}
|
||||
autoSeekToEntityId={currentJumpToOutputEntityId}
|
||||
canAccessEndpointManagement={canAccessEndpointManagement}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { Teletype } from '../../../common/types/process_tree';
|
||||
import {
|
||||
PROCESS_DATA_LIMIT_EXCEEDED_START,
|
||||
PROCESS_DATA_LIMIT_EXCEEDED_END,
|
||||
VIEW_POLICIES,
|
||||
} from './translations';
|
||||
|
||||
export const renderTruncatedMsg = (tty?: Teletype, policiesUrl?: string, processName?: string) => {
|
||||
if (tty?.columns) {
|
||||
const lineBreak = '-'.repeat(tty.columns);
|
||||
const message = ` ⚠ ${PROCESS_DATA_LIMIT_EXCEEDED_START} \x1b[1m${processName}.\x1b[22m ${PROCESS_DATA_LIMIT_EXCEEDED_END}`;
|
||||
const link = policiesUrl
|
||||
? `\x1b[${Math.min(
|
||||
message.length + 2,
|
||||
tty.columns - VIEW_POLICIES.length - 4
|
||||
)}G\x1b[1m\x1b]8;;${policiesUrl}\x1b\\[ ${VIEW_POLICIES} ]\x1b]8;;\x1b\\\x1b[22m`
|
||||
: '';
|
||||
|
||||
return `\n\x1b[33m${lineBreak}\n${message}${link}\n${lineBreak}\x1b[0m\n\n`;
|
||||
}
|
||||
};
|
|
@ -12,6 +12,7 @@ import { CoreStart } from '@kbn/core/public';
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { SearchAddon } from './xterm_search';
|
||||
import { useEuiTheme } from '../../hooks';
|
||||
import { renderTruncatedMsg } from './ansi_helpers';
|
||||
|
||||
import {
|
||||
IOLine,
|
||||
|
@ -103,6 +104,15 @@ export const useIOLines = (pages: ProcessEventsPage[] | undefined) => {
|
|||
newMarkers.push(processLineInfo);
|
||||
}
|
||||
|
||||
if (process.io.max_bytes_per_process_exceeded) {
|
||||
const marker = newMarkers.find(
|
||||
(item) => item.event.process?.entity_id === process.entity_id
|
||||
);
|
||||
if (marker) {
|
||||
marker.maxBytesExceeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const splitLines = process.io.text.split(TTY_LINE_SPLITTER_REGEX);
|
||||
const combinedLines = [splitLines[0]];
|
||||
|
||||
|
@ -158,6 +168,7 @@ export interface XtermPlayerDeps {
|
|||
hasNextPage?: boolean;
|
||||
fetchNextPage?: () => void;
|
||||
isFetching?: boolean;
|
||||
policiesUrl?: string;
|
||||
}
|
||||
|
||||
export const useXtermPlayer = ({
|
||||
|
@ -169,17 +180,20 @@ export const useXtermPlayer = ({
|
|||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetching,
|
||||
policiesUrl,
|
||||
}: XtermPlayerDeps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { font, colors } = euiTheme;
|
||||
const [currentLine, setCurrentLine] = useState(0);
|
||||
const [playSpeed] = useState(DEFAULT_TTY_PLAYSPEED_MS); // potentially configurable
|
||||
const tty = lines?.[currentLine]?.event.process?.tty;
|
||||
|
||||
const processName = lines?.[currentLine]?.event.process?.name;
|
||||
const [terminal, searchAddon] = useMemo(() => {
|
||||
const term = new Terminal({
|
||||
theme: {
|
||||
selection: colors.warning,
|
||||
selectionBackground: colors.warning,
|
||||
selectionForeground: colors.ink,
|
||||
yellow: colors.warning,
|
||||
},
|
||||
fontFamily: font.familyCode,
|
||||
fontSize: DEFAULT_TTY_FONT_SIZE,
|
||||
|
@ -187,6 +201,8 @@ export const useXtermPlayer = ({
|
|||
convertEol: true,
|
||||
rows: DEFAULT_TTY_ROWS,
|
||||
cols: DEFAULT_TTY_COLS,
|
||||
allowProposedApi: true,
|
||||
allowTransparency: true,
|
||||
});
|
||||
|
||||
const searchInstance = new SearchAddon();
|
||||
|
@ -203,7 +219,7 @@ export const useXtermPlayer = ({
|
|||
// even though we set scrollback: 0 above, xterm steals the wheel events and prevents the outer container from scrolling
|
||||
// this handler fixes that
|
||||
const onScroll = (event: WheelEvent) => {
|
||||
if ((event?.target as HTMLDivElement)?.className === 'xterm-cursor-layer') {
|
||||
if ((event?.target as HTMLDivElement)?.offsetParent?.classList.contains('xterm-screen')) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
@ -212,6 +228,7 @@ export const useXtermPlayer = ({
|
|||
|
||||
return () => {
|
||||
window.removeEventListener('wheel', onScroll, true);
|
||||
terminal.dispose();
|
||||
};
|
||||
}, [terminal, ref]);
|
||||
|
||||
|
@ -241,17 +258,30 @@ export const useXtermPlayer = ({
|
|||
if (line?.value !== undefined) {
|
||||
terminal.write(line.value);
|
||||
}
|
||||
|
||||
const nextLine = lines[lineNumber + index + 1];
|
||||
const maxBytesExceeded = line.event.process?.io?.max_bytes_per_process_exceeded;
|
||||
|
||||
// if next line is start of next event
|
||||
// and process has exceeded max bytes
|
||||
// render msg
|
||||
if (!clear && (!nextLine || nextLine.event !== line.event) && maxBytesExceeded) {
|
||||
const msg = renderTruncatedMsg(tty, policiesUrl, processName);
|
||||
if (msg) {
|
||||
terminal.write(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[lines, terminal]
|
||||
[lines, policiesUrl, processName, terminal, tty]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fontChanged = terminal.getOption('fontSize') !== fontSize;
|
||||
const fontChanged = terminal.options.fontSize !== fontSize;
|
||||
const ttyChanged = tty && (terminal.rows !== tty?.rows || terminal.cols !== tty?.columns);
|
||||
|
||||
if (fontChanged) {
|
||||
terminal.setOption('fontSize', fontSize);
|
||||
terminal.options.fontSize = fontSize;
|
||||
}
|
||||
|
||||
if (tty?.rows && tty?.columns && ttyChanged) {
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { waitFor, act } from '@testing-library/react';
|
||||
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { TTYPlayerDeps, TTYPlayer } from '.';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('TTYPlayer component', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -81,5 +82,38 @@ describe('TTYPlayer component', () => {
|
|||
|
||||
await waitForApiCall();
|
||||
});
|
||||
|
||||
it('renders a message warning when max_bytes exceeded', async () => {
|
||||
renderResult = mockedContext.render(<TTYPlayer {...props} />);
|
||||
|
||||
await waitForApiCall();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
const seekToEndBtn = renderResult.getByTestId('sessionView:TTYPlayerControlsEnd');
|
||||
|
||||
act(() => {
|
||||
userEvent.click(seekToEndBtn);
|
||||
});
|
||||
|
||||
waitFor(() => expect(renderResult.queryAllByText('Data limit reached')).toHaveLength(1));
|
||||
expect(renderResult.queryByText('[ VIEW POLICIES ]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders a message warning when max_bytes exceeded with link to policies page', async () => {
|
||||
renderResult = mockedContext.render(
|
||||
<TTYPlayer {...props} canAccessEndpointManagement={true} />
|
||||
);
|
||||
|
||||
await waitForApiCall();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
const seekToEndBtn = renderResult.getByTestId('sessionView:TTYPlayerControlsEnd');
|
||||
|
||||
act(() => {
|
||||
userEvent.click(seekToEndBtn);
|
||||
});
|
||||
|
||||
waitFor(() => expect(renderResult.queryAllByText('[ VIEW POLICIES ]')).toHaveLength(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
EuiButton,
|
||||
EuiBetaBadge,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { throttle } from 'lodash';
|
||||
import { ProcessEvent } from '../../../common/types/process_tree';
|
||||
|
@ -23,6 +25,8 @@ import {
|
|||
DEFAULT_TTY_ROWS,
|
||||
DEFAULT_TTY_COLS,
|
||||
DEFAULT_TTY_FONT_SIZE,
|
||||
POLICIES_PAGE_PATH,
|
||||
SECURITY_APP_ID,
|
||||
} from '../../../common/constants';
|
||||
import { useFetchIOEvents, useIOLines, useXtermPlayer } from './hooks';
|
||||
import { TTYPlayerControls } from '../tty_player_controls';
|
||||
|
@ -35,6 +39,7 @@ export interface TTYPlayerDeps {
|
|||
isFullscreen: boolean;
|
||||
onJumpToEvent(event: ProcessEvent): void;
|
||||
autoSeekToEntityId?: string;
|
||||
canAccessEndpointManagement?: boolean;
|
||||
}
|
||||
|
||||
export const TTYPlayer = ({
|
||||
|
@ -44,6 +49,7 @@ export const TTYPlayer = ({
|
|||
isFullscreen,
|
||||
onJumpToEvent,
|
||||
autoSeekToEntityId,
|
||||
canAccessEndpointManagement,
|
||||
}: TTYPlayerDeps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver<HTMLDivElement>({});
|
||||
|
@ -53,7 +59,16 @@ export const TTYPlayer = ({
|
|||
const { lines, processStartMarkers } = useIOLines(data?.pages);
|
||||
const [fontSize, setFontSize] = useState(DEFAULT_TTY_FONT_SIZE);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentAutoSeekEntityId, setCurrentAutoSeekEntityId] = useState('');
|
||||
const { getUrlForApp } = useKibana<CoreStart>().services.application;
|
||||
const policiesUrl = useMemo(
|
||||
() =>
|
||||
canAccessEndpointManagement
|
||||
? getUrlForApp(SECURITY_APP_ID, { path: POLICIES_PAGE_PATH })
|
||||
: '',
|
||||
[canAccessEndpointManagement, getUrlForApp]
|
||||
);
|
||||
|
||||
const { search, currentLine, seekToLine } = useXtermPlayer({
|
||||
ref,
|
||||
|
@ -64,6 +79,7 @@ export const TTYPlayer = ({
|
|||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetching,
|
||||
policiesUrl,
|
||||
});
|
||||
|
||||
const currentProcessEvent = lines[Math.min(lines.length - 1, currentLine)]?.event;
|
||||
|
@ -113,11 +129,18 @@ export const TTYPlayer = ({
|
|||
|
||||
const styles = useStyles(tty, show);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
if (searchQuery) {
|
||||
setSearchQuery('');
|
||||
}
|
||||
}, [searchQuery]);
|
||||
|
||||
const onSeekLine = useMemo(() => {
|
||||
return throttle((line: number) => {
|
||||
clearSearch();
|
||||
seekToLine(line);
|
||||
}, 100);
|
||||
}, [seekToLine]);
|
||||
}, [clearSearch, seekToLine]);
|
||||
|
||||
const onTogglePlayback = useCallback(() => {
|
||||
// if at the end, seek to beginning
|
||||
|
@ -127,6 +150,12 @@ export const TTYPlayer = ({
|
|||
setIsPlaying(!isPlaying);
|
||||
}, [currentLine, isPlaying, lines.length, seekToLine]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
clearSearch();
|
||||
}
|
||||
}, [clearSearch, isPlaying]);
|
||||
|
||||
return (
|
||||
<div css={styles.container}>
|
||||
<EuiPanel hasShadow={false} borderRadius="none" hasBorder={false} css={styles.header}>
|
||||
|
@ -140,6 +169,8 @@ export const TTYPlayer = ({
|
|||
seekToLine={seekToLine}
|
||||
xTermSearchFn={search}
|
||||
setIsPlaying={setIsPlaying}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -157,11 +188,17 @@ export const TTYPlayer = ({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon iconType="refresh" display="empty" size="m" disabled={true} />
|
||||
<EuiButtonIcon
|
||||
iconType="refresh"
|
||||
display="empty"
|
||||
size="m"
|
||||
disabled={true}
|
||||
aria-label="disabled"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon iconType="eye" disabled={true} size="m" />
|
||||
<EuiButtonIcon iconType="eye" disabled={true} size="m" aria-label="disabled" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const PROCESS_DATA_LIMIT_EXCEEDED_START = i18n.translate(
|
||||
'xpack.sessionView.processDataLimitExceededStart',
|
||||
{
|
||||
defaultMessage: 'Data limit reached for',
|
||||
}
|
||||
);
|
||||
|
||||
export const PROCESS_DATA_LIMIT_EXCEEDED_END = i18n.translate(
|
||||
'xpack.sessionView.processDataLimitExceededEnd',
|
||||
{
|
||||
defaultMessage: 'See "max_kilobytes_per_process" in advanced policy configuration.',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_POLICIES = i18n.translate('xpack.sessionView.viewPoliciesLink', {
|
||||
defaultMessage: 'VIEW POLICIES',
|
||||
});
|
|
@ -9,7 +9,7 @@
|
|||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
|
||||
import { Terminal, IDisposable, ITerminalAddon, IBufferRange } from 'xterm';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
|
@ -83,14 +83,14 @@ export class SearchAddon implements ITerminalAddon {
|
|||
|
||||
let startCol = 0;
|
||||
let startRow = 0;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
let currentSelection: IBufferRange | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
// Start from the selection end if there is a selection
|
||||
// For incremental search, use existing row
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
|
||||
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
|
||||
startRow = incremental ? currentSelection.start.y : currentSelection.end.y;
|
||||
startCol = incremental ? currentSelection.start.x : currentSelection.end.x;
|
||||
}
|
||||
|
||||
if (searchOptions?.lastLineOnly) {
|
||||
|
@ -139,7 +139,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||
|
||||
// If there is only one result, wrap back and return selection if it exists.
|
||||
if (!result && currentSelection) {
|
||||
searchPosition.startRow = currentSelection.startRow;
|
||||
searchPosition.startRow = currentSelection.start.y;
|
||||
searchPosition.startCol = 0;
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
|
@ -170,12 +170,12 @@ export class SearchAddon implements ITerminalAddon {
|
|||
let startCol = this._terminal.cols;
|
||||
let result: ISearchResult | undefined;
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
let currentSelection: IBufferRange | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
// Start from selection start if there is a selection
|
||||
startRow = currentSelection.startRow;
|
||||
startCol = currentSelection.startColumn;
|
||||
startRow = currentSelection.start.y;
|
||||
startCol = currentSelection.start.x;
|
||||
} else if (searchOptions?.lastLineOnly) {
|
||||
startRow = this._terminal.buffer.active.cursorY - 1;
|
||||
startCol = this._terminal.cols;
|
||||
|
@ -194,8 +194,8 @@ export class SearchAddon implements ITerminalAddon {
|
|||
if (!isOldResultHighlighted) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
if (currentSelection) {
|
||||
searchPosition.startRow = currentSelection.endRow;
|
||||
searchPosition.startCol = currentSelection.endColumn;
|
||||
searchPosition.startRow = currentSelection.end.y;
|
||||
searchPosition.startCol = currentSelection.end.x;
|
||||
}
|
||||
result = this._findInLine(term, searchPosition, searchOptions, true);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react';
|
|||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { ProcessEvent } from '../../../common/types/process_tree';
|
||||
import { TTYPlayerControls, TTYPlayerControlsDeps } from '.';
|
||||
import { TTYPlayerLineMarkerType } from './tty_player_controls_markers';
|
||||
|
||||
const MOCK_PROCESS_EVENT_START: ProcessEvent = {
|
||||
process: {
|
||||
|
@ -100,11 +101,11 @@ describe('TTYPlayerControls component', () => {
|
|||
expect(props.onSeekLine).toHaveBeenCalledWith(9);
|
||||
});
|
||||
|
||||
it('render output markers', async () => {
|
||||
it('render process_changed markers', async () => {
|
||||
renderResult = mockedContext.render(<TTYPlayerControls {...props} />);
|
||||
expect(
|
||||
renderResult.queryAllByRole('button', {
|
||||
name: 'output',
|
||||
name: TTYPlayerLineMarkerType.ProcessChanged,
|
||||
})
|
||||
).toHaveLength(props.processStartMarkers.length);
|
||||
});
|
||||
|
@ -115,12 +116,10 @@ describe('TTYPlayerControls component', () => {
|
|||
event: {
|
||||
process: {
|
||||
...MOCK_PROCESS_EVENT_MIDDLE,
|
||||
io: {
|
||||
max_bytes_per_process_exceeded: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
line: 2,
|
||||
maxBytesExceeded: true,
|
||||
},
|
||||
{ event: MOCK_PROCESS_EVENT_END, line: 4 },
|
||||
];
|
||||
|
@ -129,12 +128,12 @@ describe('TTYPlayerControls component', () => {
|
|||
);
|
||||
expect(
|
||||
renderResult.queryAllByRole('button', {
|
||||
name: 'output',
|
||||
name: TTYPlayerLineMarkerType.ProcessChanged,
|
||||
})
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
renderResult.queryAllByRole('button', {
|
||||
name: 'data_limited',
|
||||
name: TTYPlayerLineMarkerType.ProcessDataLimitReached,
|
||||
})
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
|
|
@ -19,9 +19,14 @@ type Props = {
|
|||
onSeekLine(line: number): void;
|
||||
};
|
||||
|
||||
export enum TTYPlayerLineMarkerType {
|
||||
ProcessChanged = 'process_changed',
|
||||
ProcessDataLimitReached = 'data_limited',
|
||||
}
|
||||
|
||||
type TTYPlayerLineMarker = {
|
||||
line: number;
|
||||
type: 'output' | 'data_limited';
|
||||
type: TTYPlayerLineMarkerType;
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
@ -44,10 +49,11 @@ export const TTYPlayerControlsMarkers = ({
|
|||
return [];
|
||||
}
|
||||
return processStartMarkers.map(
|
||||
({ event, line }) =>
|
||||
({ event, line, maxBytesExceeded }) =>
|
||||
({
|
||||
type:
|
||||
event.process?.io?.max_bytes_per_process_exceeded === true ? 'data_limited' : 'output',
|
||||
type: maxBytesExceeded
|
||||
? TTYPlayerLineMarkerType.ProcessDataLimitReached
|
||||
: TTYPlayerLineMarkerType.ProcessChanged,
|
||||
line,
|
||||
name: event.process?.name,
|
||||
} as TTYPlayerLineMarker)
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||
import { CSSObject } from '@emotion/react';
|
||||
import { useEuiTheme } from '../../../hooks';
|
||||
|
||||
type TTYPlayerLineMarkerType = 'output' | 'data_limited';
|
||||
import { TTYPlayerLineMarkerType } from '.';
|
||||
|
||||
export const useStyles = (progress: number) => {
|
||||
const { euiTheme, euiVars } = useEuiTheme();
|
||||
|
@ -30,7 +30,7 @@ export const useStyles = (progress: number) => {
|
|||
};
|
||||
|
||||
const getMarkerBackgroundColor = (type: TTYPlayerLineMarkerType, selected: boolean) => {
|
||||
if (type === 'data_limited') {
|
||||
if (type === TTYPlayerLineMarkerType.ProcessDataLimitReached) {
|
||||
return euiVars.terminalOutputMarkerWarning;
|
||||
}
|
||||
if (selected) {
|
||||
|
@ -105,7 +105,7 @@ export const useStyles = (progress: number) => {
|
|||
left: progress + '%',
|
||||
top: 16,
|
||||
fill:
|
||||
type === 'data_limited'
|
||||
type === TTYPlayerLineMarkerType.ProcessDataLimitReached
|
||||
? euiVars.terminalOutputMarkerWarning
|
||||
: euiVars.terminalOutputMarkerAccent,
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../test';
|
||||
import { sessionViewIOEventsMock } from '../../../common/mocks/responses/session_view_io_events.mock';
|
||||
import { useIOLines } from '../tty_player/hooks';
|
||||
|
@ -35,6 +34,8 @@ describe('TTYSearchBar component', () => {
|
|||
seekToLine: jest.fn(),
|
||||
xTermSearchFn: jest.fn(),
|
||||
setIsPlaying: jest.fn(),
|
||||
searchQuery: '',
|
||||
setSearchQuery: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -44,33 +45,20 @@ describe('TTYSearchBar component', () => {
|
|||
});
|
||||
|
||||
it('does a search when a user enters text and hits enter', async () => {
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} />);
|
||||
|
||||
const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input');
|
||||
if (searchInput) {
|
||||
userEvent.type(searchInput, '-h');
|
||||
fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' });
|
||||
}
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} searchQuery="-h" />);
|
||||
|
||||
expect(props.seekToLine).toHaveBeenCalledTimes(1);
|
||||
|
||||
// there is a slight delay in the seek in xtermjs, so we wait 100ms before trying to highlight a result.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
expect(props.xTermSearchFn).toHaveBeenCalledTimes(2);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6);
|
||||
expect(props.xTermSearchFn).toHaveBeenCalledTimes(1);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '-h', 6);
|
||||
expect(props.setIsPlaying).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('calls seekToline and xTermSearchFn when currentMatch changes', async () => {
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} />);
|
||||
|
||||
const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input');
|
||||
if (searchInput) {
|
||||
userEvent.type(searchInput, '-h');
|
||||
fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' });
|
||||
}
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} searchQuery="-h" />);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
|
@ -83,27 +71,23 @@ describe('TTYSearchBar component', () => {
|
|||
expect(props.seekToLine).toHaveBeenNthCalledWith(1, 26);
|
||||
expect(props.seekToLine).toHaveBeenNthCalledWith(2, 100);
|
||||
|
||||
expect(props.xTermSearchFn).toHaveBeenCalledTimes(3);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '', 0);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 6);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '-h', 13);
|
||||
expect(props.setIsPlaying).toHaveBeenCalledTimes(3);
|
||||
expect(props.xTermSearchFn).toHaveBeenCalledTimes(2);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(1, '-h', 6);
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '-h', 13);
|
||||
expect(props.setIsPlaying).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('calls xTermSearchFn with empty query when search is cleared', async () => {
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} />);
|
||||
|
||||
const searchInput = renderResult.queryByTestId('sessionView:searchBar')?.querySelector('input');
|
||||
if (searchInput) {
|
||||
userEvent.type(searchInput, '-h');
|
||||
fireEvent.keyUp(searchInput, { key: 'Enter', code: 'Enter' });
|
||||
}
|
||||
renderResult = mockedContext.render(<TTYSearchBar {...props} searchQuery="-h" />);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
userEvent.click(renderResult.getByTestId('clearSearchButton'));
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(3, '', 0);
|
||||
renderResult.rerender(<TTYSearchBar {...props} />);
|
||||
|
||||
expect(props.setSearchQuery).toHaveBeenNthCalledWith(1, '');
|
||||
expect(props.xTermSearchFn).toHaveBeenNthCalledWith(2, '', 0);
|
||||
expect(props.setIsPlaying).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,8 @@ export interface TTYSearchBarDeps {
|
|||
seekToLine(index: number): void;
|
||||
xTermSearchFn(query: string, index: number): void;
|
||||
setIsPlaying(value: boolean): void;
|
||||
searchQuery: string;
|
||||
setSearchQuery(value: string): void;
|
||||
}
|
||||
|
||||
const STRIP_NEWLINES_REGEX = /^(\r\n|\r|\n|\n\r)/;
|
||||
|
@ -29,9 +31,10 @@ export const TTYSearchBar = ({
|
|||
seekToLine,
|
||||
xTermSearchFn,
|
||||
setIsPlaying,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
}: TTYSearchBarDeps) => {
|
||||
const [currentMatch, setCurrentMatch] = useState<SearchResult | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const jumpToMatch = useCallback(
|
||||
(match) => {
|
||||
|
@ -105,7 +108,7 @@ export const TTYSearchBar = ({
|
|||
setSearchQuery(query);
|
||||
setCurrentMatch(null);
|
||||
},
|
||||
[setIsPlaying]
|
||||
[setIsPlaying, setSearchQuery]
|
||||
);
|
||||
|
||||
const onSetCurrentMatch = useCallback(
|
||||
|
|
|
@ -17,6 +17,7 @@ import { CoreStart } from '@kbn/core/public';
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { SECURITY_APP_ID, SESSION_VIEW_APP_ID } from '../../common/constants';
|
||||
|
||||
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
|
||||
|
||||
|
@ -46,8 +47,10 @@ const createCoreStartMock = (
|
|||
// Mock the certain APP Ids returned by `application.getUrlForApp()`
|
||||
coreStart.application.getUrlForApp.mockImplementation((appId) => {
|
||||
switch (appId) {
|
||||
case 'sessionView':
|
||||
case SESSION_VIEW_APP_ID:
|
||||
return '/app/sessionView';
|
||||
case SECURITY_APP_ID:
|
||||
return '/app/security';
|
||||
default:
|
||||
return `${appId} not mocked!`;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface SessionViewDeps {
|
|||
// Callback used when alert flyout panel is closed
|
||||
handleOnAlertDetailsClosed: () => void
|
||||
) => void;
|
||||
canAccessEndpointManagement?: boolean;
|
||||
}
|
||||
|
||||
export interface EuiTabProps {
|
||||
|
|
|
@ -29229,10 +29229,10 @@ xtend@~2.1.1:
|
|||
dependencies:
|
||||
object-keys "~0.4.0"
|
||||
|
||||
xterm@^4.18.0:
|
||||
version "4.18.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
|
||||
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
|
||||
xterm@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c"
|
||||
integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA==
|
||||
|
||||
y18n@^3.2.0:
|
||||
version "3.2.2"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue