diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx new file mode 100644 index 0000000000000..a37730ad1570c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { installationStatuses } from '../../../../../../common/constants'; + +import { + InstallationStatus, + getLineClampStyles, + shouldShowInstallationStatus, +} from './installation_status'; + +// Mock useEuiTheme to return a mock theme +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + useEuiTheme: () => ({ + euiTheme: { + border: { radius: { medium: '4px' } }, + size: { s: '8px', m: '16px' }, + colors: { emptyShade: '#FFFFFF' }, + }, + }), +})); + +describe('getLineClampStyles', () => { + it('returns the correct styles when lineClamp is provided', () => { + expect(getLineClampStyles(3)).toEqual( + '-webkit-line-clamp: 3;display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;' + ); + }); + + it('returns an empty string when lineClamp is not provided', () => { + expect(getLineClampStyles()).toEqual(''); + }); +}); + +describe('shouldShowInstallationStatus', () => { + it('returns false when showInstallationStatus is false', () => { + expect( + shouldShowInstallationStatus({ + installStatus: installationStatuses.Installed, + showInstallationStatus: false, + }) + ).toEqual(false); + }); + + it('returns true when showInstallationStatus is true and installStatus is installed', () => { + expect( + shouldShowInstallationStatus({ + installStatus: installationStatuses.Installed, + showInstallationStatus: true, + }) + ).toEqual(true); + }); + + it('returns true when showInstallationStatus is true and installStatus is installFailed', () => { + expect( + shouldShowInstallationStatus({ + installStatus: installationStatuses.InstallFailed, + showInstallationStatus: true, + }) + ).toEqual(true); + }); +}); + +describe('InstallationStatus', () => { + it('renders null when showInstallationStatus is false', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + }); + + it('renders the Installed status correctly', () => { + render( + + ); + expect(screen.getByText('Installed')).toBeInTheDocument(); + }); + + it('renders the Install Failed status correctly', () => { + render( + + ); + expect(screen.getByText('Installed')).toBeInTheDocument(); + }); + + it('renders null when installStatus is null or undefined', () => { + const { container } = render( + + ); + expect(container.firstChild).toBeNull(); + + const { container: undefinedContainer } = render( + + ); + expect(undefinedContainer.firstChild).toBeNull(); + }); + + it('applies the correct styles for the component', () => { + const { getByTestId } = render( + + ); + + const spacer = getByTestId('installation-status-spacer'); + 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/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx new file mode 100644 index 0000000000000..7b0a0da308c2f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/installation_status.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiCallOut, EuiSpacer, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; + +import { installationStatuses } from '../../../../../../common/constants'; +import type { EpmPackageInstallStatus } from '../../../../../../common/types'; + +const installedLabel = i18n.translate('xpack.fleet.packageCard.installedLabel', { + defaultMessage: 'Installed', +}); + +const installStatusMapToColor: Record< + string, + { color: 'success' | 'warning'; iconType: string; title: string } +> = { + installed: { + color: 'success', + iconType: 'check', + title: installedLabel, + }, + install_failed: { + color: 'warning', + iconType: 'warning', + title: installedLabel, + }, +}; + +interface InstallationStatusProps { + installStatus: EpmPackageInstallStatus | null | undefined; + showInstallationStatus?: boolean; +} + +export const getLineClampStyles = (lineClamp?: number) => + lineClamp + ? `-webkit-line-clamp: ${lineClamp};display: -webkit-box;-webkit-box-orient: vertical;overflow: hidden;` + : ''; + +export const shouldShowInstallationStatus = ({ + installStatus, + showInstallationStatus, +}: InstallationStatusProps) => + showInstallationStatus && + (installStatus === installationStatuses.Installed || + installStatus === installationStatuses.InstallFailed); + +export const InstallationStatus: React.FC = React.memo( + ({ installStatus, showInstallationStatus }) => { + const { euiTheme } = useEuiTheme(); + return shouldShowInstallationStatus({ installStatus, showInstallationStatus }) ? ( +
+ + +
+ ) : null; + } +); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx index 961fc2916cde8..716f59231a4c4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx @@ -14,6 +14,7 @@ import { useStartServices } from '../../../hooks'; import type { PackageCardProps } from './package_card'; import { PackageCard } from './package_card'; +import { getLineClampStyles, shouldShowInstallationStatus } from './installation_status'; jest.mock('../../../hooks', () => { return { @@ -38,6 +39,16 @@ jest.mock('../../../components', () => { }; }); +jest.mock('./installation_status', () => { + return { + shouldShowInstallationStatus: jest.fn(), + getLineClampStyles: jest.fn(), + InstallationStatus: () => { + return
; + }, + }; +}); + function cardProps(overrides: Partial = {}): PackageCardProps { return { id: 'card-1', @@ -60,8 +71,12 @@ function renderPackageCard(props: PackageCardProps) { describe('package card', () => { let mockNavigateToApp: jest.Mock; let mockNavigateToUrl: jest.Mock; + const mockGetLineClamp = getLineClampStyles as jest.Mock; + const mockShouldShowInstallationStatus = shouldShowInstallationStatus as jest.Mock; beforeEach(() => { + jest.clearAllMocks(); + mockNavigateToApp = useStartServices().application.navigateToApp as jest.Mock; mockNavigateToUrl = useStartServices().application.navigateToUrl as jest.Mock; }); @@ -136,4 +151,83 @@ describe('package card', () => { expect(!!collectionButton).toEqual(isCollectionCard); } ); + + describe('Installation status', () => { + it('should render installation status when showInstallationStatus is true', async () => { + const { + utils: { queryByTestId }, + } = renderPackageCard( + cardProps({ + showInstallationStatus: true, + }) + ); + const installationStatus = queryByTestId('installation-status'); + expect(installationStatus).toBeInTheDocument(); + }); + + it('should render max-height when maxCardHeight is provided', async () => { + const { + utils: { queryByTestId }, + } = renderPackageCard( + cardProps({ + maxCardHeight: 150, + }) + ); + const card = queryByTestId(`integration-card:card-1`); + expect(card).toHaveStyle('max-height: 150px'); + }); + + it('should render 1 line of description when descriptionLineClamp is provided and shouldShowInstallationStatus returns true', async () => { + mockShouldShowInstallationStatus.mockReturnValue(true); + renderPackageCard( + cardProps({ + showInstallationStatus: true, + installStatus: 'installed', + descriptionLineClamp: 3, + }) + ); + expect(mockShouldShowInstallationStatus).toHaveBeenCalledWith({ + installStatus: 'installed', + showInstallationStatus: true, + }); + expect(mockGetLineClamp).toHaveBeenCalledWith(1); + }); + + it('should render specific lines of description when descriptionLineClamp is provided and shouldShowInstallationStatus returns false', async () => { + mockShouldShowInstallationStatus.mockReturnValue(false); + renderPackageCard( + cardProps({ + showInstallationStatus: false, + installStatus: 'installed', + descriptionLineClamp: 3, + }) + ); + expect(mockShouldShowInstallationStatus).toHaveBeenCalledWith({ + installStatus: 'installed', + showInstallationStatus: false, + }); + expect(mockGetLineClamp).toHaveBeenCalledWith(3); + }); + + it('should not render line clamp when descriptionLineClamp is not provided', async () => { + mockShouldShowInstallationStatus.mockReturnValue(false); + renderPackageCard( + cardProps({ + showInstallationStatus: true, + installStatus: 'installed', + }) + ); + expect(mockShouldShowInstallationStatus).not.toHaveBeenCalled(); + }); + + it('should render specific lines of title when titleLineClamp is provided and shouldShowInstallationStatus returns false', async () => { + mockShouldShowInstallationStatus.mockReturnValue(false); + renderPackageCard( + cardProps({ + titleLineClamp: 1, + }) + ); + expect(mockGetLineClamp).toHaveBeenCalledWith(1); + }); + }); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 474ffe2e4db70..31213e5f9554a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -35,13 +35,21 @@ import { InlineReleaseBadge, WithGuidedOnboardingTour } from '../../../component import { useStartServices, useIsGuidedOnboardingActive } from '../../../hooks'; import { INTEGRATIONS_BASE_PATH, INTEGRATIONS_PLUGIN_ID } from '../../../constants'; +import { + InstallationStatus, + getLineClampStyles, + shouldShowInstallationStatus, +} from './installation_status'; + export type PackageCardProps = IntegrationCardItem; // Min-height is roughly 3 lines of content. // This keeps the cards from looking overly unbalanced because of content differences. -const Card = styled(EuiCard)<{ isquickstart?: boolean }>` +const Card = styled(EuiCard)<{ isquickstart?: boolean; $maxCardHeight?: number }>` min-height: 127px; border-color: ${({ isquickstart }) => (isquickstart ? '#ba3d76' : null)}; + ${({ $maxCardHeight }) => + $maxCardHeight ? `max-height: ${$maxCardHeight}px; overflow: hidden;` : ''}; `; export function PackageCard({ @@ -59,10 +67,15 @@ export function PackageCard({ isUnverified, isUpdateAvailable, showLabels = true, + showInstallationStatus, extraLabelsBadges, isQuickstart = false, + installStatus, onCardClick: onClickProp = undefined, isCollectionCard = false, + titleLineClamp, + descriptionLineClamp, + maxCardHeight, }: PackageCardProps) { let releaseBadge: React.ReactNode | null = null; @@ -178,6 +191,7 @@ export function PackageCard({ } onClick={onClickProp ?? onCardClick} + $maxCardHeight={maxCardHeight} > {showLabels && extraLabelsBadges ? extraLabelsBadges : null} @@ -214,6 +238,10 @@ export function PackageCard({ {releaseBadge} {hasDeferredInstallationsBadge} {collectionButton} + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx index 2ce70002627c9..4338cfb2bc918 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/grid.tsx @@ -28,6 +28,8 @@ interface GridColumnProps { isLoading: boolean; showMissingIntegrationMessage?: boolean; showCardLabels?: boolean; + scrollElementId?: string; + emptyStateStyles?: Record; } const VirtualizedRow: React.FC<{ @@ -61,6 +63,8 @@ export const GridColumn = ({ showMissingIntegrationMessage = false, showCardLabels = false, isLoading, + scrollElementId, + emptyStateStyles, }: GridColumnProps) => { const itemsSizeRefs = useRef(new Map()); const listRef = useRef(null); @@ -86,7 +90,7 @@ export const GridColumn = ({ if (!list.length) { return ( - +

@@ -107,6 +111,7 @@ export const GridColumn = ({ ); } + return ( <> {() => ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx index 8639d18b1278b..8b5498a160e5d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.stories.tsx @@ -75,6 +75,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['category_two'], + installStatus: 'installed', }, { title: 'Package Two', @@ -87,6 +88,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['category_one'], + installStatus: 'installed', }, { title: 'Package Three', @@ -99,6 +101,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['web'], + installStatus: 'installed', }, { title: 'Package Four', @@ -111,6 +114,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['category_one'], + installStatus: 'install_failed', }, { title: 'Package Five', @@ -123,6 +127,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['category_two'], + installStatus: 'install_failed', }, { title: 'Package Six', @@ -135,6 +140,7 @@ export const List = (props: Args) => ( icons: [], integration: 'integration', categories: ['category_two'], + installStatus: 'install_failed', }, ]} searchTerm="" diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx index 8a6761d48f9b1..be2b873c317db 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid/index.tsx @@ -51,6 +51,7 @@ const StickySidebar = styled(EuiFlexItem)` export interface PackageListGridProps { isLoading?: boolean; controls?: ReactNode | ReactNode[]; + emptyStateStyles?: Record; list: IntegrationCardItem[]; searchTerm: string; setSearchTerm: (search: string) => void; @@ -69,11 +70,15 @@ export interface PackageListGridProps { showMissingIntegrationMessage?: boolean; showControls?: boolean; showSearchTools?: boolean; + spacer?: boolean; + // Security Solution sends the id to determine which element to scroll when the user interacting with the package list + scrollElementId?: string; } export const PackageListGrid: FunctionComponent = ({ isLoading, controls, + emptyStateStyles, title, list, searchTerm, @@ -91,6 +96,8 @@ export const PackageListGrid: FunctionComponent = ({ showCardLabels = true, showControls = true, showSearchTools = true, + spacer = true, + scrollElementId, }) => { const localSearchRef = useLocalSearch(list, !!isLoading); @@ -267,13 +274,15 @@ export const PackageListGrid: FunctionComponent = ({ {callout} ) : null} - + {spacer && } {showMissingIntegrationMessage && ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx index bd1bc2ab91997..613970fc24085 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.test.tsx @@ -25,6 +25,19 @@ describe('BackLink', () => { expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); }); + it('renders back to selection link when onboardingLink param is provided', () => { + const expectedUrl = '/app/experimental-onboarding'; + const queryParams = new URLSearchParams(); + queryParams.set('onboardingLink', expectedUrl); + const { getByText, getByRole } = render( + + + + ); + expect(getByText('Back to selection')).toBeInTheDocument(); + expect(getByRole('link').getAttribute('href')).toBe(expectedUrl); + }); + it('renders back to selection link with params', () => { const expectedUrl = '/app/experimental-onboarding&search=aws&category=infra'; const queryParams = new URLSearchParams(); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx index 081b78de8ec51..75d3461bdfee6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/back_link.tsx @@ -17,7 +17,9 @@ interface Props { export function BackLink({ queryParams, href: integrationsHref }: Props) { const { onboardingLink } = useMemo(() => { return { - onboardingLink: queryParams.get('observabilityOnboardingLink'), + onboardingLink: + // Users from Security Solution onboarding page will have onboardingLink to redirect back to the onboarding page + queryParams.get('observabilityOnboardingLink') || queryParams.get('onboardingLink'), }; }, [queryParams]); const href = onboardingLink ?? integrationsHref; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index f826b4f5c308e..6a9b6c875e9dd 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -87,6 +87,7 @@ import { DocumentationPage, hasDocumentation } from './documentation'; import { Configs } from './configs'; import './index.scss'; +import type { InstallPkgRouteOptions } from './utils/get_install_route_options'; export type DetailViewPanelName = | 'overview' @@ -135,6 +136,11 @@ export function Detail() { const queryParams = useMemo(() => new URLSearchParams(search), [search]); const integration = useMemo(() => queryParams.get('integration'), [queryParams]); const prerelease = useMemo(() => Boolean(queryParams.get('prerelease')), [queryParams]); + /** Users from Security Solution onboarding page will have onboardingLink and onboardingAppId in the query params + ** to redirect back to the onboarding page after adding an integration + */ + const onboardingLink = useMemo(() => queryParams.get('onboardingLink'), [queryParams]); + const onboardingAppId = useMemo(() => queryParams.get('onboardingAppId'), [queryParams]); const authz = useAuthz(); const canAddAgent = authz.fleet.addAgents; @@ -388,7 +394,7 @@ export function Detail() { hash, }); - const navigateOptions = getInstallPkgRouteOptions({ + const defaultNavigateOptions: InstallPkgRouteOptions = getInstallPkgRouteOptions({ agentPolicyId: agentPolicyIdFromContext, currentPath, integration, @@ -399,6 +405,25 @@ export function Detail() { pkgkey, }); + /** Users from Security Solution onboarding page will have onboardingLink and onboardingAppId in the query params + ** to redirect back to the onboarding page after adding an integration + */ + const navigateOptions: InstallPkgRouteOptions = + onboardingAppId && onboardingLink + ? [ + defaultNavigateOptions[0], + { + ...defaultNavigateOptions[1], + state: { + ...(defaultNavigateOptions[1]?.state ?? {}), + onCancelNavigateTo: [onboardingAppId, { path: onboardingLink }], + onCancelUrl: onboardingLink, + onSaveNavigateTo: [onboardingAppId, { path: onboardingLink }], + }, + }, + ] + : defaultNavigateOptions; + services.application.navigateToApp(...navigateOptions); }, [ @@ -410,6 +435,8 @@ export function Detail() { isExperimentalAddIntegrationPageEnabled, isFirstTimeAgentUser, isGuidedOnboardingActive, + onboardingAppId, + onboardingLink, pathname, pkgkey, search, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts index 54db4346582bd..4c2cb97e295ad 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/utils/get_install_route_options.ts @@ -32,6 +32,11 @@ interface GetInstallPkgRouteOptionsParams { isGuidedOnboardingActive: boolean; } +export type InstallPkgRouteOptions = [ + string, + { path: string; state: CreatePackagePolicyRouteState } +]; + const isPackageExemptFromStepsLayout = (pkgkey: string) => EXCLUDED_PACKAGES.some((pkgname) => pkgkey.startsWith(pkgname)); /* @@ -47,7 +52,7 @@ export const getInstallPkgRouteOptions = ({ isCloud, isExperimentalAddIntegrationPageEnabled, isGuidedOnboardingActive, -}: GetInstallPkgRouteOptionsParams): [string, { path: string; state: unknown }] => { +}: GetInstallPkgRouteOptionsParams): InstallPkgRouteOptions => { const integrationOpts: { integration?: string } = integration ? { integration } : {}; const packageExemptFromStepsLayout = isPackageExemptFromStepsLayout(pkgkey); const useMultiPageLayout = diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx index 40c865f8ad4d8..422ed4c3a01e7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx @@ -85,6 +85,51 @@ describe('Card utils', () => { isUpdateAvailable: false, }); }); + + it('should return installStatus if the item is an integration', () => { + const cardItem = mapToCard({ + item: { + id: 'test', + version: '2.0.0-preview-1', + type: 'integration', + installationInfo: { + version: '1.0.0', + install_status: 'install_failed', + }, + }, + addBasePath, + getHref, + } as any); + + expect(cardItem).toMatchObject({ + release: 'ga', + version: '1.0.0', + isUpdateAvailable: true, + installStatus: 'install_failed', + }); + }); + + it('should not return installStatus if the item is not an integration', () => { + const cardItem = mapToCard({ + item: { + id: 'test', + version: '2.0.0-preview-1', + type: 'xxx', + installationInfo: { + version: '1.0.0', + install_status: 'install_failed', + }, + }, + addBasePath, + getHref, + } as any); + + expect(cardItem).toMatchObject({ + release: 'ga', + version: '1.0.0', + isUpdateAvailable: true, + }); + }); }); describe('getIntegrationLabels', () => { it('should return an empty list for an integration without errors', () => { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx index f8c579df39f3b..2fc6a82b960c9 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx @@ -27,6 +27,7 @@ import { getPackageReleaseLabel } from '../../../../../../../common/services'; import { installationStatuses } from '../../../../../../../common/constants'; import type { + EpmPackageInstallStatus, InstallFailedAttempt, IntegrationCardReleaseLabel, PackageSpecIcon, @@ -38,25 +39,32 @@ import { isPackageUnverified, isPackageUpdatable } from '../../../../services'; import type { PackageListItem } from '../../../../types'; export interface IntegrationCardItem { - url: string; - release?: IntegrationCardReleaseLabel; + categories: string[]; description: string; - name: string; - title: string; - version: string; + // Security Solution uses this prop to determine how many lines the card description should be truncated + descriptionLineClamp?: number; + extraLabelsBadges?: React.ReactNode[]; + fromIntegrations?: string; icons: Array; - integration: string; id: string; - categories: string[]; - fromIntegrations?: string; + installStatus?: EpmPackageInstallStatus; + integration: string; + isCollectionCard?: boolean; + isQuickstart?: boolean; isReauthorizationRequired?: boolean; isUnverified?: boolean; isUpdateAvailable?: boolean; - isQuickstart?: boolean; - showLabels?: boolean; - extraLabelsBadges?: React.ReactNode[]; + maxCardHeight?: number; + name: string; onCardClick?: () => void; - isCollectionCard?: boolean; + release?: IntegrationCardReleaseLabel; + showInstallationStatus?: boolean; + showLabels?: boolean; + title: string; + // Security Solution uses this prop to determine how many lines the card title should be truncated + titleLineClamp?: number; + url: string; + version: string; } export const mapToCard = ({ @@ -110,7 +118,7 @@ export const mapToCard = ({ extraLabelsBadges = getIntegrationLabels(item); } - return { + const cardResult: IntegrationCardItem = { id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}:${item.id}`, description: item.description, icons: !item.icons || !item.icons.length ? [] : item.icons, @@ -127,6 +135,12 @@ export const mapToCard = ({ isUpdateAvailable, extraLabelsBadges, }; + + if (item.type === 'integration') { + cardResult.installStatus = item.installationInfo?.install_status; + } + + return cardResult; }; export function getIntegrationLabels(item: PackageListItem): React.ReactNode[] { diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index 7661cbc64ad31..d82e9c88b7db8 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -10,6 +10,8 @@ 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'; export type { FleetSetup, FleetStart, FleetStartServices } from './plugin'; @@ -54,7 +56,7 @@ export type { UIExtensionsStorage, } from './types/ui_extensions'; -export { pagePathGetters } from './constants'; +export { pagePathGetters, EPM_API_ROUTES } from './constants'; export { pkgKeyFromPackageInfo } from './services'; export type { CustomAssetsAccordionProps } from './components/custom_assets_accordion'; export { CustomAssetsAccordion } from './components/custom_assets_accordion'; @@ -77,6 +79,7 @@ export const LazyPackagePolicyInputVarField = lazy(() => export type { PackageListGridProps } from './applications/integrations/sections/epm/components/package_list_grid'; export type { AvailablePackagesHookType } from './applications/integrations/sections/epm/screens/home/hooks/use_available_packages'; export type { IntegrationCardItem } from './applications/integrations/sections/epm/screens/home'; +export type { CategoryFacet } from './applications/integrations/sections/epm/screens/home/category_facets'; export const PackageList = () => { return import('./applications/integrations/sections/epm/components/package_list_grid');