[One Discover][Security Solution] Replace the use of Ecsflat with fieldsMetadata (#225105)

## Summary

As per
[comments](https://github.com/elastic/kibana/pull/204756#discussion_r2162038673)
by @davismcphee , this PR removes the usage of `EcsFlat` and replaces it
with `fieldsMetadata`.
This commit is contained in:
Jatin Kathuria 2025-06-25 08:28:05 +02:00 committed by GitHub
parent 401ddc0d56
commit 2ed4e8a341
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 13 deletions

View file

@ -16,6 +16,7 @@ export const ERROR_MESSAGE_FIELD = 'error.message';
export const EVENT_ORIGINAL_FIELD = 'event.original';
export const EVENT_OUTCOME_FIELD = 'event.outcome';
export const INDEX_FIELD = '_index';
export const EVENT_CATEGORY_FIELD = 'event.category';
// Trace fields
export const TRACE_ID_FIELD = 'trace.id';

View file

@ -12,7 +12,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { AlertEventOverview } from './alert_event_overview';
import type { DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { EcsFlat } from '@elastic/ecs';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { encode } from '@kbn/rison';
import { URLSearchParams } from 'url';
@ -27,6 +26,19 @@ const mockDiscoverServices = {
application: {
getUrlForApp: mockGetUrlForApp,
},
fieldsMetadata: {
useFieldsMetadata: jest.fn().mockReturnValue({
fieldsMetadata: {
'event.category': {
allowed_values: [
{ name: 'process', description: 'Process events' },
{ name: 'network', description: 'Network events' },
],
},
},
loading: false,
}),
},
};
const mockRow = {
@ -36,6 +48,7 @@ const mockRow = {
_id: 'test-id',
'@timestamp': '2021-08-02T14:00:00.000Z',
'kibana.alert.url': 'test-url',
'event.category': 'process',
};
const mockHit = {
@ -87,10 +100,7 @@ describe('AlertEventOverview', () => {
render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
expect(screen.getByTestId('expandableContent-About')).toHaveTextContent(
EcsFlat['event.category'].allowed_values.find((i) => i.name === 'process')
?.description as string
);
expect(screen.getByTestId('expandableContent-About')).toHaveTextContent('Process events');
});
test('should display timeline redirect url correctly', () => {
@ -142,5 +152,73 @@ describe('AlertEventOverview', () => {
`test-timeline-url?${searchParams}`
);
});
describe('ECS Description', () => {
test('should give ECS description for event.category field', () => {
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent('Process events');
});
test('should give placeholder ECS description when fieldsMetadata is not available', () => {
(useDiscoverServices as jest.Mock).mockReturnValue({
...mockDiscoverServices,
fieldsMetadata: {
useFieldsMetadata: jest.fn().mockReturnValue({
fieldsMetadata: undefined,
loading: false,
}),
},
});
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent(
"This field doesn't have a description because it's not part of ECS."
);
});
test('should give placeholder ECS description when event.category field is not present in fieldMetada', () => {
(useDiscoverServices as jest.Mock).mockReturnValue({
...mockDiscoverServices,
fieldsMetadata: {
useFieldsMetadata: jest.fn().mockReturnValue({
fieldsMetadata: {},
loading: false,
}),
},
});
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent(
"This field doesn't have a description because it's not part of ECS."
);
});
test('should give placeholder ECS description when event.category field is not present in hit', () => {
const localMockHit = {
flattened: {
...mockRow,
'event.category': undefined,
},
} as unknown as DataTableRecord;
render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent(
"This field doesn't have a description because it's not part of ECS."
);
});
test('should give placeholder ECS when event.category fields has a value that is not in allowed values', () => {
const localMockHit = {
flattened: {
...mockRow,
'event.category': 'unknown',
},
} as unknown as DataTableRecord;
render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
expect(screen.getByTestId('about')).toHaveTextContent(
"This field doesn't have a description because it's not part of ECS."
);
});
});
});
});

View file

@ -9,7 +9,7 @@
import React, { useMemo, useState } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { getFieldValue } from '@kbn/discover-utils';
import { fieldConstants, getFieldValue } from '@kbn/discover-utils';
import type { DocViewerComponent } from '@kbn/unified-doc-viewer/src/services/types';
import {
EuiTitle,
@ -19,6 +19,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiSkeletonText,
} from '@elastic/eui';
import * as i18n from '../translations';
import { getSecurityTimelineRedirectUrl } from '../utils';
@ -62,12 +63,18 @@ export const ExpandableSection: FC<PropsWithChildren<{ title: string }>> = ({
export const AlertEventOverview: DocViewerComponent = ({ hit }) => {
const {
application: { getUrlForApp },
fieldsMetadata,
} = useDiscoverServices();
const timelinesURL = getUrlForApp('securitySolutionUI', {
path: 'alerts',
});
const result = fieldsMetadata?.useFieldsMetadata({
attributes: ['allowed_values', 'name', 'flat_name'],
fieldNames: [fieldConstants.EVENT_CATEGORY_FIELD],
});
const reason = useMemo(() => getFieldValue(hit, 'kibana.alert.reason') as string, [hit]);
const description = useMemo(
() => getFieldValue(hit, 'kibana.alert.rule.description') as string,
@ -100,9 +107,18 @@ export const AlertEventOverview: DocViewerComponent = ({ hit }) => {
>
<EuiFlexItem>
<ExpandableSection title={i18n.aboutSectionTitle}>
<EuiText size="s" data-test-subj="about">
{getEcsAllowedValueDescription(eventCategory)}
</EuiText>
{result?.loading ? (
<EuiSkeletonText
lines={2}
size={'s'}
isLoading={result?.loading}
contentAriaLabel={i18n.ecsDescriptionLoadingAriaLable}
/>
) : (
<EuiText size="s" data-test-subj="about">
{getEcsAllowedValueDescription(result?.fieldsMetadata, eventCategory)}
</EuiText>
)}
</ExpandableSection>
</EuiFlexItem>
{description ? (

View file

@ -54,3 +54,10 @@ export const reasonSectionTitle = i18n.translate(
defaultMessage: 'Reason',
}
);
export const ecsDescriptionLoadingAriaLable = i18n.translate(
'discover.profile.security.flyout.ecsDescriptionLoadingAriaLabel',
{
defaultMessage: 'Loading ECS description',
}
);

View file

@ -7,9 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EcsFlat } from '@elastic/ecs';
import type { UseFieldsMetadataHook } from '@kbn/fields-metadata-plugin/public/hooks/use_fields_metadata';
import * as i18n from '../translations';
export type EcsAllowedValue = (typeof EcsFlat)['event.category']['allowed_values'][0];
/**
* Helper function to return the description of an allowed value of the specified field
@ -17,8 +16,11 @@ export type EcsAllowedValue = (typeof EcsFlat)['event.category']['allowed_values
* @param value
* @returns ecs description of the value
*/
export const getEcsAllowedValueDescription = (value: string): string => {
const allowedValues: EcsAllowedValue[] = EcsFlat['event.category']?.allowed_values ?? [];
export const getEcsAllowedValueDescription = (
fieldsMetadata: ReturnType<UseFieldsMetadataHook>['fieldsMetadata'] = {},
value: string
): string => {
const allowedValues = fieldsMetadata['event.category']?.allowed_values ?? [];
const result =
allowedValues?.find((item) => item.name === value)?.description ?? i18n.noEcsDescriptionReason;
return result;