[Security Solution][Resolver] Word-break long titles in related event… (#75926)
* [Security Solution][Resolver] Word-break long titles in related event description lists * word-break long titles at non-word boundaries Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c08bf7f3ca
commit
42942327e5
12 changed files with 225 additions and 122 deletions
|
@ -11,6 +11,7 @@ import * as selectors from './selectors';
|
|||
import { DataState } from '../../types';
|
||||
import { DataAction } from './action';
|
||||
import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
|
||||
/**
|
||||
* Test the data reducer and selector.
|
||||
|
@ -175,6 +176,24 @@ describe('Resolver Data Middleware', () => {
|
|||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
|
||||
);
|
||||
});
|
||||
it('should return the correct related event detail metadata for a given related event', () => {
|
||||
const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState());
|
||||
const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)(
|
||||
categoryToOverCount
|
||||
)[0];
|
||||
const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!;
|
||||
const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID(
|
||||
store.getState()
|
||||
)(firstChildNodeInTree.id, relatedEventID);
|
||||
const [, countOfSameType, , sectionData] = relatedDisplayInfo;
|
||||
const hostEntries = sectionData.filter((section) => {
|
||||
return section.sectionTitle === 'host';
|
||||
})[0].entries;
|
||||
expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' });
|
||||
expect(countOfSameType).toBe(
|
||||
eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1
|
||||
);
|
||||
});
|
||||
it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => {
|
||||
const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState());
|
||||
const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id)
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
IndexedProcessNode,
|
||||
AABB,
|
||||
VisibleEntites,
|
||||
SectionData,
|
||||
} from '../../types';
|
||||
import {
|
||||
isGraphableProcess,
|
||||
|
@ -29,11 +30,14 @@ import {
|
|||
ResolverNodeStats,
|
||||
ResolverRelatedEvents,
|
||||
SafeResolverEvent,
|
||||
EndpointEvent,
|
||||
LegacyEndpointEvent,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import * as resolverTreeModel from '../../models/resolver_tree';
|
||||
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
|
||||
import * as eventModel from '../../../../common/endpoint/models/event';
|
||||
import * as vector2 from '../../models/vector2';
|
||||
import { formatDate } from '../../view/panels/panel_content_utilities';
|
||||
|
||||
/**
|
||||
* If there is currently a request.
|
||||
|
@ -173,6 +177,100 @@ export function relatedEventsByEntityId(data: DataState): Map<string, ResolverRe
|
|||
return data.relatedEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a function that returns the information needed to display related event details based on
|
||||
* the related event's entityID and its own ID.
|
||||
*/
|
||||
export const relatedEventDisplayInfoByEntityAndSelfID: (
|
||||
state: DataState
|
||||
) => (
|
||||
entityId: string,
|
||||
relatedEventId: string | number
|
||||
) => [
|
||||
EndpointEvent | LegacyEndpointEvent | undefined,
|
||||
number,
|
||||
string | undefined,
|
||||
SectionData,
|
||||
string
|
||||
] = createSelector(relatedEventsByEntityId, function relatedEventDetails(
|
||||
/* eslint-disable no-shadow */
|
||||
relatedEventsByEntityId
|
||||
/* eslint-enable no-shadow */
|
||||
) {
|
||||
return defaultMemoize((entityId: string, relatedEventId: string | number) => {
|
||||
const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId);
|
||||
if (!relatedEventsForThisProcess) {
|
||||
return [undefined, 0, undefined, [], ''];
|
||||
}
|
||||
const specificEvent = relatedEventsForThisProcess.events.find(
|
||||
(evt) => eventModel.eventId(evt) === relatedEventId
|
||||
);
|
||||
// For breadcrumbs:
|
||||
const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent);
|
||||
const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => {
|
||||
return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal;
|
||||
}, 0);
|
||||
|
||||
// Assuming these details (agent, ecs, process) aren't as helpful, can revisit
|
||||
const { agent, ecs, process, ...relevantData } = specificEvent 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: SectionData = 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 [specificEvent, countOfCategory, specificCategory, sectionData, displayDate];
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a function that returns a function (when supplied with an entity id for a node)
|
||||
* that returns related events for a node that match an event.category (when supplied with the category)
|
||||
|
|
|
@ -122,6 +122,15 @@ export const relatedEventsByEntityId = composeSelectors(
|
|||
dataSelectors.relatedEventsByEntityId
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a function that returns the information needed to display related event details based on
|
||||
* the related event's entityID and its own ID.
|
||||
*/
|
||||
export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors(
|
||||
dataStateSelector,
|
||||
dataSelectors.relatedEventDisplayInfoByEntityAndSelfID
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a function that returns a function (when supplied with an entity id for a node)
|
||||
* that returns related events for a node that match an event.category (when supplied with the category)
|
||||
|
|
|
@ -160,6 +160,22 @@ export interface IndexedProcessNode extends BBox {
|
|||
position: Vector2;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type describing the shape of section titles and entries for description lists
|
||||
*/
|
||||
export type SectionData = Array<{
|
||||
sectionTitle: string;
|
||||
entries: Array<{ title: string; description: string }>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* The two query parameters we read/write on to control which view the table presents:
|
||||
*/
|
||||
export interface CrumbInfo {
|
||||
crumbId: string;
|
||||
crumbEvent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type containing all things to actually be rendered to the DOM.
|
||||
*/
|
||||
|
|
|
@ -8,10 +8,11 @@ import React, { memo, useMemo } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTableColumn, EuiButtonEmpty, EuiSpacer, EuiInMemoryTable } from '@elastic/eui';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { StyledBreadcrumbs } from './panel_content_utilities';
|
||||
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
/**
|
||||
* This view gives counts for all the related events of a process grouped by related event type.
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { CrumbInfo, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
/**
|
||||
* Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state.
|
||||
|
|
|
@ -23,14 +23,6 @@ const BetaHeader = styled(`header`)`
|
|||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
/**
|
||||
* The two query parameters we read/write on to control which view the table presents:
|
||||
*/
|
||||
export interface CrumbInfo {
|
||||
crumbId: string;
|
||||
crumbEvent: string;
|
||||
}
|
||||
|
||||
const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>`
|
||||
&.euiBreadcrumbs {
|
||||
background-color: ${(props) => props.background};
|
||||
|
|
|
@ -19,7 +19,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { CrumbInfo, formatDate, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { formatDate, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import {
|
||||
processPath,
|
||||
processPid,
|
||||
|
@ -31,6 +31,7 @@ import {
|
|||
import { CubeForProcess } from './cube_for_process';
|
||||
import { ResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { useResolverTheme } from '../assets';
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
const StyledDescriptionList = styled(EuiDescriptionList)`
|
||||
&.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title {
|
||||
|
|
|
@ -10,18 +10,13 @@ import { EuiTitle, EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from
|
|||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
CrumbInfo,
|
||||
formatDate,
|
||||
StyledBreadcrumbs,
|
||||
BoldCode,
|
||||
StyledTime,
|
||||
} from './panel_content_utilities';
|
||||
import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { useResolverDispatch } from '../use_resolver_dispatch';
|
||||
import { RelatedEventLimitWarning } from '../limit_warnings';
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
/**
|
||||
* This view presents a list of related events of a given type for a given process.
|
||||
|
|
|
@ -16,12 +16,13 @@ import { useSelector } from 'react-redux';
|
|||
import styled from 'styled-components';
|
||||
import * as event from '../../../../common/endpoint/models/event';
|
||||
import * as selectors from '../../store/selectors';
|
||||
import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { formatter, StyledBreadcrumbs } from './panel_content_utilities';
|
||||
import { useResolverDispatch } from '../use_resolver_dispatch';
|
||||
import { SideEffectContext } from '../side_effect_context';
|
||||
import { CubeForProcess } from './cube_for_process';
|
||||
import { SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import { LimitWarning } from '../limit_warnings';
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
const StyledLimitWarning = styled(LimitWarning)`
|
||||
flex-flow: row wrap;
|
||||
|
|
|
@ -10,58 +10,19 @@ 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 { 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';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
import { CrumbInfo } from '../../types';
|
||||
|
||||
// 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;
|
||||
}
|
||||
&.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description {
|
||||
max-width: calc(100% - 8.5em);
|
||||
|
@ -69,6 +30,12 @@ const StyledDescriptionList = memo(styled(EuiDescriptionList)`
|
|||
}
|
||||
`);
|
||||
|
||||
// Also prevents horizontal scrollbars on long descriptive names
|
||||
const StyledDescriptiveName = memo(styled(EuiText)`
|
||||
padding-right: 1em;
|
||||
overflow-wrap: break-word;
|
||||
`);
|
||||
|
||||
// Styling subtitles, per UX review:
|
||||
const StyledFlexTitle = memo(styled('h3')`
|
||||
display: flex;
|
||||
|
@ -90,6 +57,49 @@ const TitleHr = memo(() => {
|
|||
});
|
||||
TitleHr.displayName = 'TitleHR';
|
||||
|
||||
const GeneratedText = React.memo(function ({ children }) {
|
||||
return <>{processedValue()}</>;
|
||||
|
||||
function processedValue() {
|
||||
return React.Children.map(children, (child) => {
|
||||
if (typeof child === 'string') {
|
||||
const valueSplitByWordBoundaries = child.split(/\b/);
|
||||
|
||||
if (valueSplitByWordBoundaries.length < 2) {
|
||||
return valueSplitByWordBoundaries[0];
|
||||
}
|
||||
|
||||
return [
|
||||
valueSplitByWordBoundaries[0],
|
||||
...valueSplitByWordBoundaries
|
||||
.splice(1)
|
||||
.reduce(function (generatedTextMemo: Array<string | JSX.Element>, value, index) {
|
||||
return [...generatedTextMemo, value, <wbr />];
|
||||
}, []),
|
||||
];
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
GeneratedText.displayName = 'GeneratedText';
|
||||
|
||||
/**
|
||||
* Take description list entries and prepare them for display by
|
||||
* seeding with `<wbr />` tags.
|
||||
*
|
||||
* @param entries {title: string, description: string}[]
|
||||
*/
|
||||
function entriesForDisplay(entries: Array<{ title: string; description: string }>) {
|
||||
return entries.map((entry) => {
|
||||
return {
|
||||
description: <GeneratedText>{entry.description}</GeneratedText>,
|
||||
title: <GeneratedText>{entry.title}</GeneratedText>,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +148,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
|
||||
);
|
||||
|
||||
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 [
|
||||
{
|
||||
|
@ -338,15 +305,18 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({
|
|||
</StyledTime>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle"
|
||||
values={{ subject, descriptor }}
|
||||
defaultMessage="{descriptor} {subject}"
|
||||
/>
|
||||
</EuiText>
|
||||
<StyledDescriptiveName>
|
||||
<GeneratedText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle"
|
||||
values={{ subject, descriptor }}
|
||||
defaultMessage="{descriptor} {subject}"
|
||||
/>
|
||||
</GeneratedText>
|
||||
</StyledDescriptiveName>
|
||||
<EuiSpacer size="l" />
|
||||
{sections.map(({ sectionTitle, entries }, index) => {
|
||||
const displayEntries = entriesForDisplay(entries);
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{index === 0 ? null : <EuiSpacer size="m" />}
|
||||
|
@ -364,7 +334,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>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { useQueryStringKeys } from './use_query_string_keys';
|
||||
import { CrumbInfo } from './panels/panel_content_utilities';
|
||||
import { CrumbInfo } from '../types';
|
||||
|
||||
export function useResolverQueryParams() {
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue