[AI4DSOC] Alert summary table custom cell renderers (#217124)

## Summary

This PR builds up on the previous [table setup
PR](https://github.com/elastic/kibana/pull/216744) and add custom cell
renderers for the alert summary table:
- we show the package's icon for the Integration column (pointing to the
`kibana.alert.rule.parameters` field)
- we show an EuiBadge for the severity column (pointing to the
`kibana.alert.severity` field)

All the other fields remain unchanged.

| Before  | After |
| ------------- | ------------- |
| ![Screenshot 2025-04-15 at 3 01
01 PM](https://github.com/user-attachments/assets/047c7fd1-3da2-40fd-a0f4-792177454c00)
| ![Screenshot 2025-04-15 at 2 59
20 PM](https://github.com/user-attachments/assets/643510a7-5f12-4084-8101-4f027ea04099)
|

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

https://github.com/elastic/security-team/issues/11973
This commit is contained in:
Philippe Oberti 2025-04-17 19:30:05 +02:00 committed by GitHub
parent 40a9159435
commit 7160b360c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 843 additions and 40 deletions

View file

@ -0,0 +1,135 @@
/*
* 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 { BasicCellRenderer } from './basic_cell_renderer';
import { TestProviders } from '../../../../common/mock';
import { getEmptyValue } from '../../../../common/components/empty_value';
import { CellValue } from './render_cell';
describe('BasicCellRenderer', () => {
it('should handle missing field', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: 'value1',
};
const field = 'field';
const { getByText } = render(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
expect(getByText(getEmptyValue())).toBeInTheDocument();
});
it('should handle string value', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: 'value1',
};
const field = 'field1';
const { getByText } = render(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
expect(getByText('value1')).toBeInTheDocument();
});
it('should handle number value', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: 123,
};
const columnId = 'field1';
const { getByText } = render(
<TestProviders>
<CellValue alert={alert} columnId={columnId} />
</TestProviders>
);
expect(getByText('123')).toBeInTheDocument();
});
it('should handle array of booleans', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [true, false],
};
const field = 'field1';
const { getByText } = render(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
expect(getByText('true, false')).toBeInTheDocument();
});
it('should handle array of numbers', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [1, 2],
};
const field = 'field1';
const { getByText } = render(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
expect(getByText('1, 2')).toBeInTheDocument();
});
it('should handle array of null', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [null, null],
};
const field = 'field1';
const { getByText } = render(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
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(
<TestProviders>
<BasicCellRenderer alert={alert} field={field} />
</TestProviders>
);
expect(getByText('[object Object]')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 { getOrEmptyTagFromValue } from '../../../../common/components/empty_value';
import { getAlertFieldValueAsStringOrNull } from '../../../utils/type_utils';
export interface BasicCellRendererProps {
/**
* 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 all the basic table cell values.
* Component used in the AI for SOC alert summary table.
*/
export const BasicCellRenderer = memo(({ alert, field }: BasicCellRendererProps) => {
const displayValue: string | null = useMemo(
() => getAlertFieldValueAsStringOrNull(alert, field),
[alert, field]
);
return <>{getOrEmptyTagFromValue(displayValue)}</>;
});
BasicCellRenderer.displayName = 'BasicCellRenderer';

View file

@ -0,0 +1,96 @@
/*
* 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 {
ICON_TEST_ID,
KibanaAlertRelatedIntegrationsCellRenderer,
SKELETON_TEST_ID,
} from './kibana_alert_related_integrations_cell_renderer';
import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
describe('KibanaAlertRelatedIntegrationsCellRenderer', () => {
it('should handle missing field', () => {
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
integration: null,
isLoading: false,
});
const alert: Alert = {
_id: '_id',
_index: '_index',
};
const { queryByTestId } = render(<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />);
expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
});
it('should handle not finding matching integration', () => {
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
integration: null,
isLoading: false,
});
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_RULE_PARAMETERS]: ['splunk'],
};
const { queryByTestId } = render(<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />);
expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
});
it('should show loading', () => {
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
integration: null,
isLoading: true,
});
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_RULE_PARAMETERS]: ['splunk'],
};
const { getByTestId, queryByTestId } = render(
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />
);
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
});
it('should show integration icon', () => {
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
integration: { name: 'Splunk', icon: ['icon'] },
isLoading: false,
});
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_RULE_PARAMETERS]: ['splunk'],
};
const { getByTestId, queryByTestId } = render(
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />
);
expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 { JsonValue } from '@kbn/utility-types';
import { CardIcon } from '@kbn/fleet-plugin/public';
import { EuiSkeletonText } from '@elastic/eui';
import type { Alert } from '@kbn/alerting-types';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from '../../../utils/type_utils';
export const SKELETON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-skeleton';
export const ICON_TEST_ID = 'alert-summary-table-related-integrations-cell-renderer-icon';
const RELATED_INTEGRATIONS_FIELD = 'related_integrations';
const PACKAGE_FIELD = 'package';
// function is_string(value: unknown): value is string {}
export interface KibanaAlertRelatedIntegrationsCellRendererProps {
/**
* Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
*/
alert: Alert;
}
/**
* Renders an integration/package icon. Retrieves the package name from the kibana.alert.rule.parameters field in the alert,
* fetches all integrations/packages and use the icon from the one that matches by name.
* Used in AI for SOC alert summary table.
*/
export const KibanaAlertRelatedIntegrationsCellRenderer = memo(
({ alert }: KibanaAlertRelatedIntegrationsCellRendererProps) => {
const packageName: string | null = useMemo(() => {
const values: JsonValue[] | undefined = alert[ALERT_RULE_PARAMETERS];
if (Array.isArray(values) && values.length === 1) {
const value: JsonValue = values[0];
if (!isJsonObjectValue(value)) return null;
const relatedIntegration = value[RELATED_INTEGRATIONS_FIELD];
if (!isJsonObjectValue(relatedIntegration)) return null;
return getAlertFieldValueAsStringOrNull(relatedIntegration as Alert, PACKAGE_FIELD);
}
return null;
}, [alert]);
const { integration, isLoading } = useGetIntegrationFromPackageName({ packageName });
return (
<EuiSkeletonText data-test-subj={SKELETON_TEST_ID} isLoading={isLoading} lines={1}>
{integration ? (
<CardIcon
data-test-subj={ICON_TEST_ID}
icons={integration.icons}
integrationName={integration.title}
packageName={integration.name}
size="l"
version={integration.version}
/>
) : null}
</EuiSkeletonText>
);
}
);
KibanaAlertRelatedIntegrationsCellRenderer.displayName =
'KibanaAlertRelatedIntegrationsCellRenderer';

View file

@ -0,0 +1,101 @@
/*
* 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 {
BADGE_TEST_ID,
KibanaAlertSeverityCellRenderer,
} from './kibana_alert_severity_cell_renderer';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
describe('KibanaAlertSeverityCellRenderer', () => {
it('should handle missing field', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
};
const { container } = render(
<TestProviders>
<KibanaAlertSeverityCellRenderer alert={alert} />
</TestProviders>
);
expect(container).toBeEmptyDOMElement();
});
it('should show low', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_SEVERITY]: ['low'],
};
const { getByTestId } = render(
<TestProviders>
<KibanaAlertSeverityCellRenderer alert={alert} />
</TestProviders>
);
expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Low');
expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #54B399');
});
it('should show medium', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_SEVERITY]: ['medium'],
};
const { getByTestId } = render(
<TestProviders>
<KibanaAlertSeverityCellRenderer alert={alert} />
</TestProviders>
);
expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Medium');
expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #D6BF57');
});
it('should show high', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_SEVERITY]: ['high'],
};
const { getByTestId } = render(
<TestProviders>
<KibanaAlertSeverityCellRenderer alert={alert} />
</TestProviders>
);
expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('High');
expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #DA8B45');
});
it('should show critical', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_SEVERITY]: ['critical'],
};
const { getByTestId } = render(
<TestProviders>
<KibanaAlertSeverityCellRenderer alert={alert} />
</TestProviders>
);
expect(getByTestId(BADGE_TEST_ID)).toHaveTextContent('Critical');
expect(getByTestId(BADGE_TEST_ID)).toHaveStyle('--euiBadgeBackgroundColor: #E7664C');
});
});

View file

@ -0,0 +1,66 @@
/*
* 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 { EuiBadge, useEuiTheme } from '@elastic/eui';
import type { Alert } from '@kbn/alerting-types';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
import type { JsonValue } from '@kbn/utility-types';
import { getSeverityColor } from '../../alerts_kpis/severity_level_panel/helpers';
export const BADGE_TEST_ID = 'alert-summary-table-severity-cell-renderer';
/**
* Return the same string with the first letter capitalized
*/
const capitalizeFirstLetter = (value: string): string =>
String(value).charAt(0).toUpperCase() + String(value).slice(1);
export interface KibanaAlertSeverityCellRendererProps {
/**
* Alert data passed from the renderCellValue callback via the AlertWithLegacyFormats interface
*/
alert: Alert;
}
/**
* Renders a EuiBadge for the kibana.alert.severity field.
* Used in AI for SOC alert summary table.
*/
export const KibanaAlertSeverityCellRenderer = memo(
({ alert }: KibanaAlertSeverityCellRendererProps) => {
const { euiTheme } = useEuiTheme();
const displayValue: string | null = useMemo(() => {
const values: JsonValue[] | undefined = alert[ALERT_SEVERITY];
if (Array.isArray(values) && values.length === 1) {
const value: JsonValue = values[0];
return value && typeof value === 'string' ? capitalizeFirstLetter(value) : null;
}
return null;
}, [alert]);
const color: string = useMemo(
() => getSeverityColor(displayValue || '', euiTheme),
[displayValue, euiTheme]
);
return (
<>
{displayValue && (
<EuiBadge color={color} data-test-subj={BADGE_TEST_ID}>
{displayValue}
</EuiBadge>
)}
</>
);
}
);
KibanaAlertSeverityCellRenderer.displayName = 'KibanaAlertSeverityCellRenderer';

View file

@ -11,6 +11,12 @@ 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 { ICON_TEST_ID } from './kibana_alert_related_integrations_cell_renderer';
import { useGetIntegrationFromPackageName } from '../../../hooks/alert_summary/use_get_integration_from_package_name';
import { BADGE_TEST_ID } from './kibana_alert_severity_cell_renderer';
jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
describe('CellValue', () => {
it('should handle missing field', () => {
@ -131,4 +137,42 @@ describe('CellValue', () => {
expect(getByText('[object Object]')).toBeInTheDocument();
});
it('should use related integration renderer', () => {
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
integration: {},
isLoading: false,
});
const alert: Alert = {
_id: '_id',
_index: '_index',
};
const columnId = ALERT_RULE_PARAMETERS;
const { getByTestId } = render(
<TestProviders>
<CellValue alert={alert} columnId={columnId} />
</TestProviders>
);
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
});
it('should use severity renderer', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
[ALERT_SEVERITY]: ['low'],
};
const columnId = ALERT_SEVERITY;
const { getByTestId } = render(
<TestProviders>
<CellValue alert={alert} columnId={columnId} />
</TestProviders>
);
expect(getByTestId(BADGE_TEST_ID)).toBeInTheDocument();
});
});

View file

@ -5,11 +5,14 @@
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import React, { memo } from 'react';
import type { Alert } from '@kbn/alerting-types';
import type { JsonValue } from '@kbn/utility-types';
import { getOrEmptyTagFromValue } from '../../../../common/components/empty_value';
import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils';
import { BasicCellRenderer } from './basic_cell_renderer';
import { KibanaAlertSeverityCellRenderer } from './kibana_alert_severity_cell_renderer';
import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_related_integrations_cell_renderer';
// guarantees that all cells will have their values vertically centered
const styles = { display: 'flex', alignItems: 'center', height: '100%' };
export interface CellValueProps {
@ -29,36 +32,23 @@ export interface CellValueProps {
* It will be soon improved to support custom renders for specific fields (like kibana.alert.rule.parameters and kibana.alert.severity).
*/
export const CellValue = memo(({ alert, columnId }: CellValueProps) => {
const displayValue: string | null = useMemo(() => {
const cellValues: string | number | JsonValue[] = alert[columnId];
let component;
// Displays string as is.
// Joins values of array with more than one element.
// Returns null if the value is null.
// Return the string of the value otherwise.
if (typeof cellValues === 'string') {
return cellValues;
} else if (typeof cellValues === 'number') {
return cellValues.toString();
} else if (Array.isArray(cellValues)) {
if (cellValues.length > 1) {
return cellValues.join(', ');
} else {
const value: JsonValue = cellValues[0];
if (typeof value === 'string') {
return value;
} else if (value == null) {
return null;
} else {
return value.toString();
}
}
} else {
return null;
}
}, [alert, columnId]);
switch (columnId) {
case ALERT_RULE_PARAMETERS:
component = <KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />;
break;
return <div style={styles}>{getOrEmptyTagFromValue(displayValue)}</div>;
case ALERT_SEVERITY:
component = <KibanaAlertSeverityCellRenderer alert={alert} />;
break;
default:
component = <BasicCellRenderer alert={alert} field={columnId} />;
break;
}
return <div style={styles}>{component}</div>;
});
CellValue.displayName = 'CellValue';

View file

@ -13,7 +13,13 @@ import { i18n } from '@kbn/i18n';
import { TableId } from '@kbn/securitysolution-data-table';
import { AlertsTable } from '@kbn/response-ops-alerts-table';
import type { AlertsTableProps } from '@kbn/response-ops-alerts-table/types';
import { AlertConsumers } from '@kbn/rule-data-utils';
import {
ALERT_RULE_NAME,
ALERT_RULE_PARAMETERS,
ALERT_SEVERITY,
AlertConsumers,
TIMESTAMP,
} from '@kbn/rule-data-utils';
import { ESQL_RULE_TYPE_ID, QUERY_RULE_TYPE_ID } from '@kbn/securitysolution-rules';
import type {
EuiDataGridProps,
@ -48,26 +54,21 @@ const RULE_NAME_COLUMN = i18n.translate(
{ defaultMessage: 'Rule' }
);
const TIMESTAMP = '@timestamp';
const RELATED_INTEGRATION = 'kibana.alert.rule.parameters';
const SEVERITY = 'kibana.alert.severity';
const RULE_NAME = 'kibana.alert.rule.name';
const columns: EuiDataGridProps['columns'] = [
{
id: TIMESTAMP,
displayAsText: TIMESTAMP_COLUMN,
},
{
id: RELATED_INTEGRATION,
id: ALERT_RULE_PARAMETERS,
displayAsText: RELATION_INTEGRATION_COLUMN,
},
{
id: SEVERITY,
id: ALERT_SEVERITY,
displayAsText: SEVERITY_COLUMN,
},
{
id: RULE_NAME,
id: ALERT_RULE_NAME,
displayAsText: RULE_NAME_COLUMN,
},
];

View file

@ -0,0 +1,51 @@
/*
* 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 { useMemo } from 'react';
import type { PackageListItem } from '@kbn/fleet-plugin/common';
import { useFetchIntegrations } from './use_fetch_integrations';
export interface UseGetIntegrationFromRuleIdParams {
/**
*
*/
packageName: string | null;
}
export interface UseGetIntegrationFromRuleIdResult {
/**
* List of integrations ready to be consumed by the IntegrationFilterButton component
*/
integration: PackageListItem | undefined;
/**
* True while rules are being fetched
*/
isLoading: boolean;
}
/**
*
*/
export const useGetIntegrationFromPackageName = ({
packageName,
}: UseGetIntegrationFromRuleIdParams): UseGetIntegrationFromRuleIdResult => {
// Fetch all packages
const { installedPackages, isLoading } = useFetchIntegrations();
const integration = useMemo(
() => installedPackages.find((installedPackage) => installedPackage.name === packageName),
[installedPackages, packageName]
);
return useMemo(
() => ({
integration,
isLoading,
}),
[integration, isLoading]
);
};

View file

@ -0,0 +1,153 @@
/*
* 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 { Alert } from '@kbn/alerting-types';
import { getAlertFieldValueAsStringOrNull, isJsonObjectValue } from './type_utils';
import type { JsonValue } from '@kbn/utility-types';
describe('getAlertFieldValueAsStringOrNull', () => {
it('should handle missing field', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: 'value1',
};
const field = 'columnId';
const result = getAlertFieldValueAsStringOrNull(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 = getAlertFieldValueAsStringOrNull(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 = getAlertFieldValueAsStringOrNull(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 = getAlertFieldValueAsStringOrNull(alert, field);
expect(result).toEqual('true, false');
});
it('should handle array of numbers', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [1, 2],
};
const field = 'field1';
const result = getAlertFieldValueAsStringOrNull(alert, field);
expect(result).toEqual('1, 2');
});
it('should handle array of null', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [null, null],
};
const field = 'field1';
const result = getAlertFieldValueAsStringOrNull(alert, field);
expect(result).toEqual(', ');
});
it('should join array of JsonObjects', () => {
const alert: Alert = {
_id: '_id',
_index: '_index',
field1: [{ subField1: 'value1', subField2: 'value2' }],
};
const field = 'field1';
const result = getAlertFieldValueAsStringOrNull(alert, field);
expect(result).toEqual('[object Object]');
});
});
describe('isJsonObjectValue', () => {
it('should return true for JsonObject', () => {
const value: JsonValue = { test: 'value' };
const result = isJsonObjectValue(value);
expect(result).toBe(true);
});
it('should return false for null', () => {
const value: JsonValue = null;
const result = isJsonObjectValue(value);
expect(result).toBe(false);
});
it('should return false for string', () => {
const value: JsonValue = 'test';
const result = isJsonObjectValue(value);
expect(result).toBe(false);
});
it('should return false for number', () => {
const value: JsonValue = 123;
const result = isJsonObjectValue(value);
expect(result).toBe(false);
});
it('should return false for boolean', () => {
const value: JsonValue = true;
const result = isJsonObjectValue(value);
expect(result).toBe(false);
});
it('should return false for array', () => {
const value: JsonValue = ['test', 123, true];
const result = isJsonObjectValue(value);
expect(result).toBe(false);
});
});

View file

@ -0,0 +1,54 @@
/*
* 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 { JsonObject, JsonValue } from '@kbn/utility-types';
import type { Alert } from '@kbn/alerting-types';
/**
* 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 string, return it.
* If the value is an array, join the values.
* If null the value is null.
* Return the string of the value otherwise.
*/
export const getAlertFieldValueAsStringOrNull = (alert: Alert, field: string): string | null => {
const cellValues: string | number | JsonValue[] = alert[field];
if (typeof cellValues === 'string') {
return cellValues;
} else if (typeof cellValues === 'number') {
return cellValues.toString();
} else if (Array.isArray(cellValues)) {
if (cellValues.length > 1) {
return cellValues.join(', ');
} else {
const value: JsonValue = cellValues[0];
if (typeof value === 'string') {
return value;
} else if (value == null) {
return null;
} else {
return value.toString();
}
}
} else {
return null;
}
};
/**
* Guaratees that the value is of type JsonObject
*/
export const isJsonObjectValue = (value: JsonValue): value is JsonObject => {
return (
value != null &&
typeof value !== 'string' &&
typeof value !== 'number' &&
typeof value !== 'boolean' &&
!Array.isArray(value)
);
};