-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Endpoint] Host Details Policy Response Panel #63518
Changes from 7 commits
9950f9b
a79bc42
a6ea3fa
9e6c3f5
ed9f2c5
8b5182d
72e1c5e
0c9df52
f00bcbb
9b45f61
11e6f0c
135e014
469655f
a29a22f
9069a40
9bfbb22
c99b360
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import React, { memo } 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: (event: React.MouseEvent) => void; | ||
href?: string; | ||
}; | ||
}; | ||
|
||
const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)` | ||
padding: ${props => props.theme.eui.paddingSizes.s}; | ||
|
||
&.hasButtons { | ||
.buttons { | ||
padding-bottom: ${props => props.theme.eui.paddingSizes.s}; | ||
} | ||
|
||
.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 hasBorder {...otherProps} className={backButton && `hasButtons`}> | ||
{backButton && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the purpose of having a second back button? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is only 1 back button - if you look at the screen capture above, you can see the link in the sub-panel. perhaps if I rename the prop to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also - FYI: I made this a separate component because I think we'll have more use cases we will need it - at least that seems to be true for Host details. Once we have that need, we should likely move the component to a higher directory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I was thinking 'second' as in addition to the browser back button. does this do the same thing? or is it different There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes and No :) |
||
<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} | ||
> | ||
{backButton?.title} | ||
</EuiButtonEmpty> | ||
</div> | ||
)} | ||
<div className={'flyout-content'}>{children}</div> | ||
</StyledEuiFlyoutHeader> | ||
); | ||
} | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,31 +4,25 @@ | |
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import React, { useCallback, useMemo, memo, useEffect } from 'react'; | ||
import styled from 'styled-components'; | ||
import { | ||
EuiFlyout, | ||
EuiFlyoutBody, | ||
EuiFlyoutHeader, | ||
EuiTitle, | ||
EuiDescriptionList, | ||
EuiLoadingContent, | ||
EuiHorizontalRule, | ||
EuiHealth, | ||
EuiSpacer, | ||
EuiHorizontalRule, | ||
EuiLink, | ||
EuiListGroup, | ||
EuiListGroupItem, | ||
} from '@elastic/eui'; | ||
import { useHistory } from 'react-router-dom'; | ||
import styled from 'styled-components'; | ||
import { i18n } from '@kbn/i18n'; | ||
import React, { memo, useMemo } from 'react'; | ||
import { FormattedMessage } from '@kbn/i18n/react'; | ||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; | ||
import { HostMetadata } from '../../../../../common/types'; | ||
import { useHostListSelector } from './hooks'; | ||
import { urlFromQueryParams } from './url_from_query_params'; | ||
import { FormattedDateAndTime } from '../formatted_date_time'; | ||
import { uiQueryParams, detailsData, detailsError } from './../../store/hosts/selectors'; | ||
import { LinkToApp } from '../components/link_to_app'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { useHistory } from 'react-router-dom'; | ||
import { HostMetadata } from '../../../../../../common/types'; | ||
import { FormattedDateAndTime } from '../../formatted_date_time'; | ||
import { LinkToApp } from '../../components/link_to_app'; | ||
import { useHostListSelector, useHostLogsUrl } from '../hooks'; | ||
import { urlFromQueryParams } from '../url_from_query_params'; | ||
import { uiQueryParams } from '../../../store/hosts/selectors'; | ||
|
||
const HostIds = styled(EuiListGroupItem)` | ||
margin-top: 0; | ||
|
@@ -37,8 +31,10 @@ const HostIds = styled(EuiListGroupItem)` | |
} | ||
`; | ||
|
||
const HostDetails = memo(({ details }: { details: HostMetadata }) => { | ||
export const HostDetails = memo(({ details }: { details: HostMetadata }) => { | ||
const { appId, appPath, url } = useHostLogsUrl(details.host.id); | ||
const queryParams = useHostListSelector(uiQueryParams); | ||
const history = useHistory(); | ||
const detailsResultsUpper = useMemo(() => { | ||
return [ | ||
{ | ||
|
@@ -62,6 +58,14 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { | |
]; | ||
}, [details]); | ||
|
||
const policyResponseUri = useMemo(() => { | ||
return urlFromQueryParams({ | ||
...queryParams, | ||
selected_host: details.host.id, | ||
show: 'policy_response', | ||
}); | ||
}, [details.host.id, queryParams]); | ||
|
||
const detailsResultsLower = useMemo(() => { | ||
return [ | ||
{ | ||
|
@@ -74,7 +78,24 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { | |
title: i18n.translate('xpack.endpoint.host.details.policyStatus', { | ||
defaultMessage: 'Policy Status', | ||
}), | ||
description: <EuiHealth color="success">active</EuiHealth>, | ||
description: ( | ||
<EuiHealth color="success"> | ||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */} | ||
<EuiLink | ||
data-test-subj="hostnameCellLink" | ||
href={'?' + policyResponseUri.search} | ||
onClick={(ev: React.MouseEvent) => { | ||
ev.preventDefault(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this use the same logic as we do here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_to_app_event_handler.ts#L42 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No - its different here. In this case, its not a navigate to app, but rather a direct router history update. @kevinlog is assigned to issue 230 which will (I assume) add an additional hook similar to the one you referenced above, but that will use react-router's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paul-tavares gotcha. re: using the same logic, I meant should
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I thought about that. I'm hoping we will have a true hook implemented soon so that we don't keep duplicating this type of code (increase tech. debt). There are a few links already doing this through out our codebase @kevinlog you ok if I just go ahead and implement the hook for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paul-tavares yes, sounds good. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. If @oatkiller is ok with it, then I will work on that Issue next and will search/replace usages in our code base with the new callback. |
||
history.push(policyResponseUri); | ||
}} | ||
> | ||
<FormattedMessage | ||
id="xpack.endpoint.host.details.policyStatus.success" | ||
defaultMessage="Successful" | ||
/> | ||
</EuiLink> | ||
</EuiHealth> | ||
), | ||
}, | ||
{ | ||
title: i18n.translate('xpack.endpoint.host.details.ipAddress', { | ||
|
@@ -101,7 +122,15 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { | |
description: details.agent.version, | ||
}, | ||
]; | ||
}, [details.agent.version, details.endpoint.policy.id, details.host.hostname, details.host.ip]); | ||
}, [ | ||
details.agent.version, | ||
details.endpoint.policy.id, | ||
details.host.hostname, | ||
details.host.ip, | ||
history, | ||
policyResponseUri, | ||
]); | ||
|
||
return ( | ||
<> | ||
<EuiDescriptionList | ||
|
@@ -132,69 +161,3 @@ const HostDetails = memo(({ details }: { details: HostMetadata }) => { | |
</> | ||
); | ||
}); | ||
|
||
export const HostDetailsFlyout = () => { | ||
const history = useHistory(); | ||
const { notifications } = useKibana(); | ||
const queryParams = useHostListSelector(uiQueryParams); | ||
const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; | ||
const details = useHostListSelector(detailsData); | ||
const error = useHostListSelector(detailsError); | ||
|
||
const handleFlyoutClose = useCallback(() => { | ||
history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); | ||
}, [history, queryParamsWithoutSelectedHost]); | ||
|
||
useEffect(() => { | ||
if (error !== undefined) { | ||
notifications.toasts.danger({ | ||
title: ( | ||
<FormattedMessage | ||
id="xpack.endpoint.host.details.errorTitle" | ||
defaultMessage="Could not find host" | ||
/> | ||
), | ||
body: ( | ||
<FormattedMessage | ||
id="xpack.endpoint.host.details.errorBody" | ||
defaultMessage="Please exit the flyout and select an available host." | ||
/> | ||
), | ||
toastLifeTimeMs: 10000, | ||
}); | ||
} | ||
}, [error, notifications.toasts]); | ||
|
||
return ( | ||
<EuiFlyout onClose={handleFlyoutClose} data-test-subj="hostDetailsFlyout"> | ||
<EuiFlyoutHeader hasBorder> | ||
<EuiTitle size="s"> | ||
<h2 data-test-subj="hostDetailsFlyoutTitle"> | ||
{details === undefined ? <EuiLoadingContent lines={1} /> : details.host.hostname} | ||
</h2> | ||
</EuiTitle> | ||
</EuiFlyoutHeader> | ||
<EuiFlyoutBody> | ||
{details === undefined ? ( | ||
<> | ||
<EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} /> | ||
</> | ||
) : ( | ||
<HostDetails details={details} /> | ||
)} | ||
</EuiFlyoutBody> | ||
</EuiFlyout> | ||
); | ||
}; | ||
|
||
const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { | ||
const { services } = useKibana(); | ||
return useMemo(() => { | ||
const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; | ||
return { | ||
url: `${services.application.getUrlForApp('logs')}${appPath}`, | ||
appId: 'logs', | ||
appPath, | ||
}; | ||
}, [hostId, services.application]); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question about why you have to object.freeze this object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am defining this constant at the module level and want to ensure it is never mutated accidentally, since that would impact future instantiations of the component.
Maybe I should have also used a type to set the value as Immutable.