Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion frontend/src/federation/console-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,43 @@ import { protobufRegistry } from 'protobuf-registry';
import { LONG_LIVED_CACHE_STALE_TIME } from 'react-query/react-query.utils';

import { FederatedProviders } from './federated-providers';
import { federatedRootRoute } from './federated-routes';
import { TokenManager } from './token-manager';
import type { ConsoleAppProps } from './types';
import { NotFoundPage } from '../components/misc/not-found-page';
import { addBearerTokenInterceptor, checkExpiredLicenseInterceptor, config, getGrpcBasePath, setup } from '../config';
import { routeTree } from '../routeTree.gen';
import { installUISettingsSideEffects } from '../state/ui';

/**
* Re-root the generated route tree onto Console's federated root.
*
* In the federated dev build, the generated tree's root route (from
* `src/routes/__root.tsx`) can be substituted by Cloud UI's own `__root` route
* — both apps compile a module with the identical id `./src/routes/__root.tsx`,
* and in the shared rsbuild/MF dev runtime the host's wins. The result is that
* the embedded Console renders Cloud UI's root chrome (its react NuqsAdapter,
* Builder.io `<Content>`, and `<CommandPalette>`/KBar) instead of Console's own
* federated layout — which breaks nuqs (NUQS-404), crashes on KBar
* (`getState is not a function`, no `KBarProvider` in this subtree), and leaves
* the embedded sidebar empty.
*
* `federatedRootRoute` lives at a Console-unique module path
* (`src/federation/federated-routes.tsx`) that cannot collide with Cloud UI, so
* reattaching the generated child routes to it guarantees the embedded app
* renders Console's own root. Standalone (`app.tsx`) and the legacy embedded
* entry keep using the generated `routeTree` unchanged.
*/
function createFederatedRouteTree() {
const childRoutes = routeTree.children ? Object.values(routeTree.children) : [];
for (const child of childRoutes) {
child.options.getParentRoute = () => federatedRootRoute;
}
return federatedRootRoute._addFileChildren(childRoutes);
}

const federatedRouteTree = createFederatedRouteTree();

/**
* Creates an interceptor that refreshes the token on 401 and retries the request.
* Uses TokenManager for deduplication and abort support.
Expand Down Expand Up @@ -255,7 +285,7 @@ function ConsoleAppInner({
});

const r = createRouter({
routeTree,
routeTree: federatedRouteTree,
history: memoryHistory,
context: {
basePath: '',
Expand Down
36 changes: 30 additions & 6 deletions frontend/src/federation/federated-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import type { QueryClient } from '@tanstack/react-query';
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { NuqsAdapter } from 'nuqs/adapters/tanstack-router';

import { DebugHelper } from '../components/debug-helper/debug-dialog';
import AppFooter from '../components/layout/footer';
import AppPageHeader from '../components/layout/header';
import { LicenseNotification } from '../components/license/license-notification';
import { ErrorBoundary } from '../components/misc/error-boundary';
import { ErrorDisplay } from '../components/misc/error-display';
import { ErrorModalsRenderer } from '../components/misc/error-modal';
import { NullFallbackBoundary } from '../components/misc/null-fallback-boundary';
import { RouterSync } from '../components/misc/router-sync';
import { Toaster } from '../components/redpanda-ui/components/sonner';
import RequireAuth from '../components/require-auth';
import { ModalContainer } from '../utils/modal-container';

/**
Expand Down Expand Up @@ -60,11 +64,22 @@ export const federatedRootRoute = createRootRouteWithContext<FederatedRouterCont
*/
function FederatedRootLayout() {
return (
<NuqsAdapter>
<ErrorBoundary>
<FederatedAppContent />
</ErrorBoundary>
</NuqsAdapter>
<>
<RouterSync />
<NuqsAdapter>
<ErrorBoundary>
{/* RequireAuth triggers the user-data fetch (api.refreshUserData) that
gates Console's endpoint-compatibility fetch and, in turn, the
embedded sidebar items. The standalone root (__root.tsx) wraps its
embedded layout the same way. */}
<RequireAuth>
<FederatedAppContent />
</RequireAuth>
</ErrorBoundary>
{/* Cmd+Shift+D debug dialog — mirrors __root.tsx; dev-only. */}
{process.env.NODE_ENV === 'development' && <DebugHelper />}
</NuqsAdapter>
</>
);
}

Expand All @@ -73,6 +88,9 @@ function FederatedRootLayout() {
* Similar to EmbeddedLayout from __root.tsx but optimized for MF v2.0.
*/
function FederatedAppContent() {
// Mirrors __root.tsx's EmbeddedLayout so the embedded experience matches
// production: AppPageHeader renders the page title (it already suppresses
// the breadcrumb/sidebar-trigger in embedded mode — the host supplies those).
return (
<div id="mainLayout">
<NullFallbackBoundary>
Expand All @@ -82,12 +100,18 @@ function FederatedAppContent() {
<AppPageHeader />

<ErrorDisplay>
<Outlet />
<div className="pt-8">
<Outlet />
</div>
</ErrorDisplay>

<AppFooter />

<ErrorModalsRenderer />

{/* sonner isn't an MF-shared singleton, so the host's <Toaster> can't
surface Console's toasts; mirror __root.tsx's AppContent. */}
<Toaster position="top-right" richColors />
</div>
);
}
Loading