mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[AI4DSOC] Alert summary page performance improvements (#218632)
## Summary This PR applies some performance improvements to the newly created Alert summary page (for AI for SOC). Here are the multiple changes: - instead of fetching all rules in multiple places (components and hooks), we're now fetching all rules in the most top level `alert_summary.tsx` pages component. We're then passing the result down via props to the children components. Though some of the components inside the `alerts_table` component for example cannot be passed via props, so we're leveraging the `additionalContext` property to pass down rules information. Also, for the components working within the `grouping_alerts_table`, we had to wrap the whole component with a local context. - similarly, the packages were already fetched in the very top `alert_summary.tsx` pages component and were passed via props to the children components, but we applied the same logic for the `alerts_table` and the `grouping_alerts_table` components. The PR also improves the `integration_icon.tsx` component to make it more generic, and reused in all places to avoid the previous code duplication. **No UI or behavior changes are introduced!** https://github.com/user-attachments/assets/1fc1b6d0-290c-4b8e-b3e1-6ccb82f4f82b ## How to test This needs to be ran in Serverless: - `yarn es serverless --projectType security` - `yarn serverless-security --no-base-path` You also need to enable the AI for SOC tier, by adding the following to your `serverless.security.dev.yaml` file: ``` xpack.securitySolutionServerless.productTypes: [ { product_line: 'ai_soc', product_tier: 'search_ai_lake' }, ] ``` Use one of these Serverless users: - `platform_engineer` - `endpoint_operations_analyst` - `endpoint_policy_manager` - `admin` - `system_indices_superuser` Then: - generate data: `yarn test:generate:serverless-dev` - create 4 catch all rules, each with a name of a AI for SOC integration (`google_secops`, `microsoft_sentinel`,, `sentinel_one` and `crowdstrike`) - change [this line](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts#L73) to `installedPackages: availablePackages` to force having some packages installed - change [this line](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts#L63) to `r.name === p.name` to make sure there will be matches between integrations and rules ### Checklist - [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
This commit is contained in:
parent
6e4a06ee0f
commit
112eab3a65
33 changed files with 927 additions and 407 deletions
|
@ -8,51 +8,63 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
import {
|
||||
INTEGRATION_INTEGRATION_ICON_TEST_ID,
|
||||
INTEGRATION_ICON_TEST_ID,
|
||||
INTEGRATION_LOADING_SKELETON_TEST_ID,
|
||||
IntegrationIcon,
|
||||
} from './integration_icon';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
|
||||
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
|
||||
jest.mock('@kbn/fleet-plugin/public/hooks');
|
||||
|
||||
const testId = 'testid';
|
||||
const integration: PackageListItem = {
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
};
|
||||
|
||||
describe('IntegrationIcon', () => {
|
||||
it('should return a single integration icon', () => {
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: {
|
||||
title: 'title',
|
||||
icons: [{ type: 'type', src: 'src' }],
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon ruleId={'name'} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(INTEGRATION_INTEGRATION_ICON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return a single integration loading', () => {
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: {},
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
it('should render a single integration icon', () => {
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon ruleId={''} />
|
||||
<IntegrationIcon data-test-subj={testId} integration={integration} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(INTEGRATION_LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the loading skeleton', () => {
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon data-test-subj={testId} integration={undefined} isLoading={true} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render skeleton or icon', () => {
|
||||
const { queryByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon data-test-subj={testId} integration={undefined} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(queryByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).not.toBeInTheDocument();
|
||||
expect(
|
||||
queryByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,39 +9,49 @@ import React, { memo } from 'react';
|
|||
import { EuiSkeletonText } from '@elastic/eui';
|
||||
import { CardIcon } from '@kbn/fleet-plugin/public';
|
||||
import type { IconSize } from '@elastic/eui/src/components/icon/icon';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
|
||||
export const INTEGRATION_LOADING_SKELETON_TEST_ID = 'ai-for-soc-alert-integration-loading-skeleton';
|
||||
export const INTEGRATION_INTEGRATION_ICON_TEST_ID = 'ai-for-soc-alert-integration-icon';
|
||||
export const INTEGRATION_LOADING_SKELETON_TEST_ID = 'integration-loading-skeleton';
|
||||
export const INTEGRATION_ICON_TEST_ID = 'integration-icon';
|
||||
|
||||
interface IntegrationProps {
|
||||
/**
|
||||
* Id of the rule the alert was generated by
|
||||
* Optional data test subject string
|
||||
*/
|
||||
ruleId: string;
|
||||
'data-test-subj'?: string;
|
||||
/**
|
||||
* Changes the size of the icon. Uses the Eui IconSize interface.
|
||||
* Defaults to s
|
||||
*/
|
||||
iconSize?: IconSize;
|
||||
/**
|
||||
* Id of the rule the alert was generated by
|
||||
*/
|
||||
integration: PackageListItem | undefined;
|
||||
/**
|
||||
* If true, renders a EuiSkeletonText
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon for the integration that matches the rule id.
|
||||
* In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
|
||||
* Renders the icon for the integration. Renders a EuiSkeletonText if loading.
|
||||
*/
|
||||
export const IntegrationIcon = memo(({ ruleId, iconSize = 's' }: IntegrationProps) => {
|
||||
const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId });
|
||||
|
||||
return (
|
||||
export const IntegrationIcon = memo(
|
||||
({
|
||||
'data-test-subj': dataTestSubj,
|
||||
iconSize = 's',
|
||||
integration,
|
||||
isLoading = false,
|
||||
}: IntegrationProps) => (
|
||||
<EuiSkeletonText
|
||||
data-test-subj={INTEGRATION_LOADING_SKELETON_TEST_ID}
|
||||
data-test-subj={`${dataTestSubj}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`}
|
||||
isLoading={isLoading}
|
||||
lines={1}
|
||||
>
|
||||
{integration ? (
|
||||
<CardIcon
|
||||
data-test-subj={INTEGRATION_INTEGRATION_ICON_TEST_ID}
|
||||
data-test-subj={`${dataTestSubj}-${INTEGRATION_ICON_TEST_ID}`}
|
||||
icons={integration.icons}
|
||||
integrationName={integration.title}
|
||||
packageName={integration.name}
|
||||
|
@ -50,6 +60,7 @@ export const IntegrationIcon = memo(({ ruleId, iconSize = 's' }: IntegrationProp
|
|||
/>
|
||||
) : null}
|
||||
</EuiSkeletonText>
|
||||
);
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
IntegrationIcon.displayName = 'IntegrationIcon';
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { CardIcon } from '@kbn/fleet-plugin/public';
|
||||
import { IntegrationIcon } from '../common/integration_icon';
|
||||
import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date';
|
||||
|
||||
const LAST_SYNCED = i18n.translate(
|
||||
|
@ -70,12 +70,10 @@ export const IntegrationCard = memo(
|
|||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<CardIcon
|
||||
icons={integration.icons}
|
||||
integrationName={integration.title}
|
||||
packageName={integration.name}
|
||||
size="xl"
|
||||
version={integration.version}
|
||||
<IntegrationIcon
|
||||
data-test-subj={dataTestSubj}
|
||||
iconSize="xl"
|
||||
integration={integration}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -10,8 +10,9 @@ import { css } from '@emotion/react';
|
|||
import { EuiBadge, EuiCard } from '@elastic/eui';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
|
||||
import { CardIcon, useLink } from '@kbn/fleet-plugin/public';
|
||||
import { useLink } from '@kbn/fleet-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IntegrationIcon } from '../common/integration_icon';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
const SIEM_BADGE = i18n.translate('xpack.securitySolution.alertSummary.integrations.siemBadge', {
|
||||
|
@ -65,13 +66,7 @@ export const IntegrationCard = memo(
|
|||
display="plain"
|
||||
hasBorder
|
||||
icon={
|
||||
<CardIcon
|
||||
icons={integration.icons}
|
||||
integrationName={integration.title}
|
||||
packageName={integration.name}
|
||||
size="xl"
|
||||
version={integration.version}
|
||||
/>
|
||||
<IntegrationIcon data-test-subj={dataTestSubj} iconSize="xl" integration={integration} />
|
||||
}
|
||||
layout="horizontal"
|
||||
onClick={onClick}
|
||||
|
|
|
@ -37,8 +37,16 @@ const packages: PackageListItem[] = [
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
describe('<SearchBarSection />', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render all components', () => {
|
||||
(useIntegrations as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
|
@ -49,7 +57,7 @@ describe('<SearchBarSection />', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<SearchBarSection dataView={dataView} packages={packages} />
|
||||
<SearchBarSection dataView={dataView} packages={packages} ruleResponse={ruleResponse} />
|
||||
);
|
||||
|
||||
expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -64,7 +72,7 @@ describe('<SearchBarSection />', () => {
|
|||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<SearchBarSection dataView={dataView} packages={packages} />
|
||||
<SearchBarSection dataView={dataView} packages={packages} ruleResponse={ruleResponse} />
|
||||
);
|
||||
|
||||
expect(getByTestId(INTEGRATION_BUTTON_LOADING_TEST_ID)).toBeInTheDocument();
|
||||
|
|
|
@ -9,6 +9,7 @@ import React, { memo, useMemo } from 'react';
|
|||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonRectangle } from '@elastic/eui';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine';
|
||||
import { useIntegrations } from '../../../hooks/alert_summary/use_integrations';
|
||||
import { SiemSearchBar } from '../../../../common/components/search_bar';
|
||||
import { IntegrationFilterButton } from './integrations_filter_button';
|
||||
|
@ -29,6 +30,19 @@ export interface SearchBarSectionProps {
|
|||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,34 +52,36 @@ export interface SearchBarSectionProps {
|
|||
* For the AI for SOC effort, each integration has one rule associated with.
|
||||
* This means that deselecting an integration is equivalent to filtering out by the rule for that integration.
|
||||
*/
|
||||
export const SearchBarSection = memo(({ dataView, packages }: SearchBarSectionProps) => {
|
||||
const { isLoading, integrations } = useIntegrations({ packages });
|
||||
export const SearchBarSection = memo(
|
||||
({ dataView, packages, ruleResponse }: SearchBarSectionProps) => {
|
||||
const { isLoading, integrations } = useIntegrations({ packages, ruleResponse });
|
||||
|
||||
const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
|
||||
const dataViewSpec = useMemo(() => dataView.toSpec(), [dataView]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSkeletonRectangle
|
||||
data-test-subj={INTEGRATION_BUTTON_LOADING_TEST_ID}
|
||||
isLoading={isLoading}
|
||||
width={INTEGRATION_BUTTON_LOADING_WIDTH}
|
||||
height={INTEGRATION_BUTTON_LOADING_HEIGHT}
|
||||
>
|
||||
<IntegrationFilterButton integrations={integrations} />
|
||||
</EuiSkeletonRectangle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SiemSearchBar
|
||||
dataTestSubj={SEARCH_BAR_TEST_ID}
|
||||
hideFilterBar
|
||||
hideQueryMenu
|
||||
id={InputsModelId.global}
|
||||
sourcererDataView={dataViewSpec}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSkeletonRectangle
|
||||
data-test-subj={INTEGRATION_BUTTON_LOADING_TEST_ID}
|
||||
isLoading={isLoading}
|
||||
width={INTEGRATION_BUTTON_LOADING_WIDTH}
|
||||
height={INTEGRATION_BUTTON_LOADING_HEIGHT}
|
||||
>
|
||||
<IntegrationFilterButton integrations={integrations} />
|
||||
</EuiSkeletonRectangle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SiemSearchBar
|
||||
dataTestSubj={SEARCH_BAR_TEST_ID}
|
||||
hideFilterBar
|
||||
hideQueryMenu
|
||||
id={InputsModelId.global}
|
||||
sourcererDataView={dataViewSpec}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SearchBarSection.displayName = 'SearchBarSection';
|
||||
|
|
|
@ -11,7 +11,6 @@ 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', () => {
|
||||
|
@ -58,7 +57,7 @@ describe('BasicCellRenderer', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<BasicCellRenderer alert={alert} field={columnId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,13 +5,39 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getIntegrationComponent, groupStatsRenderer } from './group_stats_renderers';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
getIntegrationComponent,
|
||||
groupStatsRenderer,
|
||||
IntegrationIcon,
|
||||
TABLE_GROUP_STATS_TEST_ID,
|
||||
} from './group_stats_renderers';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import { useTableSectionContext } from './table_section_context';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
import { INTEGRATION_ICON_TEST_ID } from '../common/integration_icon';
|
||||
|
||||
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
|
||||
jest.mock('@kbn/fleet-plugin/public/hooks');
|
||||
jest.mock('./table_section_context');
|
||||
|
||||
const integration: PackageListItem = {
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
};
|
||||
|
||||
describe('getIntegrationComponent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return an empty array', () => {
|
||||
const groupStatsItems = getIntegrationComponent({
|
||||
key: '',
|
||||
|
@ -66,7 +92,51 @@ describe('getIntegrationComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('IntegrationIcon', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render integration icon', () => {
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: {},
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<IntegrationIcon ruleId={'ruleId'} />);
|
||||
|
||||
expect(
|
||||
getByTestId(`${TABLE_GROUP_STATS_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render icon', () => {
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: {},
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: undefined,
|
||||
});
|
||||
|
||||
const { queryByTestId } = render(<IntegrationIcon ruleId={'ruleId'} />);
|
||||
|
||||
expect(
|
||||
queryByTestId(`${TABLE_GROUP_STATS_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupStatsRenderer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return array of badges for signal.rule.id field', () => {
|
||||
const badges = groupStatsRenderer('signal.rule.id', {
|
||||
key: '',
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import type { GroupStatsItem, RawBucket } from '@kbn/grouping';
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IntegrationIcon } from '../common/integration_icon';
|
||||
import { IntegrationIcon as Icon } from '../common/integration_icon';
|
||||
import { useTableSectionContext } from './table_section_context';
|
||||
import { getRulesBadge, getSeverityComponent } from '../../alerts_table/grouping_settings';
|
||||
import { DEFAULT_GROUP_STATS_RENDERER } from '../../alerts_table/alerts_grouping';
|
||||
import type { AlertsGroupingAggregation } from '../../alerts_table/grouping_settings/types';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
|
||||
const STATS_GROUP_SIGNAL_RULE_ID = i18n.translate(
|
||||
'xpack.securitySolution.alertSummary.groups.integrations',
|
||||
|
@ -26,6 +28,32 @@ const STATS_GROUP_SIGNAL_RULE_ID_MULTI = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const TABLE_GROUP_STATS_TEST_ID = 'ai-for-soc-alert-table-group-stats';
|
||||
|
||||
interface IntegrationProps {
|
||||
/**
|
||||
* Id of the rule the alert was generated by
|
||||
*/
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon for the integration that matches the rule id.
|
||||
* In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
|
||||
*/
|
||||
export const IntegrationIcon = memo(({ ruleId }: IntegrationProps) => {
|
||||
const { packages, ruleResponse } = useTableSectionContext();
|
||||
const { integration } = useGetIntegrationFromRuleId({
|
||||
packages,
|
||||
rules: ruleResponse.rules,
|
||||
ruleId,
|
||||
});
|
||||
|
||||
return <Icon data-test-subj={TABLE_GROUP_STATS_TEST_ID} integration={integration} />;
|
||||
});
|
||||
|
||||
IntegrationIcon.displayName = 'IntegrationIcon';
|
||||
|
||||
/**
|
||||
* Return a renderer for integration aggregation.
|
||||
*/
|
||||
|
|
|
@ -18,15 +18,36 @@ import { render } from '@testing-library/react';
|
|||
import { defaultGroupTitleRenderers } from '../../alerts_table/grouping_settings';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import React from 'react';
|
||||
import { useTableSectionContext } from './table_section_context';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
|
||||
jest.mock('../../../hooks/alert_summary/use_get_integration_from_rule_id');
|
||||
jest.mock('./table_section_context');
|
||||
jest.mock('@kbn/fleet-plugin/public/hooks');
|
||||
|
||||
const integration: PackageListItem = {
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
};
|
||||
|
||||
describe('groupTitleRenderers', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
});
|
||||
|
||||
it('should render correctly for signal.rule.id field', () => {
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: { title: 'rule_name' },
|
||||
isLoading: false,
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: { isLoading: false },
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({ integration });
|
||||
|
||||
const { getByTestId } = render(
|
||||
groupTitleRenderers(
|
||||
|
@ -117,10 +138,18 @@ describe('groupTitleRenderers', () => {
|
|||
});
|
||||
|
||||
describe('IntegrationNameGroupContent', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
});
|
||||
|
||||
it('should render the integration name and icon when a matching rule is found', () => {
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: { isLoading: false },
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: { title: 'rule_name', icons: 'icon' },
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<IntegrationNameGroupContent title="rule.id" />);
|
||||
|
@ -134,9 +163,12 @@ describe('IntegrationNameGroupContent', () => {
|
|||
});
|
||||
|
||||
it('should render rule id when no matching rule is found', () => {
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: { isLoading: false },
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: undefined,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<IntegrationNameGroupContent title="rule.id" />);
|
||||
|
@ -152,9 +184,12 @@ describe('IntegrationNameGroupContent', () => {
|
|||
});
|
||||
|
||||
it('should render loading for signal.rule.id field when rule and packages are loading', () => {
|
||||
(useTableSectionContext as jest.Mock).mockReturnValue({
|
||||
packages: [],
|
||||
ruleResponse: { isLoading: true },
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: undefined,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(<IntegrationNameGroupContent title="rule.id" />);
|
||||
|
|
|
@ -9,7 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiTitle } from '@elastic/e
|
|||
import { isArray } from 'lodash/fp';
|
||||
import React, { memo } from 'react';
|
||||
import type { GroupPanelRenderer } from '@kbn/grouping/src';
|
||||
import { CardIcon } from '@kbn/fleet-plugin/public';
|
||||
import { IntegrationIcon } from '../common/integration_icon';
|
||||
import { useTableSectionContext } from './table_section_context';
|
||||
import { useGetIntegrationFromRuleId } from '../../../hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import { GroupWithIconContent, RuleNameGroupContent } from '../../alerts_table/grouping_settings';
|
||||
import type { AlertsGroupingAggregation } from '../../alerts_table/grouping_settings/types';
|
||||
|
@ -79,22 +80,27 @@ export const INTEGRATION_GROUP_RENDERER_LOADING_TEST_ID = 'integration-group-ren
|
|||
export const INTEGRATION_GROUP_RENDERER_TEST_ID = 'integration-group-renderer';
|
||||
export const INTEGRATION_GROUP_RENDERER_INTEGRATION_NAME_TEST_ID =
|
||||
'integration-group-renderer-integration-name';
|
||||
export const INTEGRATION_GROUP_RENDERER_INTEGRATION_ICON_TEST_ID =
|
||||
'integration-group-renderer-integration-icon';
|
||||
export const INTEGRATION_GROUP_RENDERER_INTEGRATION_ICON_TEST_ID = 'integration-group-renderer';
|
||||
export const SIGNAL_RULE_ID_GROUP_RENDERER_TEST_ID = 'signal-rule-id-group-renderer';
|
||||
|
||||
/**
|
||||
* Renders an icon and name of an integration.
|
||||
* This component needs to be used within the TableSectionContext which provides the installed packages as well as all the rules.
|
||||
*/
|
||||
export const IntegrationNameGroupContent = memo<{
|
||||
title: string | string[];
|
||||
}>(({ title }) => {
|
||||
const { integration, isLoading } = useGetIntegrationFromRuleId({ ruleId: title });
|
||||
const { packages, ruleResponse } = useTableSectionContext();
|
||||
const { integration } = useGetIntegrationFromRuleId({
|
||||
packages,
|
||||
ruleId: title,
|
||||
rules: ruleResponse.rules,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiSkeletonText
|
||||
data-test-subj={INTEGRATION_GROUP_RENDERER_LOADING_TEST_ID}
|
||||
isLoading={isLoading}
|
||||
isLoading={ruleResponse.isLoading}
|
||||
lines={1}
|
||||
>
|
||||
{integration ? (
|
||||
|
@ -104,13 +110,10 @@ export const IntegrationNameGroupContent = memo<{
|
|||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<CardIcon
|
||||
<IntegrationIcon
|
||||
data-test-subj={INTEGRATION_GROUP_RENDERER_INTEGRATION_ICON_TEST_ID}
|
||||
icons={integration.icons}
|
||||
integrationName={integration.title}
|
||||
packageName={integration.name}
|
||||
size="xl"
|
||||
version={integration.version}
|
||||
iconSize="xl"
|
||||
integration={integration}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -9,88 +9,92 @@ 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,
|
||||
TABLE_RELATED_INTEGRATION_CELL_RENDERER_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';
|
||||
import {
|
||||
INTEGRATION_ICON_TEST_ID,
|
||||
INTEGRATION_LOADING_SKELETON_TEST_ID,
|
||||
} from '../common/integration_icon';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
|
||||
jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
|
||||
jest.mock('@kbn/fleet-plugin/public/hooks');
|
||||
|
||||
const LOADING_SKELETON_TEST_ID = `${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`;
|
||||
const ICON_TEST_ID = `${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`;
|
||||
|
||||
describe('KibanaAlertRelatedIntegrationsCellRenderer', () => {
|
||||
it('should handle missing field', () => {
|
||||
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
|
||||
integration: null,
|
||||
isLoading: false,
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle missing field', () => {
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
_index: '_index',
|
||||
};
|
||||
const packages: PackageListItem[] = [];
|
||||
|
||||
const { queryByTestId } = render(<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />);
|
||||
const { queryByTestId } = render(
|
||||
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} packages={packages} />
|
||||
);
|
||||
|
||||
expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(LOADING_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'],
|
||||
[ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
|
||||
};
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'other',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'other',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Other',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
|
||||
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} />
|
||||
const { queryByTestId } = render(
|
||||
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} packages={packages} />
|
||||
);
|
||||
|
||||
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.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,
|
||||
});
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
_index: '_index',
|
||||
[ALERT_RULE_PARAMETERS]: ['splunk'],
|
||||
[ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
|
||||
};
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />
|
||||
<KibanaAlertRelatedIntegrationsCellRenderer alert={alert} packages={packages} />
|
||||
);
|
||||
|
||||
expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,26 +7,28 @@
|
|||
|
||||
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 type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { IntegrationIcon } from '../common/integration_icon';
|
||||
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';
|
||||
export const TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID =
|
||||
'alert-summary-table-related-integrations-cell-renderer';
|
||||
|
||||
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;
|
||||
/**
|
||||
* List of installed AI for SOC integrations.
|
||||
* This comes from the additionalContext property on the table.
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,7 +37,7 @@ export interface KibanaAlertRelatedIntegrationsCellRendererProps {
|
|||
* Used in AI for SOC alert summary table.
|
||||
*/
|
||||
export const KibanaAlertRelatedIntegrationsCellRenderer = memo(
|
||||
({ alert }: KibanaAlertRelatedIntegrationsCellRendererProps) => {
|
||||
({ alert, packages }: KibanaAlertRelatedIntegrationsCellRendererProps) => {
|
||||
const packageName: string | null = useMemo(() => {
|
||||
const values: JsonValue[] | undefined = alert[ALERT_RULE_PARAMETERS];
|
||||
|
||||
|
@ -52,21 +54,17 @@ export const KibanaAlertRelatedIntegrationsCellRenderer = memo(
|
|||
return null;
|
||||
}, [alert]);
|
||||
|
||||
const { integration, isLoading } = useGetIntegrationFromPackageName({ packageName });
|
||||
const integration = useMemo(
|
||||
() => packages.find((p) => p.name === packageName),
|
||||
[packages, 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>
|
||||
<IntegrationIcon
|
||||
data-test-subj={TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}
|
||||
iconSize="l"
|
||||
integration={integration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,13 +12,28 @@ 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';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import { TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID } from './kibana_alert_related_integrations_cell_renderer';
|
||||
import { INTEGRATION_ICON_TEST_ID } from '../common/integration_icon';
|
||||
|
||||
jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name');
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
|
||||
describe('CellValue', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle missing field', () => {
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
|
@ -29,7 +44,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -46,7 +61,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -63,7 +78,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -80,7 +95,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -97,7 +112,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -114,7 +129,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -131,7 +146,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -139,24 +154,22 @@ describe('CellValue', () => {
|
|||
});
|
||||
|
||||
it('should use related integration renderer', () => {
|
||||
(useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({
|
||||
integration: {},
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
_index: '_index',
|
||||
[ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }],
|
||||
};
|
||||
const columnId = ALERT_RULE_PARAMETERS;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId(`${TABLE_RELATED_INTEGRATION_CELL_RENDERER_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use severity renderer', () => {
|
||||
|
@ -169,7 +182,7 @@ describe('CellValue', () => {
|
|||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
<CellValue alert={alert} columnId={columnId} packages={packages} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { memo } from 'react';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import { ALERT_RULE_PARAMETERS, ALERT_SEVERITY } from '@kbn/rule-data-utils';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { BasicCellRenderer } from './basic_cell_renderer';
|
||||
import { KibanaAlertSeverityCellRenderer } from './kibana_alert_severity_cell_renderer';
|
||||
import { KibanaAlertRelatedIntegrationsCellRenderer } from './kibana_alert_related_integrations_cell_renderer';
|
||||
|
@ -24,6 +25,11 @@ export interface CellValueProps {
|
|||
* Column id passed from the renderCellValue callback via EuiDataGridProps['renderCellValue'] interface
|
||||
*/
|
||||
columnId: string;
|
||||
/**
|
||||
* List of installed AI for SOC integrations.
|
||||
* This comes from the additionalContext property on the table.
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,12 +37,12 @@ export interface CellValueProps {
|
|||
* 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).
|
||||
*/
|
||||
export const CellValue = memo(({ alert, columnId }: CellValueProps) => {
|
||||
export const CellValue = memo(({ alert, columnId, packages }: CellValueProps) => {
|
||||
let component;
|
||||
|
||||
switch (columnId) {
|
||||
case ALERT_RULE_PARAMETERS:
|
||||
component = <KibanaAlertRelatedIntegrationsCellRenderer alert={alert} />;
|
||||
component = <KibanaAlertRelatedIntegrationsCellRenderer alert={alert} packages={packages} />;
|
||||
break;
|
||||
|
||||
case ALERT_SEVERITY:
|
||||
|
|
|
@ -11,14 +11,35 @@ import type { DataView } from '@kbn/data-views-plugin/common';
|
|||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { Table } from './table';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
|
||||
const dataView: DataView = createStubDataView({ spec: {} });
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
describe('<Table />', () => {
|
||||
it('should render all components', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<Table dataView={dataView} groupingFilters={[]} />
|
||||
<Table
|
||||
dataView={dataView}
|
||||
groupingFilters={[]}
|
||||
packages={packages}
|
||||
ruleResponse={ruleResponse}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
|||
EuiDataGridStyle,
|
||||
EuiDataGridToolBarVisibilityOptions,
|
||||
} from '@elastic/eui';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { ActionsCell } from './actions_cell';
|
||||
import { AdditionalToolbarControls } from './additional_toolbar_controls';
|
||||
import { getDataViewStateFromIndexFields } from '../../../../common/containers/source/use_data_view';
|
||||
|
@ -36,6 +37,7 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { CellValue } from './render_cell';
|
||||
import { buildTimeRangeFilter } from '../../alerts_table/helpers';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine';
|
||||
|
||||
const TIMESTAMP_COLUMN = i18n.translate(
|
||||
'xpack.securitySolution.alertSummary.table.column.timeStamp',
|
||||
|
@ -84,6 +86,26 @@ const TOOLBAR_VISIBILITY: EuiDataGridToolBarVisibilityOptions = {
|
|||
};
|
||||
const GRID_STYLE: EuiDataGridStyle = { border: 'horizontal' };
|
||||
|
||||
interface AdditionalTableContext {
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TableProps {
|
||||
/**
|
||||
* DataView created for the alert summary page
|
||||
|
@ -93,13 +115,30 @@ export interface TableProps {
|
|||
* Groups filters passed from the GroupedAlertsTable component via the renderChildComponent callback
|
||||
*/
|
||||
groupingFilters: Filter[];
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the table showing all the alerts. This component leverages the ResponseOps AlertsTable in a similar way that the alerts page does.
|
||||
* The table is used in combination with the GroupedAlertsTable component.
|
||||
*/
|
||||
export const Table = memo(({ dataView, groupingFilters }: TableProps) => {
|
||||
export const Table = memo(({ dataView, groupingFilters, packages, ruleResponse }: TableProps) => {
|
||||
const {
|
||||
services: {
|
||||
application,
|
||||
|
@ -178,9 +217,18 @@ export const Table = memo(({ dataView, groupingFilters }: TableProps) => {
|
|||
[dataView]
|
||||
);
|
||||
|
||||
const additionalContext: AdditionalTableContext = useMemo(
|
||||
() => ({
|
||||
packages,
|
||||
ruleResponse,
|
||||
}),
|
||||
[packages, ruleResponse]
|
||||
);
|
||||
|
||||
return (
|
||||
<AlertsTable
|
||||
actionsColumnWidth={ACTION_COLUMN_WIDTH}
|
||||
additionalContext={additionalContext}
|
||||
browserFields={browserFields}
|
||||
columns={columns}
|
||||
consumers={ALERT_TABLE_CONSUMERS}
|
||||
|
|
|
@ -11,14 +11,30 @@ import type { DataView } from '@kbn/data-views-plugin/common';
|
|||
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { GROUPED_TABLE_TEST_ID, TableSection } from './table_section';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
|
||||
const dataView: DataView = createStubDataView({ spec: {} });
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
describe('<TableSection />', () => {
|
||||
it('should render all components', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<TableSection dataView={dataView} />
|
||||
<TableSection dataView={dataView} packages={packages} ruleResponse={ruleResponse} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import React, { memo, useCallback, useMemo } from 'react';
|
|||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { TableSectionContextProvider } from './table_section_context';
|
||||
import { groupStatsRenderer } from './group_stats_renderers';
|
||||
import { groupingOptions } from './grouping_options';
|
||||
import { groupTitleRenderers } from './group_title_renderers';
|
||||
|
@ -20,6 +22,7 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
|||
import { GroupedAlertsTable } from '../../alerts_table/alerts_grouping';
|
||||
import { groupStatsAggregations } from './group_stats_aggregations';
|
||||
import { useUserData } from '../../user_info';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine';
|
||||
|
||||
export const GROUPED_TABLE_TEST_ID = 'alert-summary-grouped-table';
|
||||
|
||||
|
@ -30,13 +33,30 @@ export interface TableSectionProps {
|
|||
* DataView created for the alert summary page
|
||||
*/
|
||||
dataView: DataView;
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Section rendering the table in the alert summary page.
|
||||
* This component leverages the GroupedAlertsTable and the ResponseOps AlertsTable also used in the alerts page.
|
||||
*/
|
||||
export const TableSection = memo(({ dataView }: TableSectionProps) => {
|
||||
export const TableSection = memo(({ dataView, packages, ruleResponse }: TableSectionProps) => {
|
||||
const indexNames = useMemo(() => dataView.getIndexPattern(), [dataView]);
|
||||
const { to, from } = useGlobalTime();
|
||||
|
||||
|
@ -57,29 +77,38 @@ export const TableSection = memo(({ dataView }: TableSectionProps) => {
|
|||
);
|
||||
|
||||
const renderChildComponent = useCallback(
|
||||
(groupingFilters: Filter[]) => <Table dataView={dataView} groupingFilters={groupingFilters} />,
|
||||
[dataView]
|
||||
(groupingFilters: Filter[]) => (
|
||||
<Table
|
||||
dataView={dataView}
|
||||
groupingFilters={groupingFilters}
|
||||
packages={packages}
|
||||
ruleResponse={ruleResponse}
|
||||
/>
|
||||
),
|
||||
[dataView, packages, ruleResponse]
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-test-subj={GROUPED_TABLE_TEST_ID}>
|
||||
<GroupedAlertsTable
|
||||
accordionButtonContent={groupTitleRenderers}
|
||||
accordionExtraActionGroupStats={accordionExtraActionGroupStats}
|
||||
defaultGroupingOptions={groupingOptions}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={globalQuery}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
loading={false}
|
||||
renderChildComponent={renderChildComponent}
|
||||
runtimeMappings={runtimeMappings}
|
||||
signalIndexName={indexNames}
|
||||
tableId={TableId.alertsOnAlertSummaryPage}
|
||||
to={to}
|
||||
/>
|
||||
</div>
|
||||
<TableSectionContextProvider packages={packages} ruleResponse={ruleResponse}>
|
||||
<div data-test-subj={GROUPED_TABLE_TEST_ID}>
|
||||
<GroupedAlertsTable
|
||||
accordionButtonContent={groupTitleRenderers}
|
||||
accordionExtraActionGroupStats={accordionExtraActionGroupStats}
|
||||
defaultGroupingOptions={groupingOptions}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={globalQuery}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
loading={false}
|
||||
renderChildComponent={renderChildComponent}
|
||||
runtimeMappings={runtimeMappings}
|
||||
signalIndexName={indexNames}
|
||||
tableId={TableId.alertsOnAlertSummaryPage}
|
||||
to={to}
|
||||
/>
|
||||
</div>
|
||||
</TableSectionContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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, { createContext, memo, useContext, useMemo } from 'react';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine';
|
||||
|
||||
export interface TableSectionContext {
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A context provider for the AI for SOC alert summary table grouping component.
|
||||
* This allows group stats and renderers to not have to fetch rules and packages.
|
||||
*/
|
||||
export const TableSectionContext = createContext<TableSectionContext | undefined>(undefined);
|
||||
|
||||
export type TableSectionContextProviderProps = {
|
||||
/**
|
||||
* React components to render
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
} & TableSectionContext;
|
||||
|
||||
export const TableSectionContextProvider = memo(
|
||||
({ children, packages, ruleResponse }: TableSectionContextProviderProps) => {
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
packages,
|
||||
ruleResponse,
|
||||
}),
|
||||
[packages, ruleResponse]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableSectionContext.Provider value={contextValue}>{children}</TableSectionContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TableSectionContextProvider.displayName = 'TableSectionContextProvider';
|
||||
|
||||
export const useTableSectionContext = (): TableSectionContext => {
|
||||
const contextValue = useContext(TableSectionContext);
|
||||
|
||||
if (!contextValue) {
|
||||
throw new Error('TableSectionContext can only be used within TableSectionContext provider');
|
||||
}
|
||||
|
||||
return contextValue;
|
||||
};
|
|
@ -45,6 +45,10 @@ const packages: PackageListItem[] = [
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
describe('<Wrapper />', () => {
|
||||
it('should render a loading skeleton while creating the dataView', async () => {
|
||||
|
@ -61,7 +65,7 @@ describe('<Wrapper />', () => {
|
|||
});
|
||||
|
||||
await act(async () => {
|
||||
const { getByTestId } = render(<Wrapper packages={packages} />);
|
||||
const { getByTestId } = render(<Wrapper packages={packages} ruleResponse={ruleResponse} />);
|
||||
|
||||
expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument();
|
||||
|
@ -86,7 +90,7 @@ describe('<Wrapper />', () => {
|
|||
}));
|
||||
|
||||
await act(async () => {
|
||||
const { getByTestId } = render(<Wrapper packages={packages} />);
|
||||
const { getByTestId } = render(<Wrapper packages={packages} ruleResponse={ruleResponse} />);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
|
@ -123,7 +127,7 @@ describe('<Wrapper />', () => {
|
|||
await act(async () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<Wrapper packages={packages} />
|
||||
<Wrapper packages={packages} ruleResponse={ruleResponse} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { KPIsSection } from './kpis/kpis_section';
|
||||
import { IntegrationSection } from './integrations/integration_section';
|
||||
|
@ -38,6 +39,19 @@ export interface WrapperProps {
|
|||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +60,7 @@ export interface WrapperProps {
|
|||
* Once the dataView is correctly created, we render the content.
|
||||
* If the creation fails, we show an error message.
|
||||
*/
|
||||
export const Wrapper = memo(({ packages }: WrapperProps) => {
|
||||
export const Wrapper = memo(({ packages, ruleResponse }: WrapperProps) => {
|
||||
const { data } = useKibana().services;
|
||||
const [dataView, setDataView] = useState<DataView | undefined>(undefined);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
@ -96,11 +110,15 @@ export const Wrapper = memo(({ packages }: WrapperProps) => {
|
|||
<div data-test-subj={CONTENT_TEST_ID}>
|
||||
<IntegrationSection packages={packages} />
|
||||
<EuiHorizontalRule />
|
||||
<SearchBarSection dataView={dataView} packages={packages} />
|
||||
<SearchBarSection
|
||||
dataView={dataView}
|
||||
packages={packages}
|
||||
ruleResponse={ruleResponse}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<KPIsSection dataView={dataView} />
|
||||
<EuiSpacer />
|
||||
<TableSection dataView={dataView} />
|
||||
<TableSection dataView={dataView} packages={packages} ruleResponse={ruleResponse} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import { flattenAlertType } from '../../utils/flatten_alert_type';
|
||||
import { getAlertFieldValueAsStringOrNull } from '../../utils/get_alert_field_value_as_string_or_null';
|
||||
import { getAlertFieldValueAsStringOrNull } from '../../utils/type_utils';
|
||||
import {
|
||||
PROMPT_CONTEXT_ALERT_CATEGORY,
|
||||
PROMPT_CONTEXTS,
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* 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]
|
||||
);
|
||||
};
|
|
@ -6,12 +6,10 @@
|
|||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { useFetchIntegrations } from './use_fetch_integrations';
|
||||
import { useGetIntegrationFromRuleId } from './use_get_integration_from_rule_id';
|
||||
|
||||
jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
|
||||
jest.mock('./use_fetch_integrations');
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
|
||||
describe('useGetIntegrationFromRuleId', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -19,63 +17,38 @@ describe('useGetIntegrationFromRuleId', () => {
|
|||
});
|
||||
|
||||
it('should return undefined integration when no matching rule is found', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({ data: { rules: [] }, isLoading: false });
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [],
|
||||
isLoading: false,
|
||||
});
|
||||
const packages: PackageListItem[] = [];
|
||||
const ruleId = '';
|
||||
const rules: RuleResponse[] = [];
|
||||
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ packages, ruleId, rules }));
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.integration).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should render loading true is rules are loading', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [{ name: 'rule_name' }],
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.integration).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should render loading true if packages are loading', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: { rules: [] },
|
||||
isLoading: false,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [],
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' }));
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.integration).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should render a matching integration', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: { rules: [{ id: 'rule_id', name: 'rule_name' }] },
|
||||
isLoading: false,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [{ name: 'rule_name' }],
|
||||
isLoading: false,
|
||||
});
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
},
|
||||
];
|
||||
const ruleId = 'rule_id';
|
||||
const rules: RuleResponse[] = [{ id: 'rule_id', name: 'splunk' } as RuleResponse];
|
||||
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: 'rule_id' }));
|
||||
const { result } = renderHook(() => useGetIntegrationFromRuleId({ packages, ruleId, rules }));
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.integration).toEqual({ name: 'rule_name' });
|
||||
expect(result.current.integration).toEqual({
|
||||
id: 'splunk',
|
||||
icons: [{ src: 'icon.svg', path: 'mypath/icon.svg', type: 'image/svg+xml' }],
|
||||
name: 'splunk',
|
||||
status: installationStatuses.NotInstalled,
|
||||
title: 'Splunk',
|
||||
version: '0.1.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,15 +7,23 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { useFetchIntegrations } from './use_fetch_integrations';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
|
||||
const EMPTY_ARRAY: RuleResponse[] = [];
|
||||
|
||||
export interface UseGetIntegrationFromRuleIdParams {
|
||||
/**
|
||||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Id of the rule. This should be the value from the signal.rule.id field
|
||||
*/
|
||||
ruleId: string | string[];
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[] | undefined;
|
||||
}
|
||||
|
||||
export interface UseGetIntegrationFromRuleIdResult {
|
||||
|
@ -23,44 +31,33 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that fetches rule and packages data. It then uses that data to find if there is a package (integration)
|
||||
* that matches the rule id value passed via prop (value for the signal.rule.id field).
|
||||
*
|
||||
* Hook that returns a package (integration) from a ruleId (value for the signal.rule.id field), a list of rules and packages.
|
||||
* This hook is used in the GroupedAlertTable's accordion when grouping by signal.rule.id, to render the title as well as statistics.
|
||||
*/
|
||||
export const useGetIntegrationFromRuleId = ({
|
||||
packages,
|
||||
ruleId,
|
||||
rules = EMPTY_ARRAY,
|
||||
}: UseGetIntegrationFromRuleIdParams): UseGetIntegrationFromRuleIdResult => {
|
||||
// Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
|
||||
const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
|
||||
|
||||
// Fetch all packages
|
||||
const { installedPackages, isLoading: integrationIsLoading } = useFetchIntegrations();
|
||||
|
||||
// From the ruleId (which should be a value for a signal.rule.id field) we find the rule
|
||||
// of the same id, which we then use its name to match a package's name.
|
||||
const integration: PackageListItem | undefined = useMemo(() => {
|
||||
const signalRuleId = Array.isArray(ruleId) ? ruleId[0] : ruleId;
|
||||
const rule = (data?.rules || []).find((r: RuleResponse) => r.id === signalRuleId);
|
||||
const rule = rules.find((r: RuleResponse) => r.id === signalRuleId);
|
||||
if (!rule) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return installedPackages.find((installedPackage) => installedPackage.name === rule.name);
|
||||
}, [data?.rules, installedPackages, ruleId]);
|
||||
return packages.find((p) => p.name === rule.name);
|
||||
}, [packages, rules, ruleId]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
integration,
|
||||
isLoading: ruleIsLoading || integrationIsLoading,
|
||||
}),
|
||||
[integration, integrationIsLoading, ruleIsLoading]
|
||||
[integration]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,11 +10,10 @@ import { useIntegrations } from './use_integrations';
|
|||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/common/constants';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { FILTER_KEY } from '../../components/alert_summary/search_bar/integrations_filter_button';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
|
||||
|
||||
describe('useIntegrations', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -33,18 +32,6 @@ describe('useIntegrations', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
rules: [
|
||||
{
|
||||
related_integrations: [{ package: 'splunk' }],
|
||||
id: 'SplunkRuleId',
|
||||
},
|
||||
],
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
|
@ -55,8 +42,17 @@ describe('useIntegrations', () => {
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [
|
||||
{
|
||||
related_integrations: [{ package: 'splunk' }],
|
||||
id: 'SplunkRuleId',
|
||||
} as RuleResponse,
|
||||
],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useIntegrations({ packages }));
|
||||
const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
@ -95,18 +91,6 @@ describe('useIntegrations', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
rules: [
|
||||
{
|
||||
related_integrations: [{ package: 'splunk' }],
|
||||
id: 'SplunkRuleId',
|
||||
},
|
||||
],
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
|
@ -117,8 +101,17 @@ describe('useIntegrations', () => {
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [
|
||||
{
|
||||
related_integrations: [{ package: 'splunk' }],
|
||||
id: 'SplunkRuleId',
|
||||
} as RuleResponse,
|
||||
],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useIntegrations({ packages }));
|
||||
const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
@ -138,10 +131,6 @@ describe('useIntegrations', () => {
|
|||
data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } },
|
||||
},
|
||||
});
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
|
@ -152,8 +141,12 @@ describe('useIntegrations', () => {
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useIntegrations({ packages }));
|
||||
const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
|
@ -167,10 +160,6 @@ describe('useIntegrations', () => {
|
|||
data: { query: { filterManager: { getFilters: jest.fn().mockReturnValue([]) } } },
|
||||
},
|
||||
});
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const packages: PackageListItem[] = [
|
||||
{
|
||||
|
@ -181,8 +170,12 @@ describe('useIntegrations', () => {
|
|||
version: '',
|
||||
},
|
||||
];
|
||||
const ruleResponse = {
|
||||
rules: [],
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useIntegrations({ packages }));
|
||||
const { result } = renderHook(() => useIntegrations({ packages, ruleResponse }));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isLoading: true,
|
||||
|
|
|
@ -11,7 +11,6 @@ import type {
|
|||
EuiSelectableOption,
|
||||
EuiSelectableOptionCheckedType,
|
||||
} from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { filterExistsInFiltersArray } from '../../utils/filter';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { RuleResponse } from '../../../../common/api/detection_engine';
|
||||
|
@ -24,6 +23,19 @@ export interface UseIntegrationsParams {
|
|||
* List of installed AI for SOC integrations
|
||||
*/
|
||||
packages: PackageListItem[];
|
||||
/**
|
||||
* Result from the useQuery to fetch all rules
|
||||
*/
|
||||
ruleResponse: {
|
||||
/**
|
||||
* Result from fetching all rules
|
||||
*/
|
||||
rules: RuleResponse[];
|
||||
/**
|
||||
* True while rules are being fetched
|
||||
*/
|
||||
isLoading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseIntegrationsResult {
|
||||
|
@ -42,10 +54,10 @@ export interface UseIntegrationsResult {
|
|||
* If there is no match between a package and the rules, the integration is not returned.
|
||||
* If a filter exists (we assume that this filter is negated) we do not mark the integration as checked for the EuiFilterButton.
|
||||
*/
|
||||
export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegrationsResult => {
|
||||
// Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
|
||||
const { data, isLoading } = useFindRulesQuery({});
|
||||
|
||||
export const useIntegrations = ({
|
||||
packages,
|
||||
ruleResponse,
|
||||
}: UseIntegrationsParams): UseIntegrationsResult => {
|
||||
const {
|
||||
data: {
|
||||
query: { filterManager },
|
||||
|
@ -59,7 +71,7 @@ export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegra
|
|||
const result: EuiSelectableOption[] = [];
|
||||
|
||||
packages.forEach((p: PackageListItem) => {
|
||||
const matchingRule = (data?.rules || []).find((r: RuleResponse) =>
|
||||
const matchingRule = ruleResponse.rules.find((r: RuleResponse) =>
|
||||
r.related_integrations.map((ri) => ri.package).includes(p.name)
|
||||
);
|
||||
|
||||
|
@ -83,13 +95,13 @@ export const useIntegrations = ({ packages }: UseIntegrationsParams): UseIntegra
|
|||
});
|
||||
|
||||
return result;
|
||||
}, [currentFilters, data, packages]);
|
||||
}, [currentFilters, packages, ruleResponse.rules]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
integrations,
|
||||
isLoading,
|
||||
isLoading: ruleResponse.isLoading,
|
||||
}),
|
||||
[integrations, isLoading]
|
||||
[integrations, ruleResponse.isLoading]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,12 +13,31 @@ import { LANDING_PAGE_PROMPT_TEST_ID } from '../../components/alert_summary/land
|
|||
import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url';
|
||||
import { DATA_VIEW_LOADING_PROMPT_TEST_ID } from '../../components/alert_summary/wrapper';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
|
||||
jest.mock('../../hooks/alert_summary/use_fetch_integrations');
|
||||
jest.mock('../../../common/hooks/use_add_integrations_url');
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
|
||||
|
||||
describe('<AlertSummaryPage />', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
rules: [
|
||||
{
|
||||
related_integrations: [{ package: 'splunk' }],
|
||||
id: 'SplunkRuleId',
|
||||
},
|
||||
],
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render loading logo', () => {
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations';
|
||||
import { LandingPage } from '../../components/alert_summary/landing_page/landing_page';
|
||||
import { Wrapper } from '../../components/alert_summary/wrapper';
|
||||
|
@ -21,11 +22,26 @@ const LOADING_INTEGRATIONS = i18n.translate('xpack.securitySolution.alertSummary
|
|||
/**
|
||||
* Alert summary page rendering alerts generated by AI for SOC integrations.
|
||||
* This page should be only rendered for the AI for SOC product line.
|
||||
* It fetches all the rules and packages (integration) to pass them down to the rest of the page.
|
||||
*/
|
||||
export const AlertSummaryPage = memo(() => {
|
||||
const { availablePackages, installedPackages, isLoading } = useFetchIntegrations();
|
||||
const {
|
||||
availablePackages,
|
||||
installedPackages,
|
||||
isLoading: integrationIsLoading,
|
||||
} = useFetchIntegrations();
|
||||
|
||||
if (isLoading) {
|
||||
// Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
|
||||
const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
|
||||
const ruleResponse = useMemo(
|
||||
() => ({
|
||||
rules: data?.rules || [],
|
||||
isLoading: ruleIsLoading,
|
||||
}),
|
||||
[data, ruleIsLoading]
|
||||
);
|
||||
|
||||
if (integrationIsLoading) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj={LOADING_INTEGRATIONS_TEST_ID}
|
||||
|
@ -39,7 +55,7 @@ export const AlertSummaryPage = memo(() => {
|
|||
return <LandingPage packages={availablePackages} />;
|
||||
}
|
||||
|
||||
return <Wrapper packages={installedPackages} />;
|
||||
return <Wrapper packages={installedPackages} ruleResponse={ruleResponse} />;
|
||||
});
|
||||
|
||||
AlertSummaryPage.displayName = 'AlertSummaryPage';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { IntegrationIcon } from '../../../detections/components/alert_summary/common/integration_icon';
|
||||
import { IntegrationIcon } from './integration_icon';
|
||||
import { DocumentSeverity } from '../../document_details/right/components/severity';
|
||||
import { useBasicDataFromDetailsData } from '../../document_details/shared/hooks/use_basic_data_from_details_data';
|
||||
import { FlyoutTitle } from '../../shared/components/flyout_title';
|
||||
|
@ -83,7 +83,7 @@ export const HeaderTitle = memo(() => {
|
|||
/>
|
||||
}
|
||||
>
|
||||
<IntegrationIcon ruleId={ruleId} iconSize="l" />
|
||||
<IntegrationIcon ruleId={ruleId} />
|
||||
</AlertHeaderBlock>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { useFetchIntegrations } from '../../../detections/hooks/alert_summary/use_fetch_integrations';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { useGetIntegrationFromRuleId } from '../../../detections/hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
import { usePackageIconType } from '@kbn/fleet-plugin/public/hooks';
|
||||
import { INTEGRATION_TEST_ID, IntegrationIcon } from './integration_icon';
|
||||
import {
|
||||
INTEGRATION_ICON_TEST_ID,
|
||||
INTEGRATION_LOADING_SKELETON_TEST_ID,
|
||||
} from '../../../detections/components/alert_summary/common/integration_icon';
|
||||
|
||||
jest.mock('../../../detections/hooks/alert_summary/use_fetch_integrations');
|
||||
jest.mock('../../../detection_engine/rule_management/api/hooks/use_find_rules_query');
|
||||
jest.mock('../../../detections/hooks/alert_summary/use_get_integration_from_rule_id');
|
||||
jest.mock('@kbn/fleet-plugin/public/hooks');
|
||||
|
||||
const LOADING_SKELETON_TEST_ID = `${INTEGRATION_TEST_ID}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`;
|
||||
const ICON_TEST_ID = `${INTEGRATION_TEST_ID}-${INTEGRATION_ICON_TEST_ID}`;
|
||||
|
||||
describe('IntegrationIcon', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return a single integration icon', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [],
|
||||
isLoading: false,
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: {
|
||||
title: 'title',
|
||||
icons: [{ type: 'type', src: 'src' }],
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
},
|
||||
});
|
||||
(usePackageIconType as jest.Mock).mockReturnValue('iconType');
|
||||
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon ruleId={'name'} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return the loading skeleton is rules are loading', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
isLoading: true,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [],
|
||||
isLoading: false,
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: {},
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon ruleId={''} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return the loading skeleton is integrations are loading', () => {
|
||||
(useFindRulesQuery as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
});
|
||||
(useFetchIntegrations as jest.Mock).mockReturnValue({
|
||||
installedPackages: [],
|
||||
isLoading: true,
|
||||
});
|
||||
(useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({
|
||||
integration: {},
|
||||
});
|
||||
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntegrationIcon ruleId={''} />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId(LOADING_SKELETON_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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 React, { memo } from 'react';
|
||||
import { IntegrationIcon as Icon } from '../../../detections/components/alert_summary/common/integration_icon';
|
||||
import { useFetchIntegrations } from '../../../detections/hooks/alert_summary/use_fetch_integrations';
|
||||
import { useFindRulesQuery } from '../../../detection_engine/rule_management/api/hooks/use_find_rules_query';
|
||||
import { useGetIntegrationFromRuleId } from '../../../detections/hooks/alert_summary/use_get_integration_from_rule_id';
|
||||
|
||||
export const INTEGRATION_TEST_ID = 'alert-summary-flyout';
|
||||
|
||||
interface IntegrationIconProps {
|
||||
/**
|
||||
* Id of the rule the alert was generated by
|
||||
*/
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the icon for the integration that matches the rule id.
|
||||
* It fetches all the rules and packages (integrations) to find the matching by rule id.
|
||||
* In AI for SOC, we can retrieve the integration/package that matches a specific rule, via the related_integrations field on the rule.
|
||||
*/
|
||||
export const IntegrationIcon = memo(({ ruleId }: IntegrationIconProps) => {
|
||||
// Fetch all rules. For the AI for SOC effort, there should only be one rule per integration (which means for now 5-6 rules total)
|
||||
const { data, isLoading: ruleIsLoading } = useFindRulesQuery({});
|
||||
|
||||
// Fetch all packages
|
||||
const { installedPackages, isLoading: integrationIsLoading } = useFetchIntegrations();
|
||||
|
||||
const { integration } = useGetIntegrationFromRuleId({
|
||||
packages: installedPackages,
|
||||
rules: data?.rules,
|
||||
ruleId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Icon
|
||||
data-test-subj={INTEGRATION_TEST_ID}
|
||||
iconSize="l"
|
||||
integration={integration}
|
||||
isLoading={ruleIsLoading || integrationIsLoading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
IntegrationIcon.displayName = 'IntegrationIcon';
|
Loading…
Add table
Add a link
Reference in a new issue