diff --git a/js/web/src/routeTree.gen.ts b/js/web/src/routeTree.gen.ts index 8aaec9b2..045f1d97 100644 --- a/js/web/src/routeTree.gen.ts +++ b/js/web/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as VideosRouteImport } from './routes/videos' import { Route as SettingsRouteImport } from './routes/settings' import { Route as SearchRouteImport } from './routes/search' import { Route as LoginRouteImport } from './routes/login' +import { Route as DashboardRouteRouteImport } from './routes/dashboard/route' import { Route as IndexRouteImport } from './routes/index' import { Route as SettingsIndexRouteImport } from './routes/settings/index' import { Route as DashboardIndexRouteImport } from './routes/dashboard/index' @@ -61,6 +62,11 @@ const LoginRoute = LoginRouteImport.update({ path: '/login', getParentRoute: () => rootRouteImport, } as any) +const DashboardRouteRoute = DashboardRouteRouteImport.update({ + id: '/dashboard', + path: '/dashboard', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -72,9 +78,9 @@ const SettingsIndexRoute = SettingsIndexRouteImport.update({ getParentRoute: () => SettingsRoute, } as any) const DashboardIndexRoute = DashboardIndexRouteImport.update({ - id: '/dashboard/', - path: '/dashboard/', - getParentRoute: () => rootRouteImport, + id: '/', + path: '/', + getParentRoute: () => DashboardRouteRoute, } as any) const UserIndexRoute = UserIndexRouteImport.update({ id: '/$user/', @@ -132,14 +138,14 @@ const SettingsAboutRoute = SettingsAboutRouteImport.update({ getParentRoute: () => SettingsRoute, } as any) const DashboardVideosRoute = DashboardVideosRouteImport.update({ - id: '/dashboard/videos', - path: '/dashboard/videos', - getParentRoute: () => rootRouteImport, + id: '/videos', + path: '/videos', + getParentRoute: () => DashboardRouteRoute, } as any) const DashboardUploadRoute = DashboardUploadRouteImport.update({ - id: '/dashboard/upload', - path: '/dashboard/upload', - getParentRoute: () => rootRouteImport, + id: '/upload', + path: '/upload', + getParentRoute: () => DashboardRouteRoute, } as any) const ChatPopoutUserRoute = ChatPopoutUserRouteImport.update({ id: '/chat-popout/$user', @@ -152,31 +158,31 @@ const EmbedUserIndexRoute = EmbedUserIndexRouteImport.update({ getParentRoute: () => rootRouteImport, } as any) const DashboardWebhooksIndexRoute = DashboardWebhooksIndexRouteImport.update({ - id: '/dashboard/webhooks/', - path: '/dashboard/webhooks/', - getParentRoute: () => rootRouteImport, + id: '/webhooks/', + path: '/webhooks/', + getParentRoute: () => DashboardRouteRoute, } as any) const DashboardStreamIndexRoute = DashboardStreamIndexRouteImport.update({ - id: '/dashboard/stream/', - path: '/dashboard/stream/', - getParentRoute: () => rootRouteImport, + id: '/stream/', + path: '/stream/', + getParentRoute: () => DashboardRouteRoute, } as any) const DashboardRecommendationsIndexRoute = DashboardRecommendationsIndexRouteImport.update({ - id: '/dashboard/recommendations/', - path: '/dashboard/recommendations/', - getParentRoute: () => rootRouteImport, + id: '/recommendations/', + path: '/recommendations/', + getParentRoute: () => DashboardRouteRoute, } as any) const DashboardMultistreamIndexRoute = DashboardMultistreamIndexRouteImport.update({ - id: '/dashboard/multistream/', - path: '/dashboard/multistream/', - getParentRoute: () => rootRouteImport, + id: '/multistream/', + path: '/multistream/', + getParentRoute: () => DashboardRouteRoute, } as any) const DashboardKeysIndexRoute = DashboardKeysIndexRouteImport.update({ - id: '/dashboard/keys/', - path: '/dashboard/keys/', - getParentRoute: () => rootRouteImport, + id: '/keys/', + path: '/keys/', + getParentRoute: () => DashboardRouteRoute, } as any) const EmbedInfoWidgetUserRoute = EmbedInfoWidgetUserRouteImport.update({ id: '/embed/info-widget/$user', @@ -201,6 +207,7 @@ const EmbedUserVideoTidRoute = EmbedUserVideoTidRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteRouteWithChildren '/login': typeof LoginRoute '/search': typeof SearchRoute '/settings': typeof SettingsRouteWithChildren @@ -267,6 +274,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/dashboard': typeof DashboardRouteRouteWithChildren '/login': typeof LoginRoute '/search': typeof SearchRoute '/settings': typeof SettingsRouteWithChildren @@ -302,6 +310,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/dashboard' | '/login' | '/search' | '/settings' @@ -367,6 +376,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/dashboard' | '/login' | '/search' | '/settings' @@ -401,23 +411,16 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + DashboardRouteRoute: typeof DashboardRouteRouteWithChildren LoginRoute: typeof LoginRoute SearchRoute: typeof SearchRoute SettingsRoute: typeof SettingsRouteWithChildren VideosRoute: typeof VideosRoute ChatPopoutUserRoute: typeof ChatPopoutUserRoute - DashboardUploadRoute: typeof DashboardUploadRoute - DashboardVideosRoute: typeof DashboardVideosRoute UserIndexRoute: typeof UserIndexRoute - DashboardIndexRoute: typeof DashboardIndexRoute UserVideoTidRoute: typeof UserVideoTidRoute EmbedDanmuObsUserRoute: typeof EmbedDanmuObsUserRoute EmbedInfoWidgetUserRoute: typeof EmbedInfoWidgetUserRoute - DashboardKeysIndexRoute: typeof DashboardKeysIndexRoute - DashboardMultistreamIndexRoute: typeof DashboardMultistreamIndexRoute - DashboardRecommendationsIndexRoute: typeof DashboardRecommendationsIndexRoute - DashboardStreamIndexRoute: typeof DashboardStreamIndexRoute - DashboardWebhooksIndexRoute: typeof DashboardWebhooksIndexRoute EmbedUserIndexRoute: typeof EmbedUserIndexRoute EmbedUserVideoTidRoute: typeof EmbedUserVideoTidRoute } @@ -452,6 +455,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginRouteImport parentRoute: typeof rootRouteImport } + '/dashboard': { + id: '/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof DashboardRouteRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -468,10 +478,10 @@ declare module '@tanstack/react-router' { } '/dashboard/': { id: '/dashboard/' - path: '/dashboard' + path: '/' fullPath: '/dashboard/' preLoaderRoute: typeof DashboardIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/$user/': { id: '/$user/' @@ -552,17 +562,17 @@ declare module '@tanstack/react-router' { } '/dashboard/videos': { id: '/dashboard/videos' - path: '/dashboard/videos' + path: '/videos' fullPath: '/dashboard/videos' preLoaderRoute: typeof DashboardVideosRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/dashboard/upload': { id: '/dashboard/upload' - path: '/dashboard/upload' + path: '/upload' fullPath: '/dashboard/upload' preLoaderRoute: typeof DashboardUploadRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/chat-popout/$user': { id: '/chat-popout/$user' @@ -580,38 +590,38 @@ declare module '@tanstack/react-router' { } '/dashboard/webhooks/': { id: '/dashboard/webhooks/' - path: '/dashboard/webhooks' + path: '/webhooks' fullPath: '/dashboard/webhooks/' preLoaderRoute: typeof DashboardWebhooksIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/dashboard/stream/': { id: '/dashboard/stream/' - path: '/dashboard/stream' + path: '/stream' fullPath: '/dashboard/stream/' preLoaderRoute: typeof DashboardStreamIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/dashboard/recommendations/': { id: '/dashboard/recommendations/' - path: '/dashboard/recommendations' + path: '/recommendations' fullPath: '/dashboard/recommendations/' preLoaderRoute: typeof DashboardRecommendationsIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/dashboard/multistream/': { id: '/dashboard/multistream/' - path: '/dashboard/multistream' + path: '/multistream' fullPath: '/dashboard/multistream/' preLoaderRoute: typeof DashboardMultistreamIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/dashboard/keys/': { id: '/dashboard/keys/' - path: '/dashboard/keys' + path: '/keys' fullPath: '/dashboard/keys/' preLoaderRoute: typeof DashboardKeysIndexRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof DashboardRouteRoute } '/embed/info-widget/$user': { id: '/embed/info-widget/$user' @@ -644,6 +654,32 @@ declare module '@tanstack/react-router' { } } +interface DashboardRouteRouteChildren { + DashboardUploadRoute: typeof DashboardUploadRoute + DashboardVideosRoute: typeof DashboardVideosRoute + DashboardIndexRoute: typeof DashboardIndexRoute + DashboardKeysIndexRoute: typeof DashboardKeysIndexRoute + DashboardMultistreamIndexRoute: typeof DashboardMultistreamIndexRoute + DashboardRecommendationsIndexRoute: typeof DashboardRecommendationsIndexRoute + DashboardStreamIndexRoute: typeof DashboardStreamIndexRoute + DashboardWebhooksIndexRoute: typeof DashboardWebhooksIndexRoute +} + +const DashboardRouteRouteChildren: DashboardRouteRouteChildren = { + DashboardUploadRoute: DashboardUploadRoute, + DashboardVideosRoute: DashboardVideosRoute, + DashboardIndexRoute: DashboardIndexRoute, + DashboardKeysIndexRoute: DashboardKeysIndexRoute, + DashboardMultistreamIndexRoute: DashboardMultistreamIndexRoute, + DashboardRecommendationsIndexRoute: DashboardRecommendationsIndexRoute, + DashboardStreamIndexRoute: DashboardStreamIndexRoute, + DashboardWebhooksIndexRoute: DashboardWebhooksIndexRoute, +} + +const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren( + DashboardRouteRouteChildren, +) + interface SettingsRouteChildren { SettingsAboutRoute: typeof SettingsAboutRoute SettingsAccountRoute: typeof SettingsAccountRoute @@ -678,23 +714,16 @@ const SettingsRouteWithChildren = SettingsRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + DashboardRouteRoute: DashboardRouteRouteWithChildren, LoginRoute: LoginRoute, SearchRoute: SearchRoute, SettingsRoute: SettingsRouteWithChildren, VideosRoute: VideosRoute, ChatPopoutUserRoute: ChatPopoutUserRoute, - DashboardUploadRoute: DashboardUploadRoute, - DashboardVideosRoute: DashboardVideosRoute, UserIndexRoute: UserIndexRoute, - DashboardIndexRoute: DashboardIndexRoute, UserVideoTidRoute: UserVideoTidRoute, EmbedDanmuObsUserRoute: EmbedDanmuObsUserRoute, EmbedInfoWidgetUserRoute: EmbedInfoWidgetUserRoute, - DashboardKeysIndexRoute: DashboardKeysIndexRoute, - DashboardMultistreamIndexRoute: DashboardMultistreamIndexRoute, - DashboardRecommendationsIndexRoute: DashboardRecommendationsIndexRoute, - DashboardStreamIndexRoute: DashboardStreamIndexRoute, - DashboardWebhooksIndexRoute: DashboardWebhooksIndexRoute, EmbedUserIndexRoute: EmbedUserIndexRoute, EmbedUserVideoTidRoute: EmbedUserVideoTidRoute, } diff --git a/js/web/src/routes/__root.tsx b/js/web/src/routes/__root.tsx index 3f510659..bf569b7f 100644 --- a/js/web/src/routes/__root.tsx +++ b/js/web/src/routes/__root.tsx @@ -1,4 +1,3 @@ -import DashboardChrome from "@/components/dashboard/dashboard-chrome"; import Header from "@/components/header"; import SidebarComponent from "@/components/sidebar"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -20,9 +19,6 @@ import i18next from "../lib/i18n"; /** Routes that should render without sidebar/header chrome. */ const POPOUT_PREFIXES = ["/chat-popout/", "/embed/"]; -/** Routes that should render the dashboard's own chrome, separate from the main app. */ -const DASHBOARD_PREFIX = "/dashboard"; - export const Route = createRootRoute({ component: RootLayout, pendingComponent: RouteLoadingSkeleton, @@ -83,6 +79,14 @@ function RouteLoadingSkeleton() { } function RootLayout() { + // Popout routes (chat popout, embeds) skip the regular chrome and + // render their own minimal layout. Everything else gets the full + // provider tree; the chrome decision (regular vs dashboard) is now + // owned by route layouts — see routes/dashboard/route.tsx for the + // dashboard chrome layout, and the non-dashboard routes get the + // regular chrome via ChromeLayout below. The root no longer switches + // chrome based on pathname, which closed a render race where the + // dashboard route was mounted without its DashboardStoreContext. const pathname = useRouterState({ select: (s) => s.resolvedLocation?.pathname ?? "", }); @@ -92,28 +96,12 @@ function RootLayout() { const actualPathname = pathname || browserPathname; - const isPopout = POPOUT_PREFIXES.some((p) => actualPathname.startsWith(p)); - const isDashboard = - actualPathname === DASHBOARD_PREFIX || - actualPathname.startsWith(`${DASHBOARD_PREFIX}/`); - - console.log( - "Rendering RootLayout, browser:", - actualPathname, - "router: ", - pathname, - "isPopout:", - isPopout, - isDashboard, - ); - // pause until we can get a valid pathname, to avoid rendering the wrong chrome on initial load if (!actualPathname) { return ; } - // Popout routes get no chrome — just providers and the outlet. - if (isPopout) { + if (POPOUT_PREFIXES.some((p) => actualPathname.startsWith(p))) { return ( @@ -123,24 +111,6 @@ function RootLayout() { ); } - // Dashboard routes get their own chrome, separate from the main app. - // See /dashboard/index.tsx and components/dashboard/dashboard-chrome.tsx for details. - if (isDashboard) { - // if no pathname from router, we aren't ready yet - if (!pathname) { - return ; - } - return ( - - - - - - - - ); - } - return ( diff --git a/js/web/src/routes/dashboard/route.tsx b/js/web/src/routes/dashboard/route.tsx new file mode 100644 index 00000000..08ecab1c --- /dev/null +++ b/js/web/src/routes/dashboard/route.tsx @@ -0,0 +1,17 @@ +import DashboardChrome from "@/components/dashboard/dashboard-chrome"; +import { createFileRoute } from "@tanstack/react-router"; + +// Layout route for everything under /dashboard. DashboardChrome renders +// its own for the child route, and provides the +// DashboardStoreContext, LivestreamProvider, DashboardMetricsProvider, +// and the dashboard's own SidebarProvider. +// +// Having this as a real layout route (instead of conditionally rendering +// DashboardChrome in __root.tsx based on the current pathname) avoids a +// render race: the dashboard child route's `useDashboardStore()` call +// was throwing because the dashboard chrome wasn't always mounted at the +// time the child was rendered. With a layout route, the chrome and the +// provider tree are guaranteed to be in place before the child renders. +export const Route = createFileRoute("/dashboard")({ + component: DashboardChrome, +});