diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 2bf1f58a6e12b..fd66676c78903 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -68,11 +68,13 @@ export function PackageCard({ titleLineClamp, descriptionLineClamp, maxCardHeight, + showDescription = true, + showReleaseBadge = true, }: PackageCardProps) { const theme = useEuiTheme(); let releaseBadge: React.ReactNode | null = null; - if (release && release !== 'ga') { + if (release && release !== 'ga' && showReleaseBadge) { releaseBadge = ( @@ -220,7 +222,7 @@ export function PackageCard({ ${getLineClampStyles(titleLineClamp)} } - min-height: 127px; + min-height: ${showDescription ? '127px' : null}; border-color: ${isQuickstart ? theme.euiTheme.colors.accent : null}; max-height: ${maxCardHeight ? `${maxCardHeight}px` : null}; overflow: ${maxCardHeight ? 'hidden' : null}; @@ -230,7 +232,7 @@ export function PackageCard({ layout="horizontal" title={title || ''} titleSize="xs" - description={description} + description={showDescription ? description : ''} hasBorder icon={ } onClick={onClickProp ?? onCardClick} diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx index 19f4d8740b75d..1338da4c88211 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx @@ -58,8 +58,10 @@ export interface IntegrationCardItem { name: string; onCardClick?: () => void; release?: IntegrationCardReleaseLabel; + showDescription?: boolean; showInstallationStatus?: boolean; showLabels?: boolean; + showReleaseBadge?: boolean; title: string; // Security Solution uses this prop to determine how many lines the card title should be truncated titleLineClamp?: number; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/index.ts new file mode 100644 index 0000000000000..c3554eae123dd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ +export { useEnhancedIntegrationCards } from './integrations/use_enhanced_integration_cards'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/constants.ts new file mode 100644 index 0000000000000..ffb818f8e47a3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/constants.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const RETURN_APP_ID = 'returnAppId'; +export const RETURN_PATH = 'returnPath'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.test.tsx new file mode 100644 index 0000000000000..cd5a5a8d17203 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.test.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + applyCategoryBadgeAndStyling, + useEnhancedIntegrationCards, + getCategoryBadgeIfAny, +} from './use_enhanced_integration_cards'; +import { IntegrationsFacets } from '../../../../../configurations/constants'; +import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { installationStatuses } from '@kbn/fleet-plugin/public'; +import { renderHook } from '@testing-library/react'; + +const mockCard = (name: string, categories?: string[]) => + ({ + id: `epr:${name}`, + description: 'description', + icons: [], + title: name, + url: `/app/integrations/detail/${name}-1.0.0/overview`, + integration: '', + name, + version: '1.0.0', + release: 'ga', + categories: categories ?? [], + isUnverified: false, + } as IntegrationCardItem); + +describe('applyCategoryBadgeAndStyling', () => { + const mockInt = mockCard('crowdstrike', ['edr_xdr']); + + it('should add the correct return path to the URL', () => { + const callerView = IntegrationsFacets.available; + const result = applyCategoryBadgeAndStyling(mockInt, callerView); + + const urlParams = new URLSearchParams(result.url.split('?')[1]); + expect(urlParams.get('returnPath')).toBe(`/configurations/integrations/${callerView}`); + }); + + it('should add the EDR/XDR badge if the category includes edr_xdr', () => { + const cardWithEdrXdr = { ...mockInt, categories: ['edr_xdr'] }; + const result = applyCategoryBadgeAndStyling(cardWithEdrXdr, IntegrationsFacets.available); + + expect(result.extraLabelsBadges).toHaveLength(1); + }); + + it('should add the SIEM badge if the category includes siem', () => { + const cardWithSiem = { ...mockInt, categories: ['siem'] }; + const result = applyCategoryBadgeAndStyling(cardWithSiem, IntegrationsFacets.available); + + expect(result.extraLabelsBadges).toHaveLength(1); + }); + + it('should not add any badge if the category does not include edr_xdr or siem', () => { + const cardWithOtherCategory = { ...mockInt, categories: ['other'] }; + const result = applyCategoryBadgeAndStyling( + cardWithOtherCategory, + IntegrationsFacets.available + ); + + expect(result.extraLabelsBadges).toHaveLength(0); + }); + + it('should set showDescription and showReleaseBadge to false', () => { + const result = applyCategoryBadgeAndStyling(mockInt, IntegrationsFacets.available); + + expect(result.showDescription).toBe(false); + expect(result.showReleaseBadge).toBe(false); + }); + + it('should set maxCardHeight to 88', () => { + const result = applyCategoryBadgeAndStyling(mockInt, IntegrationsFacets.available); + + expect(result.maxCardHeight).toBe(88); + }); +}); + +describe('useEnhancedIntegrationCards', () => { + const intA = mockCard('crowdstrike', ['edr_xdr']); + const intB = mockCard('google_secops', ['siem']); + const intC = mockCard('microsoft_sentinel', ['siem']); + const intD = mockCard('sentinel_one', ['edr_xdr']); + + it('should return sorted available integrations with badges applied', () => { + const mockIntegrationsList = [intA, intB, intC, intD]; + const { result } = renderHook(() => useEnhancedIntegrationCards(mockIntegrationsList)); + + expect(result.current.available).toHaveLength(4); + expect(result.current.available[0].id).toBe('epr:google_secops'); + expect(result.current.available[1].id).toBe('epr:microsoft_sentinel'); + expect(result.current.available[0].extraLabelsBadges).toHaveLength(1); + }); + + it('should return sorted installed integrations with badges applied', () => { + const mockIntegrationsList = [ + intA, + intB, + { ...intC, installStatus: installationStatuses.Installed }, + intD, + ]; + const { result } = renderHook(() => useEnhancedIntegrationCards(mockIntegrationsList)); + + expect(result.current.installed).toHaveLength(1); + expect(result.current.installed[0].id).toBe('epr:microsoft_sentinel'); + expect(result.current.installed[0].extraLabelsBadges).toHaveLength(1); + }); + + it('should handle an empty integrations list', () => { + const { result } = renderHook(() => useEnhancedIntegrationCards([])); + + expect(result.current.available).toHaveLength(0); + expect(result.current.installed).toHaveLength(0); + }); + + it('should correctly apply custom display order', () => { + const mockIntegrationsList = [intA, intB, intC, intD]; + + const shuffledList = [ + mockIntegrationsList[3], + mockIntegrationsList[1], + mockIntegrationsList[0], + mockIntegrationsList[2], + ]; + + const { result } = renderHook(() => useEnhancedIntegrationCards(shuffledList)); + + expect(result.current.available[0].id).toBe('epr:google_secops'); + expect(result.current.available[1].id).toBe('epr:microsoft_sentinel'); + expect(result.current.available[2].id).toBe('epr:sentinel_one'); + expect(result.current.available[3].id).toBe('epr:crowdstrike'); + }); +}); + +describe('getCategoryBadgeIfAny', () => { + it('should return "EDR/XDR" when the categories include "edr_xdr"', () => { + const categories = ['edr_xdr', 'other_category']; + const result = getCategoryBadgeIfAny(categories); + expect(result).toBe('EDR/XDR'); + }); + + it('should return "SIEM" when the categories include "siem"', () => { + const categories = ['siem', 'other_category']; + const result = getCategoryBadgeIfAny(categories); + expect(result).toBe('SIEM'); + }); + + it('should return "EDR/XDR" when both "edr_xdr" and "siem" are present', () => { + const categories = ['edr_xdr', 'siem']; + const result = getCategoryBadgeIfAny(categories); + // "edr_xdr" takes precedence, but we don't realistically expect both to be present + expect(result).toBe('EDR/XDR'); + }); + + it('should return null when neither "edr_xdr" nor "siem" are present', () => { + const categories = ['other_category']; + const result = getCategoryBadgeIfAny(categories); + expect(result).toBeNull(); + }); + + it('should return null when the categories array is empty', () => { + const categories: string[] = []; + const result = getCategoryBadgeIfAny(categories); + expect(result).toBeNull(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.tsx new file mode 100644 index 0000000000000..28dfd2f50cb63 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/hooks/integrations/use_enhanced_integration_cards.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiFlexItem, EuiSpacer, EuiBadge } from '@elastic/eui'; +import { installationStatuses, type IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation'; +import { CONFIGURATIONS_PATH } from '../../../../../../common/constants'; +import { IntegrationsFacets } from '../../../../../configurations/constants'; +import { RETURN_APP_ID, RETURN_PATH } from './constants'; + +const FEATURED_INTEGRATION_SORT_ORDER = [ + 'epr:splunk', + 'epr:google_secops', + 'epr:microsoft_sentinel', + 'epr:sentinel_one', + 'epr:crowdstrike', +]; +const INTEGRATION_CARD_MAX_HEIGHT_PX = 88; + +const addPathParamToUrl = (url: string, path: string) => { + const encodedPath = encodeURIComponent(path); + const paramsString = `${RETURN_APP_ID}=${SECURITY_UI_APP_ID}&${RETURN_PATH}=${encodedPath}`; + + if (url.indexOf('?') >= 0) { + return `${url}&${paramsString}`; + } + return `${url}?${paramsString}`; +}; + +export const getCategoryBadgeIfAny = (categories: string[]): string | null => { + return categories.includes('edr_xdr') ? 'EDR/XDR' : categories.includes('siem') ? 'SIEM' : null; +}; + +export const applyCategoryBadgeAndStyling = ( + card: IntegrationCardItem, + callerView: IntegrationsFacets +): IntegrationCardItem => { + const returnPath = `${CONFIGURATIONS_PATH}/integrations/${callerView}`; + const url = addPathParamToUrl(card.url, returnPath); + const categoryBadge = getCategoryBadgeIfAny(card.categories); + return { + ...card, + url, + showDescription: false, + showReleaseBadge: false, + extraLabelsBadges: categoryBadge + ? ([ + + + + {categoryBadge} + + , + ] as React.ReactNode[]) + : [], + maxCardHeight: INTEGRATION_CARD_MAX_HEIGHT_PX, + }; +}; + +const applyCustomDisplayOrder = (integrationsList: IntegrationCardItem[]) => { + return integrationsList.sort( + (a, b) => + FEATURED_INTEGRATION_SORT_ORDER.indexOf(a.id) - FEATURED_INTEGRATION_SORT_ORDER.indexOf(b.id) + ); +}; + +export const useEnhancedIntegrationCards = ( + integrationsList: IntegrationCardItem[] +): { available: IntegrationCardItem[]; installed: IntegrationCardItem[] } => { + const sorted = applyCustomDisplayOrder(integrationsList); + + const available = useMemo( + () => sorted.map((card) => applyCategoryBadgeAndStyling(card, IntegrationsFacets.available)), + [sorted] + ); + + const installed = useMemo( + () => + sorted + .map((card) => applyCategoryBadgeAndStyling(card, IntegrationsFacets.installed)) + .filter( + (card) => + card.installStatus === installationStatuses.Installed || + card.installStatus === installationStatuses.InstallFailed + ), + [sorted] + ); + + return { available, installed }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts new file mode 100644 index 0000000000000..64dbcd7dceb02 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/search_ai_lake/integrations.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/** Allow list of integrations to be available in the AI4DSOC integrations page */ +export const SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS: string[] = [ + 'crowdstrike', + 'google_secops', + 'microsoft_sentinel', + 'sentinel_one', + 'splunk', +]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/configurations/constants.ts index 07a2480752522..8f87d78e5167c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/constants.ts @@ -10,3 +10,8 @@ export enum ConfigurationTabs { basicRules = 'basic_rules', aiSettings = 'ai_settings', } + +export enum IntegrationsFacets { + available = 'browse', + installed = 'installed', +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/jest.config.js b/x-pack/solutions/security/plugins/security_solution/public/configurations/jest.config.js new file mode 100644 index 0000000000000..d16a4f5532b0e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../../..', + roots: ['/x-pack/solutions/security/plugins/security_solution/public/configurations'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/configurations', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/solutions/security/plugins/security_solution/public/configurations/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx index c81ac160e1d65..33ed8d8a79597 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx @@ -11,16 +11,16 @@ import { Routes, Route } from '@kbn/shared-ux-router'; import { Redirect } from 'react-router-dom'; import { BasicRules } from '../tabs/basic_rules'; import { AiSettings } from '../tabs/ai_settings'; -import { Integrations } from '../tabs/integrations'; import { CONFIGURATIONS_PATH } from '../../../common/constants'; import { ConfigurationTabs } from '../constants'; +import { LazyConfigurationsIntegrationsHome } from '../tabs/integrations'; export const ConfigurationsRouter = React.memo(() => { return ( { - return

{'Integrations'}

; -}; +import React from 'react'; +import { EuiSkeletonLoading } from '@elastic/eui'; +import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public'; +import { Routes, Route } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; +import { CONFIGURATIONS_PATH } from '../../../common/constants'; +import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../common/lib/search_ai_lake/integrations'; +import { useEnhancedIntegrationCards } from '../../common/lib/search_ai_lake/hooks'; +import { ConfigurationTabs, IntegrationsFacets } from '../constants'; +import { IntegrationsPage, IntegrationsSkeleton } from './integrations/components'; +import { withLazyHook } from '../../common/components/with_lazy_hook'; + +export interface IntegrationsPageProps { + useAvailablePackages: AvailablePackagesHookType; +} + +export const ConfigurationsIntegrationsHome = React.memo( + ({ useAvailablePackages }) => { + const { filteredCards, isLoading, searchTerm, setSearchTerm } = useAvailablePackages({ + prereleaseIntegrationsEnabled: true, + }); + + const allowedIntegrations = filteredCards.filter((card) => + SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS.includes(card.name) + ); + + const { available, installed } = useEnhancedIntegrationCards(allowedIntegrations); + + return ( + } + loadedContent={ + <> + + + + + + + + ( + + )} + /> + + + } + /> + ); + } +); +ConfigurationsIntegrationsHome.displayName = 'ConfigurationsIntegrationsHome'; + +export const LazyConfigurationsIntegrationsHome = withLazyHook(ConfigurationsIntegrationsHome, () => + import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook()) +); +LazyConfigurationsIntegrationsHome.displayName = 'LazyConfigurationsIntegrationsHome'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/constants.ts new file mode 100644 index 0000000000000..d229cb642927d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/constants.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const FACETS_MAX_WIDTH_PX = 216; +export const INTEGRATIONS_GRID_MAX_WIDTH_PX = 1200; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/index.ts b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/index.ts new file mode 100644 index 0000000000000..cfa10a4272b25 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { IntegrationsPage } from './integrations_page'; +export { IntegrationsSkeleton } from './integrations_skeleton'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_page.tsx new file mode 100644 index 0000000000000..40ff4ba173eef --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_page.tsx @@ -0,0 +1,84 @@ +/* + * 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, { lazy } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { noop } from 'lodash'; +import { FACETS_MAX_WIDTH_PX, INTEGRATIONS_GRID_MAX_WIDTH_PX } from './constants'; +import { IntegrationViewFacets } from './view_facets'; +import { IntegrationsFacets } from '../../../constants'; + +export const PackageListGrid = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.PackageList()) + .then((pkg) => pkg.PackageListGrid), +})); + +export interface IntegrationsGridProps { + view: IntegrationsFacets; + availableIntegrations: IntegrationCardItem[]; + installedIntegrations: IntegrationCardItem[]; + searchTerm: string; + setSearchTerm: (searchTerm: string) => void; +} + +export const IntegrationsPage = React.memo( + ({ view, availableIntegrations, installedIntegrations, searchTerm, setSearchTerm }) => { + return ( + + + + + + + + + + + + ); + } +); +IntegrationsPage.displayName = 'IntegrationsGrid'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_skeleton.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_skeleton.tsx new file mode 100644 index 0000000000000..2501aa55f0f07 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/integrations_skeleton.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexGroup, EuiSkeletonRectangle, EuiSpacer } from '@elastic/eui'; +import { FACETS_MAX_WIDTH_PX, INTEGRATIONS_GRID_MAX_WIDTH_PX } from './constants'; + +const FACET_LOADING_WIDTH = '216px'; +const FACET_LOADING_HEIGHT = '20px'; + +const SEARCH_BAR_LOADING_WIDTH = '872px'; +const SEARCH_BAR_LOADING_HEIGHT = '40px'; + +const CARD_LOADING_WIDTH = '279px'; +const CARD_LOADING_HEIGHT = '88px'; + +export const IntegrationsSkeleton: React.FC = () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.test.tsx new file mode 100644 index 0000000000000..700ec75f2f5f0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { IntegrationsFacets } from '../../../constants'; +import { IntegrationViewFacets, ALL, INSTALLED } from './view_facets'; +import { useNavigation } from '../../../../common/lib/kibana'; +import { SecurityPageName } from '@kbn/deeplinks-security'; + +jest.mock('../../../../common/lib/kibana', () => ({ + useNavigation: jest.fn(), +})); + +describe('IntegrationViewFacets', () => { + const mockNavigateTo = jest.fn(); + + beforeEach(() => { + (useNavigation as jest.Mock).mockReturnValue({ + navigateTo: mockNavigateTo, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const defaultProps = { + allCount: 10, + installedCount: 5, + selectedFacet: IntegrationsFacets.available, + }; + + it('renders the "All integrations" and "Installed integrations" buttons', () => { + render(); + + expect(screen.getByText(ALL)).toBeInTheDocument(); + expect(screen.getByText(INSTALLED)).toBeInTheDocument(); + }); + + it('calls navigateTo with the correct path when "All integrations" is clicked', () => { + render(); + + const allButton = screen.getByTestId('configurations.integrationsAll'); + fireEvent.click(allButton); + + expect(mockNavigateTo).toHaveBeenCalledWith({ + deepLinkId: SecurityPageName.configurationsIntegrations, + path: 'browse', + }); + }); + + it('calls navigateTo with the correct path when "Installed integrations" is clicked', () => { + render( + + ); + + const installedButton = screen.getByTestId('configurations.integrationsInstalled'); + fireEvent.click(installedButton); + + expect(mockNavigateTo).toHaveBeenCalledWith({ + deepLinkId: SecurityPageName.configurationsIntegrations, + path: 'installed', + }); + }); + + it('highlights the correct button based on the selectedFacet prop', () => { + const { rerender } = render(); + + const allButton = screen.getByText('All integrations'); + const installedButton = screen.getByText('Installed integrations'); + + const selectedFacetClass = 'euiFacetButton__text css-kw0vmr-euiFacetButton__text-isSelected'; + + // Check if the "All integrations" button is selected + expect(allButton).toHaveClass(selectedFacetClass); + expect(installedButton).not.toHaveClass(selectedFacetClass); + + // Rerender with the "Installed integrations" selected + rerender( + + ); + + // Check if the "Installed integrations" button is selected + expect(allButton).not.toHaveClass(selectedFacetClass); + expect(installedButton).toHaveClass(selectedFacetClass); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.tsx new file mode 100644 index 0000000000000..4e78024a3a074 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/integrations/components/view_facets.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFacetGroup, EuiFacetButton } from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { SecurityPageName } from '@kbn/deeplinks-security'; +import { IntegrationsFacets } from '../../../constants'; +import { useNavigation } from '../../../../common/lib/kibana'; + +export interface Props { + allCount: number; + installedCount: number; + selectedFacet: IntegrationsFacets; +} + +export const ALL = i18n.translate('xpack.securitySolution.configurations.integrations.allFacet', { + defaultMessage: 'All integrations', +}); + +export const INSTALLED = i18n.translate( + 'xpack.securitySolution.configurations.integrations.installedFacet', + { + defaultMessage: 'Installed integrations', + } +); + +export function IntegrationViewFacets({ allCount, installedCount, selectedFacet }: Props) { + const { navigateTo } = useNavigation(); + + return ( + + + navigateTo({ + deepLinkId: SecurityPageName.configurationsIntegrations, + path: 'browse', + }) + } + > + {ALL} + + + navigateTo({ + deepLinkId: SecurityPageName.configurationsIntegrations, + path: 'installed', + }) + } + > + {INSTALLED} + + + ); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts index 604f99913ecc9..d82358ba168d4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts @@ -8,15 +8,7 @@ import { useMemo } from 'react'; import type { PackageListItem } from '@kbn/fleet-plugin/common'; import { installationStatuses, useGetPackagesQuery } from '@kbn/fleet-plugin/public'; - -// We hardcode these here for now as we currently do not have any other way to filter out all the unwanted integrations. -const AI_FOR_SOC_INTEGRATIONS = [ - 'splunk', // doesnt yet exist - 'google_secops', - 'microsoft_sentinel', - 'sentinel_one', - 'crowdstrike', -]; +import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../../common/lib/search_ai_lake/integrations'; export interface UseFetchIntegrationsResult { /** @@ -50,7 +42,10 @@ export const useFetchIntegrations = (): UseFetchIntegrationsResult => { }); const aiForSOCPackages: PackageListItem[] = useMemo( - () => (allPackages?.items || []).filter((pkg) => AI_FOR_SOC_INTEGRATIONS.includes(pkg.name)), + () => + (allPackages?.items || []).filter((pkg) => + SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS.includes(pkg.name) + ), [allPackages] ); const availablePackages: PackageListItem[] = useMemo( diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/__snapshots__/side_navigation.test.tsx.snap b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/__snapshots__/side_navigation.test.tsx.snap index d2b718fdb9b46..5bb612b33ac93 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/__snapshots__/side_navigation.test.tsx.snap +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/__snapshots__/side_navigation.test.tsx.snap @@ -698,13 +698,13 @@ Object { Object { "children": Array [ Object { - "link": "securitySolutionUI:configurations-ai_settings", + "link": "securitySolutionUI:configurations-integrations", }, Object { "link": "securitySolutionUI:configurations-basic_rules", }, Object { - "link": "securitySolutionUI:configurations-integrations", + "link": "securitySolutionUI:configurations-ai_settings", }, ], "id": "configurations", diff --git a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_soc/ai_soc_navigation.ts b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_soc/ai_soc_navigation.ts index abc10708c0a5d..46626b82b178d 100644 --- a/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_soc/ai_soc_navigation.ts +++ b/x-pack/solutions/security/plugins/security_solution_serverless/public/navigation/ai_soc/ai_soc_navigation.ts @@ -73,13 +73,13 @@ export const createAiSocNavigationTree$ = (): Rx.Observable = renderAs: 'panelOpener', children: [ { - link: `securitySolutionUI:configurations-ai_settings`, + link: `securitySolutionUI:configurations-integrations`, }, { link: `securitySolutionUI:configurations-basic_rules`, }, { - link: `securitySolutionUI:configurations-integrations`, + link: `securitySolutionUI:configurations-ai_settings`, }, ], };