mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
f6924439cf
commit
a7204d603c
27 changed files with 498 additions and 128 deletions
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
||||
|
|
|
@ -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: [] };
|
||||
}
|
||||
|
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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();
|
|
@ -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",
|
||||
|
|
|
@ -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": "ジョブ",
|
||||
|
|
|
@ -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": "作业",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue