From 93016d487538c53943171e19fc58797793f3d815 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Tue, 12 Nov 2024 16:31:47 +0000 Subject: [PATCH 1/4] tabs content --- .../fleet/public/applications/fleet/app.tsx | 2 +- .../public/applications/integrations/app.tsx | 10 +- .../hooks/use_integrations_state.tsx | 15 +- .../hooks/use_package_install.tsx | 16 +- .../hooks/use_is_first_time_agent_user.ts | 7 +- .../sections/epm/screens/detail/index.tsx | 168 ++++++++------ .../detail/settings/uninstall_button.tsx | 7 +- .../epm/screens/detail/tabs_content.tsx | 112 ++++++++++ .../sections/epm/screens/home/card_utils.tsx | 8 +- .../plugins/fleet/public/hooks/use_authz.ts | 5 +- .../public/hooks/use_intra_app_state.tsx | 1 - ...e_ui_extension.ts => use_ui_extension.tsx} | 10 + x-pack/plugins/fleet/public/index.ts | 19 ++ .../onboarding/components/onboarding.tsx | 5 +- .../integration_card_grid_tabs.tsx | 208 +++++++++++++----- .../integrations/use_integration_card_list.ts | 71 +++++- .../components/onboarding_context.tsx | 59 ++--- .../public/onboarding/index.ts | 5 +- .../public/onboarding/routes.tsx | 3 +- .../scripts/endpoint/common/vm_services.ts | 2 +- 20 files changed, 530 insertions(+), 203 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/tabs_content.tsx rename x-pack/plugins/fleet/public/hooks/{use_ui_extension.ts => use_ui_extension.tsx} (82%) diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 8a4edef8dc87c..d015fe0e15108 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -212,7 +212,7 @@ export const FleetAppContext: React.FC<{ - + diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index 8527dbc4c6c69..c2f310ec267cf 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -38,7 +38,11 @@ import { INTEGRATIONS_ROUTING_PATHS, pagePathGetters } from './constants'; import type { UIExtensionsStorage } from './types'; import { EPMApp } from './sections/epm'; -import { PackageInstallProvider, UIExtensionsContext, FlyoutContextProvider } from './hooks'; +import { + PackageInstallProvider, + UIExtensionsContextProvider, + FlyoutContextProvider, +} from './hooks'; import { IntegrationsHeader } from './components/header'; import { AgentEnrollmentFlyout } from './components'; import { ReadOnlyContextProvider } from './hooks/use_read_only_context'; @@ -103,7 +107,7 @@ export const IntegrationsAppContext: React.FC<{ - + @@ -126,7 +130,7 @@ export const IntegrationsAppContext: React.FC<{ - + diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx index 9e4241a6e7f72..0dd7466e62cda 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx @@ -23,13 +23,20 @@ export const IntegrationsStateContextProvider: FunctionComponent<{ children?: React.ReactNode; }> = ({ children }) => { const maybeState = useIntraAppState(); - const fromIntegrationsRef = useRef(maybeState?.fromIntegrations); - + const stateRef = useRef(maybeState); + console.log('myState---', maybeState); const getFromIntegrations = useCallback(() => { - return fromIntegrationsRef.current; + return stateRef.current?.fromIntegrations; }, []); return ( - + {children} ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx index 579a711a36398..e7226c23d9060 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_package_install.tsx @@ -261,16 +261,18 @@ function usePackageInstall({ startServices }: { startServices: StartServices }) }; } -export const [ - PackageInstallProvider, - useInstallPackage, - useSetPackageInstallStatus, - useGetPackageInstallStatus, - useUninstallPackage, -] = createContainer( +export const packageInstallContainer = createContainer( usePackageInstall, (value) => value.installPackage, (value) => value.setPackageInstallStatus, (value) => value.getPackageInstallStatus, (value) => value.uninstallPackage ); + +export const [ + PackageInstallProvider, + useInstallPackage, + useSetPackageInstallStatus, + useGetPackageInstallStatus, + useUninstallPackage, +] = packageInstallContainer; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts index d57b512726449..86c5f544e7e6a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { FleetAuthz } from '../../../../../../../../common'; import { FLEET_SERVER_PACKAGE, LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, @@ -16,8 +17,10 @@ interface UseIsFirstTimeAgentUserResponse { isLoading?: boolean; } -export const useIsFirstTimeAgentUserQuery = (): UseIsFirstTimeAgentUserResponse => { - const authz = useAuthz(); +export const useIsFirstTimeAgentUserQuery = ( + authzContext?: FleetAuthz +): UseIsFirstTimeAgentUserResponse => { + const authz = useAuthz() ?? authzContext; const { data: packagePolicies, isLoading: areAgentPoliciesLoading, 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 9a707500bb03d..825b870f48aca 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 @@ -91,6 +91,7 @@ import { Configs } from './configs'; import './index.scss'; import type { InstallPkgRouteOptions } from './utils/get_install_route_options'; import { InstallButton } from './settings/install_button'; +import { TabsContent } from './tabs_content'; export type DetailViewPanelName = | 'overview' @@ -129,10 +130,17 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) { return null; } -export function Detail() { +export function Detail({ routesEnabled = true }: { routesEnabled?: boolean }) { const { getId: getAgentPolicyId } = useAgentPolicyContext(); - const { getFromIntegrations } = useIntegrationsStateContext(); - const { pkgkey, panel } = useParams(); + const { + getFromIntegrations, + pkgkey: pkgKeyContext, + panel: panelContext, + } = useIntegrationsStateContext(); + const [selectedPanel, setSelectedPanel] = useState(panelContext); + const { pkgkey: pkgkeyParam, panel: panelParam } = useParams(); + const pkgkey = pkgkeyParam || pkgKeyContext; + const panel = panelParam || selectedPanel; const { getHref, getPath } = useLink(); const history = useHistory(); const { pathname, search, hash } = useLocation(); @@ -144,7 +152,6 @@ export function Detail() { */ const onboardingLink = useMemo(() => queryParams.get('onboardingLink'), [queryParams]); const onboardingAppId = useMemo(() => queryParams.get('onboardingAppId'), [queryParams]); - const authz = useAuthz(); const canAddAgent = authz.fleet.addAgents; const canInstallPackages = authz.integrations.installPackages; @@ -178,7 +185,7 @@ export function Detail() { if (packageInfo === null || !packageInfo.name) { return undefined; } - return getPackageInstallStatus(packageInfo?.name)?.status; + return getPackageInstallStatus?.(packageInfo?.name)?.status; }, [packageInfo, getPackageInstallStatus]); const isInstalled = useMemo( () => @@ -253,7 +260,7 @@ export function Detail() { }, [packageInfoLatestPrereleaseData?.item.version]); const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } = - useIsFirstTimeAgentUserQuery(); + useIsFirstTimeAgentUserQuery(authz); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(pkgName); // Refresh package info when status change @@ -629,10 +636,16 @@ export function Detail() { ), isSelected: panel === 'overview', 'data-test-subj': `tab-overview`, - href: getHref('integration_details_overview', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_overview', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('overview'); + }, }, ]; @@ -647,10 +660,16 @@ export function Detail() { ), isSelected: panel === 'policies', 'data-test-subj': `tab-policies`, - href: getHref('integration_details_policies', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_policies', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('policies'); + }, }); } @@ -671,10 +690,16 @@ export function Detail() { ), isSelected: panel === 'assets', 'data-test-subj': `tab-assets`, - href: getHref('integration_details_assets', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_assets', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('assets'); + }, }); } @@ -689,10 +714,16 @@ export function Detail() { ), isSelected: panel === 'settings', 'data-test-subj': `tab-settings`, - href: getHref('integration_details_settings', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_settings', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('settings'); + }, }); } @@ -707,10 +738,16 @@ export function Detail() { ), isSelected: panel === 'configs', 'data-test-subj': `tab-configs`, - href: getHref('integration_details_configs', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_configs', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('configs'); + }, }); } @@ -725,10 +762,16 @@ export function Detail() { ), isSelected: panel === 'custom', 'data-test-subj': `tab-custom`, - href: getHref('integration_details_custom', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_custom', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('custom'); + }, }); } @@ -743,10 +786,16 @@ export function Detail() { ), isSelected: panel === 'api-reference', 'data-test-subj': `tab-api-reference`, - href: getHref('integration_details_api_reference', { - pkgkey: packageInfoKey, - ...(integration ? { integration } : {}), - }), + href: routesEnabled + ? getHref('integration_details_api_reference', { + pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), + }) + : undefined, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + setSelectedPanel('api-reference'); + }, }); } @@ -754,6 +803,7 @@ export function Detail() { }, [ packageInfo, panel, + routesEnabled, getHref, integration, canReadIntegrationPolicies, @@ -823,45 +873,17 @@ export function Detail() { ) : isLoading || !packageInfo ? ( ) : ( - - - - - - - - - - - - - - - {canReadIntegrationPolicies ? ( - - ) : ( - - )} - - - - - - - - - + )} ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx index aba40aeba2397..102c95e455069 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx @@ -12,7 +12,12 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { InstallStatus } from '../../../../../types'; import type { PackageInfo } from '../../../../../types'; -import { useAuthz, useGetPackageInstallStatus, useUninstallPackage } from '../../../../../hooks'; +import { + useAuthz, + useGetPackageInstallStatus, + useIntegrationsStateContext, + useUninstallPackage, +} from '../../../../../hooks'; import { ConfirmPackageUninstall } from './confirm_package_uninstall'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/tabs_content.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/tabs_content.tsx new file mode 100644 index 0000000000000..9559a4f969b2f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/tabs_content.tsx @@ -0,0 +1,112 @@ +/* + * 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 { Routes, Route } from '@kbn/shared-ux-router'; +import { Redirect } from 'react-router-dom'; + +import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants'; +import { PermissionsError } from '../../../../../fleet/layouts'; + +import type { FleetStart, FleetStartServices } from '../../../../../../plugin'; + +import { AssetsPage } from './assets'; +import { OverviewPage } from './overview'; +import { PackagePoliciesPage } from './policies'; +import { SettingsPage } from './settings'; +import { CustomViewPage } from './custom'; +import { DocumentationPage } from './documentation'; +import { Configs } from './configs'; + +export const TabsContent: React.FC<{ + canReadIntegrationPolicies: boolean; + integrationInfo: any; + latestGAVersion: string | undefined; + packageInfo: any; + packageInfoData: any; + panel: string; + refetchPackageInfo: () => void; + routesEnabled: boolean; + services: FleetStartServices & { + fleet?: FleetStart | undefined; + }; +}> = ({ + canReadIntegrationPolicies, + integrationInfo, + latestGAVersion, + packageInfo, + packageInfoData, + panel, + refetchPackageInfo, + routesEnabled, + services, +}) => { + const routesMap = { + overview: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_overview, + component: ( + + ), + }, + settings: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_settings, + component: ( + + ), + }, + assets: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_assets, + component: , + }, + configs: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_configs, + component: , + }, + policies: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_policies, + component: canReadIntegrationPolicies ? ( + + ) : ( + + ), + }, + custom: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_custom, + component: , + }, + apiReference: { + path: INTEGRATIONS_ROUTING_PATHS.integration_details_api_reference, + component: ( + + ), + }, + }; + return routesEnabled ? ( + + {Object.values(routesMap).map(({ path, component }) => ( + + {component} + + ))} + + + ) : ( + routesMap[panel].component + ); +}; + +TabsContent.displayName = 'TabsRoute'; 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 19f4d8740b75d..260c231995ed7 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 @@ -66,6 +66,7 @@ export interface IntegrationCardItem { url: string; version: string; type?: string; + pkgkey?: string; } export const mapToCard = ({ @@ -91,6 +92,9 @@ export const mapToCard = ({ let isUpdateAvailable = false; let isReauthorizationRequired = false; + + const pkgkey = item.name ? `${item.name}-${version}` : undefined; + if (item.type === 'ui_link') { uiInternalPathUrl = item.id.includes('language_client.') ? addBasePath(item.uiInternalPath) @@ -103,9 +107,8 @@ export const mapToCard = ({ isReauthorizationRequired = hasDeferredInstallations(item); } - const url = getHref('integration_details_overview', { - pkgkey: `${item.name}-${version}`, + pkgkey, ...(item.integration ? { integration: item.integration } : {}), }); @@ -136,6 +139,7 @@ export const mapToCard = ({ isUnverified, isUpdateAvailable, extraLabelsBadges, + pkgkey, }; if (item.type === 'integration') { diff --git a/x-pack/plugins/fleet/public/hooks/use_authz.ts b/x-pack/plugins/fleet/public/hooks/use_authz.ts index c7a177c34dce1..40088d086a06e 100644 --- a/x-pack/plugins/fleet/public/hooks/use_authz.ts +++ b/x-pack/plugins/fleet/public/hooks/use_authz.ts @@ -5,10 +5,13 @@ * 2.0. */ +import { useIntegrationsStateContext } from '../applications/integrations/hooks'; + import { useStartServices } from './use_core'; // Expose authz object, containing the privileges for Fleet and Integrations export function useAuthz() { const core = useStartServices(); - return core.authz; + const { fleet } = useIntegrationsStateContext(); + return core.authz ?? fleet.auth; } diff --git a/x-pack/plugins/fleet/public/hooks/use_intra_app_state.tsx b/x-pack/plugins/fleet/public/hooks/use_intra_app_state.tsx index 265d12221018a..501777bac4d11 100644 --- a/x-pack/plugins/fleet/public/hooks/use_intra_app_state.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_intra_app_state.tsx @@ -16,6 +16,5 @@ import type { AnyIntraAppRouteState } from '../types'; */ export function useIntraAppState(): S | undefined { const location = useLocation(); - return location.state as S; } diff --git a/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts b/x-pack/plugins/fleet/public/hooks/use_ui_extension.tsx similarity index 82% rename from x-pack/plugins/fleet/public/hooks/use_ui_extension.ts rename to x-pack/plugins/fleet/public/hooks/use_ui_extension.tsx index 93e316be241cb..117cb778c21b1 100644 --- a/x-pack/plugins/fleet/public/hooks/use_ui_extension.ts +++ b/x-pack/plugins/fleet/public/hooks/use_ui_extension.tsx @@ -11,6 +11,16 @@ import type { UIExtensionPoint, UIExtensionsStorage } from '../types'; export const UIExtensionsContext = React.createContext({}); +export const UIExtensionsContextProvider = ({ + values, + children, +}: { + values: UIExtensionsStorage; + children: React.ReactNode; +}) => { + return {children}; +}; + type NarrowExtensionPoint = A extends { view: V; } diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index d82e9c88b7db8..9c58978976e3d 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -10,6 +10,7 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import { lazy } from 'react'; import { FleetPlugin } from './plugin'; +import type { UIExtensionsStorage } from './types'; export type { GetPackagesResponse } from './types'; export { installationStatuses } from '../common/constants'; @@ -89,3 +90,21 @@ export const AvailablePackagesHook = () => { './applications/integrations/sections/epm/screens/home/hooks/use_available_packages' ); }; +export const Detail = () => { + return import('./applications/integrations/sections/epm/screens/detail'); +}; +export const UseIntegrationsState = () => { + return import('./applications/integrations/hooks/use_integrations_state'); +}; + +export const UsePackageInstall = () => { + return import('./applications/integrations/hooks/use_package_install'); +}; + +export const FleetStatusProvider = () => { + return import('./hooks/use_fleet_status'); +}; + +export const UIExtensionsContextProvider = () => { + return import('./hooks/use_ui_extension'); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx index 17f4840e68dc4..0b7a08ff94aa8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -18,8 +18,9 @@ import { OnboardingHeader } from './onboarding_header'; import { OnboardingBody } from './onboarding_body'; import { OnboardingFooter } from './onboarding_footer'; import { PAGE_CONTENT_WIDTH } from '../constants'; +import type { StartPlugins } from '../../types'; -export const OnboardingPage = React.memo(() => { +export const OnboardingPage = React.memo((plugins: StartPlugins) => { const spaceId = useSpaceId(); const { euiTheme } = useEuiTheme(); @@ -32,7 +33,7 @@ export const OnboardingPage = React.memo(() => { } return ( - + ({ .then((pkg) => pkg.PackageListGrid), })); +const Detail = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.Detail()) + .then((pkg) => pkg.Detail), +})); + +const IntegrationsStateContextProvider = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.UseIntegrationsState()) + .then((pkg) => pkg.IntegrationsStateContextProvider), +})); + +const PackageInstallProvider = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.UsePackageInstall()) + .then((pkg) => pkg.packageInstallContainer[0]), +})); + +const FleetStatusProvider = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.FleetStatusProvider()) + .then((pkg) => pkg.FleetStatusProvider), +})); + +const UIExtensionsContextProvider = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.UIExtensionsContextProvider()) + .then((pkg) => pkg.UIExtensionsContextProvider), +})); + export const IntegrationsCardGridTabsComponent = React.memo( ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => { const { spaceId } = useOnboardingContext(); + const startServices = useKibana().services; + const { state: routerState } = useLocation(); const scrollElement = useRef(null); const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId( spaceId, @@ -65,6 +116,11 @@ export const IntegrationsCardGridTabsComponent = React.memo setIsModalVisible(false); + const showModal = () => setIsModalVisible(true); + const modalTitleId = useGeneratedHtmlId(); const { filteredCards, isLoading, @@ -116,10 +172,11 @@ export const IntegrationsCardGridTabsComponent = React.memo - - - - + - } - > - - } - calloutTopSpacerSize="m" - categories={SEARCH_FILTER_CATEGORIES} // We do not want to show categories and subcategories as the search bar filter - emptyStateStyles={emptyStateStyles} - list={list} - scrollElementId={SCROLL_ELEMENT_ID} - searchTerm={searchTerm} - selectedCategory={selectedTab.category ?? ''} - selectedSubCategory={selectedTab.subCategory} - setCategory={setCategory} - setSearchTerm={onSearchTermChanged} - setUrlandPushHistory={noop} - setUrlandReplaceHistory={noop} - showCardLabels={false} - showControls={false} - showSearchTools={selectedTab.showSearchTools} - sortByFeaturedIntegrations={selectedTab.sortByFeaturedIntegrations} - spacer={false} + + - - - + + + } + > + + } + calloutTopSpacerSize="m" + categories={SEARCH_FILTER_CATEGORIES} // We do not want to show categories and subcategories as the search bar filter + emptyStateStyles={emptyStateStyles} + list={list} + scrollElementId={SCROLL_ELEMENT_ID} + searchTerm={searchTerm} + selectedCategory={selectedTab.category ?? ''} + selectedSubCategory={selectedTab.subCategory} + setCategory={setCategory} + setSearchTerm={onSearchTermChanged} + setUrlandPushHistory={noop} + setUrlandReplaceHistory={noop} + showCardLabels={false} + showControls={false} + showSearchTools={selectedTab.showSearchTools} + sortByFeaturedIntegrations={selectedTab.sortByFeaturedIntegrations} + spacer={false} + /> + + + + {isModalVisible && startServices && ( + + + + Modal title + + + + + + + + + + + + + + + )} + ); } ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts index ccea5299551c1..61f588cbd00ba 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts @@ -5,9 +5,9 @@ * 2.0. */ import { useMemo } from 'react'; -import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import type { FleetSetup, FleetStart, IntegrationCardItem } from '@kbn/fleet-plugin/public'; import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation'; -import { useNavigation } from '../../../../../common/lib/kibana'; +import { useKibana, useNavigation } from '../../../../../common/lib/kibana'; import { APP_INTEGRATIONS_PATH, APP_UI_ID, @@ -24,6 +24,7 @@ import { } from './constants'; import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana'; import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; +import { useOnboardingContext } from '../../../onboarding_context'; const addPathParamToUrl = (url: string, onboardingLink: string) => { const encoded = encodeURIComponent(onboardingLink); @@ -50,15 +51,28 @@ const getFilteredCards = ({ installedIntegrationList, integrationsList, navigateTo, + onCardClicked, + fleet, + application, }: { featuredCardIds?: string[]; getAppUrl: GetAppUrl; installedIntegrationList?: IntegrationCardItem[]; integrationsList: IntegrationCardItem[]; navigateTo: NavigateTo; + onCardClicked?: () => void; + fleet: FleetSetup | undefined; }) => { const securityIntegrationsList = integrationsList.map((card) => - addSecuritySpecificProps({ navigateTo, getAppUrl, card, installedIntegrationList }) + addSecuritySpecificProps({ + navigateTo, + getAppUrl, + card, + installedIntegrationList, + onCardClicked, + fleet, + application, + }) ); if (!featuredCardIds) { return { featuredCards: [], integrationCards: securityIntegrationsList }; @@ -74,23 +88,34 @@ const addSecuritySpecificProps = ({ navigateTo, getAppUrl, card, + onCardClicked, + fleet, + application, }: { navigateTo: NavigateTo; getAppUrl: GetAppUrl; card: IntegrationCardItem; installedIntegrationList?: IntegrationCardItem[]; + onCardClicked?: () => void; + fleet: FleetStart | undefined; }): IntegrationCardItem => { const onboardingLink = getAppUrl({ appId: SECURITY_UI_APP_ID, path: ONBOARDING_PATH }); const integrationRootUrl = getAppUrl({ appId: INTEGRATION_APP_ID }); + + const url = + card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink + ? addPathParamToUrl(card.url, onboardingLink) + : card.url; + const state = { onCancelNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }], onCancelUrl: onboardingLink, onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }], + pkgkey: card.pkgkey, + panel: 'overview', + fleet: { auth: fleet?.authz, config: fleet?.config }, }; - const url = - card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink - ? addPathParamToUrl(card.url, onboardingLink) - : card.url; + return { ...card, titleLineClamp: CARD_TITLE_LINE_CLAMP, @@ -101,12 +126,20 @@ const addSecuritySpecificProps = ({ onCardClick: () => { const trackId = `${TELEMETRY_INTEGRATION_CARD}_${card.id}`; trackOnboardingLinkClick(trackId); + if (url.startsWith(APP_INTEGRATIONS_PATH)) { + onCardClicked?.(); + navigateTo({ - appId: INTEGRATION_APP_ID, - path: url.slice(integrationRootUrl.length), + path: `${addPathParamToUrl(ONBOARDING_PATH, onboardingLink)}#integrations`, state, }); + + // navigateTo({ + // appId: INTEGRATION_APP_ID, + // path: url.slice(integrationRootUrl.length), + // state, + // }); } else if (url.startsWith('http') || url.startsWith('https')) { window.open(url, '_blank'); } else { @@ -119,15 +152,29 @@ const addSecuritySpecificProps = ({ export const useIntegrationCardList = ({ integrationsList, featuredCardIds, + onCardClicked, }: { integrationsList: IntegrationCardItem[]; featuredCardIds?: string[] | undefined; + onCardClicked?: () => void; }): IntegrationCardItem[] => { const { navigateTo, getAppUrl } = useNavigation(); - + // const { fleet } = useOnboardingContext(); + const { + services: { application, fleet }, + } = useKibana(); const { featuredCards, integrationCards } = useMemo( - () => getFilteredCards({ navigateTo, getAppUrl, integrationsList, featuredCardIds }), - [navigateTo, getAppUrl, integrationsList, featuredCardIds] + () => + getFilteredCards({ + navigateTo, + getAppUrl, + integrationsList, + featuredCardIds, + onCardClicked, + fleet, + application, + }), + [navigateTo, getAppUrl, integrationsList, featuredCardIds, onCardClicked, fleet, application] ); if (featuredCardIds && featuredCardIds.length > 0) { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx index 2a6597628a26d..cb7872c9fc7c3 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -19,37 +19,38 @@ export interface OnboardingContextValue { } const OnboardingContext = createContext(null); -export const OnboardingContextProvider: React.FC> = - React.memo(({ children, spaceId }) => { - const { telemetry } = useKibana().services; +export const OnboardingContextProvider: React.FC< + PropsWithChildren<{ spaceId: string; fleet: FleetStart }> +> = React.memo(({ children, spaceId, fleet }) => { + const { telemetry } = useKibana().services; - const value = useMemo( - () => ({ - spaceId, - reportCardOpen: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { - stepId: cardId, - trigger: auto ? 'navigation' : 'click', - }); - }, - reportCardComplete: (cardId, { auto = false } = {}) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { - stepId: cardId, - trigger: auto ? 'auto_check' : 'click', - }); - }, - reportCardLinkClicked: (cardId, linkId: string) => { - telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { - originStepId: cardId, - stepLinkId: linkId, - }); - }, - }), - [spaceId, telemetry] - ); + const value = useMemo( + () => ({ + spaceId, + reportCardOpen: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepOpen, { + stepId: cardId, + trigger: auto ? 'navigation' : 'click', + }); + }, + reportCardComplete: (cardId, { auto = false } = {}) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepFinished, { + stepId: cardId, + trigger: auto ? 'auto_check' : 'click', + }); + }, + reportCardLinkClicked: (cardId, linkId: string) => { + telemetry.reportEvent(OnboardingHubEventTypes.OnboardingHubStepLinkClicked, { + originStepId: cardId, + stepLinkId: linkId, + }); + }, + }), + [spaceId, telemetry] + ); - return {children}; - }); + return {children}; +}); OnboardingContextProvider.displayName = 'OnboardingContextProvider'; export const useOnboardingContext = () => { diff --git a/x-pack/plugins/security_solution/public/onboarding/index.ts b/x-pack/plugins/security_solution/public/onboarding/index.ts index b20a2777f6ee7..029406bb97a41 100644 --- a/x-pack/plugins/security_solution/public/onboarding/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/index.ts @@ -6,14 +6,15 @@ */ import type { SecuritySubPlugin } from '../app/types'; +import type { StartPlugins } from '../types'; import { routes } from './routes'; export class Onboarding { public setup() {} - public start(): SecuritySubPlugin { + public start(plugins: StartPlugins): SecuritySubPlugin { return { - routes, + routes: routes(plugins), }; } } diff --git a/x-pack/plugins/security_solution/public/onboarding/routes.tsx b/x-pack/plugins/security_solution/public/onboarding/routes.tsx index b8ac8aba3e90e..e73676db05cec 100644 --- a/x-pack/plugins/security_solution/public/onboarding/routes.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/routes.tsx @@ -8,9 +8,10 @@ import { ONBOARDING_PATH, SecurityPageName } from '../../common/constants'; import type { SecuritySubPluginRoutes } from '../app/types'; import { withSecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; +import type { StartPlugins } from '../types'; import { OnboardingPage } from './components/onboarding'; -export const routes: SecuritySubPluginRoutes = [ +export const routes: (plugins: StartPlugins) => SecuritySubPluginRoutes = (plugins) => [ { path: ONBOARDING_PATH, component: withSecurityRoutePageWrapper(OnboardingPage, SecurityPageName.landing), diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts index 084e068768e8f..7b9a6b49c2748 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/vm_services.ts @@ -57,7 +57,7 @@ const createMultipassVm = async ({ log.info(`Creating VM [${name}] using multipass`); const createResponse = await execa.command( - `multipass launch --name ${name} --disk ${disk} --cpus ${cpus} --memory ${memory}` + `multipass launch --name ${name} --disk ${disk} --cpus ${cpus} --memory ${memory} --network en0` ); log.verbose(`VM [${name}] created successfully using multipass.`, createResponse); From 1eca6455a551543d05a6ae937de2557aab66e395 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 14 Nov 2024 09:18:23 +0000 Subject: [PATCH 2/4] add integration view --- .../single_page_layout/index.tsx | 19 ++- .../components/add_integration_button.tsx | 2 +- .../hooks/use_is_first_time_agent_user.ts | 7 +- .../sections/epm/screens/detail/index.tsx | 149 ++++++++++-------- x-pack/plugins/fleet/public/index.ts | 4 + .../integration_card_grid_tabs.tsx | 40 +++-- 6 files changed, 132 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 93631f63d0e04..9af03e8916849 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState, Suspense } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, Suspense, forwardRef } from 'react'; import { useRouteMatch } from 'react-router-dom'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -20,6 +20,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, + EuiPortal, EuiSpacer, EuiSteps, } from '@elastic/eui'; @@ -76,6 +77,8 @@ import { generateNewAgentPolicyWithDefaults } from '../../../../../../../common/ import { packageHasAtLeastOneSecret } from '../utils'; +import { useIntegrationsStateContext } from '../../../../../integrations/hooks'; + import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components'; import { useDevToolsRequest, useOnSubmit, useSetupTechnology } from './hooks'; import { PostInstallCloudFormationModal } from './components/cloud_security_posture/post_install_cloud_formation_modal'; @@ -111,6 +114,9 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ } = useConfig(); const hasFleetAddAgentsPrivileges = useAuthz().fleet.addAgents; const { params } = useRouteMatch(); + const { pkgkey: pkgKeyContext } = useIntegrationsStateContext(); + const pkgkey = params.pkgkey || pkgKeyContext; + const fleetStatus = useFleetStatus(); const { docLinks } = useStartServices(); const spaceSettings = useSpaceSettingsContext(); @@ -130,7 +136,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ queryParamsPolicyId ? SelectedPolicyTab.EXISTING : SelectedPolicyTab.NEW ); - const { pkgName, pkgVersion } = splitPkgKey(params.pkgkey); + const { pkgName, pkgVersion } = splitPkgKey(pkgkey); // Fetch package info const { data: packageInfoData, @@ -230,7 +236,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({ from, - pkgkey: params.pkgkey, + pkgkey, agentPolicyId: agentPolicyIds[0], }); useEffect(() => { @@ -602,7 +608,12 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ - + {/* Only show render button bar in portal when enableRouts is false*/} + {packageInfo && (formState === 'INVALID' || hasAgentPolicyError) ? ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx index 8ff7f3e7d5ec3..7d60443e50af1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx @@ -14,7 +14,7 @@ interface AddIntegrationButtonProps { userCanInstallPackages?: boolean; missingSecurityConfiguration: boolean; packageName: string; - href: string; + href: string | undefined; onClick: Function; } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts index 86c5f544e7e6a..d57b512726449 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/hooks/use_is_first_time_agent_user.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { FleetAuthz } from '../../../../../../../../common'; import { FLEET_SERVER_PACKAGE, LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE, @@ -17,10 +16,8 @@ interface UseIsFirstTimeAgentUserResponse { isLoading?: boolean; } -export const useIsFirstTimeAgentUserQuery = ( - authzContext?: FleetAuthz -): UseIsFirstTimeAgentUserResponse => { - const authz = useAuthz() ?? authzContext; +export const useIsFirstTimeAgentUserQuery = (): UseIsFirstTimeAgentUserResponse => { + const authz = useAuthz(); const { data: packagePolicies, isLoading: areAgentPoliciesLoading, 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 825b870f48aca..effa47b277f74 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 @@ -130,7 +130,13 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) { return null; } -export function Detail({ routesEnabled = true }: { routesEnabled?: boolean }) { +export function Detail({ + routesEnabled = true, + onAddElasticDefendClicked, +}: { + routesEnabled?: boolean; + onAddElasticDefendClicked?: () => void; +}) { const { getId: getAgentPolicyId } = useAgentPolicyContext(); const { getFromIntegrations, @@ -260,7 +266,7 @@ export function Detail({ routesEnabled = true }: { routesEnabled?: boolean }) { }, [packageInfoLatestPrereleaseData?.item.version]); const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } = - useIsFirstTimeAgentUserQuery(authz); + useIsFirstTimeAgentUserQuery(); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(pkgName); // Refresh package info when status change @@ -403,66 +409,70 @@ export function Detail({ routesEnabled = true }: { routesEnabled?: boolean }) { [integrationInfo, isLoading, packageInfo, href, queryParams] ); - const handleAddIntegrationPolicyClick = useCallback( - (ev) => { - ev.preventDefault(); - // The object below, given to `createHref` is explicitly accessing keys of `location` in order - // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) - const currentPath = history.createHref({ - pathname, - search, - hash, - }); - - const defaultNavigateOptions: InstallPkgRouteOptions = getInstallPkgRouteOptions({ - agentPolicyId: agentPolicyIdFromContext, - currentPath, - integration, - isCloud, - isExperimentalAddIntegrationPageEnabled, - isFirstTimeAgentUser, - isGuidedOnboardingActive, - 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); - }, - [ - agentPolicyIdFromContext, - hash, - history, - integration, - isCloud, - isExperimentalAddIntegrationPageEnabled, - isFirstTimeAgentUser, - isGuidedOnboardingActive, - onboardingAppId, - onboardingLink, - pathname, - pkgkey, - search, - services.application, - ] - ); + // const handleAddIntegrationPolicyClick = useCallback( + // (ev) => { + // ev.preventDefault(); + // // The object below, given to `createHref` is explicitly accessing keys of `location` in order + // // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) + // const currentPath = history.createHref({ + // pathname, + // search, + // hash, + // }); + + // const defaultNavigateOptions: InstallPkgRouteOptions = getInstallPkgRouteOptions({ + // agentPolicyId: agentPolicyIdFromContext, + // currentPath, + // integration, + // isCloud, + // isExperimentalAddIntegrationPageEnabled, + // isFirstTimeAgentUser, + // isGuidedOnboardingActive, + // 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); + // }, + // [ + // agentPolicyIdFromContext, + // hash, + // history, + // integration, + // isCloud, + // isExperimentalAddIntegrationPageEnabled, + // isFirstTimeAgentUser, + // isGuidedOnboardingActive, + // onboardingAppId, + // onboardingLink, + // pathname, + // pkgkey, + // search, + // services.application, + // ] + // ); + + const handleAddIntegrationPolicyClick = useCallback(() => { + !routesEnabled && onAddElasticDefendClicked?.(); + }, [routesEnabled, onAddElasticDefendClicked]); const showVersionSelect = useMemo( () => @@ -566,13 +576,14 @@ export function Detail({ routesEnabled = true }: { routesEnabled?: boolean }) { > { export const UIExtensionsContextProvider = () => { return import('./hooks/use_ui_extension'); }; + +export const CreatePackagePolicyPage = () => { + return import('./applications/fleet/sections/agent_policy/create_package_policy_page'); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx index 2b79e691c74ca..dc2f204c87d8f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx @@ -94,11 +94,16 @@ const UIExtensionsContextProvider = lazy(async () => ({ .then((pkg) => pkg.UIExtensionsContextProvider), })); +const CreatePackagePolicyPage = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.CreatePackagePolicyPage()) + .then((pkg) => pkg.CreatePackagePolicyPage), +})); + export const IntegrationsCardGridTabsComponent = React.memo( ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => { const { spaceId } = useOnboardingContext(); const startServices = useKibana().services; - const { state: routerState } = useLocation(); const scrollElement = useRef(null); const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId( spaceId, @@ -117,9 +122,19 @@ export const IntegrationsCardGridTabsComponent = React.memo setIsModalVisible(false); - const showModal = () => setIsModalVisible(true); + const [modalView, setModalView] = useState<'overview' | 'configure-integration' | 'add-agent'>( + 'overview' + ); + const onAddElasticDefendClicked = useCallback(() => { + setModalView('configure-integration'); + }, []); + const closeModal = useCallback(() => { + setIsModalVisible(false); + setModalView('overview'); + }, []); + const showModal = useCallback(() => setIsModalVisible(true), []); + const ref = useRef(null); + let bottomBar; const modalTitleId = useGeneratedHtmlId(); const { filteredCards, @@ -133,7 +148,7 @@ export const IntegrationsCardGridTabsComponent = React.memo INTEGRATION_TABS_BY_ID[toggleIdSelected], [toggleIdSelected]); - + console.log('=ref===', ref); const onSearchTermChanged = useCallback( (searchQuery: string) => { setSearchTerm(searchQuery); @@ -172,7 +187,6 @@ export const IntegrationsCardGridTabsComponent = React.memo - - Modal title - + {modalView !== 'overview' && ( + "Step indicator placeholder" + )} - + {modalView === 'overview' && ( + + )} + {modalView === 'configure-integration' && } From 3f84dbc9d8cc2e8b6f97e6647042ba4a64b09540 Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Thu, 21 Nov 2024 14:28:07 +0000 Subject: [PATCH 3/4] integration flow complete --- .../fleet/public/applications/fleet/app.tsx | 2 +- .../hooks/navigation.tsx | 6 +- .../create_package_policy_page/index.tsx | 16 +- .../components/multi_page_steps_layout.tsx | 1 - .../install_agent/install_agent_managed.tsx | 3 + .../hooks/use_get_agent_policy_or_default.tsx | 3 +- .../multi_page_layout/index.tsx | 42 ++++- .../single_page_layout/index.tsx | 19 +- .../create_package_policy_page/types.ts | 6 +- .../hooks/use_fleet_integration_context.tsx | 58 ++++++ .../hooks/use_integrations_state.tsx | 1 - .../components/add_integration_button.tsx | 15 +- .../sections/epm/screens/detail/index.tsx | 178 +++++++++--------- .../confirm_agent_enrollment.tsx | 9 +- .../plugins/fleet/public/hooks/use_authz.ts | 6 +- x-pack/plugins/fleet/public/index.ts | 16 +- .../integration_card_grid_tabs.tsx | 91 ++++----- .../integrations/use_integration_card_list.ts | 48 ++--- 18 files changed, 307 insertions(+), 213 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/hooks/use_fleet_integration_context.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index d015fe0e15108..8a4edef8dc87c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -212,7 +212,7 @@ export const FleetAppContext: React.FC<{ - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx index 8b9ca7886d236..44302c8adcc2c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx @@ -28,11 +28,15 @@ interface UseCancelParams { } export const useCancelAddPackagePolicy = (params: UseCancelParams) => { - const { from, pkgkey, agentPolicyId } = params; + const { from, pkgkey: pkgkeyParam, agentPolicyId } = params; const { application: { navigateToApp }, } = useStartServices(); const routeState = useIntraAppState(); + const pkgkey = useMemo( + () => pkgkeyParam || routeState?.pkgkey, + [pkgkeyParam, routeState?.pkgkey] + ); const { getHref } = useLink(); const cancelClickHandler = useCallback( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index e6e904d0ebba8..13df7b84338fb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -15,11 +15,19 @@ import type { AddToPolicyParams, EditPackagePolicyFrom } from './types'; import { CreatePackagePolicySinglePage } from './single_page_layout'; import { CreatePackagePolicyMultiPage } from './multi_page_layout'; -export const CreatePackagePolicyPage: React.FC<{}> = () => { +export const CreatePackagePolicyPage: React.FC<{ + useMultiPageLayoutProp?: boolean; + originFrom?: EditPackagePolicyFrom; + propPolicyId?: string; + integrationName?: string; +}> = ({ useMultiPageLayoutProp, originFrom, propPolicyId, integrationName }) => { const { search } = useLocation(); const { params } = useRouteMatch(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const useMultiPageLayout = useMemo(() => queryParams.has('useMultiPageLayout'), [queryParams]); + const useMultiPageLayout = useMemo( + () => useMultiPageLayoutProp ?? queryParams.has('useMultiPageLayout'), + [queryParams, useMultiPageLayoutProp] + ); const queryParamsPolicyId = useMemo( () => queryParams.get('policyId') ?? undefined, [queryParams] @@ -47,11 +55,13 @@ export const CreatePackagePolicyPage: React.FC<{}> = () => { * creation possible if a user has not chosen one from the packages UI. */ const from: EditPackagePolicyFrom = - 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package'; + originFrom ?? ('policyId' in params || queryParamsPolicyId ? 'policy' : 'package'); const pageParams = { from, queryParamsPolicyId, + propPolicyId, + integrationName, prerelease, }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/multi_page_steps_layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/multi_page_steps_layout.tsx index 2ed48b4d3bb59..d9539e318c142 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/multi_page_steps_layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/multi_page_steps_layout.tsx @@ -64,7 +64,6 @@ export const MultiPageStepsLayout: React.FunctionComponent - {packageInfo && ( agentCount: enrolledAgentIds.length, showLoading: true, poll: commandCopied, + onClickViewAgents: () => { + onNext(); // Fixme: Wording does not match what it does. + }, }) ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/hooks/use_get_agent_policy_or_default.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/hooks/use_get_agent_policy_or_default.tsx index d46594ef6bb3b..ead4496b0ba7a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/hooks/use_get_agent_policy_or_default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/hooks/use_get_agent_policy_or_default.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { useEffect, useState, useMemo } from 'react'; +import { useEffect, useState, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; +import { v4 as uuidv4 } from 'uuid'; import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx index bc0cfc7adccf6..95832e80c9de2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx @@ -18,6 +18,12 @@ import { import type { AddToPolicyParams, CreatePackagePolicyParams } from '../types'; +import { useIntegrationsStateContext } from '../../../../../integrations/hooks'; + +import { CreatePackagePolicySinglePage } from '../single_page_layout'; + +import type { AgentPolicy } from '../../../../types'; + import { useGetAgentPolicyOrDefault } from './hooks'; import { @@ -42,6 +48,13 @@ const addIntegrationStep = { component: AddIntegrationPageStep, }; +const addIntegrationSingleLayoutStep = { + title: i18n.translate('xpack.fleet.createFirstPackagePolicy.addIntegrationStepTitle', { + defaultMessage: 'Add the integration', + }), + component: CreatePackagePolicySinglePage, +}; + const confirmDataStep = { title: i18n.translate('xpack.fleet.createFirstPackagePolicy.confirmDataStepTitle', { defaultMessage: 'Confirm incoming data', @@ -53,23 +66,33 @@ const fleetManagedSteps = [installAgentStep, addIntegrationStep, confirmDataStep const standaloneSteps = [addIntegrationStep, installAgentStep, confirmDataStep]; +const onboardingSteps = [addIntegrationSingleLayoutStep, installAgentStep, confirmDataStep]; + export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({ queryParamsPolicyId, prerelease, + from, + integrationName, }) => { const { params } = useRouteMatch(); - const { pkgkey, policyId, integration } = params; + // fixme + const { pkgkey: pkgkeyParam, policyId, integration: integrationParam } = params; + const { pkgkey: pkgKeyContext } = useIntegrationsStateContext(); + const pkgkey = pkgkeyParam || pkgKeyContext; const { pkgName, pkgVersion } = splitPkgKey(pkgkey); - const [onSplash, setOnSplash] = useState(true); + const [onSplash, setOnSplash] = useState(from !== 'onboarding-integration'); const [currentStep, setCurrentStep] = useState(0); const [isManaged, setIsManaged] = useState(true); const { getHref } = useLink(); const [enrolledAgentIds, setEnrolledAgentIds] = useState([]); + const [selectedAgentPolicies, setSelectedAgentPolicies] = useState(); const toggleIsManaged = (newIsManaged: boolean) => { setIsManaged(newIsManaged); setCurrentStep(0); }; - const agentPolicyId = policyId || queryParamsPolicyId; + + const integration = integrationName || integrationParam; + const agentPolicyId = selectedAgentPolicies?.[0]?.id || policyId || queryParamsPolicyId; const { data: packageInfoData, error: packageInfoError, @@ -119,13 +142,22 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({ ); } - const steps = isManaged ? fleetManagedSteps : standaloneSteps; - const stepsNext = () => { + const steps = + from === 'onboarding-integration' + ? onboardingSteps + : isManaged + ? fleetManagedSteps + : standaloneSteps; + + const stepsNext = (props?: { selectedAgentPolicies: AgentPolicy[] }) => { if (currentStep === steps.length - 1) { return; } setCurrentStep(currentStep + 1); + if (props?.selectedAgentPolicies) { + setSelectedAgentPolicies(props?.selectedAgentPolicies); + } }; const stepsBack = () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 9af03e8916849..a04a23dfa6228 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -108,6 +108,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ from, queryParamsPolicyId, prerelease, + onNext, }) => { const { agents: { enabled: isFleetEnabled }, @@ -193,6 +194,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ hasFleetAddAgentsPrivileges, }); + const handleNavigateAddAgent = useCallback(() => { + if (onNext) { + onNext({ selectedAgentPolicies: agentPolicies }); + } else { + if (savedPackagePolicy) { + navigateAddAgent(savedPackagePolicy); + } + } + }, [onNext, agentPolicies, savedPackagePolicy, navigateAddAgent]); + const setPolicyValidation = useCallback( (selectedTab: SelectedPolicyTab, updatedAgentPolicy: NewAgentPolicy) => { if (selectedTab === SelectedPolicyTab.NEW) { @@ -518,7 +529,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ savedPackagePolicy && ( navigateAddAgent(savedPackagePolicy)} + onConfirm={handleNavigateAddAgent} onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} @@ -528,7 +539,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ navigateAddAgent(savedPackagePolicy)} + onConfirm={handleNavigateAddAgent} onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} @@ -538,7 +549,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ navigateAddAgent(savedPackagePolicy)} + onConfirm={handleNavigateAddAgent} onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} @@ -548,7 +559,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ navigateAddAgent(savedPackagePolicy)} + onConfirm={handleNavigateAddAgent} onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} /> )} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index 1a203c8bf2e60..ddb0229ffc56b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -13,7 +13,8 @@ export type EditPackagePolicyFrom = | 'edit' | 'upgrade-from-fleet-policy-list' | 'upgrade-from-integrations-policy-list' - | 'upgrade-from-extension'; + | 'upgrade-from-extension' + | 'onboarding-integration'; export type PackagePolicyFormState = | 'VALID' @@ -35,5 +36,8 @@ export interface AddToPolicyParams { export type CreatePackagePolicyParams = React.FunctionComponent<{ from: EditPackagePolicyFrom; queryParamsPolicyId?: string; + propPolicyId?: string; + integrationName?: string; prerelease: boolean; + onNext?: () => void; }>; diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_fleet_integration_context.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_fleet_integration_context.tsx new file mode 100644 index 0000000000000..d4005439a39e8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_fleet_integration_context.tsx @@ -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 React, { createContext } from 'react'; + +import { + FleetStatusProvider, + KibanaVersionContext, + UIExtensionsContextProvider, +} from '../../../hooks'; + +import type { FleetStartServices } from '../../../plugin'; + +import { PackageInstallProvider } from './use_package_install'; +import { IntegrationsStateContextProvider } from './use_integrations_state'; + +interface FleetIntegrationsStateContextValue { + pkgkey: string | undefined; + startServices: FleetStartServices | undefined; +} + +const FleetIntegrationsStateContext = createContext({ + pkgkey: undefined, + startServices: undefined, +}); + +export const FleetIntegrationsStateContextProvider: React.FC<{ + children?: React.ReactNode; + values: FleetIntegrationsStateContextValue; + /* fix hard coded KibanaVersion */ +}> = ({ children, values: { startServices, kibanaVersion = '8.16.0' } }) => { + return ( + + + + + + {children} + + + + + + ); +}; + +export const useFleetIntegrationsStateContext = () => { + const ctx = React.useContext(FleetIntegrationsStateContext); + if (!ctx) { + throw new Error( + 'useFleetIntegrationsStateContext can only be used inside of FleetIntegrationsStateContextProvider' + ); + } + return ctx; +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx index 0dd7466e62cda..47243f2b30e30 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx @@ -34,7 +34,6 @@ export const IntegrationsStateContextProvider: FunctionComponent<{ getFromIntegrations, pkgkey: maybeState?.pkgkey, panel: maybeState?.panel, - fleet: maybeState?.fleet, }} > {children} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx index 7d60443e50af1..12aba97247775 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/add_integration_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButtonWithTooltip } from '../../../../../components'; @@ -15,7 +15,7 @@ interface AddIntegrationButtonProps { missingSecurityConfiguration: boolean; packageName: string; href: string | undefined; - onClick: Function; + onClick: Function | undefined; } export function AddIntegrationButton(props: AddIntegrationButtonProps) { @@ -38,15 +38,22 @@ export function AddIntegrationButton(props: AddIntegrationButtonProps) { } : undefined; + const optionalProps = useMemo( + () => ({ + ...(href ? { href } : {}), + ...(onClick ? { onClick: (e: React.MouseEvent) => onClick(e) } : {}), + }), + [href, onClick] + ); + return ( onClick(e)} data-test-subj="addIntegrationPolicyButton" tooltip={tooltip} + {...optionalProps} > void; + onAddIntegrationPolicyClick?: () => void; }) { const { getId: getAgentPolicyId } = useAgentPolicyContext(); const { @@ -409,70 +411,72 @@ export function Detail({ [integrationInfo, isLoading, packageInfo, href, queryParams] ); - // const handleAddIntegrationPolicyClick = useCallback( - // (ev) => { - // ev.preventDefault(); - // // The object below, given to `createHref` is explicitly accessing keys of `location` in order - // // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) - // const currentPath = history.createHref({ - // pathname, - // search, - // hash, - // }); - - // const defaultNavigateOptions: InstallPkgRouteOptions = getInstallPkgRouteOptions({ - // agentPolicyId: agentPolicyIdFromContext, - // currentPath, - // integration, - // isCloud, - // isExperimentalAddIntegrationPageEnabled, - // isFirstTimeAgentUser, - // isGuidedOnboardingActive, - // 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); - // }, - // [ - // agentPolicyIdFromContext, - // hash, - // history, - // integration, - // isCloud, - // isExperimentalAddIntegrationPageEnabled, - // isFirstTimeAgentUser, - // isGuidedOnboardingActive, - // onboardingAppId, - // onboardingLink, - // pathname, - // pkgkey, - // search, - // services.application, - // ] - // ); - - const handleAddIntegrationPolicyClick = useCallback(() => { - !routesEnabled && onAddElasticDefendClicked?.(); - }, [routesEnabled, onAddElasticDefendClicked]); + const handleAddIntegrationPolicyClick = useCallback( + (ev) => { + ev.preventDefault(); + // The object below, given to `createHref` is explicitly accessing keys of `location` in order + // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) + if (onAddIntegrationPolicyClick) { + onAddIntegrationPolicyClick(); + return; + } + + const currentPath = history.createHref({ + pathname, + search, + hash, + }); + + const defaultNavigateOptions: InstallPkgRouteOptions = getInstallPkgRouteOptions({ + agentPolicyId: agentPolicyIdFromContext, + currentPath, + integration, + isCloud, + isExperimentalAddIntegrationPageEnabled, + isFirstTimeAgentUser, + isGuidedOnboardingActive, + 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); + }, + [ + agentPolicyIdFromContext, + hash, + history, + integration, + isCloud, + isExperimentalAddIntegrationPageEnabled, + isFirstTimeAgentUser, + isGuidedOnboardingActive, + onAddIntegrationPolicyClick, + onboardingAppId, + onboardingLink, + pathname, + pkgkey, + search, + services.application, + ] + ); const showVersionSelect = useMemo( () => @@ -576,14 +580,17 @@ export function Detail({ > ) => { e.preventDefault(); setSelectedPanel('overview'); @@ -814,7 +823,7 @@ export function Detail({ }, [ packageInfo, panel, - routesEnabled, + originFrom, getHref, integration, canReadIntegrationPolicies, @@ -824,6 +833,7 @@ export function Detail({ showConfigTab, showCustomTab, showDocumentationTab, + routesEnabled, numOfDeferredInstallations, ]); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_agent_enrollment.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_agent_enrollment.tsx index 53448aefd44c6..f067351546bde 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_agent_enrollment.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_agent_enrollment.tsx @@ -116,9 +116,12 @@ export const ConfirmAgentEnrollment: React.FunctionComponent = ({ ); const onButtonClick = () => { - if (onClickViewAgents) onClickViewAgents(); - const href = getHref('agent_list'); - application.navigateToUrl(href); + if (onClickViewAgents) { + onClickViewAgents(); + } else { + const href = getHref('agent_list'); + application.navigateToUrl(href); + } }; if (!policyId || (agentCount === 0 && !showLoading)) { diff --git a/x-pack/plugins/fleet/public/hooks/use_authz.ts b/x-pack/plugins/fleet/public/hooks/use_authz.ts index 40088d086a06e..14a1e74388f94 100644 --- a/x-pack/plugins/fleet/public/hooks/use_authz.ts +++ b/x-pack/plugins/fleet/public/hooks/use_authz.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { useIntegrationsStateContext } from '../applications/integrations/hooks'; +import { useFleetIntegrationsStateContext } from '../applications/integrations/hooks/use_fleet_integration_context'; import { useStartServices } from './use_core'; // Expose authz object, containing the privileges for Fleet and Integrations export function useAuthz() { const core = useStartServices(); - const { fleet } = useIntegrationsStateContext(); - return core.authz ?? fleet.auth; + const { fleet } = useFleetIntegrationsStateContext(); + return core.authz ?? fleet.authz; } diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index b0faf815ae174..eea00d893c9d1 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -97,18 +97,10 @@ export const UseIntegrationsState = () => { return import('./applications/integrations/hooks/use_integrations_state'); }; -export const UsePackageInstall = () => { - return import('./applications/integrations/hooks/use_package_install'); -}; - -export const FleetStatusProvider = () => { - return import('./hooks/use_fleet_status'); -}; - -export const UIExtensionsContextProvider = () => { - return import('./hooks/use_ui_extension'); -}; - export const CreatePackagePolicyPage = () => { return import('./applications/fleet/sections/agent_policy/create_package_policy_page'); }; + +export const FleetIntegrationsStateContextProvider = () => { + return import('./applications/integrations/hooks/use_fleet_integration_context'); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx index dc2f204c87d8f..9eb1e99721dce 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx @@ -8,19 +8,13 @@ import React, { lazy, Suspense, useMemo, useCallback, useEffect, useRef, useStat import { Routes, Route } from '@kbn/shared-ux-router'; import { - EuiButton, EuiButtonGroup, - EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiModal, EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, EuiPortal, EuiSkeletonText, - EuiSpacer, useGeneratedHtmlId, } from '@elastic/eui'; import type { AvailablePackagesHookType, IntegrationCardItem } from '@kbn/fleet-plugin/public'; @@ -28,7 +22,6 @@ import { noop } from 'lodash'; import { css } from '@emotion/react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useLocation } from 'react-router-dom'; import { withLazyHook } from '../../../../../common/components/with_lazy_hook'; import { useStoredIntegrationSearchTerm, @@ -70,40 +63,25 @@ const Detail = lazy(async () => ({ .then((pkg) => pkg.Detail), })); -const IntegrationsStateContextProvider = lazy(async () => ({ - default: await import('@kbn/fleet-plugin/public') - .then((module) => module.UseIntegrationsState()) - .then((pkg) => pkg.IntegrationsStateContextProvider), -})); - -const PackageInstallProvider = lazy(async () => ({ - default: await import('@kbn/fleet-plugin/public') - .then((module) => module.UsePackageInstall()) - .then((pkg) => pkg.packageInstallContainer[0]), -})); - -const FleetStatusProvider = lazy(async () => ({ - default: await import('@kbn/fleet-plugin/public') - .then((module) => module.FleetStatusProvider()) - .then((pkg) => pkg.FleetStatusProvider), -})); - -const UIExtensionsContextProvider = lazy(async () => ({ - default: await import('@kbn/fleet-plugin/public') - .then((module) => module.UIExtensionsContextProvider()) - .then((pkg) => pkg.UIExtensionsContextProvider), -})); - const CreatePackagePolicyPage = lazy(async () => ({ default: await import('@kbn/fleet-plugin/public') .then((module) => module.CreatePackagePolicyPage()) .then((pkg) => pkg.CreatePackagePolicyPage), })); +const FleetIntegrationsStateContextProvider = lazy(async () => ({ + default: await import('@kbn/fleet-plugin/public') + .then((module) => module.FleetIntegrationsStateContextProvider()) + .then((pkg) => pkg.FleetIntegrationsStateContextProvider), +})); + export const IntegrationsCardGridTabsComponent = React.memo( ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => { const { spaceId } = useOnboardingContext(); const startServices = useKibana().services; + const { + services: { fleet }, + } = useKibana(); const scrollElement = useRef(null); const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId( spaceId, @@ -122,19 +100,22 @@ export const IntegrationsCardGridTabsComponent = React.memo( 'overview' ); - const onAddElasticDefendClicked = useCallback(() => { + const onAddIntegrationPolicyClick = useCallback(() => { setModalView('configure-integration'); }, []); const closeModal = useCallback(() => { setIsModalVisible(false); setModalView('overview'); }, []); - const showModal = useCallback(() => setIsModalVisible(true), []); + const onCardClicked = useCallback((name: string) => { + setIsModalVisible(true); + setIntegrationName(name); + }, []); const ref = useRef(null); - let bottomBar; const modalTitleId = useGeneratedHtmlId(); const { filteredCards, @@ -148,7 +129,6 @@ export const IntegrationsCardGridTabsComponent = React.memo INTEGRATION_TABS_BY_ID[toggleIdSelected], [toggleIdSelected]); - console.log('=ref===', ref); const onSearchTermChanged = useCallback( (searchQuery: string) => { setSearchTerm(searchQuery); @@ -190,7 +170,7 @@ export const IntegrationsCardGridTabsComponent = React.memo - {isModalVisible && startServices && ( + {isModalVisible && fleet && ( - {modalView !== 'overview' && ( - "Step indicator placeholder" - )} - - - - - {modalView === 'overview' && ( - - )} - {modalView === 'configure-integration' && } - - - - + + {modalView === 'overview' && ( + + )} + {modalView === 'configure-integration' && ( + + )} + diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts index 61f588cbd00ba..dde2af956ff78 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts @@ -5,9 +5,9 @@ * 2.0. */ import { useMemo } from 'react'; -import type { FleetSetup, FleetStart, IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import type { IntegrationCardItem } from '@kbn/fleet-plugin/public'; import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation'; -import { useKibana, useNavigation } from '../../../../../common/lib/kibana'; +import { useNavigation } from '../../../../../common/lib/kibana'; import { APP_INTEGRATIONS_PATH, APP_UI_ID, @@ -16,7 +16,6 @@ import { import { CARD_DESCRIPTION_LINE_CLAMP, CARD_TITLE_LINE_CLAMP, - INTEGRATION_APP_ID, MAX_CARD_HEIGHT_IN_PX, ONBOARDING_APP_ID, ONBOARDING_LINK, @@ -24,7 +23,6 @@ import { } from './constants'; import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana'; import { trackOnboardingLinkClick } from '../../../../common/lib/telemetry'; -import { useOnboardingContext } from '../../../onboarding_context'; const addPathParamToUrl = (url: string, onboardingLink: string) => { const encoded = encodeURIComponent(onboardingLink); @@ -52,16 +50,13 @@ const getFilteredCards = ({ integrationsList, navigateTo, onCardClicked, - fleet, - application, }: { featuredCardIds?: string[]; getAppUrl: GetAppUrl; installedIntegrationList?: IntegrationCardItem[]; integrationsList: IntegrationCardItem[]; navigateTo: NavigateTo; - onCardClicked?: () => void; - fleet: FleetSetup | undefined; + onCardClicked?: (integrationName: string) => void; }) => { const securityIntegrationsList = integrationsList.map((card) => addSecuritySpecificProps({ @@ -70,8 +65,6 @@ const getFilteredCards = ({ card, installedIntegrationList, onCardClicked, - fleet, - application, }) ); if (!featuredCardIds) { @@ -89,18 +82,14 @@ const addSecuritySpecificProps = ({ getAppUrl, card, onCardClicked, - fleet, - application, }: { navigateTo: NavigateTo; getAppUrl: GetAppUrl; card: IntegrationCardItem; installedIntegrationList?: IntegrationCardItem[]; - onCardClicked?: () => void; - fleet: FleetStart | undefined; + onCardClicked?: (integrationName: string) => void; }): IntegrationCardItem => { const onboardingLink = getAppUrl({ appId: SECURITY_UI_APP_ID, path: ONBOARDING_PATH }); - const integrationRootUrl = getAppUrl({ appId: INTEGRATION_APP_ID }); const url = card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink @@ -108,12 +97,14 @@ const addSecuritySpecificProps = ({ : card.url; const state = { - onCancelNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }], + onCancelNavigateTo: [ + APP_UI_ID, + { path: ONBOARDING_PATH, state: { pkgkey: card.pkgkey, onCancelUrl: onboardingLink } }, + ], onCancelUrl: onboardingLink, - onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }], + onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH, state: { pkgkey: card.pkgkey } }], pkgkey: card.pkgkey, - panel: 'overview', - fleet: { auth: fleet?.authz, config: fleet?.config }, + panel: 'overview', // Default to the overview tab on modal opened }; return { @@ -128,18 +119,12 @@ const addSecuritySpecificProps = ({ trackOnboardingLinkClick(trackId); if (url.startsWith(APP_INTEGRATIONS_PATH)) { - onCardClicked?.(); + onCardClicked?.(card.name); // fix me: type error navigateTo({ path: `${addPathParamToUrl(ONBOARDING_PATH, onboardingLink)}#integrations`, state, }); - - // navigateTo({ - // appId: INTEGRATION_APP_ID, - // path: url.slice(integrationRootUrl.length), - // state, - // }); } else if (url.startsWith('http') || url.startsWith('https')) { window.open(url, '_blank'); } else { @@ -156,13 +141,10 @@ export const useIntegrationCardList = ({ }: { integrationsList: IntegrationCardItem[]; featuredCardIds?: string[] | undefined; - onCardClicked?: () => void; + onCardClicked?: (integrationName: string) => void; }): IntegrationCardItem[] => { const { navigateTo, getAppUrl } = useNavigation(); - // const { fleet } = useOnboardingContext(); - const { - services: { application, fleet }, - } = useKibana(); + const { featuredCards, integrationCards } = useMemo( () => getFilteredCards({ @@ -171,10 +153,8 @@ export const useIntegrationCardList = ({ integrationsList, featuredCardIds, onCardClicked, - fleet, - application, }), - [navigateTo, getAppUrl, integrationsList, featuredCardIds, onCardClicked, fleet, application] + [navigateTo, getAppUrl, integrationsList, featuredCardIds, onCardClicked] ); if (featuredCardIds && featuredCardIds.length > 0) { From 3fc339fd5e09bb169024c7aa89500eeb57a6686e Mon Sep 17 00:00:00 2001 From: Angela Chuang Date: Fri, 22 Nov 2024 15:24:42 +0000 Subject: [PATCH 4/4] step count --- .../create_package_policy_page/index.tsx | 13 +++++++++- .../multi_page_layout/index.tsx | 4 +++ .../single_page_layout/index.tsx | 25 +++++++++++++------ .../integration_card_grid_tabs.tsx | 17 +++++++++++-- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 13df7b84338fb..3cf6b362e7376 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -20,7 +20,16 @@ export const CreatePackagePolicyPage: React.FC<{ originFrom?: EditPackagePolicyFrom; propPolicyId?: string; integrationName?: string; -}> = ({ useMultiPageLayoutProp, originFrom, propPolicyId, integrationName }) => { + setIntegrationStep?: (step: number) => void; + onCanceled?: () => void; +}> = ({ + useMultiPageLayoutProp, + originFrom, + propPolicyId, + integrationName, + setIntegrationStep, + onCanceled, +}) => { const { search } = useLocation(); const { params } = useRouteMatch(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); @@ -63,6 +72,8 @@ export const CreatePackagePolicyPage: React.FC<{ propPolicyId, integrationName, prerelease, + setIntegrationStep, + onCanceled, }; if (useMultiPageLayout) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx index 95832e80c9de2..1d61e8e033358 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/index.tsx @@ -73,6 +73,8 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({ prerelease, from, integrationName, + setIntegrationStep, + onCanceled, }) => { const { params } = useRouteMatch(); // fixme @@ -155,6 +157,7 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({ } setCurrentStep(currentStep + 1); + setIntegrationStep(currentStep + 1); if (props?.selectedAgentPolicies) { setSelectedAgentPolicies(props?.selectedAgentPolicies); } @@ -186,6 +189,7 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({ setIsManaged={toggleIsManaged} setEnrolledAgentIds={setEnrolledAgentIds} enrolledAgentIds={enrolledAgentIds} + onCanceled={onCanceled} /> ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index a04a23dfa6228..8524a7e987e7d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState, Suspense, forwardRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, Suspense } from 'react'; import { useRouteMatch } from 'react-router-dom'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -20,7 +20,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, - EuiPortal, EuiSpacer, EuiSteps, } from '@elastic/eui'; @@ -109,6 +108,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ queryParamsPolicyId, prerelease, onNext, + onCanceled, }) => { const { agents: { enabled: isFleetEnabled }, @@ -204,6 +204,14 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ } }, [onNext, agentPolicies, savedPackagePolicy, navigateAddAgent]); + const handleCancellation = useCallback(() => { + if (onCanceled) { + onCanceled(); + } else { + navigateAddAgentHelp(savedPackagePolicy); + } + }, [onCanceled, savedPackagePolicy, navigateAddAgentHelp]); + const setPolicyValidation = useCallback( (selectedTab: SelectedPolicyTab, updatedAgentPolicy: NewAgentPolicy) => { if (selectedTab === SelectedPolicyTab.NEW) { @@ -520,7 +528,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ agentCount={agentCount} agentPolicies={agentPolicies} onConfirm={onSubmit} - onCancel={() => setFormState('VALID')} + onCancel={() => { + setFormState('VALID'); + onCanceled?.(); + }} /> )} {formState === 'SUBMITTED_NO_AGENTS' && @@ -530,7 +541,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ navigateAddAgentHelp(savedPackagePolicy)} + onCancel={handleCancellation} /> )} {formState === 'SUBMITTED_AZURE_ARM_TEMPLATE' && @@ -540,7 +551,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ agentPolicy={agentPolicies[0]} packagePolicy={savedPackagePolicy} onConfirm={handleNavigateAddAgent} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + onCancel={handleCancellation} /> )} {formState === 'SUBMITTED_CLOUD_FORMATION' && @@ -550,7 +561,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ agentPolicy={agentPolicies[0]} packagePolicy={savedPackagePolicy} onConfirm={handleNavigateAddAgent} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + onCancel={handleCancellation} /> )} {formState === 'SUBMITTED_GOOGLE_CLOUD_SHELL' && @@ -560,7 +571,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ agentPolicy={agentPolicies[0]} packagePolicy={savedPackagePolicy} onConfirm={handleNavigateAddAgent} - onCancel={() => navigateAddAgentHelp(savedPackagePolicy)} + onCancel={handleCancellation} /> )} {packageInfo && ( diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx index 9eb1e99721dce..a58e53f93a4a8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx @@ -5,14 +5,16 @@ * 2.0. */ import React, { lazy, Suspense, useMemo, useCallback, useEffect, useRef, useState } from 'react'; -import { Routes, Route } from '@kbn/shared-ux-router'; import { + EuiButton, EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiModal, EuiModalBody, + EuiModalFooter, + EuiModalHeader, EuiPortal, EuiSkeletonText, useGeneratedHtmlId, @@ -75,6 +77,12 @@ const FleetIntegrationsStateContextProvider = lazy(async () => ({ .then((pkg) => pkg.FleetIntegrationsStateContextProvider), })); +const integrationStepMap = { + 0: 'Add integration', + 1: 'Install Elastic Agent', + 2: 'Confirm incoming data', +} + export const IntegrationsCardGridTabsComponent = React.memo( ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => { const { spaceId } = useOnboardingContext(); @@ -104,18 +112,19 @@ export const IntegrationsCardGridTabsComponent = React.memo( 'overview' ); + const [integrationStep, setIntegrationStep] = useState(0); const onAddIntegrationPolicyClick = useCallback(() => { setModalView('configure-integration'); }, []); const closeModal = useCallback(() => { setIsModalVisible(false); setModalView('overview'); + setIntegrationStep(0); }, []); const onCardClicked = useCallback((name: string) => { setIsModalVisible(true); setIntegrationName(name); }, []); - const ref = useRef(null); const modalTitleId = useGeneratedHtmlId(); const { filteredCards, @@ -256,6 +265,7 @@ export const IntegrationsCardGridTabsComponent = React.memo + {modalView === 'configure-integration' && ({`step indicator place holder. Integration step: ${integrationStepMap[integrationStep]}`})} )} + {/* Close */} )}