-
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
[Security Solution][Resolver] Word-break long titles in related event… #75926
Changes from 12 commits
b1e2bac
dd77617
889c2f2
69d3bf8
2ba7272
61a84a7
5c9b508
a96fb7b
f9c5278
0c80365
ff229ec
8389676
4c48fc5
2a10e03
b64164f
9efb55a
0be1d08
e7c1134
bccb7ab
9bc1de8
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 |
---|---|---|
|
@@ -10,58 +10,18 @@ import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from ' | |
import styled from 'styled-components'; | ||
import { useSelector } from 'react-redux'; | ||
import { FormattedMessage } from 'react-intl'; | ||
import { | ||
CrumbInfo, | ||
formatDate, | ||
StyledBreadcrumbs, | ||
BoldCode, | ||
StyledTime, | ||
} from './panel_content_utilities'; | ||
import { CrumbInfo, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; | ||
import * as event from '../../../../common/endpoint/models/event'; | ||
import { ResolverEvent } from '../../../../common/endpoint/types'; | ||
import * as selectors from '../../store/selectors'; | ||
import { useResolverDispatch } from '../use_resolver_dispatch'; | ||
import { PanelContentError } from './panel_content_error'; | ||
|
||
/** | ||
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. ℹ️ moved to selector |
||
* A helper function to turn objects into EuiDescriptionList entries. | ||
* This reflects the strategy of more or less "dumping" metadata for related processes | ||
* in description lists with little/no 'prettification'. This has the obvious drawback of | ||
* data perhaps appearing inscrutable/daunting, but the benefit of presenting these fields | ||
* to the user "as they occur" in ECS, which may help them with e.g. EQL queries. | ||
* | ||
* Given an object like: {a:{b: 1}, c: 'd'} it will yield title/description entries like so: | ||
* {title: "a.b", description: "1"}, {title: "c", description: "d"} | ||
* | ||
* @param {object} obj The object to turn into `<dt><dd>` entries | ||
*/ | ||
const objectToDescriptionListEntries = function* ( | ||
obj: object, | ||
prefix = '' | ||
): Generator<{ title: string; description: string }> { | ||
const nextPrefix = prefix.length ? `${prefix}.` : ''; | ||
for (const [metaKey, metaValue] of Object.entries(obj)) { | ||
if (typeof metaValue === 'number' || typeof metaValue === 'string') { | ||
yield { title: nextPrefix + metaKey, description: `${metaValue}` }; | ||
} else if (metaValue instanceof Array) { | ||
yield { | ||
title: nextPrefix + metaKey, | ||
description: metaValue | ||
.filter((arrayEntry) => { | ||
return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; | ||
}) | ||
.join(','), | ||
}; | ||
} else if (typeof metaValue === 'object') { | ||
yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); | ||
} | ||
} | ||
}; | ||
|
||
// Adding some styles to prevent horizontal scrollbars, per request from UX review | ||
const StyledDescriptionList = memo(styled(EuiDescriptionList)` | ||
&.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { | ||
max-width: 8em; | ||
overflow-wrap: break-word; | ||
} | ||
bkimmel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
&.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { | ||
max-width: calc(100% - 8.5em); | ||
|
@@ -90,6 +50,39 @@ const TitleHr = memo(() => { | |
}); | ||
TitleHr.displayName = 'TitleHR'; | ||
|
||
// Indicates if the User Agent supports <wbr/> | ||
const noWBRTagSupport = document.createElement('wbr') instanceof HTMLUnknownElement; | ||
bkimmel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Take description list entries and prepare them for display by | ||
* replacing Zero-Width spaces with <wbr /> tags. | ||
* | ||
* @param entries {title: string, description: string}[] | ||
*/ | ||
function entriesForDisplay(entries: Array<{ title: string; description: string }>) { | ||
return noWBRTagSupport | ||
? entries | ||
: entries.map((entry) => { | ||
return { | ||
...entry, | ||
title: ( | ||
<> | ||
{entry.title.split('\u200b').map((titlePart, i) => { | ||
return i ? ( | ||
<> | ||
<wbr /> | ||
{titlePart} | ||
</> | ||
) : ( | ||
<>{titlePart}</> | ||
); | ||
})} | ||
</> | ||
), | ||
}; | ||
}); | ||
} | ||
|
||
/** | ||
* This view presents a detailed view of all the available data for a related event, split and titled by the "section" | ||
* it appears in the underlying ResolverEvent | ||
|
@@ -138,60 +131,17 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ | |
} | ||
}, [relatedsReady, dispatch, processEntityId]); | ||
|
||
const relatedEventsForThisProcess = useSelector(selectors.relatedEventsByEntityId).get( | ||
processEntityId! | ||
const [ | ||
relatedEventToShowDetailsFor, | ||
countBySameCategory, | ||
relatedEventCategory = naString, | ||
sections, | ||
formattedDate, | ||
] = useSelector(selectors.relatedEventDisplayInfoByEntityAndSelfId)( | ||
processEntityId, | ||
relatedEventId | ||
); | ||
|
||
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. ℹ️ pulled a bunch of this junk into a selector 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. I see the rationale of having view specific logic like this tied with the component rather than the data. Even though the it doesn't actually change any of the raw data in state, I think of selectors as specifically handling some kind of expensive/calculative logic rather than anything display related. Maybe a bit off here, but I consider this more fitting in the 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. Thinking out loud a bit, I think something a la a 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. I think most of this stuff here does things that Robert has referred to in the past as "business logic" like counting things, filtering things out, selecting which category to display, etc. (and prefers that they be done in selectors) but I completely agree about thinking a little more deeply about how we organize visual components. I ended up bringing the "break-hinting" stuff back up to the front, b/c @kqualters-elastic made a good point that putting it in the selector just creates another place it can break (and Robert already had that GeneratedText thing done) but that might be a good first entry in that |
||
const [relatedEventToShowDetailsFor, countBySameCategory, relatedEventCategory] = useMemo(() => { | ||
if (!relatedEventsForThisProcess) { | ||
return [undefined, 0]; | ||
} | ||
const specificEvent = relatedEventsForThisProcess.events.find( | ||
(evt) => event.eventId(evt) === relatedEventId | ||
); | ||
// For breadcrumbs: | ||
const specificCategory = specificEvent && event.primaryEventCategory(specificEvent); | ||
const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { | ||
return event.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; | ||
}, 0); | ||
return [specificEvent, countOfCategory, specificCategory || naString]; | ||
}, [relatedEventsForThisProcess, naString, relatedEventId]); | ||
|
||
const [sections, formattedDate] = useMemo(() => { | ||
if (!relatedEventToShowDetailsFor) { | ||
// This could happen if user relaods from URL param and requests an eventId that no longer exists | ||
return [[], naString]; | ||
} | ||
// Assuming these details (agent, ecs, process) aren't as helpful, can revisit | ||
const { | ||
agent, | ||
ecs, | ||
process, | ||
...relevantData | ||
} = relatedEventToShowDetailsFor as ResolverEvent & { | ||
// Type this with various unknown keys so that ts will let us delete those keys | ||
ecs: unknown; | ||
process: unknown; | ||
}; | ||
let displayDate = ''; | ||
const sectionData: Array<{ | ||
sectionTitle: string; | ||
entries: Array<{ title: string; description: string }>; | ||
}> = Object.entries(relevantData) | ||
.map(([sectionTitle, val]) => { | ||
if (sectionTitle === '@timestamp') { | ||
displayDate = formatDate(val); | ||
return { sectionTitle: '', entries: [] }; | ||
} | ||
if (typeof val !== 'object') { | ||
return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; | ||
} | ||
return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; | ||
}) | ||
.filter((v) => v.sectionTitle !== '' && v.entries.length); | ||
return [sectionData, displayDate]; | ||
}, [relatedEventToShowDetailsFor, naString]); | ||
|
||
const waitCrumbs = useMemo(() => { | ||
return [ | ||
{ | ||
|
@@ -347,6 +297,12 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ | |
</EuiText> | ||
<EuiSpacer size="l" /> | ||
{sections.map(({ sectionTitle, entries }, index) => { | ||
/** | ||
* Replace Zero-Width Spaces with <wbr/> if the User Agent supports it. | ||
* Both will hint breaking opportunities, but <wbr/> do not copy/paste | ||
* so it's preferable to use them if the UA allows it. | ||
*/ | ||
const displayEntries = entriesForDisplay(entries); | ||
return ( | ||
<Fragment key={index}> | ||
{index === 0 ? null : <EuiSpacer size="m" />} | ||
|
@@ -364,7 +320,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ | |
align="left" | ||
titleProps={{ className: 'desc-title' }} | ||
compressed | ||
listItems={entries} | ||
listItems={displayEntries} | ||
/> | ||
{index === sections.length - 1 ? null : <EuiSpacer size="m" />} | ||
</Fragment> | ||
|
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.
❔ @oatkiller I ended up pulling this into the selector here. Is this OK?