diff --git a/packages/junior-dashboard/src/client/App.tsx b/packages/junior-dashboard/src/client/App.tsx index dd979852ad..a2f35c2d44 100644 --- a/packages/junior-dashboard/src/client/App.tsx +++ b/packages/junior-dashboard/src/client/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { lazy, useEffect, useMemo, useState, type ReactNode } from "react"; import { Navigate, Route, useLocation, useParams } from "react-router"; import { @@ -36,41 +36,20 @@ import { ConversationWorkspaceLoading } from "./conversations/ConversationWorksp import { useConversationData } from "./conversations/queries"; import { readConversationStartPosition } from "./conversations/transcriptReadPosition"; import { codeRepositoryTabs } from "./pages/code/codeRepositoryRoutes"; -import { RepositoryPage } from "./pages/code/RepositoryPage"; -import { CodePage } from "./pages/code/CodePage"; -import { LocationDetailPage } from "./pages/locations/LocationDetailPage"; -import { LocationsPage } from "./pages/locations/LocationsPage"; -import { PeoplePage } from "./pages/people/PeoplePage"; -import { PersonProfilePage } from "./pages/people/PersonProfilePage"; -import { ConnectionsPage } from "./pages/settings/ConnectionsPage"; -import { McpSettingsPage } from "./pages/settings/McpSettingsPage"; -import { PersonalTokensPage } from "./pages/settings/PersonalTokensPage"; -import { SettingsPage } from "./pages/settings/SettingsPage"; import { SettingsPageLayout } from "./pages/settings/SettingsPageLayout"; -import { SystemPage } from "./pages/system/SystemPage"; import { SystemPageLayout, SystemRouteLoading, } from "./pages/system/SystemPageLayout"; -import { WorkspaceFormPage } from "./pages/system/WorkspaceFormPage"; -import { WorkspacesPage } from "./pages/system/WorkspacesPage"; import { isMemoryLibraryPath, MemoryRouteLoading, } from "./pages/memory/MemoryPageLayout"; -import { AutomationPage } from "./pages/automations/AutomationPage"; -import { AutomationRunsPage } from "./pages/automations/AutomationRunsPage"; -import { AutomationsPage } from "./pages/automations/AutomationsPage"; import { AutomationsPageLayout, hasAutomationDetailsFrame, AutomationsRouteLoading, } from "./pages/automations/AutomationsPageLayout"; -import { - MemoryPermalinkRoute, - PluginUserPageRoute, -} from "./pages/user/PluginUserPage"; -import { AdminPage } from "./pages/admin/AdminPage"; import { buildPrimaryNavItems } from "./primaryNav"; import { TracedRoutes } from "./TracedRoutes"; import { @@ -80,6 +59,104 @@ import { import { useReviewItemCountData } from "./review-desk/queries"; import type { DashboardCoreData } from "./types"; +// Pages outside the conversation workspace load on first visit, so the +// workspace's first paint skips their code and the chart library. +const RepositoryPage = lazy(() => + import("./pages/code/RepositoryPage").then((module) => ({ + default: module.RepositoryPage, + })), +); +const CodePage = lazy(() => + import("./pages/code/CodePage").then((module) => ({ + default: module.CodePage, + })), +); +const LocationDetailPage = lazy(() => + import("./pages/locations/LocationDetailPage").then((module) => ({ + default: module.LocationDetailPage, + })), +); +const LocationsPage = lazy(() => + import("./pages/locations/LocationsPage").then((module) => ({ + default: module.LocationsPage, + })), +); +const PeoplePage = lazy(() => + import("./pages/people/PeoplePage").then((module) => ({ + default: module.PeoplePage, + })), +); +const PersonProfilePage = lazy(() => + import("./pages/people/PersonProfilePage").then((module) => ({ + default: module.PersonProfilePage, + })), +); +const ConnectionsPage = lazy(() => + import("./pages/settings/ConnectionsPage").then((module) => ({ + default: module.ConnectionsPage, + })), +); +const McpSettingsPage = lazy(() => + import("./pages/settings/McpSettingsPage").then((module) => ({ + default: module.McpSettingsPage, + })), +); +const PersonalTokensPage = lazy(() => + import("./pages/settings/PersonalTokensPage").then((module) => ({ + default: module.PersonalTokensPage, + })), +); +const SettingsPage = lazy(() => + import("./pages/settings/SettingsPage").then((module) => ({ + default: module.SettingsPage, + })), +); +const SystemPage = lazy(() => + import("./pages/system/SystemPage").then((module) => ({ + default: module.SystemPage, + })), +); +const WorkspaceFormPage = lazy(() => + import("./pages/system/WorkspaceFormPage").then((module) => ({ + default: module.WorkspaceFormPage, + })), +); +const WorkspacesPage = lazy(() => + import("./pages/system/WorkspacesPage").then((module) => ({ + default: module.WorkspacesPage, + })), +); +const AutomationPage = lazy(() => + import("./pages/automations/AutomationPage").then((module) => ({ + default: module.AutomationPage, + })), +); +const AutomationRunsPage = lazy(() => + import("./pages/automations/AutomationRunsPage").then((module) => ({ + default: module.AutomationRunsPage, + })), +); +const AutomationsPage = lazy(() => + import("./pages/automations/AutomationsPage").then((module) => ({ + default: module.AutomationsPage, + })), +); +const MemoryPermalinkRoute = lazy(() => + import("./pages/user/PluginUserPage").then((module) => ({ + default: module.MemoryPermalinkRoute, + })), +); +const PluginUserPageRoute = lazy(() => + import("./pages/user/PluginUserPage").then((module) => ({ + default: module.PluginUserPageRoute, + })), +); +const AdminPage = lazy(() => + import("./pages/admin/AdminPage").then((module) => ({ + default: module.AdminPage, + })), +); + const dashboardNoise = { backgroundImage: "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitchTiles'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.7'/%3E%3C/svg%3E\")", @@ -295,7 +372,7 @@ export function DashboardShell() { /> } /> - + }> } path="/tasks" /> } path="/tasks/*" /> ` children like ``, and name the open pageload or * navigation span after the route pattern that matched, such as - * `/conversations/:conversationId`. + * `/conversations/:conversationId`. Lazy pages show `fallback` while their + * code loads. */ -export function TracedRoutes(props: { children: ReactNode }) { +export function TracedRoutes(props: { + children: ReactNode; + fallback: ReactNode; +}) { const location = useLocation(); const routes = createRoutesFromChildren(props.children); const element = useRoutes(routes, location); @@ -36,5 +40,5 @@ export function TracedRoutes(props: { children: ReactNode }) { useLayoutEffect(() => { if (pattern) nameRouteSpan(pattern); }, [location.key, pattern]); - return element; + return {element}; } diff --git a/packages/junior-dashboard/src/client/code-highlighter.ts b/packages/junior-dashboard/src/client/code-highlighter.ts new file mode 100644 index 0000000000..6993262190 --- /dev/null +++ b/packages/junior-dashboard/src/client/code-highlighter.ts @@ -0,0 +1,26 @@ +import { + createBundledHighlighter, + createSingletonShorthands, + guessEmbeddedLanguages, +} from "shiki/core"; + +import { dashboardCodeLanguages } from "./code-languages"; + +/** + * Highlight code with the dashboard languages and themes. Themes, grammars, + * and the engine load in on-demand chunks. + */ +export const { codeToHtml } = createSingletonShorthands( + createBundledHighlighter({ + langs: dashboardCodeLanguages, + themes: { + "github-dark": () => import("shiki/themes/github-dark.mjs"), + "github-light": () => import("shiki/themes/github-light.mjs"), + }, + engine: async () => { + const { createOnigurumaEngine } = await import("shiki/engine/oniguruma"); + return createOnigurumaEngine(import("shiki/wasm")); + }, + }), + { guessEmbeddedLanguages }, +); diff --git a/packages/junior-dashboard/src/client/code.tsx b/packages/junior-dashboard/src/client/code.tsx index 09643d1720..29844b8d6a 100644 --- a/packages/junior-dashboard/src/client/code.tsx +++ b/packages/junior-dashboard/src/client/code.tsx @@ -1,35 +1,13 @@ import type { ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import type { BundledLanguage } from "shiki/bundle/web"; -import { - createBundledHighlighter, - createSingletonShorthands, - guessEmbeddedLanguages, -} from "shiki/core"; import { cn } from "./styles"; -import { dashboardCodeLanguages } from "./code-languages"; import { buildSearchDecorations, useTranscriptSearch, } from "./conversations/transcriptSearch"; -// Split themes, language grammars, and the engine into on-demand chunks. -const { codeToHtml } = createSingletonShorthands( - createBundledHighlighter({ - langs: dashboardCodeLanguages, - themes: { - "github-dark": () => import("shiki/themes/github-dark.mjs"), - "github-light": () => import("shiki/themes/github-light.mjs"), - }, - engine: async () => { - const { createOnigurumaEngine } = await import("shiki/engine/oniguruma"); - return createOnigurumaEngine(import("shiki/wasm")); - }, - }), - { guessEmbeddedLanguages }, -); - declare const shikiHtmlBrand: unique symbol; /** HTML generated by Shiki and safe to pass to the highlighted code shell. */ @@ -51,6 +29,8 @@ export function HighlightedCode(props: { const decorations = normalizedQuery ? buildSearchDecorations(props.code, normalizedQuery) : []; + // Shiki loads on the first code block, not with the dashboard entry. + const { codeToHtml } = await import("./code-highlighter"); return (await codeToHtml(props.code, { decorations: decorations.length ? decorations : undefined, lang: props.language, diff --git a/packages/junior-dashboard/src/client/components/ProfileMenu.tsx b/packages/junior-dashboard/src/client/components/ProfileMenu.tsx index fd49e2f41a..6fb44d8e68 100644 --- a/packages/junior-dashboard/src/client/components/ProfileMenu.tsx +++ b/packages/junior-dashboard/src/client/components/ProfileMenu.tsx @@ -12,7 +12,7 @@ import type { PersonalSpendReport } from "@sentry/junior/api/schema"; import type { PluginUserPageLink } from "@sentry/junior-plugin-api"; import { formatCostSummary, peoplePath } from "../format"; -import { pluginUserPagePath } from "../pages/user/PluginUserPage"; +import { pluginUserPagePath } from "../pages/user/pluginUserPageRoutes"; import { cn, dashboardInteractiveTextClass } from "../styles"; import type { Identity } from "../types"; import { ThemeSwitcher } from "./ThemeSwitcher"; diff --git a/packages/junior-dashboard/src/client/pages/admin/AdminPage.tsx b/packages/junior-dashboard/src/client/pages/admin/AdminPage.tsx index be23e135ad..f8819975cc 100644 --- a/packages/junior-dashboard/src/client/pages/admin/AdminPage.tsx +++ b/packages/junior-dashboard/src/client/pages/admin/AdminPage.tsx @@ -5,7 +5,7 @@ import type { PluginUserPageLink } from "@sentry/junior-plugin-api"; import { Card } from "../../components/layout/Card"; import { PageHeader } from "../../components/layout/PageHeader"; import { dashboardContainerClass } from "../../styles"; -import { pluginUserPagePath } from "../user/PluginUserPage"; +import { pluginUserPagePath } from "../user/pluginUserPageRoutes"; import { ServiceAccountsSection, useServiceAccounts, diff --git a/packages/junior-dashboard/src/client/pages/user/PluginUserPage.tsx b/packages/junior-dashboard/src/client/pages/user/PluginUserPage.tsx index 66a8ec6136..3c3a4cff42 100644 --- a/packages/junior-dashboard/src/client/pages/user/PluginUserPage.tsx +++ b/packages/junior-dashboard/src/client/pages/user/PluginUserPage.tsx @@ -27,12 +27,6 @@ import { type PluginUserPageRecordAction, } from "./pluginUserPageData"; -/** Build the canonical dashboard path for a plugin-owned page. */ -export function pluginUserPagePath(pluginName: string, pageId: string): string { - if (pluginName === "memory" && pageId === "memories") return "/memories"; - return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`; -} - /** Render the memory library for its first-class permalink route. */ export function MemoryPermalinkRoute(props: { pages: PluginUserPageLink[] }) { const page = props.pages.find( diff --git a/packages/junior-dashboard/src/client/pages/user/pluginUserPageRoutes.ts b/packages/junior-dashboard/src/client/pages/user/pluginUserPageRoutes.ts new file mode 100644 index 0000000000..9bec8c5371 --- /dev/null +++ b/packages/junior-dashboard/src/client/pages/user/pluginUserPageRoutes.ts @@ -0,0 +1,5 @@ +/** Build the canonical dashboard path for a plugin-owned page. */ +export function pluginUserPagePath(pluginName: string, pageId: string): string { + if (pluginName === "memory" && pageId === "memories") return "/memories"; + return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`; +} diff --git a/packages/junior-dashboard/src/client/primaryNav.ts b/packages/junior-dashboard/src/client/primaryNav.ts index 95676eb66e..c9f08b4b98 100644 --- a/packages/junior-dashboard/src/client/primaryNav.ts +++ b/packages/junior-dashboard/src/client/primaryNav.ts @@ -1,7 +1,7 @@ import type { PluginUserPageLink } from "@sentry/junior-plugin-api"; import type { DashboardHeaderNavItem } from "./components/layout/DashboardHeader"; -import { pluginUserPagePath } from "./pages/user/PluginUserPage"; +import { pluginUserPagePath } from "./pages/user/pluginUserPageRoutes"; const AUTH_PRIMARY_NAV_PREFIXES = [ "/admin",