[Security Solution][Endpoint] Response actions console UI re-styling to match UX mocks (phase 1) (#132757)

- Updates the main console component to match UX mocks
- Updates different output messages to match UX mocks
- includes console side-panel for help output that display the default help content
This commit is contained in:
Paul Tavares 2022-05-30 11:39:47 -04:00 committed by GitHub
parent f6924439cf
commit a7204d603c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 498 additions and 128 deletions

View file

@ -6,7 +6,9 @@
*/
import React, { memo, PropsWithChildren, useEffect } from 'react';
import { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCode, EuiText } from '@elastic/eui';
import { UnsupportedMessageCallout } from './unsupported_message_callout';
import { ParsedCommandInput } from '../service/parsed_command_input';
import { CommandDefinition, CommandExecutionComponentProps } from '../types';
import { CommandInputUsage } from './command_usage';
@ -30,10 +32,29 @@ export const BadArgument = memo<CommandExecutionComponentProps>(({ command, setS
}, [setStatus]);
return (
<EuiCallOut color="danger" data-test-subj={getTestId('badArgument')}>
{store.errorMessage}
<CommandInputUsage commandDef={command.commandDefinition} />
</EuiCallOut>
<UnsupportedMessageCallout
header={
<FormattedMessage
id="xpack.securitySolution.console.badArgument.title"
defaultMessage="Unsupported argument!"
/>
}
data-test-subj={getTestId('badArgument')}
>
<>
{store.errorMessage}
<CommandInputUsage commandDef={command.commandDefinition} />
<EuiText size="s">
<FormattedMessage
id="xpack.securitySolution.console.badArgument.helpMessage"
defaultMessage="Type {helpCmd} for assistance."
values={{
helpCmd: <EuiCode>{`${command.commandDefinition.name} --help`}</EuiCode>,
}}
/>
</EuiText>
</>
</UnsupportedMessageCallout>
);
});
BadArgument.displayName = 'BadArgument';

View file

@ -15,6 +15,10 @@ import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
import { useDataTestSubj } from '../../hooks/state_selectors/use_data_test_subj';
const CommandInputContainer = styled.div`
background-color: ${({ theme: { eui } }) => eui.euiColorGhost};
border-radius: ${({ theme: { eui } }) => eui.euiBorderRadius};
padding: ${({ theme: { eui } }) => eui.paddingSizes.s};
.prompt {
padding-right: 1ch;
}

View file

@ -23,9 +23,11 @@ const NOOP = () => undefined;
const KeyCaptureContainer = styled.span`
display: inline-block;
position: relative;
position: absolute;
width: 1px;
height: 1em;
left: -5px;
top: -5px;
overflow: hidden;
.invisible-input {

View file

@ -6,7 +6,16 @@
*/
import React, { memo, useMemo } from 'react';
import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import {
EuiBadge,
EuiCode,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CommandDefinition } from '../types';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
@ -25,8 +34,8 @@ export const CommandList = memo<CommandListProps>(({ commands }) => {
id="xpack.securitySolution.console.commandList.footerText"
defaultMessage="For more details on the commands above use the {helpOption} argument. Example: {cmdExample}"
values={{
helpOption: '--help',
cmdExample: <code>{'some-command --help'}</code>,
helpOption: <EuiCode>{'--help'}</EuiCode>,
cmdExample: <EuiCode>{'some-command --help'}</EuiCode>,
}}
/>
);
@ -34,20 +43,25 @@ export const CommandList = memo<CommandListProps>(({ commands }) => {
return (
<>
<EuiFlexGroup wrap gutterSize="xs" data-test-subj={getTestId('commandList')}>
<EuiFlexGroup gutterSize="l" direction="column" data-test-subj={getTestId('commandList')}>
{commands.map(({ name, about }) => {
return (
<EuiFlexItem grow={2} style={{ flexBasis: '20%' }} key={name}>
<EuiFlexItem grow={2} key={name}>
<EuiDescriptionList
compressed
listItems={[{ title: name, description: about }]}
listItems={[{ title: <EuiBadge>{name}</EuiBadge>, description: about }]}
data-test-subj={getTestId('commandList-command')}
/>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
<EuiText>{footerMessage}</EuiText>
<EuiSpacer />
<EuiText size="s">
<EuiTextColor color="subdued">{footerMessage}</EuiTextColor>
</EuiText>
</>
);
});

View file

@ -7,6 +7,8 @@
import React, { memo, useMemo } from 'react';
import {
EuiBadge,
EuiCode,
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
@ -15,20 +17,42 @@ import {
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { usageFromCommandDefinition } from '../service/usage_from_command_definition';
import { CommandDefinition } from '../types';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
const getArgumentsForCommand = (command: CommandDefinition): string => {
let requiredArgs = '';
let optionalArgs = '';
if (command.args) {
for (const [argName, argDefinition] of Object.entries(command.args)) {
if (argDefinition.required) {
if (requiredArgs.length) {
requiredArgs += ' ';
}
requiredArgs += `--${argName}`;
} else {
if (optionalArgs.length) {
optionalArgs += ' ';
}
optionalArgs += `--${argName}`;
}
}
}
return `${requiredArgs} ${optionalArgs.length > 0 ? `[${optionalArgs}]` : ''}`.trim();
};
export const CommandInputUsage = memo<Pick<CommandUsageProps, 'commandDef'>>(({ commandDef }) => {
const usageHelp = useMemo(() => {
return usageFromCommandDefinition(commandDef);
return getArgumentsForCommand(commandDef);
}, [commandDef]);
return (
<EuiFlexGroup>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText>
<EuiText size="s">
<FormattedMessage
id="xpack.securitySolution.console.commandUsage.inputUsage"
defaultMessage="Usage:"
@ -37,8 +61,9 @@ export const CommandInputUsage = memo<Pick<CommandUsageProps, 'commandDef'>>(({
</EuiFlexItem>
<EuiFlexItem grow>
<code>
<EuiText>
<code>{usageHelp}</code>
<EuiText size="s">
<EuiBadge>{commandDef.name}</EuiBadge>
<EuiCode transparentBackground={true}>{usageHelp}</EuiCode>
</EuiText>
</code>
</EuiFlexItem>

View file

@ -0,0 +1,51 @@
/*
* 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, { memo, useCallback } from 'react';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_state_dispatch';
import { useWithSidePanel } from '../hooks/state_selectors/use_with_side_panel';
import { ConsoleProps } from '..';
const HELP_LABEL = i18n.translate('xpack.securitySolution.console.layoutHeader.helpButtonLabel', {
defaultMessage: 'Show help',
});
export type ConsoleHeaderProps = Pick<ConsoleProps, 'TitleComponent'>;
export const ConsoleHeader = memo<ConsoleHeaderProps>(({ TitleComponent }) => {
const dispatch = useConsoleStateDispatch();
const panelCurrentlyShowing = useWithSidePanel().show;
const isHelpOpen = panelCurrentlyShowing === 'help';
const handleHelpButtonOnClick = useCallback(() => {
dispatch({
type: 'showSidePanel',
payload: { show: isHelpOpen ? null : 'help' },
});
}, [dispatch, isHelpOpen]);
return (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow className="eui-textTruncate">
{TitleComponent ? <TitleComponent /> : ''}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
onClick={handleHelpButtonOnClick}
iconType="help"
title={HELP_LABEL}
aria-label={HELP_LABEL}
isSelected={isHelpOpen}
display={isHelpOpen ? 'fill' : 'empty'}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
});
ConsoleHeader.displayName = 'ConsoleHeader';

View file

@ -24,9 +24,6 @@ export const ConsoleStateProvider = memo<ConsoleStateProviderProps>(
initiateState
);
// FIXME:PT should handle cases where props that are in the store change
// Probably need to have a `useAffect()` that just does a `dispatch()` to update those.
return (
<ConsoleStateContext.Provider value={{ state, dispatch }}>
{children}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { handleSidePanel } from './state_update_handlers/handle_side_panel';
import { handleUpdateCommandState } from './state_update_handlers/handle_update_command_state';
import type { ConsoleDataState, ConsoleStoreReducer } from './types';
import { handleExecuteCommand } from './state_update_handlers/handle_execute_command';
@ -27,6 +28,7 @@ export const initiateState = ({
HelpComponent,
dataTestSubj,
commandHistory: [],
sidePanel: { show: null },
};
};
@ -43,6 +45,9 @@ export const stateDataReducer: ConsoleStoreReducer = (state, action) => {
case 'updateCommandStoreState':
return handleUpdateCommandState(state, action);
case 'showSidePanel':
return handleSidePanel(state, action);
case 'clear':
return { ...state, commandHistory: [] };
}

View file

@ -110,7 +110,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-unknownCommandError').textContent).toEqual(
'Unknown commandFor a list of available command, enter: help'
'Unsupported text/command!The text you entered foo-foo is unsupported! Click or type help for assistance.'
);
});
});
@ -121,7 +121,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'command does not support any argumentsUsage:cmd1'
'Unsupported argument!command does not support any argumentsUsage:cmd1Type cmd1 --help for assistance.'
);
});
});
@ -132,7 +132,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'unsupported argument: --fooUsage:cmd2 --file [--ext --bad]'
'Unsupported argument!unsupported argument: --fooUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.'
);
});
});
@ -143,7 +143,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'missing required argument: --fileUsage:cmd2 --file [--ext --bad]'
'Unsupported argument!missing required argument: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.'
);
});
});
@ -154,7 +154,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'argument can only be used once: --fileUsage:cmd2 --file [--ext --bad]'
'Unsupported argument!argument can only be used once: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.'
);
});
});
@ -165,7 +165,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'invalid argument value: --bad. This is a bad valueUsage:cmd2 --file [--ext --bad]'
'Unsupported argument!invalid argument value: --bad. This is a bad valueUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.'
);
});
});
@ -176,7 +176,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'missing required arguments: --fileUsage:cmd2 --file [--ext --bad]'
'Unsupported argument!missing required arguments: --fileUsage:cmd2--file [--ext --bad]Type cmd2 --help for assistance.'
);
});
});
@ -187,7 +187,7 @@ describe('When a Console command is entered by the user', () => {
await waitFor(() => {
expect(renderResult.getByTestId('test-badArgument').textContent).toEqual(
'at least one argument must be usedUsage:cmd4 [--foo --bar]'
'Unsupported argument!at least one argument must be usedUsage:cmd4[--foo --bar]Type cmd4 --help for assistance.'
);
});
});

View file

@ -0,0 +1,30 @@
/*
* 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 { ConsoleDataAction, ConsoleStoreReducer } from '../types';
type SidePanelAction = ConsoleDataAction & {
type: 'showSidePanel';
};
export const handleSidePanel: ConsoleStoreReducer<SidePanelAction> = (state, action) => {
switch (action.type) {
case 'showSidePanel':
if (state.sidePanel.show !== action.payload.show) {
return {
...state,
sidePanel: {
...state.sidePanel,
show: action.payload.show,
},
};
}
break;
}
return state;
};

View file

@ -22,9 +22,15 @@ export interface ConsoleDataState {
* List of commands entered by the user and being shown in the UI
*/
commandHistory: CommandHistoryItem[];
/** Component defined on input to the Console that will handle the `help` command */
HelpComponent?: CommandExecutionComponent;
dataTestSubj?: string;
sidePanel: {
show: null | 'help'; // will have other values in the future
};
}
export interface CommandHistoryItem {
@ -42,6 +48,10 @@ export type ConsoleDataAction =
| { type: 'scrollDown' }
| { type: 'executeCommand'; payload: { input: string } }
| { type: 'clear' }
| {
type: 'showSidePanel';
payload: { show: ConsoleDataState['sidePanel']['show'] };
}
| {
type: 'updateCommandStoreState';
payload: {

View file

@ -6,7 +6,7 @@
*/
import React, { memo, PropsWithChildren, ReactNode } from 'react';
import { EuiCallOut } from '@elastic/eui';
import { EuiPanel } from '@elastic/eui';
import { MaybeImmutable } from '../../../../../common/endpoint/types';
import { Command } from '..';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
@ -20,15 +20,14 @@ export const HelpOutput = memo<HelpOutputProps>(({ title, children }) => {
const getTestId = useTestIdGenerator(useDataTestSubj());
return (
<EuiCallOut
title={title}
color="primary"
size="s"
iconType="help"
<EuiPanel
hasShadow={false}
color="transparent"
paddingSize="none"
data-test-subj={getTestId('helpOutput')}
>
{children}
</EuiCallOut>
</EuiPanel>
);
});
HelpOutput.displayName = 'HelpOutput';

View file

@ -7,22 +7,25 @@
import React, { memo, PropsWithChildren } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
const StyledEuiFlexItemHistoryItem = styled(EuiFlexItem)`
border-bottom: ${({ theme: { eui } }) => eui.euiBorderWidthThin} dashed
${({ theme: { eui } }) => eui.euiBorderColor};
padding: ${({ theme: { eui } }) => eui.paddingSizes.xl} 0;
`;
export type HistoryItemProps = PropsWithChildren<{}>;
export const HistoryItem = memo<HistoryItemProps>(({ children }) => {
const getTestId = useTestIdGenerator(useDataTestSubj());
return (
<EuiFlexItem
grow={true}
style={{ flexBasis: '100%' }}
data-test-subj={getTestId('historyItem')}
>
<StyledEuiFlexItemHistoryItem grow={true} data-test-subj={getTestId('historyItem')}>
{children}
</EuiFlexItem>
</StyledEuiFlexItemHistoryItem>
);
});

View file

@ -42,9 +42,10 @@ export const HistoryOutput = memo<OutputHistoryProps>((commonProps) => {
data-test-subj={getTestId('historyOutput')}
{...commonProps}
wrap={true}
direction="row"
alignItems="flexEnd"
direction="column"
alignItems="stretch"
responsive={false}
gutterSize="none"
>
{historyBody}
</EuiFlexGroup>

View file

@ -0,0 +1,43 @@
/*
* 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, { memo, ReactNode } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
export interface SidePanelContentLayoutProps {
children: ReactNode;
headerContent?: ReactNode;
}
/**
* A layout component for displaying content in the right-side panel of the console
*/
export const SidePanelContentLayout = memo<SidePanelContentLayoutProps>(
({ headerContent, children }) => {
return (
<EuiFlexGroup
direction="column"
responsive={false}
className="eui-fullHeight"
gutterSize="none"
>
{headerContent && (
<>
<EuiFlexItem grow={false} className="layout-container">
{headerContent}
</EuiFlexItem>
<EuiHorizontalRule margin="none" />
</>
)}
<EuiFlexItem grow className="eui-scrollBar eui-yScroll layout-container">
<div>{children}</div>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
SidePanelContentLayout.displayName = 'SidePanelContentLayout';

View file

@ -0,0 +1,50 @@
/*
* 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, { memo, ReactNode, useMemo } from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CommandList } from '../command_list';
import { useWithCommandList } from '../../hooks/state_selectors/use_with_command_list';
import { SidePanelContentLayout } from './side_panel_content_layout';
import { useWithSidePanel } from '../../hooks/state_selectors/use_with_side_panel';
export const SidePanelContentManager = memo(() => {
const commands = useWithCommandList();
const show = useWithSidePanel().show;
const panelHeader: ReactNode = useMemo(() => {
if (show === 'help') {
return (
<EuiText size="s">
<strong>
<FormattedMessage
id="xpack.securitySolution.console.sidePanel.helpTitle"
defaultMessage="Help"
/>
</strong>
</EuiText>
);
}
return null;
}, [show]);
const panelBody: ReactNode = useMemo(() => {
if (show === 'help') {
return <CommandList commands={commands} />;
}
return null;
}, [commands, show]);
if (!show) {
return null;
}
return <SidePanelContentLayout headerContent={panelHeader}>{panelBody}</SidePanelContentLayout>;
});
SidePanelContentManager.displayName = 'RightPanelContentManager';

View file

@ -0,0 +1,26 @@
/*
* 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, { memo } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { SidePanelContentManager } from './side_panel_content_manager';
import { useWithSidePanel } from '../../hooks/state_selectors/use_with_side_panel';
export const SidePanelFlexItem = memo((props) => {
const isPanelOpened = Boolean(useWithSidePanel().show);
if (!isPanelOpened) {
return null;
}
return (
<EuiFlexItem grow={false} className="layout-rightPanel">
<SidePanelContentManager />
</EuiFlexItem>
);
});
SidePanelFlexItem.displayName = 'SidePanelFlexItem';

View file

@ -5,38 +5,47 @@
* 2.0.
*/
import React, { memo, useEffect } from 'react';
import { EuiCallOut, EuiText } from '@elastic/eui';
import React, { memo, useEffect, useMemo } from 'react';
import { EuiCode, EuiIcon } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { UnsupportedMessageCallout } from './unsupported_message_callout';
import { CommandExecutionComponentProps } from '../types';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
export const UnknownCommand = memo<CommandExecutionComponentProps>(({ setStatus }) => {
export const UnknownCommand = memo<CommandExecutionComponentProps>(({ command, setStatus }) => {
const getTestId = useTestIdGenerator(useDataTestSubj());
const message = useMemo(() => {
return (
<FormattedMessage
id="xpack.securitySolution.console.unknownCommand.helpMessage"
defaultMessage="The text you entered {userInput} is unsupported! Click {helpIcon} or type {helpCmd} for assistance."
values={{
userInput: <EuiCode>{command.input}</EuiCode>,
helpIcon: <EuiIcon type="help" />,
helpCmd: <EuiCode>{'help'}</EuiCode>,
}}
/>
);
}, [command.input]);
useEffect(() => {
setStatus('success');
}, [setStatus]);
return (
<EuiCallOut color="danger" data-test-subj={getTestId('unknownCommandError')}>
<EuiText>
<UnsupportedMessageCallout
header={
<FormattedMessage
id="xpack.securitySolution.console.unknownCommand.title"
defaultMessage="Unknown command"
defaultMessage="Unsupported text/command!"
/>
</EuiText>
<EuiText size="xs">
<FormattedMessage
id="xpack.securitySolution.console.unknownCommand.helpMessage"
defaultMessage="For a list of available command, enter: {helpCmd}"
values={{
helpCmd: <code>{'help'}</code>,
}}
/>
</EuiText>
</EuiCallOut>
}
data-test-subj={getTestId('unknownCommandError')}
>
{message}
</UnsupportedMessageCallout>
);
});
UnknownCommand.displayName = 'UnknownCommand';

View file

@ -0,0 +1,37 @@
/*
* 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, { memo, ReactNode } from 'react';
import { EuiText, EuiTextColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const UNSUPPORTED_LABEL = i18n.translate(
'xpack.securitySolution.console.unsupportedMessageCallout.title',
{ defaultMessage: 'Unsupported' }
);
export interface UnsupportedMessageCalloutProps {
children: ReactNode;
header?: ReactNode;
'data-test-subj'?: string;
}
export const UnsupportedMessageCallout = memo<UnsupportedMessageCalloutProps>(
({ children, header = UNSUPPORTED_LABEL, 'data-test-subj': dataTestSubj }) => {
return (
<div data-test-subj={dataTestSubj}>
<EuiText size="s">
<EuiTextColor color="danger">{header}</EuiTextColor>
</EuiText>
<EuiText size="s">
<EuiTextColor color="subdued">{children}</EuiTextColor>
</EuiText>
</div>
);
}
);
UnsupportedMessageCallout.displayName = 'UnsupportedMessageCallout';

View file

@ -6,29 +6,63 @@
*/
import React, { memo, useCallback, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { HistoryOutput } from './components/history_output';
import { ConsoleHeader } from './components/console_header';
import { CommandInput, CommandInputProps } from './components/command_input';
import { ConsoleProps } from './types';
import { ConsoleStateProvider } from './components/console_state';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
import { useWithManagedConsole } from './components/console_manager/console_manager';
// FIXME:PT implement dark mode for the console or light mode switch
import { HistoryOutput } from './components/history_output';
import { SidePanelFlexItem } from './components/side_panel/side_panel_flex_item';
const ConsoleWindow = styled.div`
height: 100%;
background-color: ${({ theme: { eui } }) => eui.euiPageBackgroundColor};
// FIXME: IMPORTANT - this should NOT be used in production
// dark mode on light theme / light mode on dark theme
filter: invert(100%);
.ui-panel {
min-width: ${({ theme }) => theme.eui.euiBreakpoints.s};
.layout {
height: 100%;
min-height: 300px;
overflow-y: auto;
&-hideOverflow {
overflow: hidden;
}
&-bottomBorder {
border-bottom: ${({ theme: { eui } }) => eui.paddingSizes.s} solid
${({ theme: { eui } }) => eui.euiPageBackgroundColor};
}
&-container {
padding: ${({ theme: { eui } }) => eui.paddingSizes.l}
${({ theme: { eui } }) => eui.paddingSizes.l} ${({ theme: { eui } }) => eui.paddingSizes.s}
${({ theme: { eui } }) => eui.paddingSizes.l};
}
&-header {
border-bottom: 1px solid ${({ theme: { eui } }) => eui.euiColorLightShade};
}
&-rightPanel {
width: 35%;
background-color: ${({ theme: { eui } }) => eui.euiColorGhost};
border-bottom: ${({ theme: { eui } }) => eui.paddingSizes.s} solid
${({ theme: { eui } }) => eui.euiPageBackgroundColor};
}
&-historyOutput {
overflow: auto;
}
&-historyViewport {
height: 100%;
overflow-x: hidden;
}
&-commandInput {
padding-top: ${({ theme: { eui } }) => eui.paddingSizes.xs};
}
}
.descriptionList-20_80 {
@ -45,8 +79,8 @@ const ConsoleWindow = styled.div`
`;
export const Console = memo<ConsoleProps>(
({ prompt, commands, HelpComponent, managedKey, ...commonProps }) => {
const consoleWindowRef = useRef<HTMLDivElement | null>(null);
({ prompt, commands, HelpComponent, TitleComponent, managedKey, ...commonProps }) => {
const scrollingViewport = useRef<HTMLDivElement | null>(null);
const inputFocusRef: CommandInputProps['focusRef'] = useRef(null);
const getTestId = useTestIdGenerator(commonProps['data-test-subj']);
const managedConsole = useWithManagedConsole(managedKey);
@ -55,8 +89,8 @@ export const Console = memo<ConsoleProps>(
// We need the `setTimeout` here because in some cases, the command output
// will take a bit of time to populate its content due to the use of Promises
setTimeout(() => {
if (consoleWindowRef.current) {
consoleWindowRef.current.scrollTop = consoleWindowRef.current.scrollHeight;
if (scrollingViewport.current) {
scrollingViewport.current.scrollTop = scrollingViewport.current.scrollHeight;
}
}, 1);
@ -85,25 +119,48 @@ export const Console = memo<ConsoleProps>(
*/}
{!managedConsole || managedConsole.isOpen ? (
<ConsoleWindow onClick={handleConsoleClick} {...commonProps}>
<EuiPanel
className="ui-panel"
panelRef={consoleWindowRef}
<EuiFlexGroup
direction="column"
className="layout"
gutterSize="none"
responsive={false}
data-test-subj={getTestId('mainPanel')}
>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={true}>
<HistoryOutput />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CommandInput prompt={prompt} focusRef={inputFocusRef} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiFlexItem grow={false} className="layout-container layout-header">
<ConsoleHeader TitleComponent={TitleComponent} />
</EuiFlexItem>
<EuiFlexItem grow className="layout-hideOverflow">
<EuiFlexGroup gutterSize="none" responsive={false} className="layout-hideOverflow">
<EuiFlexItem className="eui-fullHeight layout-hideOverflow">
<EuiFlexGroup
direction="column"
gutterSize="none"
responsive={false}
className="layout-hideOverflow"
>
<EuiFlexItem grow className="layout-historyOutput">
<div
className="layout-container layout-historyViewport eui-scrollBar eui-yScroll"
ref={scrollingViewport}
>
<HistoryOutput />
</div>
</EuiFlexItem>
<EuiFlexItem grow={false} className="layout-container layout-commandInput">
<CommandInput prompt={prompt} focusRef={inputFocusRef} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{<SidePanelFlexItem />}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</ConsoleWindow>
) : null}
</ConsoleStateProvider>
);
}
);
Console.displayName = 'Console';

View file

@ -0,0 +1,13 @@
/*
* 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 { ConsoleDataState } from '../../components/console_state/types';
import { useConsoleStore } from '../../components/console_state/console_state';
export const useWithSidePanel = (): ConsoleDataState['sidePanel'] => {
return useConsoleStore().state.sidePanel;
};

View file

@ -1,33 +0,0 @@
/*
* 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 { CommandDefinition } from '../types';
export const usageFromCommandDefinition = (command: CommandDefinition): string => {
let requiredArgs = '';
let optionalArgs = '';
if (command.args) {
for (const [argName, argDefinition] of Object.entries(command.args)) {
if (argDefinition.required) {
if (requiredArgs.length) {
requiredArgs += ' ';
}
requiredArgs += `--${argName}`;
} else {
if (optionalArgs.length) {
optionalArgs += ' ';
}
optionalArgs += `--${argName}`;
}
}
}
return `${command.name} ${requiredArgs} ${
optionalArgs.length > 0 ? `[${optionalArgs}]` : ''
}`.trim();
};

View file

@ -113,12 +113,20 @@ export interface ConsoleProps extends CommonProps {
* The list of Commands that will be available in the console for the user to execute
*/
commands: CommandDefinition[];
/**
* If defined, then the `help` builtin command will display this output instead of the default one
* which is generated out of the Command list.
*/
HelpComponent?: CommandExecutionComponent;
/**
* A component to be used in the Console's header title area (left side)
*/
TitleComponent?: ComponentType;
prompt?: string;
/**
* For internal use only!
* Provided by the ConsoleManager to indicate that the console is being managed by it

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { useCallback } from 'react';
import { getEndpointResponseActionsConsoleCommands } from '../../components/endpoint_console/endpoint_response_actions_console_commands';
import React, { useCallback } from 'react';
import { getEndpointResponseActionsConsoleCommands } from '../../components/endpoint_console';
import { useConsoleManager } from '../../components/console';
import type { HostMetadata } from '../../../../common/endpoint/types';
@ -31,6 +31,7 @@ export const useShowEndpointResponseActionsConsole = (): ShowEndpointResponseAct
commands: getEndpointResponseActionsConsoleCommands(endpointAgentId),
'data-test-subj': 'endpointResponseActionsConsole',
prompt: `endpoint-${endpointMetadata.agent.version}`,
TitleComponent: () => <>{endpointMetadata.host.name}</>,
},
})
.show();

View file

@ -23712,7 +23712,6 @@
"xpack.securitySolution.console.commandValidation.oneArgIsRequired": "au moins un argument doit être utilisé.",
"xpack.securitySolution.console.commandValidation.unknownArgument": "argument(s) inconnu(s) : {unknownArgs}",
"xpack.securitySolution.console.commandValidation.unsupportedArg": "argument non pris en charge : {argName}",
"xpack.securitySolution.console.unknownCommand.helpMessage": "Pour obtenir la liste des commandes disponibles, entrez : {helpCmd}.",
"xpack.securitySolution.console.unknownCommand.title": "Commande inconnue",
"xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "Impossible d'interroger les données d'anomalies",
"xpack.securitySolution.containers.anomalies.stackByJobId": "tâche",

View file

@ -23849,7 +23849,6 @@
"xpack.securitySolution.console.commandValidation.oneArgIsRequired": "1つ以上の引数を使用する必要があります",
"xpack.securitySolution.console.commandValidation.unknownArgument": "不明な引数:{unknownArgs}",
"xpack.securitySolution.console.commandValidation.unsupportedArg": "サポートされていない引数:{argName}",
"xpack.securitySolution.console.unknownCommand.helpMessage": "使用可能なコマンドのリストについては、{helpCmd}を入力します",
"xpack.securitySolution.console.unknownCommand.title": "不明なコマンド",
"xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした",
"xpack.securitySolution.containers.anomalies.stackByJobId": "ジョブ",

View file

@ -23881,7 +23881,6 @@
"xpack.securitySolution.console.commandValidation.oneArgIsRequired": "必须至少使用一个参数",
"xpack.securitySolution.console.commandValidation.unknownArgument": "未知参数:{unknownArgs}",
"xpack.securitySolution.console.commandValidation.unsupportedArg": "不支持的参数:{argName}",
"xpack.securitySolution.console.unknownCommand.helpMessage": "如需可用命令列表,请输入:{helpCmd}",
"xpack.securitySolution.console.unknownCommand.title": "未知命令",
"xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据",
"xpack.securitySolution.containers.anomalies.stackByJobId": "作业",