diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.test.tsx new file mode 100644 index 0000000000000..39991045b0406 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; +import type { Alert } from '@kbn/alerting-types'; +import { TestProviders } from '../../../../common/mock'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { DatetimeSchemaCellRenderer } from './datetime_schema_cell_renderer'; + +describe('DatetimeSchemaCellRenderer', () => { + it('should handle missing field', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field'; + + const { getByText } = render( + + + + ); + + expect(getByText(getEmptyValue())).toBeInTheDocument(); + }); + + it('should handle string value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('value1')).toBeInTheDocument(); + }); + + it('should handle number value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 123, + }; + const columnId = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('Jan 1, 1970 @ 00:00:00.123')).toBeInTheDocument(); + }); + + it('should handle array of booleans', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [true, false], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('true')).toBeInTheDocument(); + }); + + it('should handle array of numbers', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [1, 2], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('Jan 1, 1970 @ 00:00:00.001')).toBeInTheDocument(); + }); + + it('should handle array of null', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [null, null], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('—')).toBeInTheDocument(); + }); + + it('should join array of JsonObjects', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [{ subField1: 'value1', subField2: 'value2' }], + }; + const field = 'field1'; + + const { getByText } = render( + + + + ); + + expect(getByText('[object Object]')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.tsx new file mode 100644 index 0000000000000..cab59e417be1b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/datetime_schema_cell_renderer.tsx @@ -0,0 +1,39 @@ +/* + * 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, useMemo } from 'react'; +import type { Alert } from '@kbn/alerting-types'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { getAlertFieldValueAsStringOrNumberOrNull } from '../../../utils/type_utils'; + +export interface DatetimeSchemaCellRendererProps { + /** + * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface + */ + alert: Alert; + /** + * Column id passed from the renderCellValue callback via EuiDataGridProps['renderCellValue'] interface + */ + field: string; +} + +/** + * Renders the value of a field of type date (when the schema is 'datetime'). + * Component used in the AI for SOC alert summary table. + */ +export const DatetimeSchemaCellRenderer = memo( + ({ alert, field }: DatetimeSchemaCellRendererProps) => { + const displayValue: number | string | null = useMemo( + () => getAlertFieldValueAsStringOrNumberOrNull(alert, field), + [alert, field] + ); + + return ; + } +); + +DatetimeSchemaCellRenderer.displayName = 'DatetimeSchemaCellRenderer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx index 21aa8e243ebed..7543576694c5a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx @@ -11,7 +11,7 @@ import type { Alert } from '@kbn/alerting-types'; import { CellValue } from './render_cell'; import { TestProviders } from '../../../../common/mock'; import { getEmptyValue } from '../../../../common/components/empty_value'; -import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY, TIMESTAMP } from '@kbn/rule-data-utils'; import { BADGE_TEST_ID } from './kibana_alert_severity_cell_renderer'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses } from '@kbn/fleet-plugin/common/constants'; @@ -41,10 +41,11 @@ describe('CellValue', () => { field1: 'value1', }; const columnId = 'columnId'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -58,10 +59,11 @@ describe('CellValue', () => { field1: 'value1', }; const columnId = 'field1'; + const schema = 'string'; const { getByText } = render( - + ); @@ -75,10 +77,11 @@ describe('CellValue', () => { field1: 123, }; const columnId = 'field1'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -92,10 +95,11 @@ describe('CellValue', () => { field1: [true, false], }; const columnId = 'field1'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -109,10 +113,11 @@ describe('CellValue', () => { field1: [1, 2], }; const columnId = 'field1'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -126,10 +131,11 @@ describe('CellValue', () => { field1: [null, null], }; const columnId = 'field1'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -143,10 +149,11 @@ describe('CellValue', () => { field1: [{ subField1: 'value1', subField2: 'value2' }], }; const columnId = 'field1'; + const schema = 'unknown'; const { getByText } = render( - + ); @@ -160,10 +167,11 @@ describe('CellValue', () => { [ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }], }; const columnId = ALERT_RULE_PARAMETERS; + const schema = 'unknown'; const { getByTestId } = render( - + ); @@ -179,13 +187,32 @@ describe('CellValue', () => { [ALERT_SEVERITY]: ['low'], }; const columnId = ALERT_SEVERITY; + const schema = 'unknown'; const { getByTestId } = render( - + ); expect(getByTestId(BADGE_TEST_ID)).toBeInTheDocument(); }); + + it('should use datetime renderer', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + [TIMESTAMP]: [1735754400000], + }; + const columnId = TIMESTAMP; + const schema = 'datetime'; + + const { getByText } = render( + + + + ); + + expect(getByText('Jan 1, 2025 @ 18:00:00.000')).toBeInTheDocument(); + }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx index ab54d3ed8f885..04730185b9e0a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { memo } from 'react'; -import type { Alert } from '@kbn/alerting-types'; +import React, { type ComponentProps, memo } from 'react'; import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils'; -import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import type { GetTableProp } from './types'; +import { DatetimeSchemaCellRenderer } from './datetime_schema_cell_renderer'; import { BasicCellRenderer } from './basic_cell_renderer'; import { KibanaAlertSeverityCellRenderer } from './kibana_alert_severity_cell_renderer'; import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_related_integrations_cell_renderer'; @@ -16,42 +16,50 @@ import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_relat // guarantees that all cells will have their values vertically centered const styles = { display: 'flex', alignItems: 'center', height: '100%' }; -export interface CellValueProps { +const DATETIME_SCHEMA = 'datetime'; + +export type CellValueProps = Pick< + ComponentProps>, /** * Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface */ - alert: Alert; + | 'alert' /** * Column id passed from the renderCellValue callback via EuiDataGridProps['renderCellValue'] interface */ - columnId: string; + | 'columnId' /** * List of installed AI for SOC integrations. * This comes from the additionalContext property on the table. */ - packages: PackageListItem[]; -} + | 'packages' + /** + * Type of field used to drive how we render the value in the BasicCellRenderer. + * This comes from EuiDataGrid. + */ + | 'schema' +>; /** * Component used in the AI for SOC alert summary table. - * It renders all the values currently as simply as possible (see code comments below). - * It will be soon improved to support custom renders for specific fields (like kibana.alert.rule.parameters and kibana.alert.severity). + * It renders some of the value with custom renderers for some specific columns: + * - kibana.alert.rule.parameters + * - kibana.alert.severity + * It also renders some schema types specifically (this property come from EuiDataGrid): + * - datetime + * Finally it renders the rest as basic strings. */ -export const CellValue = memo(({ alert, columnId, packages }: CellValueProps) => { +export const CellValue = memo(({ alert, columnId, packages, schema }: CellValueProps) => { let component; - switch (columnId) { - case ALERT_RULE_PARAMETERS: - component = ; - break; - - case ALERT_SEVERITY: - component = ; - break; - - default: - component = ; - break; + if (columnId === ALERT_RULE_PARAMETERS) { + component = ; + } else if (columnId === ALERT_SEVERITY) { + component = ; + } else if (schema === DATETIME_SCHEMA) { + component = ; + } else { + component = ; } return
{component}
; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/types.ts new file mode 100644 index 0000000000000..ab25e1f137d00 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/types.ts @@ -0,0 +1,12 @@ +/* + * 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 type { AlertsTablePropsWithRef } from '@kbn/response-ops-alerts-table/types'; +import type { AdditionalTableContext } from './table'; + +export type TableProps = AlertsTablePropsWithRef; +export type GetTableProp = NonNullable; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx index 943b64f5a883c..34ddf88fdb52a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.test.tsx @@ -6,7 +6,11 @@ */ import type { Alert } from '@kbn/alerting-types'; -import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from './type_utils'; +import { + getAlertFieldValueAsStringOrNull, + getAlertFieldValueAsStringOrNumberOrNull, + isJsonObjectValue, +} from './type_utils'; import type { JsonValue } from '@kbn/utility-types'; describe('getAlertFieldValueAsStringOrNull', () => { @@ -102,6 +106,99 @@ describe('getAlertFieldValueAsStringOrNull', () => { }); }); +describe('getAlertFieldValueAsStringOrNumberOrNull', () => { + it('should handle missing field', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'columnId'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toBe(null); + }); + + it('should handle string value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 'value1', + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual('value1'); + }); + + it('should handle a number value', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: 123, + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual(123); + }); + + it('should handle array of booleans', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [true, false], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual('true'); + }); + + it('should handle array of numbers', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [1, 2], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual(1); + }); + + it('should handle array of null', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [null, null], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual(null); + }); + + it('should join array of JsonObjects', () => { + const alert: Alert = { + _id: '_id', + _index: '_index', + field1: [{ subField1: 'value1', subField2: 'value2' }], + }; + const field = 'field1'; + + const result = getAlertFieldValueAsStringOrNumberOrNull(alert, field); + + expect(result).toEqual('[object Object]'); + }); +}); + describe('isJsonObjectValue', () => { it('should return true for JsonObject', () => { const value: JsonValue = { test: 'value' }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts index f05bdef660eed..cf0b9fcaefb74 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/utils/type_utils.ts @@ -41,7 +41,36 @@ export const getAlertFieldValueAsStringOrNull = (alert: Alert, field: string): s }; /** - * Guaratees that the value is of type JsonObject + * Takes an Alert object and a field string as input and returns the value for the field as a string. + * If the value is already a number or a string, return it. + * If the value is an array, return the first value only. + * If null the value is null. + * Return the string of the value otherwise. + */ +export const getAlertFieldValueAsStringOrNumberOrNull = ( + alert: Alert, + field: string +): number | string | null => { + const cellValues: string | number | JsonValue[] = alert[field]; + + if (typeof cellValues === 'number' || typeof cellValues === 'string') { + return cellValues; + } else if (Array.isArray(cellValues)) { + const value: JsonValue = cellValues[0]; + if (typeof value === 'number' || typeof value === 'string') { + return value; + } else if (value == null) { + return null; + } else { + return value.toString(); + } + } else { + return null; + } +}; + +/** + * Guarantees that the value is of type JsonObject */ export const isJsonObjectValue = (value: JsonValue): value is JsonObject => { return (