Skip to content

Commit

Permalink
[Security Solution][Endpoint] UX adjustment and improvement for Respo…
Browse files Browse the repository at this point in the history
…nder (#136347)

- Remove the word "ENDPOINT" from the console's header information
- Add additional spacing between the Host name and the "Last seen" information that is shown in the console's header
- Change console header area to be same color as page background (white in light theme)
- adjust the header area (and other primary console containers) to have 16px padding
- Change input area placeholder text
- increase the bottom border of the input area, when focused, to 2px (`euiBorderThick`)
- adjust the footer hint test area to have 4px padding at top/bottom.
- adjust padding and spacing for the command output area
- change behaviour around the focusing on the input area:
    -  it no longer auto-focuses on the input area every time the user clicks on the page. It now behaves more like how other UI interfaces work (original intent was more for when the POC was done where it was attempting to mirror a "real" CLI terminal)
    - The input area now also support "tab"'ing into it
    - The input area will continue to receive focus when Responder is initially opened
  • Loading branch information
paul-tavares authored Jul 18, 2022
1 parent fef8e72 commit ec5eb44
Show file tree
Hide file tree
Showing 24 changed files with 112 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
<UserCommandInput input={command.input} />
</div>
<div>
{/* UX desire for 12px (current theme): achieved with EuiSpace sizes - s (8px) + xs (4px) */}
<EuiSpacer size="s" />
<EuiSpacer size="xs" />

<RenderComponent
command={command}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import { ConsoleText } from './console_text';

const COMMAND_EXECUTION_RESULT_SUCCESS_TITLE = i18n.translate(
'xpack.securitySolution.commandExecutionResult.successTitle',
{ defaultMessage: 'Success. Action was complete.' }
{ defaultMessage: 'Action completed.' }
);
const COMMAND_EXECUTION_RESULT_FAILURE_TITLE = i18n.translate(
'xpack.securitySolution.commandExecutionResult.failureTitle',
{ defaultMessage: 'Error. Action failed.' }
{ defaultMessage: 'Action failed.' }
);
const COMMAND_EXECUTION_RESULT_PENDING = i18n.translate(
'xpack.securitySolution.commandExecutionResult.pending',
{ defaultMessage: 'Action pending' }
{ defaultMessage: 'Action pending.' }
);

export type CommandExecutionResultProps = PropsWithChildren<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('When entering data into the Console input', () => {
expect(getUserInputText()).toEqual('c');
expect(getRightOfCursorText()).toEqual('md1 ');

expect(getFooterText()).toEqual('cmd1 ');
expect(getFooterText()).toEqual('Hit enter to execute');
});

// FIXME:PT uncomment once task OLM task #4384 is implemented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const CommandInputContainer = styled.div`
}
&.active {
border-bottom: solid ${({ theme: { eui } }) => eui.euiBorderWidthThin}
${({ theme: { eui } }) => eui.euiColorPrimary};
border-bottom: ${({ theme: { eui } }) => eui.euiBorderThick};
border-bottom-color: ${({ theme: { eui } }) => eui.euiColorPrimary};
}
.textEntered {
Expand Down Expand Up @@ -256,6 +256,12 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
[dispatch, rightOfCursor.text]
);

const handleOnFocus = useCallback(() => {
if (!isKeyInputBeingCaptured) {
dispatch({ type: 'addFocusToKeyCapture' });
}
}, [dispatch, isKeyInputBeingCaptured]);

// Execute the command if one was ENTER'd.
useEffect(() => {
if (commandToExecute) {
Expand All @@ -271,6 +277,8 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
className={focusClassName}
onClick={handleTypingAreaClick}
ref={containerRef}
tabIndex={0}
onFocus={handleOnFocus}
>
<EuiFlexGroup
wrap={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,9 @@ const UNKNOWN_COMMAND_HINT = (commandName: string) =>
values: { commandName },
});

const COMMAND_USAGE_HINT = (usage: string) =>
i18n.translate('xpack.securitySolution.useInputHints.commandUsage', {
defaultMessage: '{usage}',
values: {
usage,
},
});
const NO_ARGUMENTS_HINT = i18n.translate('xpack.securitySolution.useInputHints.noArguments', {
defaultMessage: 'Hit enter to execute',
});

/**
* Auto-generates console footer "hints" while user is interacting with the input area
Expand All @@ -48,18 +44,43 @@ export const useInputHints = () => {
if (commandEntered && !isInputPopoverOpen) {
// Is valid command name? ==> show usage
if (commandEnteredDefinition) {
const exampleInstruction = commandEnteredDefinition?.exampleInstruction ?? '';
const exampleUsage = commandEnteredDefinition?.exampleUsage ?? '';

let hint = exampleInstruction ?? '';

if (exampleUsage) {
if (exampleInstruction) {
// leading space below is intentional
hint += ` ${i18n.translate('xpack.securitySolution.useInputHints.exampleInstructions', {
defaultMessage: 'Ex: [ {exampleUsage} ]',
values: {
exampleUsage,
},
})}`;
} else {
hint += exampleUsage;
}
}

// If the command did not define any hint, then generate the command useage from the definition.
// If the command did define `exampleInstruction` but not `exampleUsage`, then generate the
// usage from the command definition and then append it.
//
// Generated usage is only created if the command has arguments.
if (!hint || !exampleUsage) {
const commandArguments = getArgumentsForCommand(commandEnteredDefinition);

if (commandArguments.length > 0) {
hint += `${commandEnteredDefinition.name} ${commandArguments}`;
} else {
hint += NO_ARGUMENTS_HINT;
}
}

dispatch({
type: 'updateFooterContent',
payload: {
value:
commandEnteredDefinition.exampleUsage && commandEnteredDefinition.exampleInstruction
? `${commandEnteredDefinition.exampleInstruction} Ex: [${commandEnteredDefinition.exampleUsage}]`
: COMMAND_USAGE_HINT(
`${commandEnteredDefinition.name} ${getArgumentsForCommand(
commandEnteredDefinition
)}`
),
},
payload: { value: hint },
});
} else {
dispatch({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export const CommandList = memo<CommandListProps>(({ commands, display = 'defaul
};
},
});

dispatch({ type: 'addFocusToKeyCapture' });
},
[dispatch]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PageOverlay } from '../../../../page_overlay/page_overlay';
import { ConsoleExitModal } from './console_exit_modal';

const BACK_LABEL = i18n.translate('xpack.securitySolution.consolePageOverlay.backButtonLabel', {
defaultMessage: 'Return to page content',
defaultMessage: 'Back',
});

export interface ConsolePageOverlayProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ describe('When a Console command is entered by the user', () => {

await waitFor(() => {
expect(renderResult.getByTestId('test-unknownCommandError').textContent).toEqual(
'Unsupported text/commandThe text you entered foo-foo is unsupported! Click or type help for assistance.'
'Unsupported text/commandThe text you entered foo-foo is unsupported! Click Help or type help for assistance.'
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import type { ConsoleDataAction, ConsoleStoreReducer } from '../types';
export const INPUT_DEFAULT_PLACEHOLDER_TEXT = i18n.translate(
'xpack.securitySolution.handleInputAreaState.inputPlaceholderText',
{
defaultMessage:
'Click here to type and submit an action. For assistance, use the "help" action',
defaultMessage: 'Submit response action',
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const UnknownCommand = memo<CommandExecutionComponentProps>(({ command, s
<ConsoleCodeBlock>
<FormattedMessage
id="xpack.securitySolution.console.unknownCommand.helpMessage"
defaultMessage="The text you entered {userInput} is unsupported! Click {helpIcon} or type {helpCmd} for assistance."
defaultMessage="The text you entered {userInput} is unsupported! Click {helpIcon} Help or type {helpCmd} for assistance."
values={{
userInput: (
<ConsoleCodeBlock bold inline>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('When using Console component', () => {

it('should focus on input area when it gains focus', () => {
render();
userEvent.click(renderResult.getByTestId('test-mainPanel'));
userEvent.click(renderResult.getByTestId('test-mainPanel-inputArea'));

expect(document.activeElement!.classList.contains('invisible-input')).toBe(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,28 @@ const ConsoleWindow = styled.div`
&-container {
padding: ${({ theme: { eui } }) => eui.euiSizeL} ${({ theme: { eui } }) => eui.euiSizeL}
${({ theme: { eui } }) => eui.euiSizeS} ${({ theme: { eui } }) => eui.euiSizeM};
${({ theme: { eui } }) => eui.euiSizeL} ${({ theme: { eui } }) => eui.euiSizeL};
}
&-header {
background-color: ${({ theme: { eui } }) => eui.euiColorEmptyShade};
border-bottom: 1px solid ${({ theme: { eui } }) => eui.euiColorLightShade};
border-top-left-radius: ${({ theme: { eui } }) => eui.euiBorderRadiusSmall};
border-top-right-radius: ${({ theme: { eui } }) => eui.euiBorderRadiusSmall};
padding: ${({ theme: { eui } }) => eui.euiSize} ${({ theme: { eui } }) => eui.euiSize}
${({ theme: { eui } }) => eui.euiSize} ${({ theme: { eui } }) => eui.euiSize};
}
&-footer,
&-commandInput {
padding-top: ${({ theme: { eui } }) => eui.euiSizeXS};
padding-bottom: ${({ theme: { eui } }) => eui.euiSizeXS};
}
&-footer {
padding-top: 0;
padding-bottom: ${({ theme: { eui } }) => eui.euiSizeXS};
}
&-rightPanel {
width: 35%;
background-color: ${({ theme: { eui } }) => eui.euiFormBackgroundColor};
Expand Down Expand Up @@ -138,7 +147,7 @@ export const Console = memo<ConsoleProps>(
HelpComponent={HelpComponent}
dataTestSubj={commonProps['data-test-subj']}
>
<ConsoleWindow onClick={setFocusOnInput} {...commonProps}>
<ConsoleWindow {...commonProps}>
<EuiFlexGroup className="layout" gutterSize="none" responsive={false}>
<EuiFlexItem>
<EuiFlexGroup
Expand All @@ -148,7 +157,7 @@ export const Console = memo<ConsoleProps>(
responsive={false}
data-test-subj={getTestId('mainPanel')}
>
<EuiFlexItem grow={false} className="layout-container layout-header">
<EuiFlexItem grow={false} className="layout-header">
<ConsoleHeader TitleComponent={TitleComponent} />
</EuiFlexItem>

Expand All @@ -173,7 +182,12 @@ export const Console = memo<ConsoleProps>(
<HistoryOutput />
</div>
</EuiFlexItem>
<EuiFlexItem grow={false} className="layout-container layout-commandInput">
<EuiFlexItem
onClick={setFocusOnInput}
grow={false}
className="layout-container layout-commandInput"
data-test-subj={getTestId('mainPanel-inputArea')}
>
<CommandInput prompt={prompt} focusRef={inputFocusRef} />
</EuiFlexItem>
<EuiFlexItem grow={false} className="layout-container layout-footer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ export const getArgumentsForCommand = (command: CommandDefinition): string[] =>
optional: optionalArgs,
});
})
: [buildArgumentText({ required: requiredArgs, optional: optionalArgs })];
: requiredArgs || optionalArgs
? [buildArgumentText({ required: requiredArgs, optional: optionalArgs })]
: [];
};

export const parsedPidOrEntityIdParameter = (parameters: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,18 @@ export interface CommandDefinition<TMeta = any> {
/** If all args are optional, but at least one must be defined, set to true */
mustHaveArgs?: boolean;

exampleUsage?: string;
/**
* Displayed in the input hint area when the user types the command. The Command usage will be
* appended to this value
*/
exampleInstruction?: string;

/**
* Displayed in the input hint area when the user types the command. This value will override
* the command usage generated by the console from the Command Definition
*/
exampleUsage?: string;

/**
* Validate the command entered by the user. This is called only after the Console has ran
* through all of its builtin validations (based on `CommandDefinition`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import type { CommandExecutionResultComponent } from '../console/components/comm
import type { ImmutableArray } from '../../../../common/endpoint/types';

export const ActionError = memo<{
title: string;
dataTestSubj?: string;
errors: ImmutableArray<string>;
title?: string;
ResultComponent: CommandExecutionResultComponent;
dataTestSubj?: string;
}>(({ title, dataTestSubj, errors, ResultComponent }) => {
return (
<ResultComponent showAs="failure" title={title} data-test-subj={dataTestSubj}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('Responder header endpoint info', () => {
});
it('should show endpoint name', async () => {
const name = await renderResult.findByTestId('responderHeaderEndpointName');
expect(name.textContent).toBe(`ENDPOINT ${endpointDetails.metadata.host.name}`);
expect(name.textContent).toBe(`${endpointDetails.metadata.host.name}`);
});
it('should show agent and isolation status', async () => {
const agentStatus = await renderResult.findByTestId(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*/

import React, { memo, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLoadingContent, EuiToolTip } from '@elastic/eui';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiLoadingContent,
EuiToolTip,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import { useGetEndpointDetails } from '../../hooks/endpoint/use_get_endpoint_details';
import { useGetEndpointPendingActionsSummary } from '../../hooks/endpoint/use_get_endpoint_pending_actions_summary';
Expand Down Expand Up @@ -60,13 +67,7 @@ export const HeaderEndpointInfo = memo<HeaderEndpointInfoProps>(({ endpointId })
anchorClassName="eui-textTruncate"
>
<EuiText size="s" data-test-subj="responderHeaderEndpointName">
<h6 className="eui-textTruncate">
<FormattedMessage
id="xpack.securitySolution.responder.header.endpointName"
defaultMessage="ENDPOINT {name}"
values={{ name: endpointDetails.metadata.host.name }}
/>
</h6>
<h6 className="eui-textTruncate">{endpointDetails.metadata.host.name}</h6>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
Expand All @@ -81,6 +82,7 @@ export const HeaderEndpointInfo = memo<HeaderEndpointInfoProps>(({ endpointId })
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="s" data-test-subj="responderHeaderLastSeen">
<FormattedMessage
id="xpack.securitySolution.responder.header.lastSeen"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import React, { memo, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import type { ActionDetails } from '../../../../common/endpoint/types';
import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details';
import type { EndpointCommandDefinitionMeta } from './types';
Expand Down Expand Up @@ -81,10 +80,6 @@ export const IsolateActionResult = memo<
if (completedActionDetails?.errors) {
return (
<ActionError
title={i18n.translate(
'xpack.securitySolution.endpointResponseActions.isolate.errorMessageTitle',
{ defaultMessage: 'Error. Isolate action failed.' }
)}
dataTestSubj={'isolateErrorCallout'}
errors={completedActionDetails?.errors}
ResultComponent={ResultComponent}
Expand All @@ -93,14 +88,6 @@ export const IsolateActionResult = memo<
}

// Show Success
return (
<ResultComponent
title={i18n.translate(
'xpack.securitySolution.endpointResponseActions.isolate.successMessageTitle',
{ defaultMessage: 'Success. Host isolated.' }
)}
data-test-subj="isolateSuccessCallout"
/>
);
return <ResultComponent showAs="success" data-test-subj="isolateSuccessCallout" />;
});
IsolateActionResult.displayName = 'IsolateActionResult';
Loading

0 comments on commit ec5eb44

Please sign in to comment.