Skip to content

Commit

Permalink
[Security Solution][Endpoint] Add an additional hint message for Cons…
Browse files Browse the repository at this point in the history
…ole commands pending more than 15s (#135500)

* Add `enteredAt` to CommandHistoryItem and some refactoring in `handleExecuteCommand`

* New components: ConsoleText + LongRunningCommandHint

* Adds logic to display the Long running command hint to the execution output

* use ConsoleText in command execution result

* Fix type issue

* tests for logic around showing long running command hint

* Apply suggestions from code review

From Ash

Co-authored-by: Ashokaditya <1849116+ashokaditya@users.noreply.github.com>

Co-authored-by: Ashokaditya <1849116+ashokaditya@users.noreply.github.com>
Co-authored-by: Kevin Logan <kevin.logan@elastic.co>
  • Loading branch information
3 people authored Jul 6, 2022
1 parent 741f8b2 commit 092c8e1
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 168 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 from 'react';
import { ConsoleProps } from '..';
import { AppContextTestRender } from '../../../../common/mock/endpoint';
import { getConsoleTestSetup } from '../mocks';
import { act } from '@testing-library/react';
import { CommandExecutionComponentProps } from '../types';

describe('When using CommandExecutionOutput component', () => {
let render: (props?: Partial<ConsoleProps>) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let setCmd1ToComplete: () => void;

beforeEach(() => {
const { renderConsole, commands, enterCommand } = getConsoleTestSetup();

const cmd1 = commands.find((command) => command.name === 'cmd1');

if (!cmd1) {
throw new Error('cmd1 command not found in test mocks');
}

(cmd1.RenderComponent as jest.Mock).mockImplementation(
(props: CommandExecutionComponentProps) => {
setCmd1ToComplete = () => props.setStatus('success');

return <div>{'output'}</div>;
}
);

render = (props = {}) => {
renderResult = renderConsole(props);
enterCommand('cmd1');
return renderResult;
};
});

it('should show long running hint message if pending and >15s have passed', () => {
jest.useFakeTimers();
render();

expect(renderResult.queryByTestId('test-longRunningCommandHint')).toBeNull();

act(() => {
jest.advanceTimersByTime(16 * 1000);
});

expect(renderResult.getByTestId('test-longRunningCommandHint')).not.toBeNull();
});

it('should remove long running hint message if command completes', async () => {
jest.useFakeTimers();
render();

act(() => {
jest.advanceTimersByTime(16 * 1000);
});

expect(renderResult.getByTestId('test-longRunningCommandHint')).not.toBeNull();

act(() => {
setCmd1ToComplete();
});

expect(renderResult.queryByTestId('test-longRunningCommandHint')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* 2.0.
*/

import React, { memo, useCallback, useMemo } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { EuiLoadingChart, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import moment from 'moment';
import { LongRunningCommandHint } from './long_running_command_hint';
import { CommandExecutionResult } from './command_execution_result';
import type { CommandExecutionComponentProps } from '../types';
import type { CommandExecutionState, CommandHistoryItem } from './console_state/types';
Expand All @@ -26,9 +28,10 @@ export interface CommandExecutionOutputProps {
item: CommandHistoryItem;
}
export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
({ item: { command, state, id } }) => {
({ item: { command, state, id, enteredAt } }) => {
const dispatch = useConsoleStateDispatch();
const RenderComponent = command.commandDefinition.RenderComponent;
const [isLongRunningCommand, setIsLongRunningCommand] = useState(false);

const isRunning = useMemo(() => {
return state.status === 'pending';
Expand Down Expand Up @@ -62,6 +65,30 @@ export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
[dispatch, id]
);

// keep track if this becomes a long running command
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;

if (isRunning && !isLongRunningCommand) {
const elapsedSeconds = moment().diff(moment(enteredAt), 'seconds');

if (elapsedSeconds >= 15) {
setIsLongRunningCommand(true);
return;
}

timeoutId = setTimeout(() => {
setIsLongRunningCommand(true);
}, (15 - elapsedSeconds) * 1000);
}

return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [enteredAt, isLongRunningCommand, isRunning]);

return (
<CommandOutputContainer>
<div>
Expand All @@ -80,6 +107,13 @@ export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
/>

{isRunning && <EuiLoadingChart className="busy-indicator" mono={true} />}

{isRunning && isLongRunningCommand && (
<>
<EuiSpacer size="s" />
<LongRunningCommandHint />
</>
)}
</div>
</CommandOutputContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import React, { memo, PropsWithChildren, ComponentType, useMemo } from 'react';
import type { ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { CommonProps, EuiPanel, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui';
import { CommonProps, EuiPanel, EuiSpacer } from '@elastic/eui';
import classNames from 'classnames';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import { ConsoleText } from './console_text';

const COMMAND_EXECUTION_RESULT_SUCCESS_TITLE = i18n.translate(
'xpack.securitySolution.commandExecutionResult.successTitle',
Expand Down Expand Up @@ -84,24 +85,19 @@ export const CommandExecutionResult = memo<CommandExecutionResultProps>(
data-test-subj={dataTestSubj ? dataTestSubj : getTestId('commandExecutionResult')}
>
{showAs === 'pending' ? (
<EuiText size="s">
<EuiTextColor color="subdued">
{children ?? COMMAND_EXECUTION_RESULT_PENDING}
</EuiTextColor>
</EuiText>
<ConsoleText>{children ?? COMMAND_EXECUTION_RESULT_PENDING}</ConsoleText>
) : (
<>
{showTitle && (
<>
<EuiText size="s">
<EuiTextColor color={showAs === 'success' ? 'success' : 'danger'}>
{title
? title
: showAs === 'success'
? COMMAND_EXECUTION_RESULT_SUCCESS_TITLE
: COMMAND_EXECUTION_RESULT_FAILURE_TITLE}
</EuiTextColor>
</EuiText>
<ConsoleText color={showAs === 'success' ? 'success' : 'danger'}>
{title
? title
: showAs === 'success'
? COMMAND_EXECUTION_RESULT_SUCCESS_TITLE
: COMMAND_EXECUTION_RESULT_FAILURE_TITLE}
</ConsoleText>

<EuiSpacer size="s" />
</>
)}
Expand Down
Loading

0 comments on commit 092c8e1

Please sign in to comment.