mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Endpoint] Fix UX bugs (#121410)
* move back button over flyout header fixes elastic/security-team/issues/2394 * remove border on details and activity log tabs fixes elastic/security-team/issues/2395 * update isolation action icons fixes elastic/security-team/issues/2397 * update link copy fixes security-team/issues/2398 * fix gutter size fixes elastic/security-team/issues/2396 * fix refresh button fill color fixes elastic/security-team/issues/2396 * rename review changes * redundant classnames, implicit prop value review change * consistent prop name all the way through Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4c1d1aef73
commit
b1b3726458
15 changed files with 76 additions and 153 deletions
|
@ -32,33 +32,34 @@ const QueryStringInput = withKibana(QueryStringInputUI);
|
|||
|
||||
// @internal
|
||||
export interface QueryBarTopRowProps {
|
||||
query?: Query;
|
||||
onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void;
|
||||
onChange: (payload: { dateRange: TimeRange; query?: Query }) => void;
|
||||
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
||||
customSubmitButton?: any;
|
||||
dataTestSubj?: string;
|
||||
disableAutoFocus?: boolean;
|
||||
screenTitle?: string;
|
||||
indexPatterns?: Array<IIndexPattern | string>;
|
||||
isLoading?: boolean;
|
||||
prepend?: React.ComponentProps<typeof EuiFieldText>['prepend'];
|
||||
showQueryInput?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
dateRangeFrom?: string;
|
||||
dateRangeTo?: string;
|
||||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
customSubmitButton?: any;
|
||||
isDirty: boolean;
|
||||
timeHistory?: TimeHistoryContract;
|
||||
indicateNoData?: boolean;
|
||||
disableAutoFocus?: boolean;
|
||||
fillSubmitButton: boolean;
|
||||
iconType?: EuiIconProps['type'];
|
||||
placeholder?: string;
|
||||
indexPatterns?: Array<IIndexPattern | string>;
|
||||
indicateNoData?: boolean;
|
||||
isClearable?: boolean;
|
||||
isDirty: boolean;
|
||||
isLoading?: boolean;
|
||||
isRefreshPaused?: boolean;
|
||||
nonKqlMode?: 'lucene' | 'text';
|
||||
nonKqlModeHelpText?: string;
|
||||
onChange: (payload: { dateRange: TimeRange; query?: Query }) => void;
|
||||
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void;
|
||||
placeholder?: string;
|
||||
prepend?: React.ComponentProps<typeof EuiFieldText>['prepend'];
|
||||
query?: Query;
|
||||
refreshInterval?: number;
|
||||
screenTitle?: string;
|
||||
showQueryInput?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
timeHistory?: TimeHistoryContract;
|
||||
timeRangeForSuggestionsOverride?: boolean;
|
||||
}
|
||||
|
||||
|
@ -229,7 +230,7 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) {
|
|||
isDisabled={isDateRangeInvalid}
|
||||
isLoading={props.isLoading}
|
||||
onClick={onClickSubmitButton}
|
||||
fill={false}
|
||||
fill={props.fillSubmitButton}
|
||||
data-test-subj="querySubmitButton"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -77,6 +77,8 @@ export interface SearchBarOwnProps {
|
|||
nonKqlModeHelpText?: string;
|
||||
// defines padding; use 'inPage' to avoid extra padding; use 'detached' if the searchBar appears at the very top of the view, without any wrapper
|
||||
displayStyle?: 'inPage' | 'detached';
|
||||
// super update button background fill control
|
||||
fillSubmitButton?: boolean;
|
||||
}
|
||||
|
||||
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
||||
|
@ -365,6 +367,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
onSubmit={this.onQueryBarSubmit}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
isLoading={this.props.isLoading}
|
||||
fillSubmitButton={this.props.fillSubmitButton || false}
|
||||
prepend={this.props.showFilterBar ? savedQueryManagement : undefined}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
dateRangeFrom={this.state.dateRangeFrom}
|
||||
|
|
|
@ -61,6 +61,7 @@ export const AdminSearchBar = memo(() => {
|
|||
indexPatterns={clonedIndexPatterns}
|
||||
timeHistory={timeHistory}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
fillSubmitButton={true}
|
||||
isLoading={false}
|
||||
iconType="search"
|
||||
showFilterBar={false}
|
||||
|
|
|
@ -5,15 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo, useMemo, MouseEventHandler } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './flyout_sub_header';
|
||||
import { EuiButtonEmpty, CommonProps } from '@elastic/eui';
|
||||
import { getEndpointDetailsPath } from '../../../../../common/routing';
|
||||
import { useNavigateByRouterEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
|
||||
import { useEndpointSelector } from '../../hooks';
|
||||
import { uiQueryParams } from '../../../store/selectors';
|
||||
import { useAppUrl } from '../../../../../../common/lib/kibana/hooks';
|
||||
|
||||
type BackButtonProps = CommonProps & {
|
||||
backButton?: {
|
||||
title: string;
|
||||
onClick: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
href?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const BackToEndpointDetailsFlyoutSubHeader = memo<{ endpointId: string }>(
|
||||
({ endpointId }) => {
|
||||
const { getAppUrl } = useAppUrl();
|
||||
|
@ -28,13 +36,11 @@ export const BackToEndpointDetailsFlyoutSubHeader = memo<{ endpointId: string }>
|
|||
}),
|
||||
[currentUrlQueryParams, endpointId]
|
||||
);
|
||||
|
||||
const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsRoutePath);
|
||||
|
||||
const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => {
|
||||
const backButtonProps = useMemo((): BackButtonProps['backButton'] => {
|
||||
return {
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.policyResponse.backLinkTitle', {
|
||||
defaultMessage: 'Endpoint Details',
|
||||
defaultMessage: 'Endpoint details',
|
||||
}),
|
||||
href: getAppUrl({ path: detailsRoutePath }),
|
||||
onClick: backToDetailsClickHandler,
|
||||
|
@ -42,10 +48,19 @@ export const BackToEndpointDetailsFlyoutSubHeader = memo<{ endpointId: string }>
|
|||
}, [backToDetailsClickHandler, getAppUrl, detailsRoutePath]);
|
||||
|
||||
return (
|
||||
<FlyoutSubHeader
|
||||
backButton={backButtonProp}
|
||||
data-test-subj="endpointDetailsPolicyResponseFlyoutHeader"
|
||||
/>
|
||||
<div>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiButtonEmpty
|
||||
flush="both"
|
||||
data-test-subj="flyoutSubHeaderBackButton"
|
||||
iconType="arrowLeft"
|
||||
size="xs"
|
||||
href={backButtonProps?.href ?? ''}
|
||||
onClick={backButtonProps?.onClick}
|
||||
>
|
||||
{backButtonProps?.title}
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -68,7 +68,9 @@ export const EndpointDetailsFlyoutTabs = memo(
|
|||
return (
|
||||
<>
|
||||
<EndpointDetailsFlyoutHeader hostname={hostname} hasBorder>
|
||||
<EuiTabs style={{ marginBottom: '-25px' }}>{renderTabs}</EuiTabs>
|
||||
<EuiTabs bottomBorder={false} style={{ marginBottom: '-25px' }}>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
</EndpointDetailsFlyoutHeader>
|
||||
<EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody">
|
||||
{selectedTab?.content}
|
||||
|
|
|
@ -9,10 +9,9 @@ import React, { memo, useCallback, useState } from 'react';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { EuiForm } from '@elastic/eui';
|
||||
import { EuiForm, EuiFlyoutBody } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HostMetadata } from '../../../../../../../common/endpoint/types';
|
||||
import { BackToEndpointDetailsFlyoutSubHeader } from './back_to_endpoint_details_flyout_subheader';
|
||||
import {
|
||||
EndpointIsolatedFormProps,
|
||||
EndpointIsolateForm,
|
||||
|
@ -20,7 +19,6 @@ import {
|
|||
EndpointUnisolateForm,
|
||||
ActionCompletionReturnButton,
|
||||
} from '../../../../../../common/components/endpoint/host_isolation';
|
||||
import { FlyoutBodyNoTopPadding } from './flyout_body_no_top_padding';
|
||||
import { getEndpointDetailsPath } from '../../../../../common/routing';
|
||||
import { useEndpointSelector } from '../../hooks';
|
||||
import {
|
||||
|
@ -87,15 +85,13 @@ export const EndpointIsolationFlyoutPanel = memo<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<BackToEndpointDetailsFlyoutSubHeader endpointId={hostMeta.agent.id} />
|
||||
|
||||
{wasSuccessful && (
|
||||
<EndpointIsolateSuccess
|
||||
hostName={hostMeta.host.name}
|
||||
isolateAction={isCurrentlyIsolated ? 'unisolateHost' : 'isolateHost'}
|
||||
/>
|
||||
)}
|
||||
<FlyoutBodyNoTopPadding>
|
||||
<EuiFlyoutBody>
|
||||
{wasSuccessful ? (
|
||||
<ActionCompletionReturnButton
|
||||
onClick={handleCancel}
|
||||
|
@ -120,7 +116,7 @@ export const EndpointIsolationFlyoutPanel = memo<{
|
|||
/>
|
||||
</EuiForm>
|
||||
)}
|
||||
</FlyoutBodyNoTopPadding>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,19 +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 styled from 'styled-components';
|
||||
import { EuiFlyoutBody } from '@elastic/eui';
|
||||
|
||||
/**
|
||||
* Removes the `padding-top` from the `EuiFlyoutBody` component. Normally done when there is a
|
||||
* sub-header present above the flyout body.
|
||||
*/
|
||||
export const FlyoutBodyNoTopPadding = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflowContent {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
|
@ -9,13 +9,16 @@ import React, { memo } from 'react';
|
|||
import { EuiFlyoutHeader, EuiLoadingContent, EuiToolTip, EuiTitle } from '@elastic/eui';
|
||||
import { useEndpointSelector } from '../../hooks';
|
||||
import { detailsLoading } from '../../../store/selectors';
|
||||
import { BackToEndpointDetailsFlyoutSubHeader } from './back_to_endpoint_details_flyout_subheader';
|
||||
|
||||
export const EndpointDetailsFlyoutHeader = memo(
|
||||
({
|
||||
endpointId,
|
||||
hasBorder = false,
|
||||
hostname,
|
||||
children,
|
||||
}: {
|
||||
endpointId?: string;
|
||||
hasBorder?: boolean;
|
||||
hostname?: string;
|
||||
children?: React.ReactNode | React.ReactNodeArray;
|
||||
|
@ -24,6 +27,8 @@ export const EndpointDetailsFlyoutHeader = memo(
|
|||
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder={hasBorder}>
|
||||
{endpointId && <BackToEndpointDetailsFlyoutSubHeader endpointId={endpointId} />}
|
||||
|
||||
{hostDetailsLoading ? (
|
||||
<EuiLoadingContent lines={1} />
|
||||
) : (
|
||||
|
|
|
@ -1,79 +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 React, { memo, MouseEventHandler } from 'react';
|
||||
import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type FlyoutSubHeaderProps = CommonProps & {
|
||||
children?: React.ReactNode;
|
||||
backButton?: {
|
||||
title: string;
|
||||
onClick: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
href?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)`
|
||||
padding: ${(props) => props.theme.eui.paddingSizes.s};
|
||||
|
||||
&.hasButtons {
|
||||
.buttons {
|
||||
padding-bottom: ${(props) => props.theme.eui.paddingSizes.s};
|
||||
}
|
||||
|
||||
.flyoutSubHeaderBackButton {
|
||||
font-size: ${(props) => props.theme.eui.euiFontSizeXS};
|
||||
}
|
||||
.back-button-content {
|
||||
padding-left: 0;
|
||||
&-text {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flyout-content {
|
||||
padding-left: ${(props) => props.theme.eui.paddingSizes.m};
|
||||
}
|
||||
`;
|
||||
|
||||
const BUTTON_CONTENT_PROPS = Object.freeze({ className: 'back-button-content' });
|
||||
const BUTTON_TEXT_PROPS = Object.freeze({ className: 'back-button-content-text' });
|
||||
|
||||
/**
|
||||
* A Eui Flyout Header component that has its styles adjusted to display a panel sub-header.
|
||||
* Component also provides a way to display a "back" button above the header title.
|
||||
*/
|
||||
export const FlyoutSubHeader = memo<FlyoutSubHeaderProps>(
|
||||
({ children, backButton, ...otherProps }) => {
|
||||
return (
|
||||
<StyledEuiFlyoutHeader {...otherProps} className={backButton && `hasButtons`}>
|
||||
{backButton && (
|
||||
<div className="buttons">
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="flyoutSubHeaderBackButton"
|
||||
iconType="arrowLeft"
|
||||
contentProps={BUTTON_CONTENT_PROPS}
|
||||
textProps={BUTTON_TEXT_PROPS}
|
||||
size="xs"
|
||||
href={backButton?.href ?? ''}
|
||||
onClick={backButton?.onClick}
|
||||
className="flyoutSubHeaderBackButton"
|
||||
>
|
||||
{backButton?.title}
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
)}
|
||||
<div className={'flyout-content'}>{children}</div>
|
||||
</StyledEuiFlyoutHeader>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FlyoutSubHeader.displayName = 'FlyoutSubHeader';
|
|
@ -38,13 +38,11 @@ import {
|
|||
import { useEndpointSelector } from '../hooks';
|
||||
import * as i18 from '../translations';
|
||||
import { ActionsMenu } from './components/actions_menu';
|
||||
import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpoint_details_flyout_subheader';
|
||||
import {
|
||||
EndpointDetailsFlyoutTabs,
|
||||
EndpointDetailsTabsTypes,
|
||||
} from './components/endpoint_details_tabs';
|
||||
import { EndpointIsolationFlyoutPanel } from './components/endpoint_isolate_flyout_panel';
|
||||
import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding';
|
||||
import { EndpointDetailsFlyoutHeader } from './components/flyout_header';
|
||||
import { EndpointActivityLog } from './endpoint_activity_log';
|
||||
import { EndpointDetailsContent } from './endpoint_details_content';
|
||||
|
@ -126,7 +124,11 @@ export const EndpointDetails = memo(() => {
|
|||
return (
|
||||
<>
|
||||
{(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && (
|
||||
<EndpointDetailsFlyoutHeader hostname={hostDetails?.host?.hostname} />
|
||||
<EndpointDetailsFlyoutHeader
|
||||
hasBorder
|
||||
endpointId={hostDetails?.agent.id}
|
||||
hostname={hostDetails?.host?.hostname}
|
||||
/>
|
||||
)}
|
||||
{hostDetails === undefined ? (
|
||||
<EuiFlyoutBody>
|
||||
|
@ -174,9 +176,7 @@ const PolicyResponseFlyoutPanel = memo<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<BackToEndpointDetailsFlyoutSubHeader endpointId={hostMeta.agent.id} />
|
||||
|
||||
<FlyoutBodyNoTopPadding
|
||||
<EuiFlyoutBody
|
||||
data-test-subj="endpointDetailsPolicyResponseFlyoutBody"
|
||||
className="endpointDetailsPolicyResponseFlyoutBody"
|
||||
>
|
||||
|
@ -218,7 +218,7 @@ const PolicyResponseFlyoutPanel = memo<{
|
|||
responseAttentionCount={responseAttentionCount}
|
||||
/>
|
||||
)}
|
||||
</FlyoutBodyNoTopPadding>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -65,7 +65,7 @@ export const useEndpointActionItems = (
|
|||
// Un-isolate is always available to users regardless of license level
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'unIsolateLink',
|
||||
icon: 'logoSecurity',
|
||||
icon: 'lockOpen',
|
||||
key: 'unIsolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
|
@ -83,7 +83,7 @@ export const useEndpointActionItems = (
|
|||
// For Platinum++ licenses, users also have ability to isolate
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'isolateLink',
|
||||
icon: 'logoSecurity',
|
||||
icon: 'lock',
|
||||
key: 'isolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
|
|
|
@ -1125,9 +1125,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display policy response sub-panel', async () => {
|
||||
expect(
|
||||
await renderResult.findByTestId('endpointDetailsPolicyResponseFlyoutHeader')
|
||||
).not.toBeNull();
|
||||
expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).not.toBeNull();
|
||||
expect(
|
||||
await renderResult.findByTestId('endpointDetailsPolicyResponseFlyoutBody')
|
||||
).not.toBeNull();
|
||||
|
@ -1214,7 +1212,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should include the back to details link', async () => {
|
||||
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
|
||||
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
|
||||
expect(subHeaderBackLink.textContent).toBe('Endpoint details');
|
||||
expect(subHeaderBackLink.getAttribute('href')).toEqual(
|
||||
`${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=details`
|
||||
);
|
||||
|
|
|
@ -658,7 +658,7 @@ export const EndpointList = () => {
|
|||
</>
|
||||
)}
|
||||
{transformFailedCallout}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
{shouldShowKQLBar && (
|
||||
<EuiFlexItem>
|
||||
<AdminSearchBar />
|
||||
|
|
|
@ -121,7 +121,7 @@ describe('Policy Details', () => {
|
|||
|
||||
const backToListLink = policyView.find('BackToExternalAppButton');
|
||||
expect(backToListLink.prop('backButtonUrl')).toBe(`/app/security${endpointListPath}`);
|
||||
expect(backToListLink.text()).toBe('Back to endpoint hosts');
|
||||
expect(backToListLink.text()).toBe('View all endpoints');
|
||||
|
||||
const pageTitle = policyView.find('span[data-test-subj="header-page-title"]');
|
||||
expect(pageTitle).toHaveLength(1);
|
||||
|
|
|
@ -57,7 +57,7 @@ export const PolicyDetails = React.memo(() => {
|
|||
backButtonLabel: i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policy.details.backToListTitle',
|
||||
{
|
||||
defaultMessage: 'Back to endpoint hosts',
|
||||
defaultMessage: 'View all endpoints',
|
||||
}
|
||||
),
|
||||
backButtonUrl: getAppUrl({ path: endpointListPath }),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue