diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx index a37730ad1570c..bff42170311d4 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx @@ -124,7 +124,6 @@ describe('InstallationStatus', () => { const callout = getByTestId('installation-status-callout'); expect(spacer).toHaveStyle('background: #FFFFFF'); - expect(callout).toHaveStyle('padding: 8px 16px'); expect(callout).toHaveTextContent('Installed'); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx index 7b0a0da308c2f..8a92635751c6b 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx @@ -7,9 +7,9 @@ import React from 'react'; -import { EuiCallOut, EuiSpacer, useEuiTheme } from '@elastic/eui'; +import { COLOR_MODES_STANDARD, EuiCallOut, EuiIcon, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; +import { css } from '@emotion/css'; import { installationStatuses } from '../../../../../../common/constants'; import type { EpmPackageInstallStatus } from '../../../../../../common/types'; @@ -37,8 +37,50 @@ const installStatusMapToColor: Record< interface InstallationStatusProps { installStatus: EpmPackageInstallStatus | null | undefined; showInstallationStatus?: boolean; + compressed?: boolean; } +const useInstallationStatusStyles = () => { + const { euiTheme, colorMode } = useEuiTheme(); + const successBackgroundColor = euiTheme.colors.backgroundBaseSuccess; + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + + return { + installationStatus: css` + position: absolute; + border-radius: 0 0 ${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium}; + bottom: 0; + left: 0; + width: 100%; + overflow: hidden; + `, + compressedInstallationStatus: css` + position: absolute; + border-radius: 0 ${euiTheme.border.radius.medium} ${euiTheme.border.radius.medium} 0; + bottom: 0; + right: 0; + width: 65px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + background-color: ${isDarkMode ? euiTheme.colors.success : successBackgroundColor}; + color: ${isDarkMode ? euiTheme.colors.emptyShade : euiTheme.colors.textSuccess}; + `, + compressedInstallationStatusIcon: css` + color: ${isDarkMode ? euiTheme.colors.emptyShade : euiTheme.colors.textSuccess}; + `, + installationStatusCallout: css` + padding: ${euiTheme.size.s} ${euiTheme.size.m}; + text-align: center; + `, + installationStatusSpacer: css` + background: ${euiTheme.colors.emptyShade}; + `, + }; +}; + export const getLineClampStyles = (lineClamp?: number) => lineClamp ? `-webkit-line-clamp: ${lineClamp};display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;` @@ -53,35 +95,32 @@ export const shouldShowInstallationStatus = ({ installStatus === installationStatuses.InstallFailed); export const InstallationStatus: React.FC = React.memo( - ({ installStatus, showInstallationStatus }) => { - const { euiTheme } = useEuiTheme(); + ({ installStatus, showInstallationStatus, compressed }) => { + const styles = useInstallationStatusStyles(); + + const cardPanelClassName = compressed + ? styles.compressedInstallationStatus + : styles.installationStatus; + return shouldShowInstallationStatus({ installStatus, showInstallationStatus }) ? ( -
- - -
+ compressed ? ( +
+ +
+ ) : ( +
+ + +
+ ) ) : null; } ); 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 fd66676c78903..dec54eb63f0c3 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 @@ -60,6 +60,7 @@ export function PackageCard({ isUpdateAvailable, showLabels = true, showInstallationStatus, + showCompressedInstallationStatus, extraLabelsBadges, isQuickstart = false, installStatus, @@ -256,6 +257,7 @@ export function PackageCard({ 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 1338da4c88211..a513728879a5a 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 @@ -60,6 +60,7 @@ export interface IntegrationCardItem { release?: IntegrationCardReleaseLabel; showDescription?: boolean; showInstallationStatus?: boolean; + showCompressedInstallationStatus?: boolean; showLabels?: boolean; showReleaseBadge?: boolean; title: string; diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.test.tsx index 62f48464783f6..542ba15504b0c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.test.tsx @@ -11,8 +11,8 @@ import { NoDataFound } from './no_data_found'; import { renderWithTestProvider } from '../../test/test_provider'; // Mocking components which implementation details are out of scope for this unit test -jest.mock('../../../onboarding/components/onboarding_context', () => ({ - OnboardingContextProvider: () =>
, +jest.mock('../../../common/lib/integrations/hooks/integration_context', () => ({ + IntegrationContextProvider: () =>
, })); jest.mock('../../../common/hooks/use_space_id'); @@ -40,6 +40,6 @@ describe('NoDataFound Component', () => { screen.getByRole('heading', { name: /connect sources to discover assets/i }) ).toBeInTheDocument(); - expect(screen.getByTestId('onboarding-grid')).toBeInTheDocument(); + expect(screen.getByTestId('integration-grid')).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.tsx b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.tsx index ced7ac62c0964..3ae655561c964 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/asset_inventory/components/onboarding/no_data_found.tsx @@ -16,13 +16,13 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { OnboardingContextProvider } from '../../../onboarding/components/onboarding_context'; import { useSpaceId } from '../../../common/hooks/use_space_id'; import { AssetInventoryTitle } from '../asset_inventory_title'; import { AssetInventoryLoading } from '../asset_inventory_loading'; import illustration from '../../../common/images/integrations_light.png'; -import { IntegrationsCardGridTabs } from '../../../onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs'; import { TEST_SUBJ_ONBOARDING_NO_DATA_FOUND } from '../../constants'; +import { SecurityIntegrations } from '../../../common/lib/integrations/components'; +import { IntegrationContextProvider } from '../../../common/lib/integrations/hooks/integration_context'; export const NoDataFound = () => { const spaceId = useSpaceId(); @@ -68,9 +68,9 @@ export const NoDataFound = () => { - - - + + + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_integrations_url.ts b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_integrations_url.ts index e80c1e871d49a..700e776c4455f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_integrations_url.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_add_integrations_url.ts @@ -6,23 +6,44 @@ */ import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { ADD_DATA_PATH, ADD_THREAT_INTELLIGENCE_DATA_PATH } from '../../../common/constants'; +import { useNavigateTo, useGetAppUrl } from '@kbn/security-solution-navigation'; +import { + ADD_DATA_PATH, + ADD_THREAT_INTELLIGENCE_DATA_PATH, + SECURITY_FEATURE_ID, + SecurityPageName, +} from '../../../common/constants'; import { isThreatIntelligencePath } from '../../helpers'; -import { useKibana, useNavigateTo } from '../lib/kibana'; +import { useKibana } from '../lib/kibana'; +import { hasCapabilities } from '../lib/capabilities'; export const useAddIntegrationsUrl = () => { const { http: { basePath: { prepend }, }, + application: { capabilities }, } = useKibana().services; const { pathname } = useLocation(); + const { getAppUrl } = useGetAppUrl(); const { navigateTo } = useNavigateTo(); const isThreatIntelligence = isThreatIntelligencePath(pathname); + const hasSearchAILakeAccess = hasCapabilities(capabilities, [ + [`${SECURITY_FEATURE_ID}.external_detections`], + ]); - const integrationsUrl = isThreatIntelligence ? ADD_THREAT_INTELLIGENCE_DATA_PATH : ADD_DATA_PATH; + const searchAILakeIntegrationsPath = getAppUrl({ + deepLinkId: SecurityPageName.configurationsIntegrations, + path: 'browse', + }); + + const integrationsUrl = isThreatIntelligence + ? ADD_THREAT_INTELLIGENCE_DATA_PATH + : hasSearchAILakeAccess + ? searchAILakeIntegrationsPath + : ADD_DATA_PATH; const href = useMemo(() => prepend(integrationsUrl), [prepend, integrationsUrl]); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/index.tsx new file mode 100644 index 0000000000000..39e4652fe6dfe --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/index.tsx @@ -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. + */ +import React from 'react'; + +export const SecurityIntegrations = () =>
; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/integration_card_grid_tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/integration_card_grid_tabs.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/integration_card_grid_tabs.tsx rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/integration_card_grid_tabs.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/package_list_grid.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/package_list_grid.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/package_list_grid.tsx rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/package_list_grid.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/with_filtered_integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/with_filtered_integrations.tsx new file mode 100644 index 0000000000000..bc3f25f0ada64 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/__mocks__/with_filtered_integrations.tsx @@ -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. + */ +import React from 'react'; + +export const WithFilteredIntegrations = () =>
; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/available_integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/available_integrations.tsx new file mode 100644 index 0000000000000..b0df5de2a0b23 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/available_integrations.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public'; +import type { UseSelectedTabReturn } from '../hooks/use_selected_tab'; +import type { IntegrationCardMetadata, RenderChildrenType, TopCalloutRenderer } from '../types'; +import { useFilterCards } from '../hooks/use_filter_cards'; + +export const AvailableIntegrationsComponent: React.FC<{ + useAvailablePackages: AvailablePackagesHookType; + renderChildren: RenderChildrenType; + prereleaseIntegrationsEnabled: boolean; + checkCompleteMetadata?: IntegrationCardMetadata; + selectedTabResult: UseSelectedTabReturn; + topCalloutRenderer?: TopCalloutRenderer; +}> = ({ + useAvailablePackages, + renderChildren, + prereleaseIntegrationsEnabled, + checkCompleteMetadata, + selectedTabResult, + topCalloutRenderer, +}) => { + const { availablePackagesResult, allowedIntegrations } = useFilterCards({ + featuredCardIds: selectedTabResult.selectedTab?.featuredCardIds, + useAvailablePackages, + prereleaseIntegrationsEnabled, + }); + + return renderChildren({ + allowedIntegrations, + availablePackagesResult, + checkCompleteMetadata, + selectedTabResult, + topCalloutRenderer, + }); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/index.tsx new file mode 100644 index 0000000000000..7965c62f1fbcb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/index.tsx @@ -0,0 +1,33 @@ +/* + * 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 { useSelectedTab } from '../hooks/use_selected_tab'; +import { INTEGRATION_TABS } from '../configs/integration_tabs_configs'; +import { IntegrationsCardGridTabs } from './integration_card_grid_tabs'; +import { WithFilteredIntegrations } from './with_filtered_integrations'; +import type { IntegrationCardMetadata, TopCalloutRenderer } from '../types'; +import { useIntegrationContext } from '../hooks/integration_context'; + +export const SecurityIntegrations: React.FC<{ + checkCompleteMetadata?: IntegrationCardMetadata; + topCalloutRenderer?: TopCalloutRenderer; +}> = ({ checkCompleteMetadata, topCalloutRenderer }) => { + const { spaceId } = useIntegrationContext(); + const selectedTabResult = useSelectedTab({ + spaceId, + integrationTabs: INTEGRATION_TABS, + }); + return ( + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs.tsx new file mode 100644 index 0000000000000..6c481686c080b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs.tsx @@ -0,0 +1,41 @@ +/* + * 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 type { RenderChildrenType } from '../types'; +import { useIntegrationCardList } from '../hooks/use_integration_card_list'; +import { IntegrationsCardGridTabsComponent } from './integration_card_grid_tabs_component'; + +export const DEFAULT_CHECK_COMPLETE_METADATA = { + installedIntegrationsCount: 0, + isAgentRequired: false, +}; + +export const IntegrationsCardGridTabs: RenderChildrenType = ({ + topCalloutRenderer, + allowedIntegrations, + availablePackagesResult, + checkCompleteMetadata = DEFAULT_CHECK_COMPLETE_METADATA, + selectedTabResult, +}) => { + const list = useIntegrationCardList({ + integrationsList: allowedIntegrations, + featuredCardIds: selectedTabResult.selectedTab?.featuredCardIds, + }); + + const { installedIntegrationsCount, isAgentRequired } = checkCompleteMetadata; + + return ( + + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.test.tsx similarity index 50% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.test.tsx index 8abc77140d917..a94f943a2518c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.test.tsx @@ -7,21 +7,20 @@ import React from 'react'; import { render, fireEvent, waitFor, act } from '@testing-library/react'; -import { IntegrationsCardGridTabsComponent } from './integration_card_grid_tabs'; +import { IntegrationsCardGridTabsComponent } from './integration_card_grid_tabs_component'; import * as module from '@kbn/fleet-plugin/public'; import { useStoredIntegrationSearchTerm, useStoredIntegrationTabId, -} from '../../../hooks/use_stored_state'; -import { DEFAULT_TAB } from './constants'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; - -jest.mock('../../../onboarding_context'); -jest.mock('../../../hooks/use_stored_state'); -jest.mock('../../../lib/telemetry'); -jest.mock('../../../../../common/lib/kibana', () => ({ - ...jest.requireActual('../../../../../common/lib/kibana'), +} from '../hooks/use_stored_state'; +import { INTEGRATION_TABS } from '../configs/integration_tabs_configs'; +import { mockTrackLinkClick } from '../hooks/__mocks__/mocks'; + +jest.mock('../hooks/integration_context'); +jest.mock('../hooks/use_stored_state'); +jest.mock('../../kibana', () => ({ + ...jest.requireActual('../../kibana'), useNavigation: jest.fn().mockReturnValue({ navigateTo: jest.fn(), getAppUrl: jest.fn(), @@ -39,7 +38,6 @@ jest .mockImplementation(() => Promise.resolve({ PackageListGrid: mockPackageList })); describe('IntegrationsCardGridTabsComponent', () => { - const mockUseAvailablePackages = jest.fn(); const mockSetTabId = jest.fn(); const mockSetCategory = jest.fn(); const mockSetSelectedSubCategory = jest.fn(); @@ -47,49 +45,43 @@ describe('IntegrationsCardGridTabsComponent', () => { const props = { installedIntegrationsCount: 1, isAgentRequired: false, - useAvailablePackages: mockUseAvailablePackages, + availablePackagesResult: { + isLoading: false, + setCategory: mockSetCategory, + setSelectedSubCategory: mockSetSelectedSubCategory, + setSearchTerm: mockSetSearchTerm, + searchTerm: 'new search term', + }, + integrationList: [], + selectedTabResult: { + selectedTab: INTEGRATION_TABS[0], + toggleIdSelected: INTEGRATION_TABS[0].id, + setSelectedTabIdToStorage: mockSetTabId, + integrationTabs: INTEGRATION_TABS, + }, }; beforeEach(() => { jest.clearAllMocks(); - (useStoredIntegrationTabId as jest.Mock).mockReturnValue([DEFAULT_TAB.id, jest.fn()]); + (useStoredIntegrationTabId as jest.Mock).mockReturnValue([INTEGRATION_TABS[0].id, jest.fn()]); (useStoredIntegrationSearchTerm as jest.Mock).mockReturnValue(['', jest.fn()]); }); it('renders loading skeleton when data is loading', () => { - mockUseAvailablePackages.mockReturnValue({ - isLoading: true, - filteredCards: [], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - }); - - const { getByTestId } = render( - - ); + const testProps = { + ...props, + availablePackagesResult: { + ...props.availablePackagesResult, + isLoading: true, + }, + }; + const { getByTestId } = render(); expect(getByTestId('loadingPackages')).toBeInTheDocument(); }); it('renders the package list when data is available', async () => { - mockUseAvailablePackages.mockReturnValue({ - isLoading: false, - filteredCards: [{ id: 'card1', name: 'Card 1', url: 'https://mock-url' }], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - }); - - const { getByTestId } = render( - - ); + const { getByTestId } = render(); await waitFor(() => { expect(getByTestId('packageList')).toBeInTheDocument(); @@ -99,20 +91,7 @@ describe('IntegrationsCardGridTabsComponent', () => { it('saves the selected tab to storage', () => { (useStoredIntegrationTabId as jest.Mock).mockReturnValue(['recommended', mockSetTabId]); - mockUseAvailablePackages.mockReturnValue({ - isLoading: false, - filteredCards: [], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - }); - - const { getByTestId } = render( - - ); + const { getByTestId } = render(); const tabButton = getByTestId('user'); @@ -125,20 +104,7 @@ describe('IntegrationsCardGridTabsComponent', () => { it('tracks the tab clicks', () => { (useStoredIntegrationTabId as jest.Mock).mockReturnValue(['recommended', mockSetTabId]); - mockUseAvailablePackages.mockReturnValue({ - isLoading: false, - filteredCards: [], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - }); - - const { getByTestId } = render( - - ); + const { getByTestId } = render(); const tabButton = getByTestId('user'); @@ -146,24 +112,11 @@ describe('IntegrationsCardGridTabsComponent', () => { fireEvent.click(tabButton); }); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith('tab_user'); + expect(mockTrackLinkClick).toHaveBeenCalledWith('tab_user'); }); it('renders no search tools when showSearchTools is false', async () => { - mockUseAvailablePackages.mockReturnValue({ - isLoading: false, - filteredCards: [], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - }); - - render( - - ); + render(); await waitFor(() => { expect(mockPackageList.mock.calls[0][0].showSearchTools).toEqual(false); @@ -177,21 +130,7 @@ describe('IntegrationsCardGridTabsComponent', () => { mockSetSearchTermToStorage, ]); - mockUseAvailablePackages.mockReturnValue({ - isLoading: false, - filteredCards: [], - setCategory: mockSetCategory, - setSelectedSubCategory: mockSetSelectedSubCategory, - setSearchTerm: mockSetSearchTerm, - searchTerm: 'new search term', - }); - - render( - - ); + render(); await waitFor(() => { expect(mockPackageList.mock.calls[0][0].searchTerm).toEqual('new search term'); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.tsx similarity index 60% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.tsx index aea4f628b40ef..43374033cb66b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/integration_card_grid_tabs_component.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { lazy, Suspense, useMemo, useCallback, useEffect, useRef } from 'react'; +import React, { lazy, Suspense, useCallback, useEffect, useRef } from 'react'; import { COLOR_MODES_STANDARD, EuiButtonGroup, @@ -13,36 +13,38 @@ import { EuiSkeletonText, useEuiTheme, } from '@elastic/eui'; -import type { AvailablePackagesHookType, IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; import { noop } from 'lodash'; import { css } from '@emotion/react'; -import { withLazyHook } from '../../../../../common/components/with_lazy_hook'; +import { useIntegrationCardGridTabsStyles } from '../hooks/integration_card_grid_tabs.styles'; import { - useStoredIntegrationSearchTerm, - useStoredIntegrationTabId, -} from '../../../hooks/use_stored_state'; -import { useOnboardingContext } from '../../../onboarding_context'; -import { - DEFAULT_TAB, + DEFAULT_INTEGRATION_CARD_CONTENT_HEIGHT, LOADING_SKELETON_TEXT_LINES, SCROLL_ELEMENT_ID, SEARCH_FILTER_CATEGORIES, TELEMETRY_INTEGRATION_TAB, - WITHOUT_SEARCH_BOX_HEIGHT, - WITH_SEARCH_BOX_HEIGHT, -} from './constants'; -import { INTEGRATION_TABS, INTEGRATION_TABS_BY_ID } from './integration_tabs_configs'; -import { useIntegrationCardList } from './use_integration_card_list'; -import { IntegrationTabId } from './types'; -import { IntegrationCardTopCallout } from './callouts/integration_card_top_callout'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; -import { useIntegrationCardGridTabsStyles } from './integration_card_grid_tabs.styles'; +} from '../constants'; +import type { AvailablePackagesResult } from '../types'; +import { IntegrationTabId } from '../types'; +import type { UseSelectedTabReturn } from '../hooks/use_selected_tab'; +import { useStoredIntegrationSearchTerm } from '../hooks/use_stored_state'; +import { useIntegrationContext } from '../hooks/integration_context'; export interface IntegrationsCardGridTabsProps { installedIntegrationsCount: number; isAgentRequired: boolean; - useAvailablePackages: AvailablePackagesHookType; + availablePackagesResult: AvailablePackagesResult; + topCalloutRenderer?: React.FC<{ + installedIntegrationsCount: number; + isAgentRequired: boolean; + selectedTabId: IntegrationTabId; + }>; + integrationList: IntegrationCardItem[]; + selectedTabResult: UseSelectedTabReturn; + packageListGridOptions?: { + showCardLabels?: boolean; + }; } const emptyStateStyles = { paddingTop: '16px' }; @@ -53,16 +55,26 @@ export const PackageListGrid = lazy(async () => ({ .then((pkg) => pkg.PackageListGrid), })); +// beware if local storage, need to add project id to the key export const IntegrationsCardGridTabsComponent = React.memo( - ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => { - const { spaceId } = useOnboardingContext(); + ({ + isAgentRequired, + installedIntegrationsCount, + topCalloutRenderer: TopCallout, + integrationList, + availablePackagesResult, + selectedTabResult, + packageListGridOptions, + }) => { + const { + spaceId, + telemetry: { trackLinkClick }, + } = useIntegrationContext(); const scrollElement = useRef(null); const { colorMode } = useEuiTheme(); const isDark = colorMode === COLOR_MODES_STANDARD.dark; - const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId( - spaceId, - DEFAULT_TAB.id - ); + const { selectedTab, toggleIdSelected, setSelectedTabIdToStorage, integrationTabs } = + selectedTabResult; const [searchTermFromStorage, setSearchTermToStorage] = useStoredIntegrationSearchTerm(spaceId); const onTabChange = useCallback( (stringId: string) => { @@ -70,23 +82,13 @@ export const IntegrationsCardGridTabsComponent = React.memo INTEGRATION_TABS_BY_ID[toggleIdSelected], [toggleIdSelected]); + const { isLoading, searchTerm, setCategory, setSearchTerm, setSelectedSubCategory } = + availablePackagesResult; const buttonGroupStyles = useIntegrationCardGridTabsStyles(); @@ -129,11 +131,6 @@ export const IntegrationsCardGridTabsComponent = React.memo - - - + {integrationTabs.length > 1 && ( + + + + )} + TopCallout ? ( + + ) : null } calloutTopSpacerSize="m" categories={SEARCH_FILTER_CATEGORIES} // We do not want to show categories and subcategories as the search bar filter emptyStateStyles={emptyStateStyles} - list={list} + list={integrationList} // Todo: fix this scrollElementId={SCROLL_ELEMENT_ID} searchTerm={searchTerm} selectedCategory={selectedTab.category ?? ''} @@ -198,7 +197,7 @@ export const IntegrationsCardGridTabsComponent = React.memo import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook()), - -); -IntegrationsCardGridTabs.displayName = 'IntegrationsCardGridTabs'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/with_filtered_integrations.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/with_filtered_integrations.tsx new file mode 100644 index 0000000000000..a5303823d102d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/components/with_filtered_integrations.tsx @@ -0,0 +1,21 @@ +/* + * 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 { EuiSkeletonText } from '@elastic/eui'; +import { withLazyHook } from '../../../components/with_lazy_hook'; +import { LOADING_SKELETON_TEXT_LINES } from '../constants'; +import { AvailableIntegrationsComponent } from './available_integrations'; + +export const WithFilteredIntegrations = withLazyHook( + AvailableIntegrationsComponent, + () => import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook()), + +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/configs/integration_tabs_configs.ts similarity index 91% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/configs/integration_tabs_configs.ts index 2e673d98278a3..8d1ee44c4dbc3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/configs/integration_tabs_configs.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { IntegrationTabId, type Tab } from './types'; +import { INTEGRATION_CARD_HEIGHT } from '../constants'; +import { IntegrationTabId, type Tab } from '../types'; export const INTEGRATION_TABS: Tab[] = [ { @@ -28,6 +29,7 @@ export const INTEGRATION_TABS: Tab[] = [ 'epr:network_traffic', 'epr:osquery_manager', ], + height: `${INTEGRATION_CARD_HEIGHT * 3.5}px`, }, { category: 'security', @@ -77,7 +79,3 @@ export const INTEGRATION_TABS: Tab[] = [ sortByFeaturedIntegrations: true, }, ]; - -export const INTEGRATION_TABS_BY_ID = Object.fromEntries( - INTEGRATION_TABS.map((tab) => [tab.id, tab]) -) as Record; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/constants/index.ts similarity index 56% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/constants/index.ts index fe0895b7a192a..cc9cc4c5882ed 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/constants/index.ts @@ -4,27 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import type { CategoryFacet } from '@kbn/fleet-plugin/public'; -import { INTEGRATION_TABS } from './integration_tabs_configs'; -import type { Tab } from './types'; -export const ADD_AGENT_PATH = `/agents`; -export const AGENT_INDEX = `logs-elastic_agent*`; -export const CARD_DESCRIPTION_LINE_CLAMP = 3; // 3 lines of text -export const CARD_TITLE_LINE_CLAMP = 1; // 1 line of text -export const DEFAULT_TAB: Tab = INTEGRATION_TABS[0]; -export const FLEET_APP_ID = `fleet`; +export const INTEGRATION_CARD_HEIGHT = 156; +export const TELEMETRY_INTEGRATION_CARD = `card`; +export const MAX_CARD_HEIGHT_IN_PX = 127; // px export const INTEGRATION_APP_ID = `integrations`; +export const CARD_TITLE_LINE_CLAMP = 1; // 1 line of text +export const CARD_DESCRIPTION_LINE_CLAMP = 3; // 3 lines of text export const LOADING_SKELETON_TEXT_LINES = 10; // 10 lines of text -export const MAX_CARD_HEIGHT_IN_PX = 127; // px export const SCROLL_ELEMENT_ID = 'integrations-scroll-container'; export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = []; -export const WITH_SEARCH_BOX_HEIGHT = '568px'; -export const WITHOUT_SEARCH_BOX_HEIGHT = '513px'; -export const TELEMETRY_MANAGE_INTEGRATIONS = `manage_integrations`; -export const TELEMETRY_ENDPOINT_LEARN_MORE = `endpoint_learn_more`; -export const TELEMETRY_AGENTLESS_LEARN_MORE = `agentless_learn_more`; -export const TELEMETRY_AGENT_REQUIRED = `agent_required`; -export const TELEMETRY_INTEGRATION_CARD = `card`; +export const DEFAULT_INTEGRATION_CARD_CONTENT_HEIGHT = `${INTEGRATION_CARD_HEIGHT * 3.5 + 55}px`; export const TELEMETRY_INTEGRATION_TAB = `tab`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/integration_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/integration_context.tsx new file mode 100644 index 0000000000000..7485fcbdc1793 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/integration_context.tsx @@ -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. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IntegrationContextValue } from '../integration_context'; +import { mockIntegrationContext } from './mocks'; + +export const useIntegrationContext = (): IntegrationContextValue => { + return mockIntegrationContext(); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/mocks.ts new file mode 100644 index 0000000000000..e27e078d6a1dd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/__mocks__/mocks.ts @@ -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. + */ + +export const mockTrackLinkClick = jest.fn(); + +export const telemetry = { + trackLinkClick: mockTrackLinkClick, +}; +export const mockTelemetry = jest.fn(() => telemetry); + +export const integrationContext = { + spaceId: 'default', + telemetry: mockTelemetry(), +}; +export const mockIntegrationContext = jest.fn(() => integrationContext); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.styles.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/integration_card_grid_tabs.styles.ts similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.styles.ts rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/integration_card_grid_tabs.styles.ts diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/integration_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/integration_context.tsx new file mode 100644 index 0000000000000..a205d10366fb0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/integration_context.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PropsWithChildren } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; + +export type TrackLinkClick = (link: string) => void; + +export interface IntegrationContextValue { + spaceId: string; + telemetry: { trackLinkClick?: TrackLinkClick }; +} +const IntegrationContext = createContext(null); + +export const IntegrationContextProvider: React.FC< + PropsWithChildren<{ spaceId: string; trackLinkClick?: TrackLinkClick }> +> = React.memo(({ children, spaceId, trackLinkClick }) => { + const value = useMemo( + () => ({ spaceId, telemetry: { trackLinkClick } }), + [spaceId, trackLinkClick] + ); + + return {children}; +}); +IntegrationContextProvider.displayName = 'IntegrationContextProvider'; + +export const useIntegrationContext = () => { + const context = useContext(IntegrationContext); + if (!context) { + throw new Error( + 'No IntegrationContext found. Please wrap the application with IntegrationContextProvider' + ); + } + return context; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_filter_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_filter_cards.tsx new file mode 100644 index 0000000000000..17e652312ca3f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_filter_cards.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public'; +import { useMemo } from 'react'; + +export const useFilterCards = ({ + useAvailablePackages, + featuredCardIds, + prereleaseIntegrationsEnabled, +}: { + useAvailablePackages: AvailablePackagesHookType; + featuredCardIds?: string[]; + prereleaseIntegrationsEnabled: boolean; +}) => { + const { + isLoading, + searchTerm, + setCategory, + setSearchTerm, + setSelectedSubCategory, + filteredCards, + } = useAvailablePackages({ + prereleaseIntegrationsEnabled, + }); + + return useMemo( + () => ({ + availablePackagesResult: { + isLoading, + searchTerm, + setCategory, + setSearchTerm, + setSelectedSubCategory, + }, + allowedIntegrations: filteredCards.filter( + (card) => + (featuredCardIds?.includes(card.name) || featuredCardIds?.includes(card.id)) ?? true + ), + }), + [ + featuredCardIds, + filteredCards, + isLoading, + searchTerm, + setCategory, + setSearchTerm, + setSelectedSubCategory, + ] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.test.ts similarity index 88% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.test.ts index 555d68c73a830..5e69784835495 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.test.ts @@ -7,11 +7,12 @@ import { renderHook } from '@testing-library/react'; import { useIntegrationCardList } from './use_integration_card_list'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; +import { mockTrackLinkClick } from './__mocks__/mocks'; -jest.mock('../../../lib/telemetry'); -jest.mock('../../../../../common/lib/kibana', () => ({ - ...jest.requireActual('../../../../../common/lib/kibana'), +jest.mock('./integration_context'); + +jest.mock('../../kibana', () => ({ + ...jest.requireActual('../../kibana'), useNavigation: jest.fn().mockReturnValue({ navigateTo: jest.fn(), getAppUrl: jest.fn().mockReturnValue(''), @@ -87,6 +88,6 @@ describe('useIntegrationCardList', () => { const card = result.current[0]; card.onCardClick?.(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith('card_epr:endpoint'); + expect(mockTrackLinkClick).toHaveBeenCalledWith('card_epr:endpoint'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.ts similarity index 76% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts rename to x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.ts index e623846e4157c..2d569055e7be6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_integration_card_list.ts @@ -7,19 +7,21 @@ import { useMemo } from 'react'; import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation'; -import { getIntegrationLinkState } from '../../../../../common/hooks/integrations/use_integration_link_state'; -import { addPathParamToUrl } from '../../../../../common/utils/integrations'; -import { useNavigation } from '../../../../../common/lib/kibana'; -import { APP_INTEGRATIONS_PATH, ONBOARDING_PATH } from '../../../../../../common/constants'; +import { useNavigation } from '../../kibana'; +import { APP_INTEGRATIONS_PATH, ONBOARDING_PATH } from '../../../../../common/constants'; + import { CARD_DESCRIPTION_LINE_CLAMP, CARD_TITLE_LINE_CLAMP, INTEGRATION_APP_ID, MAX_CARD_HEIGHT_IN_PX, TELEMETRY_INTEGRATION_CARD, -} from './constants'; -import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; +} from '../constants'; +import type { GetAppUrl, NavigateTo } from '../../kibana'; +import type { TrackLinkClick } from './integration_context'; +import { useIntegrationContext } from './integration_context'; +import { getIntegrationLinkState } from '../../../hooks/integrations/use_integration_link_state'; +import { addPathParamToUrl } from '../../../utils/integrations'; const extractFeaturedCards = (filteredCards: IntegrationCardItem[], featuredCardIds: string[]) => { return filteredCards.reduce((acc, card) => { @@ -36,15 +38,23 @@ const getFilteredCards = ({ installedIntegrationList, integrationsList, navigateTo, + trackLinkClick, }: { featuredCardIds?: string[]; getAppUrl: GetAppUrl; installedIntegrationList?: IntegrationCardItem[]; integrationsList: IntegrationCardItem[]; navigateTo: NavigateTo; + trackLinkClick?: TrackLinkClick; }) => { const securityIntegrationsList = integrationsList.map((card) => - addSecuritySpecificProps({ navigateTo, getAppUrl, card, installedIntegrationList }) + addSecuritySpecificProps({ + navigateTo, + getAppUrl, + card, + installedIntegrationList, + trackLinkClick, + }) ); if (!featuredCardIds) { return { featuredCards: [], integrationCards: securityIntegrationsList }; @@ -60,11 +70,13 @@ export const addSecuritySpecificProps = ({ navigateTo, getAppUrl, card, + trackLinkClick, }: { navigateTo: NavigateTo; getAppUrl: GetAppUrl; card: IntegrationCardItem; installedIntegrationList?: IntegrationCardItem[]; + trackLinkClick?: TrackLinkClick; }): IntegrationCardItem => { const onboardingLink = getAppUrl({ appId: SECURITY_UI_APP_ID, path: ONBOARDING_PATH }); const integrationRootUrl = getAppUrl({ appId: INTEGRATION_APP_ID }); @@ -73,6 +85,7 @@ export const addSecuritySpecificProps = ({ card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink ? addPathParamToUrl(card.url, ONBOARDING_PATH) : card.url; + return { ...card, titleLineClamp: CARD_TITLE_LINE_CLAMP, @@ -82,7 +95,7 @@ export const addSecuritySpecificProps = ({ url, onCardClick: () => { const trackId = `${TELEMETRY_INTEGRATION_CARD}_${card.id}`; - trackOnboardingLinkClick(trackId); + trackLinkClick?.(trackId); if (url.startsWith(APP_INTEGRATIONS_PATH)) { navigateTo({ appId: INTEGRATION_APP_ID, @@ -106,10 +119,19 @@ export const useIntegrationCardList = ({ featuredCardIds?: string[] | undefined; }): IntegrationCardItem[] => { const { navigateTo, getAppUrl } = useNavigation(); - + const { + telemetry: { trackLinkClick }, + } = useIntegrationContext(); const { featuredCards, integrationCards } = useMemo( - () => getFilteredCards({ navigateTo, getAppUrl, integrationsList, featuredCardIds }), - [navigateTo, getAppUrl, integrationsList, featuredCardIds] + () => + getFilteredCards({ + navigateTo, + getAppUrl, + integrationsList, + featuredCardIds, + trackLinkClick, + }), + [navigateTo, getAppUrl, integrationsList, featuredCardIds, trackLinkClick] ); if (featuredCardIds && featuredCardIds.length > 0) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_selected_tab.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_selected_tab.tsx new file mode 100644 index 0000000000000..32420203a4f96 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_selected_tab.tsx @@ -0,0 +1,41 @@ +/* + * 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 { useStoredIntegrationTabId } from './use_stored_state'; +import type { Tab } from '../types'; + +export type UseSelectedTabReturn = ReturnType; + +export const useSelectedTab = ({ + spaceId, + integrationTabs, +}: { + spaceId: string; + integrationTabs: Tab[]; +}) => { + const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId( + spaceId, + integrationTabs[0].id + ); + + const integrationTabsById = useMemo( + () => Object.fromEntries(integrationTabs.map((tab: Tab) => [tab.id, tab])), + [integrationTabs] + ); + + const selectedTab = useMemo( + /** + * When toggleIdSelected from the local storage is not found in the integrationTabs, + * we fallback to the first tab in the integrationTabs array. + */ + () => integrationTabsById[toggleIdSelected] ?? integrationTabs[0], + [integrationTabs, integrationTabsById, toggleIdSelected] + ); + + return { selectedTab, toggleIdSelected, setSelectedTabIdToStorage, integrationTabs }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_stored_state.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_stored_state.ts new file mode 100644 index 0000000000000..0443c1e97bd8f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/hooks/use_stored_state.ts @@ -0,0 +1,43 @@ +/* + * 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 useLocalStorage from 'react-use/lib/useLocalStorage'; +import type { IntegrationTabId } from '../types'; + +const LocalStorageKey = { + selectedIntegrationTabId: 'securitySolution.onboarding.selectedIntegrationTabId', + integrationSearchTerm: 'securitySolution.onboarding.integrationSearchTerm', +} as const; + +/** + * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. + */ +export const useDefinedLocalStorage = (key: string, defaultValue: T) => { + const [value, setValue] = useLocalStorage(key, defaultValue); + return [value ?? defaultValue, setValue] as const; +}; + +/** + * Stores the selected integration tab ID per space + */ +export const useStoredIntegrationTabId = ( + spaceId: string, + defaultSelectedTabId: IntegrationTabId +) => + useDefinedLocalStorage( + `${LocalStorageKey.selectedIntegrationTabId}.${spaceId}`, + defaultSelectedTabId + ); + +/** + * Stores the integration search term per space + */ +export const useStoredIntegrationSearchTerm = (spaceId: string) => + useDefinedLocalStorage( + `${LocalStorageKey.integrationSearchTerm}.${spaceId}`, + null + ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/types/index.ts b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/types/index.ts new file mode 100644 index 0000000000000..4dfcb54abd8dd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/lib/integrations/types/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { AvailablePackagesHookType, IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import type { UseSelectedTabReturn } from '../hooks/use_selected_tab'; + +export interface IntegrationCardMetadata { + installedIntegrationsCount: number; + isAgentRequired: boolean; +} + +export interface Tab { + category: string; + featuredCardIds?: Array; + iconType?: string; + id: IntegrationTabId; + label: string; + overflow?: 'hidden' | 'scroll'; + showSearchTools?: boolean; + subCategory?: string; + sortByFeaturedIntegrations: boolean; + height?: string; +} + +export enum IntegrationTabId { + recommended = 'recommended', + recommendedSearchAILake = 'recommendedSearchAILake', + network = 'network', + user = 'user', + endpoint = 'endpoint', + cloud = 'cloud', + threatIntel = 'threatIntel', + all = 'all', +} + +export type TopCalloutRenderer = React.FC<{ + installedIntegrationsCount: number; + isAgentRequired: boolean; + selectedTabId: IntegrationTabId; +}>; + +export type AvailablePackagesResult = Pick< + ReturnType, + 'isLoading' | 'searchTerm' | 'setCategory' | 'setSearchTerm' | 'setSelectedSubCategory' +>; + +export type RenderChildrenType = React.FC<{ + allowedIntegrations: IntegrationCardItem[]; + availablePackagesResult: AvailablePackagesResult; + checkCompleteMetadata?: IntegrationCardMetadata; + featuredCardIds?: string[]; + selectedTabResult: UseSelectedTabReturn; + topCalloutRenderer?: TopCalloutRenderer; +}>; 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 index 28dfd2f50cb63..076995185716c 100644 --- 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 @@ -13,6 +13,12 @@ import { CONFIGURATIONS_PATH } from '../../../../../../common/constants'; import { IntegrationsFacets } from '../../../../../configurations/constants'; import { RETURN_APP_ID, RETURN_PATH } from './constants'; +export interface EnhancedCardOptions { + showInstallationStatus?: boolean; + showCompressedInstallationStatus?: boolean; + returnPath?: string; +} + const FEATURED_INTEGRATION_SORT_ORDER = [ 'epr:splunk', 'epr:google_secops', @@ -38,14 +44,17 @@ export const getCategoryBadgeIfAny = (categories: string[]): string | null => { export const applyCategoryBadgeAndStyling = ( card: IntegrationCardItem, - callerView: IntegrationsFacets + callerView: IntegrationsFacets, + options?: EnhancedCardOptions ): IntegrationCardItem => { - const returnPath = `${CONFIGURATIONS_PATH}/integrations/${callerView}`; + const returnPath = options?.returnPath ?? `${CONFIGURATIONS_PATH}/integrations/${callerView}`; const url = addPathParamToUrl(card.url, returnPath); const categoryBadge = getCategoryBadgeIfAny(card.categories); return { ...card, url, + showInstallationStatus: options?.showInstallationStatus, + showCompressedInstallationStatus: options?.showCompressedInstallationStatus, showDescription: false, showReleaseBadge: false, extraLabelsBadges: categoryBadge @@ -62,7 +71,9 @@ export const applyCategoryBadgeAndStyling = ( }; }; -const applyCustomDisplayOrder = (integrationsList: IntegrationCardItem[]) => { +const applyCustomDisplayOrder = ( + integrationsList: IntegrationCardItem[] +): IntegrationCardItem[] => { return integrationsList.sort( (a, b) => FEATURED_INTEGRATION_SORT_ORDER.indexOf(a.id) - FEATURED_INTEGRATION_SORT_ORDER.indexOf(b.id) @@ -70,13 +81,17 @@ const applyCustomDisplayOrder = (integrationsList: IntegrationCardItem[]) => { }; export const useEnhancedIntegrationCards = ( - integrationsList: IntegrationCardItem[] + integrationsList: IntegrationCardItem[], + options?: EnhancedCardOptions ): { available: IntegrationCardItem[]; installed: IntegrationCardItem[] } => { const sorted = applyCustomDisplayOrder(integrationsList); const available = useMemo( - () => sorted.map((card) => applyCategoryBadgeAndStyling(card, IntegrationsFacets.available)), - [sorted] + () => + sorted.map((card) => + applyCategoryBadgeAndStyling(card, IntegrationsFacets.available, options) + ), + [sorted, options] ); const installed = useMemo( diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/integrations_cards.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/integrations_cards.tsx index 07ee2ce17068b..8dffbe941312d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/integrations_cards.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring_onboarding/integrations_cards.tsx @@ -12,8 +12,8 @@ import { useIntegrationLinkState } from '../../../common/hooks/integrations/use_ import { addPathParamToUrl } from '../../../common/utils/integrations'; import { ENTITY_ANALYTICS_PRIVILEGED_USER_MONITORING_PATH } from '../../../../common/constants'; import { useNavigation } from '../../../common/lib/kibana'; -import { INTEGRATION_APP_ID } from '../../../onboarding/components/onboarding_body/cards/integrations/constants'; import { useEntityAnalyticsIntegrations } from './hooks/use_integrations'; +import { INTEGRATION_APP_ID } from '../../../common/lib/integrations/constants'; /** * This component has to be wrapped by react Suspense. diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts index cabdf29fe57f1..91bbd5954ee88 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/__mocks__/mocks.ts @@ -4,23 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.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. - */ export const mockReportCardOpen = jest.fn(); export const mockReportCardComplete = jest.fn(); export const mockReportCardLinkClicked = jest.fn(); export const mockReportCardSelectorClicked = jest.fn(); +export const mockTrackLinkClick = jest.fn(); export const telemetry = { reportCardOpen: mockReportCardOpen, reportCardComplete: mockReportCardComplete, reportCardLinkClicked: mockReportCardLinkClicked, reportCardSelectorClicked: mockReportCardSelectorClicked, + trackLinkClick: mockTrackLinkClick, }; export const mockTelemetry = jest.fn(() => telemetry); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts index 2e382929c39e5..881831681935d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/hooks/use_stored_state.ts @@ -7,8 +7,8 @@ import useLocalStorage from 'react-use/lib/useLocalStorage'; import type { OnboardingCardId } from '../../constants'; -import type { IntegrationTabId } from '../onboarding_body/cards/integrations/types'; import type { CardSelectorListItem } from '../onboarding_body/cards/common/card_selector_list'; +import { useDefinedLocalStorage } from '../../../common/lib/integrations/hooks/use_stored_state'; const LocalStorageKey = { avcBannerDismissed: 'securitySolution.onboarding.avcBannerDismissed', @@ -16,20 +16,10 @@ const LocalStorageKey = { completeCards: 'securitySolution.onboarding.completeCards', expandedCard: 'securitySolution.onboarding.expandedCard', urlDetails: 'securitySolution.onboarding.urlDetails', - selectedIntegrationTabId: 'securitySolution.onboarding.selectedIntegrationTabId', selectedCardItemId: 'securitySolution.onboarding.selectedCardItem', - integrationSearchTerm: 'securitySolution.onboarding.integrationSearchTerm', assistantConnectorId: 'securitySolution.onboarding.assistantCard.connectorId', } as const; -/** - * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. - */ -export const useDefinedLocalStorage = (key: string, defaultValue: T) => { - const [value, setValue] = useLocalStorage(key, defaultValue); - return [value ?? defaultValue, setValue] as const; -}; - /** * Stores the AVC banner dismissed state */ @@ -52,7 +42,7 @@ export const useStoredUrlDetails = (spaceId: string) => * Stores the selected selectable card ID per space */ export const useStoredSelectedCardItemId = ( - cardType: 'alerts' | 'dashboards' | 'rules', + cardType: 'alerts' | 'dashboards' | 'rules' | 'knowledgeSource', spaceId: string, defaultSelectedCardItemId: CardSelectorListItem['id'] ) => @@ -61,27 +51,6 @@ export const useStoredSelectedCardItemId = ( defaultSelectedCardItemId ); -/** - * Stores the selected integration tab ID per space - */ -export const useStoredIntegrationTabId = ( - spaceId: string, - defaultSelectedTabId: IntegrationTabId -) => - useDefinedLocalStorage( - `${LocalStorageKey.selectedIntegrationTabId}.${spaceId}`, - defaultSelectedTabId - ); - -/** - * Stores the integration search term per space - */ -export const useStoredIntegrationSearchTerm = (spaceId: string) => - useDefinedLocalStorage( - `${LocalStorageKey.integrationSearchTerm}.${spaceId}`, - null - ); - /** * Stores the integration search term per space */ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/lib/telemetry.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/lib/telemetry.ts index a88ae651ae600..332e9c5be1675 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/lib/telemetry.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/lib/telemetry.ts @@ -7,6 +7,8 @@ import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../common/lib/telemetry'; +export type TrackLinkClick = (linkId: string) => void; + export const trackOnboardingLinkClick = (linkId: string) => { track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.ONBOARDING}_${linkId}`); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts index 59d11314171a6..32eaac17cd736 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts @@ -14,13 +14,21 @@ import { alertsCardConfig } from './cards/alerts'; import { assistantCardConfig } from './cards/assistant'; import { aiConnectorCardConfig } from './cards/siem_migrations/ai_connector'; import { startMigrationCardConfig } from './cards/siem_migrations/start_migration'; +import { integrationsSearchAILakeCardConfig } from './cards/search_ai_lake/integrations_search_ai_lake'; +import { knowledgeSourceCardConfig } from './cards/search_ai_lake/knowledge_source'; +import { llmConnectorCardConfig } from './cards/search_ai_lake/llm'; export const defaultBodyConfig: OnboardingGroupConfig[] = [ { title: i18n.translate('xpack.securitySolution.onboarding.dataGroup.title', { defaultMessage: 'Ingest your data', }), - cards: [integrationsCardConfig, dashboardsCardConfig], + cards: [ + integrationsCardConfig, + integrationsSearchAILakeCardConfig, + dashboardsCardConfig, + knowledgeSourceCardConfig, + ], }, { title: i18n.translate('xpack.securitySolution.onboarding.alertsGroup.title', { @@ -35,6 +43,12 @@ export const defaultBodyConfig: OnboardingGroupConfig[] = [ // TODO: Add attackDiscoveryCardConfig when it is ready (https://github.com/elastic/kibana/issues/189487) cards: [assistantCardConfig], }, + { + title: i18n.translate('xpack.securitySolution.onboarding.customizeLLM.title', { + defaultMessage: 'Customize your LLM', + }), + cards: [llmConnectorCardConfig], + }, ]; export const siemMigrationsBodyConfig: OnboardingGroupConfig[] = [ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts index 97eb85636913f..11dcab9da2043 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts @@ -11,6 +11,7 @@ import { OnboardingCardId } from '../../../../constants'; import { ALERTS_CARD_TITLE } from './translations'; import alertsIcon from './images/alerts_icon.png'; import alertsDarkIcon from './images/alerts_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../common/constants'; export const alertsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.alerts, @@ -24,4 +25,5 @@ export const alertsCardConfig: OnboardingCardConfig = { './alerts_card' ) ), + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.detections`], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts index e1a8640249b39..6c13e8c0f6d40 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts @@ -10,8 +10,9 @@ import { AssistantIcon } from '@kbn/ai-assistant-icon'; import type { OnboardingCardConfig } from '../../../../types'; import { OnboardingCardId } from '../../../../constants'; import { ASSISTANT_CARD_TITLE } from './translations'; -import { checkAssistantCardComplete } from './assistant_check_complete'; +import { checkAssistantCardComplete } from '../common/connectors/assistant_check_complete'; import type { AssistantCardMetadata } from './types'; +import { SECURITY_FEATURE_ID } from '../../../../../../common/constants'; export const assistantCardConfig: OnboardingCardConfig = { id: OnboardingCardId.assistant, @@ -25,6 +26,8 @@ export const assistantCardConfig: OnboardingCardConfig = ) ), checkComplete: checkAssistantCardComplete, - capabilitiesRequired: ['securitySolutionAssistant.ai-assistant'], + capabilitiesRequired: [ + ['securitySolutionAssistant.ai-assistant', `${SECURITY_FEATURE_ID}.detections`], + ], licenseTypeRequired: 'enterprise', }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_selector_list.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_selector_list.tsx index 3f00656b319cf..12019f4dcbaaa 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_selector_list.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_selector_list.tsx @@ -13,9 +13,10 @@ import type { DashboardsCardItemId } from '../dashboards/types'; import { useCardSelectorListStyles } from './card_selector_list.styles'; import { HEIGHT_ANIMATION_DURATION } from '../../onboarding_card_panel.styles'; import { useOnboardingContext } from '../../../onboarding_context'; +import type { KnowledgeSourceCardItemId } from '../search_ai_lake/knowledge_source/types'; export interface CardSelectorListItem { - id: RulesCardItemId | AlertsCardItemId | DashboardsCardItemId; + id: RulesCardItemId | AlertsCardItemId | DashboardsCardItemId | KnowledgeSourceCardItemId; title: string; description: string; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/assistant_check_complete.ts similarity index 80% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/assistant_check_complete.ts index 27bc4fafff872..4609bbc9846a8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/connectors/assistant_check_complete.ts @@ -6,10 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import type { OnboardingCardCheckComplete } from '../../../../types'; -import { loadAiConnectors } from '../common/connectors/ai_connectors'; -import { getConnectorsAuthz } from '../common/connectors/authz'; -import type { AssistantCardMetadata } from './types'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import { loadAiConnectors } from './ai_connectors'; +import { getConnectorsAuthz } from './authz'; +import type { AssistantCardMetadata } from '../../assistant/types'; const completeBadgeText = (count: number) => i18n.translate('xpack.securitySolution.onboarding.assistantCard.badge.completeText', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agent_required_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/agent_required_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agent_required_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/agent_required_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agentless_available_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/agentless_available_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agentless_available_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/agentless_available_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/endpoint_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/endpoint_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/endpoint_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/endpoint_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/installed_integrations_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/installed_integrations_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/installed_integrations_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/installed_integrations_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/integration_card_top_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/integration_card_top_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/integration_card_top_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/integration_card_top_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/manage_integrations_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/manage_integrations_callout.tsx similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/manage_integrations_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/__mocks__/manage_integrations_callout.tsx diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.test.tsx similarity index 79% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.test.tsx index 4f5ae2f919d66..4a9a859fed382 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.test.tsx @@ -13,11 +13,10 @@ import React from 'react'; import { render } from '@testing-library/react'; import { AgentRequiredCallout } from './agent_required_callout'; -import { TestProviders } from '../../../../../../common/mock/test_providers'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { TestProviders } from '../../../../../../../common/mock/test_providers'; +import { mockTrackLinkClick } from '../../../../../../../common/lib/integrations/hooks/__mocks__/mocks'; -jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../lib/telemetry'); +jest.mock('../../../../../../../common/lib/integrations/hooks/integration_context'); describe('AgentRequiredCallout', () => { beforeEach(() => { @@ -38,6 +37,6 @@ describe('AgentRequiredCallout', () => { getByTestId('agentLink').click(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agent_required'); + expect(mockTrackLinkClick).toHaveBeenCalledWith('agent_required'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.tsx similarity index 80% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.tsx index 763dfe749adba..0cf7f0cddb841 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agent_required_callout.tsx @@ -8,24 +8,28 @@ import React, { useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiIcon } from '@elastic/eui'; -import { LinkAnchor } from '../../../../../../common/components/links'; -import { CardCallOut } from '../../common/card_callout'; -import { useNavigation } from '../../../../../../common/lib/kibana'; +import { LinkAnchor } from '../../../../../../../common/components/links'; +import { CardCallOut } from '../../card_callout'; +import { useNavigation } from '../../../../../../../common/lib/kibana'; import { FLEET_APP_ID, ADD_AGENT_PATH, TELEMETRY_AGENT_REQUIRED } from '../constants'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { useIntegrationContext } from '../../../../../../../common/lib/integrations/hooks/integration_context'; const fleetAgentLinkProps = { appId: FLEET_APP_ID, path: ADD_AGENT_PATH }; export const AgentRequiredCallout = React.memo(() => { const { getAppUrl, navigateTo } = useNavigation(); const addAgentLink = getAppUrl(fleetAgentLinkProps); + const { + telemetry: { trackLinkClick }, + } = useIntegrationContext(); + const onAddAgentClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); - trackOnboardingLinkClick(TELEMETRY_AGENT_REQUIRED); + trackLinkClick?.(TELEMETRY_AGENT_REQUIRED); navigateTo(fleetAgentLinkProps); }, - [navigateTo] + [navigateTo, trackLinkClick] ); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.test.tsx similarity index 82% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.test.tsx index e761381747f46..5d013ea34f1ad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.test.tsx @@ -7,13 +7,13 @@ import { render } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../../../../../common/mock/test_providers'; +import { TestProviders } from '../../../../../../../common/mock/test_providers'; import { AgentlessAvailableCallout } from './agentless_available_callout'; -import { useKibana } from '../../../../../../common/lib/kibana'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { useKibana } from '../../../../../../../common/lib/kibana'; +import { mockTrackLinkClick } from '../../../../../__mocks__/mocks'; -jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../lib/telemetry'); +jest.mock('../../../../../../../common/lib/kibana'); +jest.mock('../../../../../onboarding_context'); describe('AgentlessAvailableCallout', () => { const mockUseKibana = useKibana as jest.Mock; @@ -70,6 +70,6 @@ describe('AgentlessAvailableCallout', () => { getByTestId('agentlessLearnMoreLink').click(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith('agentless_learn_more'); + expect(mockTrackLinkClick).toHaveBeenCalledWith('agentless_learn_more'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.tsx similarity index 86% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.tsx index 81c4db22f39ab..191467c9745cc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/agentless_available_callout.tsx @@ -10,19 +10,22 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiIcon, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { useKibana } from '../../../../../../common/lib/kibana'; -import { LinkAnchor } from '../../../../../../common/components/links'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; -import { CardCallOut } from '../../common/card_callout'; +import { useKibana } from '../../../../../../../common/lib/kibana'; +import { LinkAnchor } from '../../../../../../../common/components/links'; +import { CardCallOut } from '../../card_callout'; import { TELEMETRY_AGENTLESS_LEARN_MORE } from '../constants'; +import { useOnboardingContext } from '../../../../../onboarding_context'; export const AgentlessAvailableCallout = React.memo(() => { const { euiTheme } = useEuiTheme(); const { docLinks } = useKibana().services; + const { + telemetry: { trackLinkClick }, + } = useOnboardingContext(); const onClick = useCallback(() => { - trackOnboardingLinkClick(TELEMETRY_AGENTLESS_LEARN_MORE); - }, []); + trackLinkClick?.(TELEMETRY_AGENTLESS_LEARN_MORE); + }, [trackLinkClick]); /* @ts-expect-error: add the blog link to `packages/kbn-doc-links/src/get_doc_links.ts` when it is ready and remove this exit condition*/ if (!docLinks.links.fleet.agentlessBlog) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.test.tsx similarity index 76% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.test.tsx index 7d89003359743..df23ab0e838ef 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.test.tsx @@ -13,11 +13,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { EndpointCallout } from './endpoint_callout'; -import { TestProviders } from '../../../../../../common/mock/test_providers'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { TestProviders } from '../../../../../../../common/mock/test_providers'; +import { mockTrackLinkClick } from '../../../../../../../common/lib/integrations/hooks/__mocks__/mocks'; -jest.mock('../../../../../../common/lib/kibana'); -jest.mock('../../../../lib/telemetry'); +jest.mock('../../../../../../../common/lib/kibana'); +jest.mock('../../../../../../../common/lib/integrations/hooks/integration_context'); describe('EndpointCallout', () => { beforeEach(() => { @@ -38,6 +38,6 @@ describe('EndpointCallout', () => { getByTestId('endpointLearnMoreLink').click(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith('endpoint_learn_more'); + expect(mockTrackLinkClick).toHaveBeenCalledWith('endpoint_learn_more'); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.tsx similarity index 83% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.tsx index b761a17901a38..d2fae2e733bac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/endpoint_callout.tsx @@ -10,18 +10,22 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiIcon, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; -import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; -import { LinkAnchor } from '../../../../../../common/components/links'; -import { CardCallOut } from '../../common/card_callout'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { useKibana } from '../../../../../../../common/lib/kibana'; +import { LinkAnchor } from '../../../../../../../common/components/links'; +import { CardCallOut } from '../../card_callout'; import { TELEMETRY_ENDPOINT_LEARN_MORE } from '../constants'; +import { useIntegrationContext } from '../../../../../../../common/lib/integrations/hooks/integration_context'; export const EndpointCallout = React.memo(() => { const { euiTheme } = useEuiTheme(); const { docLinks } = useKibana().services; + const { + telemetry: { trackLinkClick }, + } = useIntegrationContext(); + const onClick = useCallback(() => { - trackOnboardingLinkClick(TELEMETRY_ENDPOINT_LEARN_MORE); - }, []); + trackLinkClick?.(TELEMETRY_ENDPOINT_LEARN_MORE); + }, [trackLinkClick]); return ( { - if (!installedIntegrationsCount) { - return null; - } - - return isAgentRequired ? ( - - ) : ( - - ); +export const InstalledIntegrationsCalloutComponent: React.FC<{ + installedIntegrationsCount: number; + isAgentRequired: boolean; +}> = ({ installedIntegrationsCount, isAgentRequired }) => { + if (!installedIntegrationsCount) { + return null; } -); + + return isAgentRequired ? ( + + ) : ( + + ); +}; + +export const InstalledIntegrationsCallout = React.memo(InstalledIntegrationsCalloutComponent); InstalledIntegrationsCallout.displayName = 'InstalledIntegrationsCallout'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.test.tsx similarity index 90% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.test.tsx index 9cf346aeed901..5a1b81d773c08 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.test.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { of } from 'rxjs'; import { IntegrationCardTopCallout } from './integration_card_top_callout'; -import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; -import { IntegrationTabId } from '../types'; +import { useOnboardingService } from '../../../../../hooks/use_onboarding_service'; +import { IntegrationTabId } from '../../../../../../../common/lib/integrations/types'; -jest.mock('../../../../hooks/use_onboarding_service', () => ({ +jest.mock('../../../../../hooks/use_onboarding_service', () => ({ useOnboardingService: jest.fn(), })); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.tsx new file mode 100644 index 0000000000000..c09d3fc2c54a2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/integration_card_top_callout.tsx @@ -0,0 +1,65 @@ +/* + * 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 useObservable from 'react-use/lib/useObservable'; + +import { useOnboardingService } from '../../../../../hooks/use_onboarding_service'; +import { AgentlessAvailableCallout } from './agentless_available_callout'; +import { InstalledIntegrationsCallout } from './installed_integrations_callout'; +import { EndpointCallout } from './endpoint_callout'; +import { IntegrationTabId } from '../../../../../../../common/lib/integrations/types'; + +export const useShowInstalledCallout = ({ + installedIntegrationsCount, + isAgentRequired, +}: { + installedIntegrationsCount: number; + isAgentRequired: boolean; +}) => { + return installedIntegrationsCount > 0 || isAgentRequired; +}; + +export const IntegrationCardTopCalloutComponent: React.FC<{ + installedIntegrationsCount: number; + isAgentRequired: boolean; + selectedTabId: IntegrationTabId; +}> = ({ installedIntegrationsCount, isAgentRequired, selectedTabId }) => { + const { isAgentlessAvailable$ } = useOnboardingService(); + const isAgentlessAvailable = useObservable(isAgentlessAvailable$, undefined); + const showInstalledCallout = useShowInstalledCallout({ + installedIntegrationsCount, + isAgentRequired, + }); + const showAgentlessCallout = + isAgentlessAvailable && + installedIntegrationsCount === 0 && + selectedTabId !== IntegrationTabId.endpoint; + const showEndpointCallout = + installedIntegrationsCount === 0 && selectedTabId === IntegrationTabId.endpoint; + + if (!showAgentlessCallout && !showEndpointCallout && !showInstalledCallout) { + return null; + } + + return ( + <> + {showEndpointCallout && } + {showAgentlessCallout && } + {showInstalledCallout && ( + + )} + + ); +}; + +export const IntegrationCardTopCallout = React.memo(IntegrationCardTopCalloutComponent); + +IntegrationCardTopCallout.displayName = 'IntegrationCardTopCallout'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.test.tsx similarity index 84% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.test.tsx index 5f16bf3981f5f..27fec4759e01e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.test.tsx @@ -7,16 +7,16 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ManageIntegrationsCallout } from './manage_integrations_callout'; -import { TestProviders } from '../../../../../../common/mock/test_providers'; - -jest.mock('../../../../../../common/hooks/use_add_integrations_url', () => ({ +import { TestProviders } from '../../../../../../../common/mock/test_providers'; +jest.mock('../../../../../../../common/lib/integrations/hooks/integration_context'); +jest.mock('../../../../../../../common/hooks/use_add_integrations_url', () => ({ useAddIntegrationsUrl: jest.fn().mockReturnValue({ href: '/test-url', onClick: jest.fn(), }), })); -jest.mock('../../common/card_callout', () => ({ +jest.mock('../../card_callout', () => ({ CardCallOut: ({ text }: { text: React.ReactNode }) =>
{text}
, })); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.tsx similarity index 81% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.tsx index 4085f2310d570..4fa45abb877cb 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/callouts/manage_integrations_callout.tsx @@ -8,23 +8,26 @@ import React, { useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiIcon } from '@elastic/eui'; -import { LinkAnchor } from '../../../../../../common/components/links'; -import { CardCallOut } from '../../common/card_callout'; -import { useAddIntegrationsUrl } from '../../../../../../common/hooks/use_add_integrations_url'; -import { trackOnboardingLinkClick } from '../../../../lib/telemetry'; +import { LinkAnchor } from '../../../../../../../common/components/links'; +import { CardCallOut } from '../../card_callout'; +import { useAddIntegrationsUrl } from '../../../../../../../common/hooks/use_add_integrations_url'; import { TELEMETRY_MANAGE_INTEGRATIONS } from '../constants'; +import { useIntegrationContext } from '../../../../../../../common/lib/integrations/hooks/integration_context'; export const ManageIntegrationsCallout = React.memo( ({ installedIntegrationsCount }: { installedIntegrationsCount: number }) => { const { href: integrationUrl, onClick: onAddIntegrationClicked } = useAddIntegrationsUrl(); + const { + telemetry: { trackLinkClick }, + } = useIntegrationContext(); const onClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); - trackOnboardingLinkClick(TELEMETRY_MANAGE_INTEGRATIONS); + trackLinkClick?.(TELEMETRY_MANAGE_INTEGRATIONS); onAddIntegrationClicked(e); }, - [onAddIntegrationClicked] + [onAddIntegrationClicked, trackLinkClick] ); if (!installedIntegrationsCount) { diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/constants.ts new file mode 100644 index 0000000000000..5724df0c7bbd4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/constants.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. + */ + +export const ADD_AGENT_PATH = `/agents`; +export const AGENT_INDEX = `logs-elastic_agent*`; +export const FLEET_APP_ID = `fleet`; +export const INTEGRATION_CARD_HEIGHT = 156; +export const TELEMETRY_MANAGE_INTEGRATIONS = `manage_integrations`; +export const TELEMETRY_ENDPOINT_LEARN_MORE = `endpoint_learn_more`; +export const TELEMETRY_AGENTLESS_LEARN_MORE = `agentless_learn_more`; +export const TELEMETRY_AGENT_REQUIRED = `agent_required`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/images/integrations_icon.png similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/images/integrations_icon.png diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon_dark.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/images/integrations_icon_dark.png similarity index 100% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon_dark.png rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/images/integrations_icon_dark.png diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.test.ts similarity index 96% rename from x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts rename to x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.test.ts index 961f1981291b8..910b49674f6c8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.test.ts @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { checkIntegrationsCardComplete } from './integrations_check_complete'; import { installationStatuses } from '@kbn/fleet-plugin/public'; -import type { StartServices } from '../../../../../types'; +import type { StartServices } from '../../../../../../types'; import { lastValueFrom } from 'rxjs'; +import { checkIntegrationsCardComplete } from '../../integrations/integrations_check_complete'; jest.mock('rxjs', () => ({ ...jest.requireActual('rxjs'), diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.ts new file mode 100644 index 0000000000000..7e7bc28947d8b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/integrations/integrations_check_complete.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { GetPackagesResponse, IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { EPM_API_ROUTES, installationStatuses } from '@kbn/fleet-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { lastValueFrom } from 'rxjs'; +import { AGENT_INDEX } from './constants'; +import type { StartServices } from '../../../../../../types'; + +export const getCompleteBadgeText = (installedCount: number) => + i18n.translate('xpack.securitySolution.onboarding.integrationsCard.badge.completeText', { + defaultMessage: '{count} {count, plural, one {integration} other {integrations}} added', + values: { count: installedCount }, + }); + +export const getIntegrationList = async ( + services: StartServices, + /** + * The list of available integrations to check against. + * If provided, only installed integrations that are in this list will be considered complete. + * If not provided, all installed integrations will be considered complete. + */ + availableIntegrations?: Array +) => { + const packageData = await services.http + .get(EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, { + version: '2023-10-31', + }) + .catch((err: Error) => { + const emptyItems: GetPackagesResponse['items'] = []; + services.notifications.toasts.addError(err, { + title: i18n.translate( + 'xpack.securitySolution.onboarding.integrationsCard.checkComplete.fetchIntegrations.errorTitle', + { + defaultMessage: 'Error fetching integrations data', + } + ), + }); + return { items: emptyItems }; + }); + + const installedPackages = packageData?.items?.filter((pkg) => { + const integrationCardId = `epr:${pkg.id}`; + const isInstalled = + pkg.status === installationStatuses.Installed || + pkg.status === installationStatuses.InstallFailed; + return availableIntegrations + ? isInstalled && + availableIntegrations.some( + (availableIntegration) => + availableIntegration === integrationCardId || availableIntegration === pkg.name + ) + : isInstalled; + }); + const isComplete = installedPackages && installedPackages.length > 0; + + return { isComplete, installedPackages }; +}; + +export const getAgentsData = async (services: StartServices, isComplete: boolean) => { + const agentsData = await lastValueFrom( + services.data.search.search({ + params: { index: AGENT_INDEX, body: { size: 1 } }, + }) + ).catch((err: Error) => { + services.notifications.toasts.addError(err, { + title: i18n.translate( + 'xpack.securitySolution.onboarding.integrationsCard.checkComplete.fetchAgents.errorTitle', + { + defaultMessage: 'Error fetching agents data', + } + ), + }); + return { rawResponse: { hits: { total: 0 } } }; + }); + + const agentsDataAvailable = !!agentsData?.rawResponse?.hits?.total; + const isAgentRequired = isComplete && !agentsDataAvailable; + return { isAgentRequired, agentsData }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts index 5b0a4e44bdcad..36184a40eb250 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -11,6 +11,7 @@ import { OnboardingCardId } from '../../../../constants'; import { DASHBOARDS_CARD_TITLE } from './translations'; import dashboardsIcon from './images/dashboards_icon.png'; import dashboardsDarkIcon from './images/dashboards_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../common/constants'; export const dashboardsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.dashboards, @@ -24,5 +25,5 @@ export const dashboardsCardConfig: OnboardingCardConfig = { './dashboards_card' ) ), - capabilitiesRequired: 'dashboard_v2.show', + capabilitiesRequired: [['dashboard_v2.show', `${SECURITY_FEATURE_ID}.detections`]], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx deleted file mode 100644 index 40f4ae95cf088..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx +++ /dev/null @@ -1,53 +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 React from 'react'; -import useObservable from 'react-use/lib/useObservable'; - -import { useOnboardingService } from '../../../../hooks/use_onboarding_service'; -import { AgentlessAvailableCallout } from './agentless_available_callout'; -import { InstalledIntegrationsCallout } from './installed_integrations_callout'; -import { IntegrationTabId } from '../types'; -import { EndpointCallout } from './endpoint_callout'; - -export const IntegrationCardTopCallout = React.memo( - ({ - installedIntegrationsCount, - isAgentRequired, - selectedTabId, - }: { - installedIntegrationsCount: number; - isAgentRequired: boolean; - selectedTabId: IntegrationTabId; - }) => { - const { isAgentlessAvailable$ } = useOnboardingService(); - const isAgentlessAvailable = useObservable(isAgentlessAvailable$, undefined); - - const showAgentlessCallout = - isAgentlessAvailable && - installedIntegrationsCount === 0 && - selectedTabId !== IntegrationTabId.endpoint; - const showEndpointCallout = - installedIntegrationsCount === 0 && selectedTabId === IntegrationTabId.endpoint; - const showInstalledCallout = installedIntegrationsCount > 0 || isAgentRequired; - - return ( - <> - {showEndpointCallout && } - {showAgentlessCallout && } - {showInstalledCallout && ( - - )} - - ); - } -); - -IntegrationCardTopCallout.displayName = 'IntegrationCardTopCallout'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index ba7b89d5aba7b..490a0b2b882d4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -10,9 +10,10 @@ import { i18n } from '@kbn/i18n'; import type { OnboardingCardConfig } from '../../../../types'; import { checkIntegrationsCardComplete } from './integrations_check_complete'; import { OnboardingCardId } from '../../../../constants'; -import type { IntegrationCardMetadata } from './types'; -import integrationsIcon from './images/integrations_icon.png'; -import integrationsDarkIcon from './images/integrations_icon_dark.png'; +import integrationsIcon from '../common/integrations/images/integrations_icon.png'; +import integrationsDarkIcon from '../common/integrations/images/integrations_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../common/constants'; +import type { IntegrationCardMetadata } from '../../../../../common/lib/integrations/types'; export const integrationsCardConfig: OnboardingCardConfig = { id: OnboardingCardId.integrations, @@ -29,5 +30,5 @@ export const integrationsCardConfig: OnboardingCardConfig { @@ -38,6 +39,6 @@ describe('IntegrationsCard', () => { /> ); expect(queryByTestId('loadingInstalledIntegrations')).not.toBeInTheDocument(); - expect(queryByTestId('integrationsCardGridTabs')).toBeInTheDocument(); + expect(queryByTestId('securityIntegrations')).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx index 2fafc0405efe4..fe29daad91385 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx @@ -8,23 +8,32 @@ import React from 'react'; import type { OnboardingCardComponent } from '../../../../types'; import { OnboardingCardContentPanel } from '../common/card_content_panel'; -import { IntegrationsCardGridTabs } from './integration_card_grid_tabs'; import { CenteredLoadingSpinner } from '../../../../../common/components/centered_loading_spinner'; -import type { IntegrationCardMetadata } from './types'; +import { useOnboardingContext } from '../../../onboarding_context'; +import type { IntegrationCardMetadata } from '../../../../../common/lib/integrations/types'; +import { SecurityIntegrations } from '../../../../../common/lib/integrations/components'; +import { IntegrationCardTopCallout } from '../common/integrations/callouts/integration_card_top_callout'; +import { IntegrationContextProvider } from '../../../../../common/lib/integrations/hooks/integration_context'; export const IntegrationsCard: OnboardingCardComponent = React.memo( ({ checkCompleteMetadata }) => { + const { + spaceId, + telemetry: { trackLinkClick }, + } = useOnboardingContext(); + if (!checkCompleteMetadata) { return ; } - const { installedIntegrationsCount, isAgentRequired } = checkCompleteMetadata; return ( - + + + ); } diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts index d4193dd8b9ded..7f92e38b9783a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts @@ -5,66 +5,21 @@ * 2.0. */ -import type { GetPackagesResponse } from '@kbn/fleet-plugin/public'; -import { EPM_API_ROUTES, installationStatuses } from '@kbn/fleet-plugin/public'; -import { i18n } from '@kbn/i18n'; -import { lastValueFrom } from 'rxjs'; -import type { OnboardingCardCheckComplete } from '../../../../types'; -import { AGENT_INDEX } from './constants'; +import type { IntegrationCardMetadata } from '../../../../../common/lib/integrations/types'; import type { StartServices } from '../../../../../types'; -import type { IntegrationCardMetadata } from './types'; +import type { OnboardingCardCheckComplete } from '../../../../types'; +import { + getAgentsData, + getCompleteBadgeText, + getIntegrationList, +} from '../common/integrations/integrations_check_complete'; export const checkIntegrationsCardComplete: OnboardingCardCheckComplete< IntegrationCardMetadata > = async (services: StartServices) => { - const packageData = await services.http - .get(EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, { - version: '2023-10-31', - }) - .catch((err: Error) => { - services.notifications.toasts.addError(err, { - title: i18n.translate( - 'xpack.securitySolution.onboarding.integrationsCard.checkComplete.fetchIntegrations.errorTitle', - { - defaultMessage: 'Error fetching integrations data', - } - ), - }); - return { items: [] }; - }); - - const agentsData = await lastValueFrom( - services.data.search.search({ - params: { index: AGENT_INDEX, body: { size: 1 } }, - }) - ).catch((err: Error) => { - services.notifications.toasts.addError(err, { - title: i18n.translate( - 'xpack.securitySolution.onboarding.integrationsCard.checkComplete.fetchAgents.errorTitle', - { - defaultMessage: 'Error fetching agents data', - } - ), - }); - return { rawResponse: { hits: { total: 0 } } }; - }); - - const installed = packageData?.items?.filter( - (pkg) => - pkg.status === installationStatuses.Installed || - pkg.status === installationStatuses.InstallFailed - ); - const isComplete = installed && installed.length > 0; - const agentsDataAvailable = !!agentsData?.rawResponse?.hits?.total; - const isAgentRequired = isComplete && !agentsDataAvailable; + const { isComplete, installedPackages } = await getIntegrationList(services); - const completeBadgeText = i18n.translate( - 'xpack.securitySolution.onboarding.integrationsCard.badge.completeText', - { - defaultMessage: '{count} {count, plural, one {integration} other {integrations}} added', - values: { count: installed.length }, - } - ); + const { isAgentRequired } = await getAgentsData(services, isComplete); if (!isComplete) { return { @@ -78,9 +33,9 @@ export const checkIntegrationsCardComplete: OnboardingCardCheckComplete< return { isComplete, - completeBadgeText, + completeBadgeText: getCompleteBadgeText(installedPackages.length), metadata: { - installedIntegrationsCount: installed.length, + installedIntegrationsCount: installedPackages.length, isAgentRequired, }, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts deleted file mode 100644 index 849e9cdd2336b..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts +++ /dev/null @@ -1,32 +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. - */ -export interface Tab { - category: string; - featuredCardIds?: string[]; - iconType?: string; - id: IntegrationTabId; - label: string; - overflow?: 'hidden' | 'scroll'; - showSearchTools?: boolean; - subCategory?: string; - sortByFeaturedIntegrations: boolean; -} - -export enum IntegrationTabId { - recommended = 'recommended', - network = 'network', - user = 'user', - endpoint = 'endpoint', - cloud = 'cloud', - threatIntel = 'threatIntel', - all = 'all', -} - -export interface IntegrationCardMetadata { - installedIntegrationsCount: number; - isAgentRequired: boolean; -} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts index 59b86df834646..65a6160870f2f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts @@ -12,6 +12,7 @@ import { RULES_CARD_TITLE } from './translations'; import { checkRulesComplete } from './rules_check_complete'; import rulesIcon from './images/rules_icon.png'; import rulesDarkIcon from './images/rules_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../common/constants'; export const rulesCardConfig: OnboardingCardConfig = { id: OnboardingCardId.rules, @@ -26,4 +27,5 @@ export const rulesCardConfig: OnboardingCardConfig = { ) ), checkComplete: checkRulesComplete, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.detections`], }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/index.ts new file mode 100644 index 0000000000000..5a73524a4d600 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { OnboardingCardConfig } from '../../../../../types'; +import { checkIntegrationsCardComplete } from './integrations_check_complete'; +import { OnboardingCardId } from '../../../../../constants'; +import integrationsIcon from '../../common/integrations/images/integrations_icon.png'; +import integrationsDarkIcon from '../../common/integrations/images/integrations_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../../common/constants'; +import type { IntegrationCardMetadata } from '../../../../../../common/lib/integrations/types'; + +export const integrationsSearchAILakeCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.integrationsSearchAILake, + title: i18n.translate('xpack.securitySolution.onboarding.integrationsSearchAILakeCard.title', { + defaultMessage: 'Add data with integrations', + }), + icon: integrationsIcon, + iconDark: integrationsDarkIcon, + Component: React.lazy( + () => + import( + /* webpackChunkName: "onboarding_search_ai_lake_integrations_card" */ + './integrations_card' + ) + ), + checkComplete: checkIntegrationsCardComplete, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.external_detections`], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integration_tabs_configs.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integration_tabs_configs.ts new file mode 100644 index 0000000000000..38e65251d8131 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integration_tabs_configs.ts @@ -0,0 +1,25 @@ +/* + * 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 { INTEGRATION_CARD_HEIGHT } from '../../common/integrations/constants'; +import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../../../../../common/lib/search_ai_lake/integrations'; +import { IntegrationTabId, type Tab } from '../../../../../../common/lib/integrations/types'; + +export const INTEGRATION_TABS: Tab[] = [ + { + category: '', + id: IntegrationTabId.recommendedSearchAILake, + label: 'Recommended', + overflow: 'hidden', + showSearchTools: false, + // Fleet has a default sorting for integrations by category that Security Solution does not want to apply + // so we need to disable the sorting for the recommended tab to allow static ordering according to the featuredCardIds + sortByFeaturedIntegrations: false, + featuredCardIds: SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS, + height: `${INTEGRATION_CARD_HEIGHT * 1.8}px`, + }, +]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.test.tsx new file mode 100644 index 0000000000000..b520dfc7f48e5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 IntegrationsCard from './integrations_card'; +import { render } from '@testing-library/react'; +jest.mock('../../../../onboarding_context'); +jest.mock('../../../../../../common/lib/integrations/components/with_filtered_integrations'); +const props = { + setComplete: jest.fn(), + checkComplete: jest.fn(), + isCardComplete: jest.fn(), + setExpandedCardId: jest.fn(), + isCardAvailable: jest.fn(), +}; + +describe('IntegrationsCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a loading spinner when checkCompleteMetadata is undefined', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('loadingInstalledIntegrations')).toBeInTheDocument(); + }); + + it('renders the content', () => { + const { queryByTestId } = render( + + ); + expect(queryByTestId('loadingInstalledIntegrations')).not.toBeInTheDocument(); + expect(queryByTestId('withFilteredIntegrations')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.tsx new file mode 100644 index 0000000000000..e39730067b43d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_card.tsx @@ -0,0 +1,88 @@ +/* + * 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 type { OnboardingCardComponent } from '../../../../../types'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; +import { INTEGRATION_TABS } from './integration_tabs_configs'; +import { ManageIntegrationsCallout } from '../../common/integrations/callouts/manage_integrations_callout'; +import { useOnboardingContext } from '../../../../onboarding_context'; +import { useEnhancedIntegrationCards } from '../../../../../../common/lib/search_ai_lake/hooks'; +import { useSelectedTab } from '../../../../../../common/lib/integrations/hooks/use_selected_tab'; +import type { + RenderChildrenType, + IntegrationCardMetadata, +} from '../../../../../../common/lib/integrations/types'; +import { WithFilteredIntegrations } from '../../../../../../common/lib/integrations/components/with_filtered_integrations'; +import { IntegrationsCardGridTabsComponent } from '../../../../../../common/lib/integrations/components/integration_card_grid_tabs_component'; +import { DEFAULT_CHECK_COMPLETE_METADATA } from '../../../../../../common/lib/integrations/components/integration_card_grid_tabs'; +import { IntegrationContextProvider } from '../../../../../../common/lib/integrations/hooks/integration_context'; +import { ONBOARDING_PATH } from '../../../../../../../common/constants'; + +const IntegrationsCardGridTabs: RenderChildrenType = ({ + allowedIntegrations, + availablePackagesResult, + checkCompleteMetadata = DEFAULT_CHECK_COMPLETE_METADATA, + selectedTabResult, +}) => { + const { available: list } = useEnhancedIntegrationCards(allowedIntegrations, { + showInstallationStatus: true, + showCompressedInstallationStatus: true, + returnPath: ONBOARDING_PATH, + }); + const { installedIntegrationsCount, isAgentRequired } = checkCompleteMetadata; + + return ( + + ); +}; + +export const IntegrationsCard: OnboardingCardComponent = React.memo( + ({ checkCompleteMetadata }) => { + const { + spaceId, + telemetry: { trackLinkClick }, + } = useOnboardingContext(); + + const selectedTabResult = useSelectedTab({ + spaceId, + integrationTabs: INTEGRATION_TABS, + }); + + if (!checkCompleteMetadata) { + return ; + } + + return ( + + + + + + ); + } +); +IntegrationsCard.displayName = 'IntegrationsCard'; + +// eslint-disable-next-line import/no-default-export +export default IntegrationsCard; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_check_complete.ts new file mode 100644 index 0000000000000..70009d0ffc40f --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/integrations_search_ai_lake/integrations_check_complete.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IntegrationCardMetadata } from '../../../../../../common/lib/integrations/types'; +import type { StartServices } from '../../../../../../types'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import { + getCompleteBadgeText, + getAgentsData, + getIntegrationList, +} from '../../common/integrations/integrations_check_complete'; +import { INTEGRATION_TABS } from './integration_tabs_configs'; + +export const checkIntegrationsCardComplete: OnboardingCardCheckComplete< + IntegrationCardMetadata +> = async (services: StartServices) => { + const { isComplete, installedPackages } = await getIntegrationList( + services, + INTEGRATION_TABS[0].featuredCardIds + ); + + const { isAgentRequired } = await getAgentsData(services, isComplete); + + if (!isComplete) { + return { + isComplete, + metadata: { + installedIntegrationsCount: 0, + isAgentRequired: false, + }, + }; + } + + return { + isComplete, + completeBadgeText: getCompleteBadgeText(installedPackages.length), + metadata: { + installedIntegrationsCount: installedPackages.length, + isAgentRequired, + }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/constants.ts new file mode 100644 index 0000000000000..69fef7bf2a720 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CardSelectorListItem } from '../../common/card_selector_list'; +import { KNOWLEDGE_SOURCE_CARD_ITEMS } from './knowledge_source_card_config'; + +export const DEFAULT_KNOWLEDGE_SOURCE_CARD_ITEM_SELECTED: CardSelectorListItem = + KNOWLEDGE_SOURCE_CARD_ITEMS[0]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/custom_rules.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/custom_rules.png new file mode 100644 index 0000000000000..d9272d3ad908b Binary files /dev/null and b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/custom_rules.png differ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/preview_rules.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/preview_rules.png new file mode 100644 index 0000000000000..7407c55c5c124 Binary files /dev/null and b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/preview_rules.png differ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon.png new file mode 100644 index 0000000000000..89f8732bbd56d Binary files /dev/null and b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon.png differ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon_dark.png b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon_dark.png new file mode 100644 index 0000000000000..2c70f9dd62f8c Binary files /dev/null and b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/images/rules_icon_dark.png differ diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/index.ts new file mode 100644 index 0000000000000..75a2308976a83 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/index.ts @@ -0,0 +1,31 @@ +/* + * 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 type { OnboardingCardConfig } from '../../../../../types'; +import { OnboardingCardId } from '../../../../../constants'; +import { KNOWLEDGE_SOURCE_CARD_TITLE } from './translations'; +import { checkKnowledgeSourceComplete } from './knowledge_source_check_complete'; +import rulesIcon from './images/rules_icon.png'; +import rulesDarkIcon from './images/rules_icon_dark.png'; +import { SECURITY_FEATURE_ID } from '../../../../../../../common/constants'; + +export const knowledgeSourceCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.knowledgeSource, + title: KNOWLEDGE_SOURCE_CARD_TITLE, + icon: rulesIcon, + iconDark: rulesDarkIcon, + Component: React.lazy( + () => + import( + /* webpackChunkName: "onboarding_knowledge_source_card" */ + './knowledge_source_card' + ) + ), + checkComplete: checkKnowledgeSourceComplete, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.external_detections`], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.test.tsx new file mode 100644 index 0000000000000..624e55d7742fd --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../../../common/mock'; +import { OnboardingContextProvider } from '../../../../onboarding_context'; +import KnowledgeSourceCard from './knowledge_source_card'; +import { ExperimentalFeaturesService } from '../../../../../../common/experimental_features_service'; + +jest.mock('../../../../../../common/experimental_features_service', () => ({ + ExperimentalFeaturesService: { get: jest.fn() }, +})); +const mockExperimentalFeatures = ExperimentalFeaturesService.get as jest.Mock; + +const mockSetComplete = jest.fn(); +const mockSetExpandedCardId = jest.fn(); +const mockIsCardComplete = jest.fn(); +const mockIsCardAvailable = jest.fn(); + +const props = { + setComplete: mockSetComplete, + checkComplete: jest.fn(), + isCardComplete: mockIsCardComplete, + setExpandedCardId: mockSetExpandedCardId, + isExpanded: true, + isCardAvailable: jest.fn(), +}; + +describe('KnowledgeSourceCard', () => { + beforeEach(() => { + mockExperimentalFeatures.mockReturnValue({}); + jest.clearAllMocks(); + }); + + it('description should be in the document', () => { + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId('knowledgeSourceCardDescription')).toBeInTheDocument(); + }); + + it('card callout should not be rendered if integrations card is not available', () => { + mockIsCardAvailable.mockReturnValueOnce(false); + + const { queryByText } = render( + + + + + + ); + + expect(queryByText('To add knowledge sources add integrations first.')).not.toBeInTheDocument(); + }); + + it('renders an enabled button if integrations card is complete', () => { + mockIsCardAvailable.mockReturnValueOnce(true); + mockIsCardComplete.mockReturnValueOnce(true); + + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId('knowledgeSourceCardButton').querySelector('button')).not.toBeDisabled(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.tsx new file mode 100644 index 0000000000000..057fabfac5ce1 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { KNOWLEDGE_BASE_TAB } from '@kbn/elastic-assistant/impl/assistant/settings/const'; +import { SecuritySolutionLinkButton } from '../../../../../../common/components/links'; +import { OnboardingCardId } from '../../../../../constants'; +import type { OnboardingCardComponent } from '../../../../../types'; +import { OnboardingCardContentAssetPanel } from '../../common/card_content_asset_panel'; +import { CardCallOut } from '../../common/card_callout'; + +import { CardSubduedText } from '../../common/card_subdued_text'; +import * as i18n from './translations'; +import type { CardSelectorListItem } from '../../common/card_selector_list'; +import { CardSelectorList } from '../../common/card_selector_list'; +import { useOnboardingContext } from '../../../../onboarding_context'; +import { + KNOWLEDGE_SOURCE_CARD_ITEMS_BY_ID, + KNOWLEDGE_SOURCE_CARD_ITEMS, +} from './knowledge_source_card_config'; +import { DEFAULT_KNOWLEDGE_SOURCE_CARD_ITEM_SELECTED } from './constants'; +import type { CardSelectorAssetListItem } from '../../types'; +import { useStoredSelectedCardItemId } from '../../../../hooks/use_stored_state'; + +export const KnowledgeSourceCard: OnboardingCardComponent = ({ + isCardComplete, + setExpandedCardId, + isCardAvailable, +}) => { + const { spaceId } = useOnboardingContext(); + + const [selectedRuleId, setSelectedRuleId] = useStoredSelectedCardItemId( + 'knowledgeSource', + spaceId, + DEFAULT_KNOWLEDGE_SOURCE_CARD_ITEM_SELECTED.id + ); + const selectedCardItem = useMemo( + () => KNOWLEDGE_SOURCE_CARD_ITEMS_BY_ID[selectedRuleId], + [selectedRuleId] + ); + + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrationsSearchAILake), + [isCardComplete] + ); + + const isIntegrationsCardAvailable = useMemo( + () => isCardAvailable(OnboardingCardId.integrationsSearchAILake), + [isCardAvailable] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrationsSearchAILake, { scroll: true }); + }, [setExpandedCardId]); + + const onSelectCard = useCallback( + (item: CardSelectorListItem) => { + setSelectedRuleId(item.id); + }, + [setSelectedRuleId] + ); + + return ( + + + + + {i18n.KNOWLEDGE_SOURCE_CARD_DESCRIPTION} + + + + {isIntegrationsCardAvailable && !isIntegrationsCardComplete && ( + <> + + + + + {i18n.KNOWLEDGE_SOURCE_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + + + } + /> + + )} + + + + {i18n.KNOWLEDGE_SOURCE_CARD_ADD_BUTTON} + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default KnowledgeSourceCard; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card_config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card_config.ts new file mode 100644 index 0000000000000..8b20c6e8377cb --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_card_config.ts @@ -0,0 +1,63 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { KnowledgeSourceCardItemId } from './types'; +import type { CardSelectorAssetListItem } from '../../types'; +import { CardAssetType } from '../../types'; + +export const KNOWLEDGE_SOURCE_CARD_ITEMS: CardSelectorAssetListItem[] = [ + { + id: KnowledgeSourceCardItemId.install, + title: i18n.translate('xpack.securitySolution.onboarding.knowledgeSourceCards.install.title', { + defaultMessage: 'How to add knowledge sources', + }), + description: i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCards.install.description', + { + defaultMessage: 'Connect internal data to enrich the AI Assistant’s context', + } + ), + // FIXME: update the video + asset: { + type: CardAssetType.video, + source: 'https://ela.st/ai4dsoc-gs1', + alt: i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCards.install.description', + { + defaultMessage: 'Connect internal data to enrich the AI Assistant’s context', + } + ), + }, + }, + { + id: KnowledgeSourceCardItemId.create, + title: i18n.translate('xpack.securitySolution.onboarding.knowledgeSourceCards.create.title', { + defaultMessage: 'Leveraging knowledge sources in a cyber context', + }), + description: i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCards.create.description', + { + defaultMessage: 'Use custom data to improve threat detection and response', + } + ), + asset: { + type: CardAssetType.video, + source: 'http://ela.st/ai4dsoc-gs2', + alt: i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCards.create.description', + { + defaultMessage: 'Use custom data to improve threat detection and response', + } + ), + }, + }, +]; + +export const KNOWLEDGE_SOURCE_CARD_ITEMS_BY_ID = Object.fromEntries( + KNOWLEDGE_SOURCE_CARD_ITEMS.map((card) => [card.id, card]) +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_check_complete.ts new file mode 100644 index 0000000000000..7d6507b144885 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/knowledge_source_check_complete.ts @@ -0,0 +1,28 @@ +/* + * 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 { getKnowledgeBaseStatus } from '@kbn/elastic-assistant/impl/assistant/api/knowledge_base/api'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import { KNOWLEDGE_SOURCE_CARD_CHECK_COMPLETE_ERROR_MESSAGE } from './translations'; + +export const checkKnowledgeSourceComplete: OnboardingCardCheckComplete = async ({ + http, + notifications: { toasts }, +}) => { + const kbStatus = await getKnowledgeBaseStatus({ + http, + }); + if (kbStatus instanceof Error) { + toasts.addError(kbStatus, { title: KNOWLEDGE_SOURCE_CARD_CHECK_COMPLETE_ERROR_MESSAGE }); + return { + isComplete: false, + }; + } + return { + isComplete: (kbStatus?.elser_exists && kbStatus?.security_labs_exists) ?? false, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/translations.ts new file mode 100644 index 0000000000000..168650e005e46 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/translations.ts @@ -0,0 +1,58 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const KNOWLEDGE_SOURCE_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.title', + { + defaultMessage: 'Add knowledge sources', + } +); + +export const KNOWLEDGE_SOURCE_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.description', + { + defaultMessage: + 'Integrate knowledge from third-party sources to use with the AI Assistant during incident response.', + } +); + +export const KNOWLEDGE_SOURCE_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.calloutIntegrationsText', + { + defaultMessage: 'To add knowledge sources add integrations first.', + } +); + +export const KNOWLEDGE_SOURCE_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations step', + } +); + +export const KNOWLEDGE_SOURCE_CARD_ADD_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.addButton', + { + defaultMessage: 'Add knowledge sources', + } +); + +export const KNOWLEDGE_SOURCE_CARD_CHECK_COMPLETE_ERROR_MESSAGE = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.checkCompleteErrorMessage', + { + defaultMessage: 'Failed to check Card knowledge source completion.', + } +); + +export const KNOWLEDGE_SOURCE_CARD_STEP_SELECTOR_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.knowledgeSourceCard.stepSelectorTitle', + { + defaultMessage: 'More detail on how to use knowledge sources', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/types.ts new file mode 100644 index 0000000000000..2b3d103298a38 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/knowledge_source/types.ts @@ -0,0 +1,11 @@ +/* + * 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 enum KnowledgeSourceCardItemId { + install = 'install', + create = 'create', +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/connectors_check_complete.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/connectors_check_complete.ts new file mode 100644 index 0000000000000..0270cbeec873e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/connectors_check_complete.ts @@ -0,0 +1,35 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { OnboardingCardCheckComplete } from '../../../../../types'; +import { loadAiConnectors } from '../../common/connectors/ai_connectors'; +import { getConnectorsAuthz } from '../../common/connectors/authz'; +import type { AIConnectorCardMetadata } from './types'; + +const completeBadgeText = (count: number) => + i18n.translate('xpack.securitySolution.onboarding.llmConnector.badge.completeText', { + defaultMessage: '{count} AI {count, plural, one {connector} other {connectors}} added', + values: { count }, + }); + +export const checkAiConnectorsCardComplete: OnboardingCardCheckComplete< + AIConnectorCardMetadata +> = async ({ http, application }) => { + const authz = getConnectorsAuthz(application.capabilities); + + if (!authz.canReadConnectors) { + return { isComplete: false, metadata: { connectors: [], ...authz } }; + } + + const aiConnectors = await loadAiConnectors(http); + + return { + isComplete: aiConnectors.length > 0, + completeBadgeText: completeBadgeText(aiConnectors.length), + metadata: { connectors: aiConnectors, ...authz }, + }; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/index.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/index.ts new file mode 100644 index 0000000000000..7f1c867396ed7 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { AssistantIcon } from '@kbn/ai-assistant-icon'; +import type { OnboardingCardConfig } from '../../../../../types'; +import { OnboardingCardId } from '../../../../../constants'; +import { AI_CONNECTOR_CARD_TITLE } from './translations'; +import type { AIConnectorCardMetadata } from './types'; +import { SECURITY_FEATURE_ID } from '../../../../../../../common/constants'; +import { checkAssistantCardComplete } from '../../common/connectors/assistant_check_complete'; + +export const llmConnectorCardConfig: OnboardingCardConfig = { + id: OnboardingCardId.searchAiLakeLLM, + title: AI_CONNECTOR_CARD_TITLE, + icon: AssistantIcon, + Component: React.lazy( + () => + import( + /* webpackChunkName: "onboarding_search_ai_lake_llm_connector_card" */ + './llm_connector_card' + ) + ), + checkComplete: checkAssistantCardComplete, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.external_detections`], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/llm_connector_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/llm_connector_card.tsx new file mode 100644 index 0000000000000..4c5eae8bec468 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/llm_connector_card.tsx @@ -0,0 +1,126 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; +import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; +import type { OnboardingCardComponent } from '../../../../../types'; +import * as i18n from './translations'; +import { OnboardingCardContentPanel } from '../../common/card_content_panel'; +import { ConnectorCards } from '../../common/connectors/connector_cards'; +import { CardSubduedText } from '../../common/card_subdued_text'; +import { ConnectorsMissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; +import type { AIConnector } from '../../common/connectors/types'; +import type { AIConnectorCardMetadata } from './types'; +import { useDefinedLocalStorage } from '../../../../../../common/lib/integrations/hooks/use_stored_state'; + +const LlmPerformanceMatrixDocsLink = React.memo<{ text: string }>(({ text }) => { + const { llmPerformanceMatrix } = useKibana().services.docLinks.links.securitySolution; + return ( + + {text} + + ); +}); +LlmPerformanceMatrixDocsLink.displayName = 'LlmPerformanceMatrixDocsLink'; + +const SiemMigrationDocsLink = React.memo<{ text: string }>(({ text }) => { + const { siemMigrations } = useKibana().services.docLinks.links.securitySolution; + return ( + + {text} + + ); +}); +SiemMigrationDocsLink.displayName = 'SiemMigrationDocsLink'; + +export const AIConnectorCard: OnboardingCardComponent = ({ + checkCompleteMetadata, + checkComplete, + setComplete, +}) => { + const { siemMigrations } = useKibana().services; + const [storedConnectorId, setStoredConnectorId] = useDefinedLocalStorage( + siemMigrations.rules.connectorIdStorage.key, + undefined + ); + const setSelectedConnector = useCallback( + (connector: AIConnector) => { + setStoredConnectorId(connector.id); + setComplete(true); + siemMigrations.rules.telemetry.reportConnectorSelected({ connector }); + }, + [setComplete, setStoredConnectorId, siemMigrations] + ); + + const isInferenceConnector = useMemo(() => { + if (!checkCompleteMetadata?.connectors?.length || !storedConnectorId) { + return false; + } + const connector = checkCompleteMetadata.connectors.find((c) => c.id === storedConnectorId); + return connector?.actionTypeId === '.inference' ?? false; + }, [checkCompleteMetadata, storedConnectorId]); + + if (!checkCompleteMetadata) { + return ( + + + + ); + } + + const { connectors, canExecuteConnectors, canCreateConnectors } = checkCompleteMetadata; + + return ( + + {canExecuteConnectors ? ( + + + + {i18n.AI_CONNECTOR_CARD_DESCRIPTION_START} + {isInferenceConnector ? ( + , + docsLink: , + }} + /> + ) : ( + , + docsLink: , + }} + /> + )} + + + + + + + ) : ( + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default AIConnectorCard; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/translations.ts new file mode 100644 index 0000000000000..a797896040b0b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/translations.ts @@ -0,0 +1,43 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const AI_CONNECTOR_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.title', + { + defaultMessage: 'Configure LLMs', + } +); + +export const AI_CONNECTOR_CARD_DESCRIPTION_START = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.descriptionStart', + { defaultMessage: 'This feature relies on an AI connector for rule translation. ' } +); + +export const AI_CONNECTOR_CARD_DESCRIPTION_INFERENCE_CONNECTOR = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.descriptionInferenceConnector', + { + defaultMessage: + 'The Elastic-provided connector is selected by default. You can configure another connector and model if you prefer. ', + } +); + +export const LLM_MATRIX_LINK = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.llmMatrixLink', + { defaultMessage: 'model performance' } +); + +export const AI_POWERED_MIGRATIONS_LINK = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.siemMigrationLink', + { defaultMessage: 'AI-powered SIEM migration' } +); + +export const LEARN_MORE_LINK = i18n.translate( + 'xpack.securitySolution.onboarding.llmConnector.learnMoreLink', + { defaultMessage: 'Learn more' } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/types.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/types.ts new file mode 100644 index 0000000000000..c736760b541be --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/search_ai_lake/llm/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ActionConnector } from '@kbn/alerts-ui-shared'; +import type { ConnectorsAuthz } from '../../common/connectors/authz'; + +export interface AIConnectorCardMetadata extends ConnectorsAuthz { + connectors: ActionConnector[]; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx index 4fa8e2e1c7320..4c5eae8bec468 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_body/cards/siem_migrations/ai_connector/ai_connector_card.tsx @@ -10,7 +10,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { CenteredLoadingSpinner } from '../../../../../../common/components/centered_loading_spinner'; import { useKibana } from '../../../../../../common/lib/kibana/kibana_react'; -import { useDefinedLocalStorage } from '../../../../hooks/use_stored_state'; import type { OnboardingCardComponent } from '../../../../../types'; import * as i18n from './translations'; import { OnboardingCardContentPanel } from '../../common/card_content_panel'; @@ -19,6 +18,7 @@ import { CardSubduedText } from '../../common/card_subdued_text'; import { ConnectorsMissingPrivilegesCallOut } from '../../common/connectors/missing_privileges'; import type { AIConnector } from '../../common/connectors/types'; import type { AIConnectorCardMetadata } from './types'; +import { useDefinedLocalStorage } from '../../../../../../common/lib/integrations/hooks/use_stored_state'; const LlmPerformanceMatrixDocsLink = React.memo<{ text: string }>(({ text }) => { const { llmPerformanceMatrix } = useKibana().services.docLinks.links.securitySolution; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index 864804e0e4d40..6015b3905a776 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -19,6 +19,7 @@ import type { } from '../types'; import { onboardingConfig } from '../config'; import { useOnboardingTelemetry, type OnboardingTelemetry } from './onboarding_telemetry'; +import type { TrackLinkClick } from './lib/telemetry'; export type OnboardingConfig = Map; export interface OnboardingContextValue { @@ -28,18 +29,19 @@ export interface OnboardingContextValue { } const OnboardingContext = createContext(null); -export const OnboardingContextProvider: React.FC> = - React.memo(({ children, spaceId }) => { - const config = useFilteredConfig(); - const telemetry = useOnboardingTelemetry(); +export const OnboardingContextProvider: React.FC< + PropsWithChildren<{ spaceId: string; trackLinkClick?: TrackLinkClick }> +> = React.memo(({ children, spaceId, trackLinkClick }) => { + const config = useFilteredConfig(); + const telemetry = useOnboardingTelemetry({ trackLinkClick }); - const value = useMemo( - () => ({ spaceId, telemetry, config }), - [spaceId, telemetry, config] - ); + const value = useMemo( + () => ({ spaceId, telemetry, config }), + [spaceId, telemetry, config] + ); - return {children}; - }); + return {children}; +}); OnboardingContextProvider.displayName = 'OnboardingContextProvider'; export const useOnboardingContext = () => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx index 2b663add12248..f7e4c9de279a0 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.test.tsx @@ -7,11 +7,12 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { trackOnboardingLinkClick } from '../lib/telemetry'; import { FooterLinkItem } from './onboarding_footer'; import { OnboardingFooterLinkItemId, TELEMETRY_FOOTER_LINK } from './constants'; +import { mockTrackLinkClick } from '../__mocks__/mocks'; jest.mock('../lib/telemetry'); +jest.mock('../onboarding_context'); describe('OnboardingFooterComponent', () => { beforeEach(() => { @@ -45,7 +46,7 @@ describe('OnboardingFooterComponent', () => { ); getByTestId('footerLinkItem').click(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith( + expect(mockTrackLinkClick).toHaveBeenCalledWith( `${TELEMETRY_FOOTER_LINK}_${OnboardingFooterLinkItemId.documentation}` ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx index 9db64386be067..2cc4d561ff617 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx @@ -9,9 +9,9 @@ import React, { useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { useFooterStyles } from './onboarding_footer.styles'; import { useFooterItems } from './footer_items'; -import { trackOnboardingLinkClick } from '../lib/telemetry'; import type { OnboardingFooterLinkItemId } from './constants'; import { TELEMETRY_FOOTER_LINK } from './constants'; +import { useOnboardingContext } from '../onboarding_context'; export const OnboardingFooter = React.memo(() => { const styles = useFooterStyles(); @@ -43,9 +43,13 @@ interface FooterLinkItemProps { export const FooterLinkItem = React.memo( ({ id, title, icon, description, link }) => { + const { + telemetry: { trackLinkClick }, + } = useOnboardingContext(); + const onClickWithReport = useCallback(() => { - trackOnboardingLinkClick(`${TELEMETRY_FOOTER_LINK}_${id}`); - }, [id]); + trackLinkClick?.(`${TELEMETRY_FOOTER_LINK}_${id}`); + }, [id, trackLinkClick]); return ( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx index febc8431627b8..b722f54630036 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { render } from '@testing-library/react'; import { LinkCard } from './link_card'; import { OnboardingHeaderCardId, TELEMETRY_HEADER_CARD } from '../../constants'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; +import { mockTrackLinkClick } from '../../../__mocks__/mocks'; jest.mock('../../../lib/telemetry'); +jest.mock('../../../onboarding_context'); describe('DataIngestionHubHeaderCardComponent', () => { beforeEach(() => { @@ -46,7 +47,7 @@ describe('DataIngestionHubHeaderCardComponent', () => { ); getByTestId('headerCardLink').click(); - expect(trackOnboardingLinkClick).toHaveBeenCalledWith( + expect(mockTrackLinkClick).toHaveBeenCalledWith( `${TELEMETRY_HEADER_CARD}_${OnboardingHeaderCardId.demo}` ); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx index 0f86bdd087c18..fa22ab6d511e4 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx @@ -8,10 +8,10 @@ import React, { useCallback } from 'react'; import { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import classNames from 'classnames'; -import { trackOnboardingLinkClick } from '../../../lib/telemetry'; import { useCardStyles } from './link_card.styles'; import type { OnboardingHeaderCardId } from '../../constants'; import { TELEMETRY_HEADER_CARD } from '../../constants'; +import { useOnboardingContext } from '../../../onboarding_context'; interface LinkCardProps { id: OnboardingHeaderCardId; @@ -28,11 +28,13 @@ export const LinkCard: React.FC = React.memo( ({ id, icon, title, description, onClick, href, target, linkText }) => { const cardStyles = useCardStyles(); const cardClassName = classNames(cardStyles, 'headerCard'); - + const { + telemetry: { trackLinkClick }, + } = useOnboardingContext(); const onClickWithReport = useCallback(() => { - trackOnboardingLinkClick(`${TELEMETRY_HEADER_CARD}_${id}`); + trackLinkClick?.(`${TELEMETRY_HEADER_CARD}_${id}`); onClick?.(); - }, [id, onClick]); + }, [id, onClick, trackLinkClick]); return ( { const currentUser = useCurrentUser(); @@ -36,6 +38,17 @@ export const OnboardingHeader = React.memo(() => { // Full name could be null, user name should always exist const currentUserName = currentUser?.fullName || currentUser?.username; + const { capabilities } = useKibana().services.application; + + const filteredHeaderConfig = useMemo(() => { + return ( + headerConfig.find( + (item) => + !item.capabilitiesRequired || + (item.capabilitiesRequired && hasCapabilities(capabilities, item.capabilitiesRequired)) + ) ?? defaultHeaderConfig + ); + }, [capabilities]); return ( <> @@ -43,22 +56,22 @@ export const OnboardingHeader = React.memo(() => { {currentUserName && ( - {i18n.ONBOARDING_PAGE_TITLE(currentUserName)} + {filteredHeaderConfig.getTitle(currentUserName)} )} -

{i18n.ONBOARDING_PAGE_SUBTITLE}

+

{filteredHeaderConfig.subTitle}

- {i18n.ONBOARDING_PAGE_DESCRIPTION} + {filteredHeaderConfig.description} diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_configs.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_configs.ts new file mode 100644 index 0000000000000..636ed97971fa2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header_configs.ts @@ -0,0 +1,33 @@ +/* + * 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 { SECURITY_FEATURE_ID } from '../../../../common/constants'; +import type { OnboardingConfigAvailabilityProps } from '../../types'; +import * as i18n from './translations'; + +interface HeaderConfig { + getTitle: (name: string) => string; + subTitle: string; + description: string; + capabilitiesRequired?: OnboardingConfigAvailabilityProps['capabilitiesRequired']; +} + +export const defaultHeaderConfig: HeaderConfig = { + getTitle: i18n.ONBOARDING_PAGE_TITLE, + subTitle: i18n.ONBOARDING_PAGE_SUBTITLE, + description: i18n.ONBOARDING_PAGE_DESCRIPTION, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.detections`], +}; + +export const headerConfig: HeaderConfig[] = [ + defaultHeaderConfig, + { + getTitle: i18n.ONBOARDING_PAGE_TITLE, + subTitle: i18n.ONBOARDING_SEARCH_AI_LAKE_PAGE_TITLE, + description: i18n.ONBOARDING_SEARCH_AI_LAKE_PAGE_SUB_DESCRIPTION, + capabilitiesRequired: [`${SECURITY_FEATURE_ID}.external_detections`], + }, +]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts index 90f5db2d77b30..6b3b102cc6fec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts @@ -26,3 +26,17 @@ export const ONBOARDING_PAGE_DESCRIPTION = i18n.translate( defaultMessage: `A SIEM with AI-driven security analytics, XDR and Cloud Security.`, } ); + +export const ONBOARDING_SEARCH_AI_LAKE_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.searchAILake.title', + { + defaultMessage: `Welcome to Elastic’s AI for the SOC`, + } +); + +export const ONBOARDING_SEARCH_AI_LAKE_PAGE_SUB_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.searchAILake.subDescription', + { + defaultMessage: `Empowering SOCs for faster threat detection, investigation, and response.`, + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts index aed608186d64c..d8c9bcc388a92 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.test.ts @@ -24,6 +24,8 @@ const telemetryMock = { reportEvent: jest.fn() }; services: { telemetry: telemetryMock }, }); +const mockTrackLinkClick = jest.fn(); + describe('useOnboardingTelemetry', () => { beforeEach(() => { jest.clearAllMocks(); @@ -31,7 +33,9 @@ describe('useOnboardingTelemetry', () => { describe('when opening a card', () => { it('should report card open event on default topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardOpen('testCard' as OnboardingCardId); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -41,7 +45,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card open event on another topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardOpen('testCard2' as OnboardingCardId); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( OnboardingHubEventTypes.OnboardingHubStepOpen, @@ -50,7 +56,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card auto open event', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardOpen('testCard' as OnboardingCardId, { auto: true }); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( OnboardingHubEventTypes.OnboardingHubStepOpen, @@ -61,7 +69,9 @@ describe('useOnboardingTelemetry', () => { describe('when completing a card', () => { it('should report card complete event on the default topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardComplete('testCard' as OnboardingCardId); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -71,7 +81,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card complete event on the another topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardComplete('testCard2' as OnboardingCardId); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -81,7 +93,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card auto complete event', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardComplete('testCard' as OnboardingCardId, { auto: true }); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -93,7 +107,9 @@ describe('useOnboardingTelemetry', () => { describe('when clicking a card link', () => { it('should report card link clicked event on the default topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardLinkClicked('testCard' as OnboardingCardId, 'link1'); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -103,7 +119,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card link clicked event on another topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardLinkClicked('testCard2' as OnboardingCardId, 'link1'); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -115,7 +133,9 @@ describe('useOnboardingTelemetry', () => { describe('when clicking a card selector', () => { it('should report card selector clicked event on the default topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardSelectorClicked('testCard' as OnboardingCardId, 'selector1'); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( @@ -125,7 +145,9 @@ describe('useOnboardingTelemetry', () => { }); it('should report card selector clicked event on another topic', () => { - const { result } = renderHook(useOnboardingTelemetry); + const { result } = renderHook(() => + useOnboardingTelemetry({ trackLinkClick: mockTrackLinkClick }) + ); result.current.reportCardSelectorClicked('testCard2' as OnboardingCardId, 'selector2'); expect(telemetryMock.reportEvent).toHaveBeenCalledWith( diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts index 87977e34eebb0..e23369cab7659 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/components/onboarding_telemetry.ts @@ -11,15 +11,21 @@ import type { OnboardingCardId } from '../constants'; import { OnboardingTopicId } from '../constants'; import { OnboardingHubEventTypes } from '../../common/lib/telemetry'; import { onboardingConfig } from '../config'; +import type { TrackLinkClick } from './lib/telemetry'; export interface OnboardingTelemetry { reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void; reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void; reportCardSelectorClicked: (cardId: OnboardingCardId, selectorId: string) => void; + trackLinkClick?: TrackLinkClick; } -export const useOnboardingTelemetry = (): OnboardingTelemetry => { +export const useOnboardingTelemetry = ({ + trackLinkClick, +}: { + trackLinkClick?: TrackLinkClick; +}): OnboardingTelemetry => { const { telemetry } = useKibana().services; return useMemo( () => ({ @@ -47,8 +53,9 @@ export const useOnboardingTelemetry = (): OnboardingTelemetry => { selectorId, }); }, + trackLinkClick, }), - [telemetry] + [telemetry, trackLinkClick] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts index bbe7f19cd1c53..20a08d544f875 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/config.ts @@ -12,6 +12,7 @@ import { siemMigrationsBodyConfig, } from './components/onboarding_body/body_config'; import type { TopicConfig } from './types'; +import { SECURITY_FEATURE_ID } from '../../common/constants'; export const onboardingConfig: TopicConfig[] = [ { @@ -28,5 +29,6 @@ export const onboardingConfig: TopicConfig[] = [ }), body: siemMigrationsBodyConfig, disabledExperimentalFlagRequired: 'siemMigrationsDisabled', + capabilitiesRequired: `${SECURITY_FEATURE_ID}.advancedInsights`, }, ]; diff --git a/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts b/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts index 0e6c94c7ed23f..d9b85015136cf 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/onboarding/constants.ts @@ -13,6 +13,9 @@ export enum OnboardingTopicId { export enum OnboardingCardId { integrations = 'integrations', + integrationsSearchAILake = 'integrations_search_ai_lake', + knowledgeSource = 'knowledge_source', + searchAiLakeLLM = 'search_ai_lake_llm', dashboards = 'dashboards', rules = 'rules', alerts = 'alerts',