diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5956c69f1eda2..bf5ff2d7bcb9c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2298,6 +2298,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/solutions/security/plugins/security_solution/common/timelines @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/common/components/alerts_viewer @elastic/security-threat-hunting-investigations +/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/detections/components/alerts_table/timeline_action @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/common/components/event_details @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/common/components/events_viewer @elastic/security-threat-hunting-investigations @@ -2317,6 +2318,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/solutions/security/plugins/security_solution/public/flyout/rule_details @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/investigations @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/detections/configurations/security_solution_detections @elastic/security-threat-hunting-investigations +/x-pack/solutions/security/plugins/security_solution/public/detections/pages/detections/alert_summary @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/common/hooks/use_resolve_conflict.tsx @elastic/security-threat-hunting-investigations /x-pack/solutions/security/plugins/security_solution/public/common/components/drag_and_drop @elastic/security-threat-hunting-investigations diff --git a/x-pack/platform/plugins/shared/fleet/public/index.ts b/x-pack/platform/plugins/shared/fleet/public/index.ts index d82e9c88b7db8..9d6d6278da810 100644 --- a/x-pack/platform/plugins/shared/fleet/public/index.ts +++ b/x-pack/platform/plugins/shared/fleet/public/index.ts @@ -10,6 +10,7 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import { lazy } from 'react'; import { FleetPlugin } from './plugin'; + export type { GetPackagesResponse } from './types'; export { installationStatuses } from '../common/constants'; @@ -89,3 +90,6 @@ export const AvailablePackagesHook = () => { './applications/integrations/sections/epm/screens/home/hooks/use_available_packages' ); }; + +export { useGetPackagesQuery } from './hooks/use_request/epm'; +export { useGetSettingsQuery } from './hooks/use_request/settings'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts index 5f62097b0d20c..61f3e1ef7f532 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/app/translations.ts @@ -112,6 +112,10 @@ export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', defaultMessage: 'Alerts', }); +export const ALERT_SUMMARY = i18n.translate('xpack.securitySolution.navigation.alertSummary', { + defaultMessage: 'Alert summary', +}); + export const ATTACK_DISCOVERY = i18n.translate( 'xpack.securitySolution.navigation.attackDiscovery', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx index a8140042a2042..8db6d69faf5ad 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.test.tsx @@ -13,21 +13,17 @@ import { SecurityPageName } from '../../../../common'; import { TestProviders } from '../../mock'; import { generateHistoryMock } from '../../utils/route/mocks'; import type { LinkInfo } from '../../links'; +import { useLinkInfo } from '../../links'; +import { useUpsellingPage } from '../../hooks/use_upselling'; + +jest.mock('../../links'); +jest.mock('../../hooks/use_upselling'); const defaultLinkInfo: LinkInfo = { id: SecurityPageName.exploreLanding, title: 'test', path: '/test', }; -const mockGetLink = jest.fn((): LinkInfo | undefined => defaultLinkInfo); -jest.mock('../../links', () => ({ - useLinkInfo: () => mockGetLink(), -})); - -const mockUseUpsellingPage = jest.fn(); -jest.mock('../../hooks/use_upselling', () => ({ - useUpsellingPage: () => mockUseUpsellingPage(), -})); const REDIRECT_COMPONENT_SUBJ = 'redirect-component'; const mockRedirect = jest.fn(() =>
); @@ -47,8 +43,17 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( ); describe('SecurityRoutePageWrapper', () => { - it('should render children when authorized', () => { - mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo }); // authorized + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render UpsellPage when it is available', () => { + const TEST_ID = 'test-upsell-page'; + const TestUpsellPage = () =>
; + + (useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo); + (useUpsellingPage as jest.Mock).mockReturnValue(TestUpsellPage); + const { getByTestId } = render( @@ -56,14 +61,55 @@ describe('SecurityRoutePageWrapper', () => { { wrapper: Wrapper } ); - expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument(); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + }); + + it('should redirect when link missing and redirectOnMissing flag present', () => { + (useLinkInfo as jest.Mock).mockReturnValue(undefined); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); + + const { getByTestId } = render( + + + , + { wrapper: Wrapper } + ); + + expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument(); + }); + + it('should redirect when link missing and redirectIfUnauthorized flag present', () => { + (useLinkInfo as jest.Mock).mockReturnValue(undefined); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); + + const { getByTestId } = render( + + + , + { wrapper: Wrapper } + ); + + expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument(); }); - it('should render UpsellPage when unauthorized and UpsellPage is available', () => { - const TestUpsellPage = () =>
; + it('should redirect when link is unauthorized and redirectIfUnauthorized flag present', () => { + (useLinkInfo as jest.Mock).mockReturnValue({ ...defaultLinkInfo, unauthorized: true }); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); + + const { getByTestId } = render( + + + , + { wrapper: Wrapper } + ); + + expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument(); + }); + + it('should render NoPrivilegesPage when link missing and UpsellPage is undefined', () => { + (useLinkInfo as jest.Mock).mockReturnValue(undefined); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); - mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true }); - mockUseUpsellingPage.mockReturnValue(TestUpsellPage); const { getByTestId } = render( @@ -71,12 +117,13 @@ describe('SecurityRoutePageWrapper', () => { { wrapper: Wrapper } ); - expect(getByTestId('test-upsell-page')).toBeInTheDocument(); + expect(getByTestId('noPrivilegesPage')).toBeInTheDocument(); }); - it('should render NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => { - mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true }); - mockUseUpsellingPage.mockReturnValue(undefined); + it('should render NoPrivilegesPage when unauthorized and UpsellPage is undefined', () => { + (useLinkInfo as jest.Mock).mockReturnValue({ ...defaultLinkInfo, unauthorized: true }); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); + const { getByTestId } = render( @@ -87,16 +134,17 @@ describe('SecurityRoutePageWrapper', () => { expect(getByTestId('noPrivilegesPage')).toBeInTheDocument(); }); - it('should redirect when link missing and redirectOnMissing flag present', () => { - mockGetLink.mockReturnValueOnce(undefined); + it('should render children when authorized', () => { + (useLinkInfo as jest.Mock).mockReturnValue(defaultLinkInfo); + (useUpsellingPage as jest.Mock).mockReturnValue(undefined); const { getByTestId } = render( - + , { wrapper: Wrapper } ); - expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument(); + expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument(); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx index 3351666c64529..aca57b1726083 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx @@ -18,6 +18,10 @@ import { SpyRoute } from '../../utils/route/spy_routes'; interface SecurityRoutePageWrapperProps { pageName: SecurityPageName; redirectOnMissing?: boolean; + /** + * Used primarily in the AI for SOC tier, to allow redirecting to the home page instead of showing the NoPrivileges page. + */ + redirectIfUnauthorized?: boolean; } /** @@ -40,13 +44,14 @@ interface SecurityRoutePageWrapperProps { export const SecurityRoutePageWrapper: FC> = ({ children, pageName, + redirectIfUnauthorized, redirectOnMissing, }) => { const link = useLinkInfo(pageName); - const UpsellingPage = useUpsellingPage(pageName); - // The upselling page is only returned when the license/product requirements are not met, + // The upselling page is only returned when the license/product requirements are not met. // When it is defined it must be rendered, no need to check anything else. + const UpsellingPage = useUpsellingPage(pageName); if (UpsellingPage) { return ( <> @@ -56,28 +61,38 @@ export const SecurityRoutePageWrapper: FC; + } + const isAuthorized = link != null && !link.unauthorized; - if (isAuthorized) { + + // Allows a redirect to the home page if the link is undefined or unauthorized. + // This is used in the AI for SOC tier (for the Alert Summary page for example), as it does not make sense to show the NoPrivilegesPage. + if (redirectIfUnauthorized && !isAuthorized) { + return ; + } + + // Show the no privileges page if the link is undefined or unauthorized. + if (!isAuthorized) { return ( - - {children} + <> - + docLinks.siem.privileges} + /> + ); } - if (redirectOnMissing && link == null) { - return ; // redirects to the home page - } - + // Show the actual application page. return ( - <> + + {children} - docLinks.siem.privileges} - /> - + ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx new file mode 100644 index 0000000000000..38a2836ba45ca --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/landing_page/landing_page.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { EuiText } from '@elastic/eui'; + +export const LANDING_PAGE_PROMPT_TEST_ID = 'alert-summary-landing-page-prompt'; + +export interface LandingPageProps { + /** + * List of available AI for SOC integrations + */ + packages: PackageListItem[]; +} + +/** + * Displays a gif of the alerts summary page, with empty prompt showing the top 2 available AI for SOC packages. + * This page is rendered when no AI for SOC packages are installed. + */ +export const LandingPage = memo(({ packages }: LandingPageProps) => { + return {'Landing page'}; +}); + +LandingPage.displayName = 'LandingPage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx new file mode 100644 index 0000000000000..d6393d718dcd4 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/components/alert_summary/wrapper.tsx @@ -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, { memo } from 'react'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { EuiText } from '@elastic/eui'; + +export const DATA_VIEW_LOADING_PROMPT_TEST_ID = 'alert-summary-data-view-loading-prompt'; + +export interface WrapperProps { + /** + * List of installed Ai for SOC integrations + */ + packages: PackageListItem[]; +} + +/** + * Creates a new dataView with the alert indices while displaying a loading skeleton. + * Display the alert summary page content if the dataView is correctly created. + * This page is rendered when there are AI for SOC packages installed. + */ +export const Wrapper = memo(({ packages }: WrapperProps) => { + return {'Wrapper'}; +}); + +Wrapper.displayName = 'Wrapper'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts new file mode 100644 index 0000000000000..f5805d315c778 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { renderHook } from '@testing-library/react'; +import { useFetchIntegrations } from './use_fetch_integrations'; +import { useKibana } from '../../../common/lib/kibana'; +import { + installationStatuses, + useGetPackagesQuery, + useGetSettingsQuery, +} from '@kbn/fleet-plugin/public'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('@kbn/fleet-plugin/public'); + +describe('useFetchIntegrations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return isLoading true', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + fleet: { + authz: { + fleet: { + readSettings: true, + }, + }, + }, + }, + }); + (useGetSettingsQuery as jest.Mock).mockReturnValue({ + isFetchedAfterMount: true, + }); + (useGetPackagesQuery as jest.Mock).mockReturnValue({ + data: [], + isLoading: true, + }); + + const { result } = renderHook(() => useFetchIntegrations()); + + expect(result.current.availablePackage).toHaveLength(0); + expect(result.current.installedPackages).toHaveLength(0); + expect(result.current.isLoading).toBe(true); + }); + + it('should return availablePackage and installedPackages', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + fleet: { + authz: { + fleet: { + readSettings: true, + }, + }, + }, + }, + }); + (useGetSettingsQuery as jest.Mock).mockReturnValue({ + isFetchedAfterMount: true, + }); + (useGetPackagesQuery as jest.Mock).mockReturnValue({ + data: { + items: [ + { + name: 'splunk', + status: installationStatuses.Installed, + }, + { + name: 'google_secops', + status: installationStatuses.InstallFailed, + }, + { + name: 'microsoft_sentinel', + status: installationStatuses.NotInstalled, + }, + { name: 'unknown' }, + ], + }, + isLoading: false, + }); + + const { result } = renderHook(() => useFetchIntegrations()); + + expect(result.current.availablePackage).toHaveLength(1); + expect(result.current.availablePackage[0].name).toBe('microsoft_sentinel'); + + expect(result.current.installedPackages).toHaveLength(2); + expect(result.current.installedPackages[0].name).toBe('splunk'); + expect(result.current.installedPackages[1].name).toBe('google_secops'); + + expect(result.current.isLoading).toBe(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts new file mode 100644 index 0000000000000..bc3cb7dddba54 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/hooks/alert_summary/use_fetch_integrations.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import type { PackageListItem } from '@kbn/fleet-plugin/common'; +import { + installationStatuses, + useGetPackagesQuery, + useGetSettingsQuery, +} from '@kbn/fleet-plugin/public'; +import { useKibana } from '../../../common/lib/kibana'; + +// We hardcode these here for now as we currently do not have any other way to filter out all the unwanted integrations. +const AI_FOR_SOC_INTEGRATIONS = [ + 'splunk', // doesnt yet exist + 'google_secops', + 'microsoft_sentinel', + 'sentinel_one', + 'crowdstrike', +]; + +export interface UseFetchIntegrationsResult { + /** + * Is true while the data is loading + */ + isLoading: boolean; + /** + * The AI for SOC installed integrations (see list in the constant above) + */ + installedPackages: PackageListItem[]; + /** + * The AI for SOC not-installed integrations (see list in the constant above) + */ + availablePackage: PackageListItem[]; +} + +/** + * Fetches all integrations, then returns the installed and non-installed ones filtered with a list of + * hard coded AI for SOC integrations: + * - splunk + * - google_secops + * - microsoft_sentinel + * - sentinel_one + * - crowdstrike + */ +export const useFetchIntegrations = (): UseFetchIntegrationsResult => { + const { fleet } = useKibana().services; + const isAuthorizedToFetchSettings = fleet?.authz.fleet.readSettings; + const { isFetchedAfterMount: isSettingsFetched } = useGetSettingsQuery({ + enabled: isAuthorizedToFetchSettings, + }); + const shouldFetchPackages = !isAuthorizedToFetchSettings || isSettingsFetched; + const { data: allPackages, isLoading } = useGetPackagesQuery( + { + prerelease: true, + }, + { + enabled: shouldFetchPackages, + } + ); + + const aiForSOCPackages: PackageListItem[] = useMemo( + () => (allPackages?.items || []).filter((pkg) => AI_FOR_SOC_INTEGRATIONS.includes(pkg.name)), + [allPackages] + ); + const availablePackage: PackageListItem[] = useMemo( + () => aiForSOCPackages.filter((pkg) => pkg.status === installationStatuses.NotInstalled), + [aiForSOCPackages] + ); + const installedPackages: PackageListItem[] = useMemo( + () => + aiForSOCPackages.filter( + (pkg) => + pkg.status === installationStatuses.Installed || + pkg.status === installationStatuses.InstallFailed + ), + [aiForSOCPackages] + ); + + return useMemo( + () => ({ + isLoading, + installedPackages, + availablePackage, + }), + [isLoading, installedPackages, availablePackage] + ); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts index 982df1c5a6f8c..29679805cd12c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/links.ts @@ -4,20 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { i18n } from '@kbn/i18n'; import { + ALERT_SUMMARY_PATH, ALERTS_PATH, - SecurityPageName, SECURITY_FEATURE_ID, - ALERT_SUMMARY_PATH, + SecurityPageName, } from '../../common/constants'; -import { ALERTS } from '../app/translations'; +import { ALERT_SUMMARY, ALERTS } from '../app/translations'; import type { LinkItem } from '../common/links/types'; export const alertsLink: LinkItem = { - id: SecurityPageName.alerts, - title: ALERTS, - path: ALERTS_PATH, capabilities: [`${SECURITY_FEATURE_ID}.show`], globalNavPosition: 3, globalSearchKeywords: [ @@ -25,12 +23,12 @@ export const alertsLink: LinkItem = { defaultMessage: 'Alerts', }), ], + id: SecurityPageName.alerts, + path: ALERTS_PATH, + title: ALERTS, }; export const alertSummaryLink: LinkItem = { - id: SecurityPageName.alertSummary, - path: ALERT_SUMMARY_PATH, - title: 'Alert summary', capabilities: [[`${SECURITY_FEATURE_ID}.show`, `${SECURITY_FEATURE_ID}.alerts_summary`]], globalNavPosition: 3, globalSearchKeywords: [ @@ -39,4 +37,7 @@ export const alertSummaryLink: LinkItem = { }), ], hideTimeline: true, + id: SecurityPageName.alertSummary, + path: ALERT_SUMMARY_PATH, + title: ALERT_SUMMARY, }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.tsx new file mode 100644 index 0000000000000..90aafcacfbe46 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.test.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 React from 'react'; +import { render } from '@testing-library/react'; +import { AlertSummaryPage, LOADING_INTEGRATIONS_TEST_ID } from './alert_summary'; +import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations'; +import { LANDING_PAGE_PROMPT_TEST_ID } from '../../components/alert_summary/landing_page/landing_page'; +import { useAddIntegrationsUrl } from '../../../common/hooks/use_add_integrations_url'; +import { DATA_VIEW_LOADING_PROMPT_TEST_ID } from '../../components/alert_summary/wrapper'; + +jest.mock('../../hooks/alert_summary/use_fetch_integrations'); +jest.mock('../../../common/hooks/use_add_integrations_url'); + +describe('', () => { + it('should render loading logo', () => { + (useFetchIntegrations as jest.Mock).mockReturnValue({ + isLoading: true, + }); + + const { getByTestId } = render(); + expect(getByTestId(LOADING_INTEGRATIONS_TEST_ID)).toHaveTextContent('Loading integrations'); + }); + + it('should render landing page if no packages are installed', () => { + (useFetchIntegrations as jest.Mock).mockReturnValue({ + installedPackages: [], + isLoading: false, + }); + (useAddIntegrationsUrl as jest.Mock).mockReturnValue({ + onClick: jest.fn(), + }); + + const { getByTestId, queryByTestId } = render(); + expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(LANDING_PAGE_PROMPT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render wrapper if packages are installed', () => { + (useFetchIntegrations as jest.Mock).mockReturnValue({ + availablePackage: [], + installedPackages: [{ id: 'id' }], + isLoading: false, + }); + + const { getByTestId, queryByTestId } = render(); + expect(queryByTestId(LOADING_INTEGRATIONS_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(LANDING_PAGE_PROMPT_TEST_ID)).not.toBeInTheDocument(); + expect(getByTestId(DATA_VIEW_LOADING_PROMPT_TEST_ID)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx new file mode 100644 index 0000000000000..ed6ad550a17b5 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/alert_summary.tsx @@ -0,0 +1,45 @@ +/* + * 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 { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useFetchIntegrations } from '../../hooks/alert_summary/use_fetch_integrations'; +import { LandingPage } from '../../components/alert_summary/landing_page/landing_page'; +import { Wrapper } from '../../components/alert_summary/wrapper'; + +export const LOADING_INTEGRATIONS_TEST_ID = 'alert-summary-loading-integrations'; + +const LOADING_INTEGRATIONS = i18n.translate('xpack.securitySolution.alertSummary.loading', { + defaultMessage: 'Loading integrations', +}); + +/** + * Alert summary page rendering alerts generated by AI for SOC integrations. + * This page should be only rendered for the AI for SOC product line. + */ +export const AlertSummaryPage = memo(() => { + const { availablePackage, installedPackages, isLoading } = useFetchIntegrations(); + + if (isLoading) { + return ( + } + title={

{LOADING_INTEGRATIONS}

} + /> + ); + } + + if (installedPackages.length === 0) { + return ; + } + + return ; +}); + +AlertSummaryPage.displayName = 'AlertSummaryPage'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx new file mode 100644 index 0000000000000..ef5b054d56a61 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/pages/alert_summary/index.tsx @@ -0,0 +1,32 @@ +/* + * 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 { Route, Routes } from '@kbn/shared-ux-router'; +import { SecurityRoutePageWrapper } from '../../../common/components/security_route_page_wrapper'; +import { AlertSummaryPage } from './alert_summary'; +import { NotFoundPage } from '../../../app/404'; +import { ALERT_SUMMARY_PATH, SecurityPageName } from '../../../../common/constants'; +import { PluginTemplateWrapper } from '../../../common/components/plugin_template_wrapper'; + +const AlertSummaryRoute = () => ( + + + + + +); + +export const AlertSummaryContainer: React.FC = React.memo(() => { + return ( + + + + + ); +}); +AlertSummaryContainer.displayName = 'AlertSummaryContainer'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx b/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx index afb2db8aa8e9e..aa5691593fa9a 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detections/routes.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import type { RouteProps, RouteComponentProps } from 'react-router-dom'; +import type { RouteComponentProps, RouteProps } from 'react-router-dom'; import { Redirect } from 'react-router-dom'; -import { ALERTS_PATH, DETECTIONS_PATH } from '../../common/constants'; +import { AlertSummaryContainer } from './pages/alert_summary'; +import { ALERT_SUMMARY_PATH, ALERTS_PATH, DETECTIONS_PATH } from '../../common/constants'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; import { Alerts } from './pages/alerts'; @@ -34,4 +35,8 @@ export const routes: RouteProps[] = [ path: ALERTS_PATH, component: AlertsRoutes, }, + { + path: ALERT_SUMMARY_PATH, + component: AlertSummaryContainer, + }, ];