mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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 | | ------------- | ------------- | |  |  | ### 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:
parent
40a9159435
commit
7160b360c7
12 changed files with 843 additions and 40 deletions
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue