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 (