diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx index 0f0aee8ed8970..5caccdca1b86a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.test.tsx @@ -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'); + }); + it('should render a single integration icon', () => { const { getByTestId } = render( - + ); - expect(getByTestId(INTEGRATION_INTEGRATION_ICON_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).toBeInTheDocument(); }); - it('should return a single integration loading', () => { - (useGetIntegrationFromRuleId as jest.Mock).mockReturnValue({ - integration: {}, - isLoading: true, - }); - + it('should render the loading skeleton', () => { const { getByTestId } = render( - + + + ); + + expect(getByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`)).toBeInTheDocument(); + }); + + it('should not render skeleton or icon', () => { + const { queryByTestId } = render( + + ); - expect(getByTestId(INTEGRATION_LOADING_SKELETON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(`${testId}-${INTEGRATION_ICON_TEST_ID}`)).not.toBeInTheDocument(); + expect( + queryByTestId(`${testId}-${INTEGRATION_LOADING_SKELETON_TEST_ID}`) + ).not.toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx index 9646dedae979e..69e1a169b7a12 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/common/integration_icon.tsx @@ -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) => ( {integration ? ( ) : null} - ); -}); + ) +); + IntegrationIcon.displayName = 'IntegrationIcon'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx index 4e72e0bbad89d..4e62ab1b52ef0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/integrations/integration_card.tsx @@ -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( > - diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx index c538d232e9070..494abf10baf5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/integration_card.tsx @@ -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={ - + } layout="horizontal" onClick={onClick} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx index cc0f4a97abd4c..77ce19d921616 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.test.tsx @@ -37,8 +37,16 @@ const packages: PackageListItem[] = [ version: '', }, ]; +const ruleResponse = { + rules: [], + isLoading: false, +}; describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render all components', () => { (useIntegrations as jest.Mock).mockReturnValue({ isLoading: false, @@ -49,7 +57,7 @@ describe('', () => { }); const { getByTestId, queryByTestId } = render( - + ); expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); @@ -64,7 +72,7 @@ describe('', () => { }); const { getByTestId, queryByTestId } = render( - + ); expect(getByTestId(INTEGRATION_BUTTON_LOADING_TEST_ID)).toBeInTheDocument(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx index 572565852b111..f4c2491d58bb8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/search_bar/search_bar_section.tsx @@ -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 ( - - - - - - - - - - - ); -}); + return ( + + + + + + + + + + + ); + } +); SearchBarSection.displayName = 'SearchBarSection'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx index f48b8891e3a50..0979afec3e9c8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/basic_cell_renderer.test.tsx @@ -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( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx index 1507dbfd17dc7..24747cab70d4e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.test.tsx @@ -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(); + + 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(); + + 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: '', diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx index 5860da4058356..51a5a46b290ab 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_stats_renderers.tsx @@ -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 ; +}); + +IntegrationIcon.displayName = 'IntegrationIcon'; + /** * Return a renderer for integration aggregation. */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx index 692048a894bfe..685bbc966a91c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.test.tsx @@ -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(); @@ -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(); @@ -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(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx index 08bc96dc5f4de..9f69e5cb28f3e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/group_title_renderers.tsx @@ -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 ( {integration ? ( @@ -104,13 +110,10 @@ export const IntegrationNameGroupContent = memo<{ alignItems="center" > - diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx index 15db07b321448..d39c2dac6e5bc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.test.tsx @@ -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'); - -describe('KibanaAlertRelatedIntegrationsCellRenderer', () => { - it('should handle missing field', () => { - (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ - integration: null, - isLoading: false, - }); - - const alert: Alert = { - _id: '_id', - _index: '_index', - }; +jest.mock('@kbn/fleet-plugin/public/hooks'); - const { queryByTestId } = render(); +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}`; - expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); - expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument(); +describe('KibanaAlertRelatedIntegrationsCellRenderer', () => { + beforeEach(() => { + jest.clearAllMocks(); }); - it('should handle not finding matching integration', () => { - (useGetIntegrationFromPackageName as jest.Mock).mockReturnValue({ - integration: null, - isLoading: false, - }); - + it('should handle missing field', () => { const alert: Alert = { _id: '_id', _index: '_index', - [ALERT_RULE_PARAMETERS]: ['splunk'], }; + const packages: PackageListItem[] = []; - const { queryByTestId } = render(); + const { queryByTestId } = render( + + ); - expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(LOADING_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, - }); - + it('should handle not finding matching integration', () => { const alert: Alert = { _id: '_id', _index: '_index', - [ALERT_RULE_PARAMETERS]: ['splunk'], + [ALERT_RULE_PARAMETERS]: [{ related_integrations: { package: ['splunk'] } }], }; - - const { getByTestId, queryByTestId } = render( - + 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( + ); - 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( - + ); - expect(queryByTestId(SKELETON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(LOADING_SKELETON_TEST_ID)).not.toBeInTheDocument(); expect(getByTestId(ICON_TEST_ID)).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx index 3a8716855824c..928770da54a32 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/kibana_alert_related_integrations_cell_renderer.tsx @@ -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 ( - - {integration ? ( - - ) : null} - + ); } ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx index 880c8c4551bd2..21aa8e243ebed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.test.tsx @@ -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'; - -jest.mock('../../../hooks/alert_summary/use_get_integration_from_package_name'); +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'; + +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( - + ); @@ -46,7 +61,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -63,7 +78,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -80,7 +95,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -97,7 +112,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -114,7 +129,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -131,7 +146,7 @@ describe('CellValue', () => { const { getByText } = render( - + ); @@ -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( - + ); - 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( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx index a7de92212a0ac..ab54d3ed8f885 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/render_cell.tsx @@ -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 = ; + component = ; break; case ALERT_SEVERITY: diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx index f045ec086c91f..66b6760169da5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.test.tsx @@ -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('', () => { it('should render all components', () => { const { getByTestId } = render( -
+
); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx index 87d581dbc52f8..305601f8eb9fe 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table.tsx @@ -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 ( ', () => { it('should render all components', () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx index db350df9f66e8..f7b05ff7804da 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section.tsx @@ -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[]) =>
, - [dataView] + (groupingFilters: Filter[]) => ( +
+ ), + [dataView, packages, ruleResponse] ); return ( -
- -
+ +
+ +
+
); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx new file mode 100644 index 0000000000000..6ada4adff9597 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/table/table_section_context.tsx @@ -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(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 ( + {children} + ); + } +); + +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; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx index cc8f88d51e58d..ab08c6ff4f4aa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.test.tsx @@ -45,6 +45,10 @@ const packages: PackageListItem[] = [ version: '', }, ]; +const ruleResponse = { + rules: [], + isLoading: false, +}; describe('', () => { it('should render a loading skeleton while creating the dataView', async () => { @@ -61,7 +65,7 @@ describe('', () => { }); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument(); expect(getByTestId(SKELETON_TEST_ID)).toBeInTheDocument(); @@ -86,7 +90,7 @@ describe('', () => { })); await act(async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); await new Promise(process.nextTick); @@ -123,7 +127,7 @@ describe('', () => { await act(async () => { const { getByTestId } = render( - + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx index 2f5b65aebf9d5..afa8215c01be8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx @@ -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(undefined); const [loading, setLoading] = useState(true); @@ -96,11 +110,15 @@ export const Wrapper = memo(({ packages }: WrapperProps) => {
- + - +
)} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts index 65bd594945b2d..555f562e2afa8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_assistant.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts deleted file mode 100644 index 7c3bf90ee684f..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_package_name.ts +++ /dev/null @@ -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] - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts index b4980a7a82767..e7d6a51fae800 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.test.ts @@ -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 { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: '' })); - - expect(result.current.isLoading).toBe(false); - expect(result.current.integration).toBe(undefined); - }); + const packages: PackageListItem[] = []; + const ruleId = ''; + const rules: RuleResponse[] = []; - 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({ packages, ruleId, rules })); - 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({ packages, ruleId, rules })); + + 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', }); - - const { result } = renderHook(() => useGetIntegrationFromRuleId({ ruleId: 'rule_id' })); - - expect(result.current.isLoading).toBe(false); - expect(result.current.integration).toEqual({ name: 'rule_name' }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts index 5d1638aee5446..068ebb9f01f7c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_get_integration_from_rule_id.ts @@ -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] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts index 312ee7c7c7dcf..bd3cc9820e34e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.test.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts index 962b215f8276f..7e5b4cd0748a9 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_integrations.ts @@ -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] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx index 34cbc5b513ab7..8dbe827ca7c2c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx @@ -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('', () => { + 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, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx index 15f3e992dc092..ad14707a6d644 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx @@ -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(); - - if (isLoading) { + const { + availablePackages, + installedPackages, + isLoading: integrationIsLoading, + } = useFetchIntegrations(); + + // 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 ( { return ; } - return ; + return ; }); AlertSummaryPage.displayName = 'AlertSummaryPage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx index e5d66bf82d6d9..8227f572b6e3b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/header_title.tsx @@ -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(() => { /> } > - + diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx new file mode 100644 index 0000000000000..3a0086f05cf3d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + 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( + + + + ); + + expect(getByTestId(LOADING_SKELETON_TEST_ID)).toBeInTheDocument(); + expect(queryByTestId(ICON_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx new file mode 100644 index 0000000000000..125a964d549f1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/ai_for_soc/components/integration_icon.tsx @@ -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 ( + + ); +}); + +IntegrationIcon.displayName = 'IntegrationIcon';