From 5c8d1a56450464680aa6e3ba5d3accc18d4673da Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 4 May 2026 18:50:21 -0700 Subject: [PATCH] fix(desktop): consolidate v2 workspace context behind a single provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 workspace tree had two independent useLiveQuery lookups for the workspace — one in the layout (driving WorkspaceTrpcProvider mounting) and one in the page (driving WorkspaceContent rendering). During a workspaceId change they could disagree for a render: the layout would strip the provider while the page still rendered TerminalPane, and useWorkspaceClient threw "must be used within WorkspaceClientProvider". Layout now owns the existence check (creating / error / not-found states render directly, no Outlet without a provider). A new WorkspaceProvider takes the resolved workspace, derives hostUrl once from useLocalHostService, and mounts WorkspaceTrpcProvider with a key of `${workspace.id}:${hostUrl}` — so the same component decides what to render and what context to provide. Descendants now read workspace + hostUrl via useWorkspace() instead of accepting workspaceId as a prop. This eliminates the same drift class at the inner-hook layer (a future caller could otherwise pass a stale URL param while the trpc client is bound to a different workspace). --- .../V2NotificationStatusIndicator.tsx | 6 +- .../useClearActivePaneAttention.ts | 13 ++- .../useDirtyTabCloseGuard.ts | 9 +- .../ChatPaneTitle/ChatPaneTitle.tsx | 1 - .../hooks/usePaneRegistry/usePaneRegistry.tsx | 12 +- .../useV2PresetExecution.ts | 10 +- .../useV2WorkspacePaneLayout.ts | 19 +--- .../useWorkspaceFileNavigation.ts | 8 +- .../v2-workspace/$workspaceId/page.tsx | 107 ++---------------- .../FileDocumentStoreProvider.tsx | 16 ++- .../_dashboard/v2-workspace/layout.tsx | 76 ++++++------- .../WorkspaceProvider/WorkspaceProvider.tsx | 60 ++++++++++ .../providers/WorkspaceProvider/index.ts | 1 + 13 files changed, 148 insertions(+), 190 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/WorkspaceProvider.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2NotificationStatusIndicator/V2NotificationStatusIndicator.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2NotificationStatusIndicator/V2NotificationStatusIndicator.tsx index 55aa9879e9b..b47f8e5f2bc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2NotificationStatusIndicator/V2NotificationStatusIndicator.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2NotificationStatusIndicator/V2NotificationStatusIndicator.tsx @@ -1,3 +1,4 @@ +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; import { useV2SourcesNotificationStatus, @@ -5,17 +6,16 @@ import { } from "renderer/stores/v2-notifications"; interface V2NotificationStatusIndicatorProps { - workspaceId: string; sources: Iterable; className?: string; } export function V2NotificationStatusIndicator({ - workspaceId, sources, className, }: V2NotificationStatusIndicatorProps) { - const status = useV2SourcesNotificationStatus(workspaceId, sources); + const { workspace } = useWorkspace(); + const status = useV2SourcesNotificationStatus(workspace.id, sources); if (!status) return null; return ; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useClearActivePaneAttention/useClearActivePaneAttention.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useClearActivePaneAttention/useClearActivePaneAttention.ts index 800b0d1eac0..ff7a6371ef3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useClearActivePaneAttention/useClearActivePaneAttention.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useClearActivePaneAttention/useClearActivePaneAttention.ts @@ -1,5 +1,6 @@ import type { WorkspaceStore } from "@superset/panes"; import { useEffect } from "react"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { getV2NotificationSourcesForPane, useV2NotificationStore, @@ -10,19 +11,21 @@ import type { StoreApi } from "zustand/vanilla"; import type { PaneViewerData } from "../../types"; export function useClearActivePaneAttention({ - workspaceId, store, }: { - workspaceId: string; store: StoreApi>; }): void { + const { workspace } = useWorkspace(); const activePane = useStore(store, (state) => { const tab = state.tabs.find( (candidate) => candidate.id === state.activeTabId, ); return tab?.activePaneId ? tab.panes[tab.activePaneId] : undefined; }); - const activePaneStatus = useV2PaneNotificationStatus(workspaceId, activePane); + const activePaneStatus = useV2PaneNotificationStatus( + workspace.id, + activePane, + ); const clearSourceAttention = useV2NotificationStore( (state) => state.clearSourceAttention, ); @@ -30,7 +33,7 @@ export function useClearActivePaneAttention({ useEffect(() => { if (activePaneStatus !== "review") return; for (const source of getV2NotificationSourcesForPane(activePane)) { - clearSourceAttention(source, workspaceId); + clearSourceAttention(source, workspace.id); } - }, [activePane, activePaneStatus, clearSourceAttention, workspaceId]); + }, [activePane, activePaneStatus, clearSourceAttention, workspace.id]); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts index 01ea5cc54e0..47987b3b924 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts @@ -2,6 +2,7 @@ import type { WorkspaceProps } from "@superset/panes"; import { alert } from "@superset/ui/atoms/Alert"; import { useCallback } from "react"; import { getBaseName } from "renderer/lib/pathBasename"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { getDocument } from "../../state/fileDocumentStore"; import type { FilePaneData, PaneViewerData } from "../../types"; @@ -9,11 +10,9 @@ type OnBeforeCloseTab = NonNullable< WorkspaceProps["onBeforeCloseTab"] >; -export function useDirtyTabCloseGuard({ - workspaceId, -}: { - workspaceId: string; -}): OnBeforeCloseTab { +export function useDirtyTabCloseGuard(): OnBeforeCloseTab { + const { workspace } = useWorkspace(); + const workspaceId = workspace.id; return useCallback( (tab) => { const dirtyPanes = Object.values(tab.panes).filter((pane) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx index a9dc3377b4f..ed4938c2bf3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx @@ -45,7 +45,6 @@ export function ChatPaneTitle({ context, workspaceId }: ChatPaneTitleProps) { onDeleteSession={handleDeleteSession} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index 41413a0f9c0..573b67cf636 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -26,6 +26,7 @@ import { useHotkeyDisplay } from "renderer/hotkeys"; import { getBaseName } from "renderer/lib/pathBasename"; import { consumeTerminalBackgroundIntent } from "renderer/lib/terminal/terminal-background-intents"; import { terminalRuntimeRegistry } from "renderer/lib/terminal/terminal-runtime-registry"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils"; import { clearV2TerminalRunStatus, @@ -107,10 +108,12 @@ interface UsePaneRegistryOptions { onRevealPath: (path: string) => void; } -export function usePaneRegistry( - workspaceId: string, - { onOpenFile, onRevealPath }: UsePaneRegistryOptions, -): PaneRegistry { +export function usePaneRegistry({ + onOpenFile, + onRevealPath, +}: UsePaneRegistryOptions): PaneRegistry { + const { workspace } = useWorkspace(); + const workspaceId = workspace.id; const clearShortcut = useHotkeyDisplay("CLEAR_TERMINAL").text; const scrollToBottomShortcut = useHotkeyDisplay("SCROLL_TO_BOTTOM").text; const workspaceTrpcUtils = workspaceTrpc.useUtils(); @@ -254,7 +257,6 @@ export function usePaneRegistry(
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts index d953a450a56..55d59b84e3a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts @@ -2,6 +2,7 @@ import type { CreatePaneInput, WorkspaceStore } from "@superset/panes"; import { toast } from "@superset/ui/sonner"; import { useLiveQuery } from "@tanstack/react-db"; import { useCallback, useMemo } from "react"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { V2TerminalPresetRow } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { getPresetLaunchPlan } from "renderer/stores/tabs/preset-launch"; @@ -27,14 +28,11 @@ function resolveTarget(executionMode: V2TerminalPresetRow["executionMode"]) { interface UseV2PresetExecutionArgs { store: StoreApi>; - workspaceId: string; - projectId: string; } -export function useV2PresetExecution({ - store, - projectId, -}: UseV2PresetExecutionArgs) { +export function useV2PresetExecution({ store }: UseV2PresetExecutionArgs) { + const { workspace } = useWorkspace(); + const projectId = workspace.projectId; const collections = useCollections(); const { data: allPresets = [] } = useLiveQuery( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts index 36da8ff40c6..5fbad4e3b2e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts @@ -2,7 +2,7 @@ import { createWorkspaceStore, type WorkspaceState } from "@superset/panes"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useEffect, useMemo, useRef, useState } from "react"; -import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { PaneViewerData } from "../../types"; @@ -16,17 +16,10 @@ function getSnapshot(state: WorkspaceState): string { return JSON.stringify(state); } -interface UseV2WorkspacePaneLayoutParams { - projectId: string; - workspaceId: string; -} - -export function useV2WorkspacePaneLayout({ - projectId, - workspaceId, -}: UseV2WorkspacePaneLayoutParams) { +export function useV2WorkspacePaneLayout() { + const { workspace } = useWorkspace(); + const workspaceId = workspace.id; const collections = useCollections(); - const { ensureWorkspaceInSidebar } = useDashboardSidebarState(); const [store] = useState(() => createWorkspaceStore({ initialState: EMPTY_STATE, @@ -52,10 +45,6 @@ export function useV2WorkspacePaneLayout({ [localWorkspaceState], ); - useEffect(() => { - ensureWorkspaceInSidebar(workspaceId, projectId); - }, [ensureWorkspaceInSidebar, projectId, workspaceId]); - useEffect(() => { const nextSnapshot = getSnapshot(persistedPaneLayout); if (nextSnapshot === lastSyncedSnapshotRef.current) { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts index 9f5eb8910de..bcc0e8ed0c7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceFileNavigation/useWorkspaceFileNavigation.ts @@ -2,6 +2,7 @@ import type { WorkspaceStore } from "@superset/panes"; import { workspaceTrpc } from "@superset/workspace-client"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { V2UserPreferencesApi } from "renderer/hooks/useV2UserPreferences"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { toAbsoluteWorkspacePath, toRelativeWorkspacePath, @@ -20,12 +21,10 @@ interface PendingReveal { } export function useWorkspaceFileNavigation({ - workspaceId, store, setRightSidebarOpen, setRightSidebarTab, }: { - workspaceId: string; store: StoreApi>; setRightSidebarOpen: V2UserPreferencesApi["setRightSidebarOpen"]; setRightSidebarTab: V2UserPreferencesApi["setRightSidebarTab"]; @@ -42,12 +41,13 @@ export function useWorkspaceFileNavigation({ recentFiles: RecentFile[]; openFilePaths: Set; } { + const { workspace } = useWorkspace(); const workspaceQuery = workspaceTrpc.workspace.get.useQuery({ - id: workspaceId, + id: workspace.id, }); const worktreePath = workspaceQuery.data?.worktreePath ?? ""; - const { recentFiles, recordView } = useRecentlyViewedFiles(workspaceId); + const { recentFiles, recordView } = useRecentlyViewedFiles(workspace.id); const activeFilePanePath = useStore(store, (state) => { const tab = state.tabs.find( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 01427d962b8..931d4ecaae0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -1,19 +1,13 @@ import { Workspace } from "@superset/panes"; -import { eq } from "@tanstack/db"; -import { useLiveQuery } from "@tanstack/react-db"; import { createFileRoute } from "@tanstack/react-router"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences"; import { useHotkey } from "renderer/hotkeys"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { CommandPalette } from "renderer/screens/main/components/CommandPalette"; import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel"; import { getV2NotificationSourcesForTab } from "renderer/stores/v2-notifications"; -import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; -import { WorkspaceCreateErrorState } from "../components/WorkspaceCreateErrorState"; -import { WorkspaceCreatingState } from "../components/WorkspaceCreatingState"; -import { WorkspaceNotFoundState } from "../components/WorkspaceNotFoundState"; +import { useWorkspace } from "../providers/WorkspaceProvider"; import { AddTabMenu } from "./components/AddTabMenu"; import { V2NotificationStatusIndicator } from "./components/V2NotificationStatusIndicator"; import { V2PresetsBar } from "./components/V2PresetsBar"; @@ -72,7 +66,6 @@ export const Route = createFileRoute( }); function V2WorkspacePage() { - const { workspaceId } = Route.useParams(); const { terminalId, chatSessionId, @@ -81,98 +74,18 @@ function V2WorkspacePage() { openUrlTarget, openUrlRequestId, } = Route.useSearch(); - const collections = useCollections(); + const { workspace } = useWorkspace(); + const workspaceId = workspace.id; - const { data: workspaces } = useLiveQuery( - (q) => - q - .from({ v2Workspaces: collections.v2Workspaces }) - .where(({ v2Workspaces }) => eq(v2Workspaces.id, workspaceId)), - [collections, workspaceId], - ); - const workspace = workspaces?.[0] ?? null; - const inFlight = useWorkspaceCreatesStore((store) => - store.entries.find((entry) => entry.snapshot.id === workspaceId), - ); - - if (!workspaces) { - return
; - } - - if (!workspace) { - if (inFlight?.state === "creating") { - return ( - - ); - } - if (inFlight?.state === "error") { - return ( - - ); - } - return ; - } - - return ( - // key={workspaceId} so each workspace gets its own pane store rather - // than sharing one and replaceState-ing data across switches. - - ); -} - -function WorkspaceContent({ - projectId, - workspaceId, - terminalId, - chatSessionId, - focusRequestId, - openUrl, - openUrlTarget, - openUrlRequestId, -}: { - projectId: string; - workspaceId: string; - terminalId?: string; - chatSessionId?: string; - focusRequestId?: string; - openUrl?: string; - openUrlTarget?: V2WorkspaceUrlOpenTarget; - openUrlRequestId?: string; -}) { const { preferences: v2UserPreferences, setRightSidebarOpen, setRightSidebarTab, setRightSidebarWidth, } = useV2UserPreferences(); - const { store } = useV2WorkspacePaneLayout({ - projectId, - workspaceId, - }); - useClearActivePaneAttention({ workspaceId, store }); - const { matchedPresets, executePreset } = useV2PresetExecution({ - store, - workspaceId, - projectId, - }); + const { store } = useV2WorkspacePaneLayout(); + useClearActivePaneAttention({ store }); + const { matchedPresets, executePreset } = useV2PresetExecution({ store }); useConsumeAutomationRunLink({ store, terminalId, @@ -194,13 +107,12 @@ function WorkspaceContent({ recentFiles, openFilePaths, } = useWorkspaceFileNavigation({ - workspaceId, store, setRightSidebarOpen, setRightSidebarTab, }); - const paneRegistry = usePaneRegistry(workspaceId, { + const paneRegistry = usePaneRegistry({ onOpenFile: openFilePane, onRevealPath: revealPath, }); @@ -216,7 +128,7 @@ function WorkspaceContent({ const [quickOpenOpen, setQuickOpenOpen] = useState(false); const handleQuickOpen = useCallback(() => setQuickOpenOpen(true), []); const defaultPaneActions = useDefaultPaneActions(); - const onBeforeCloseTab = useDirtyTabCloseGuard({ workspaceId }); + const onBeforeCloseTab = useDirtyTabCloseGuard(); const sidebarOpen = v2UserPreferences.rightSidebarOpen; // Fallback for rows persisted before the rightSidebarWidth field existed — @@ -258,7 +170,7 @@ function WorkspaceContent({ useHotkey("QUICK_OPEN", handleQuickOpen); return ( - +
( )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/state/fileDocumentStore/FileDocumentStoreProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/state/fileDocumentStore/FileDocumentStoreProvider.tsx index 7476fb01fff..a6721a54a34 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/state/fileDocumentStore/FileDocumentStoreProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/state/fileDocumentStore/FileDocumentStoreProvider.tsx @@ -1,18 +1,16 @@ import type { ReactNode } from "react"; import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; +import { useWorkspace } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider"; import { dispatchFsEvent } from "./fileDocumentStore"; -interface FileDocumentStoreProviderProps { - workspaceId: string; - children: ReactNode; -} - export function FileDocumentStoreProvider({ - workspaceId, children, -}: FileDocumentStoreProviderProps) { - useWorkspaceEvent("fs:events", workspaceId, (event) => { - dispatchFsEvent(workspaceId, event); +}: { + children: ReactNode; +}) { + const { workspace } = useWorkspace(); + useWorkspaceEvent("fs:events", workspace.id, (event) => { + dispatchFsEvent(workspace.id, event); }); return <>{children}; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx index 39c0cdf92cc..a565d35d761 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx @@ -1,17 +1,14 @@ -import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; import { useEffect, useRef } from "react"; -import { env } from "renderer/env.renderer"; -import { - getHostServiceHeaders, - getHostServiceWsToken, -} from "renderer/lib/host-service-auth"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; -import { WorkspaceTrpcProvider } from "./providers/WorkspaceTrpcProvider"; +import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; +import { WorkspaceCreateErrorState } from "./components/WorkspaceCreateErrorState"; +import { WorkspaceCreatingState } from "./components/WorkspaceCreatingState"; +import { WorkspaceNotFoundState } from "./components/WorkspaceNotFoundState"; +import { WorkspaceProvider } from "./providers/WorkspaceProvider"; export const Route = createFileRoute("/_authenticated/_dashboard/v2-workspace")( { @@ -27,34 +24,23 @@ function V2WorkspaceLayout() { const workspaceId = workspaceMatch !== false ? workspaceMatch.workspaceId : null; const collections = useCollections(); - const { machineId, activeHostUrl } = useLocalHostService(); const { ensureWorkspaceInSidebar } = useDashboardSidebarState(); - const { data: workspaces = [], isReady } = useLiveQuery( + const { data: workspaces, isReady } = useLiveQuery( (q) => q .from({ v2Workspaces: collections.v2Workspaces }) - .where(({ v2Workspaces }) => eq(v2Workspaces.id, workspaceId ?? "")) - .select(({ v2Workspaces }) => ({ - id: v2Workspaces.id, - organizationId: v2Workspaces.organizationId, - hostId: v2Workspaces.hostId, - projectId: v2Workspaces.projectId, - branch: v2Workspaces.branch, - })), + .where(({ v2Workspaces }) => eq(v2Workspaces.id, workspaceId ?? "")), [collections, workspaceId], ); - const workspace = workspaces[0] ?? null; - - const isLocal = workspace?.hostId === machineId; - const hostUrl = !workspace - ? null - : isLocal - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${buildHostRoutingKey(workspace.organizationId, workspace.hostId)}`; + const workspace = workspaces?.[0] ?? null; + const inFlight = useWorkspaceCreatesStore((store) => + workspaceId + ? store.entries.find((entry) => entry.snapshot.id === workspaceId) + : undefined, + ); const lastEnsuredWorkspaceIdRef = useRef(null); - useEffect(() => { if (!workspace || lastEnsuredWorkspaceIdRef.current === workspace.id) return; @@ -62,23 +48,35 @@ function V2WorkspaceLayout() { ensureWorkspaceInSidebar(workspace.id, workspace.projectId); }, [ensureWorkspaceInSidebar, workspace]); - if (!workspaceId || !isReady) { - return null; + if (!workspaceId || !isReady || !workspaces) { + return
; } - if (!workspace || !hostUrl) { - return ; + if (!workspace) { + if (inFlight?.state === "creating") { + return ( + + ); + } + if (inFlight?.state === "error") { + return ( + + ); + } + return ; } return ( - getHostServiceHeaders(hostUrl)} - wsToken={() => getHostServiceWsToken(hostUrl)} - > + - + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/WorkspaceProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/WorkspaceProvider.tsx new file mode 100644 index 00000000000..0e373546db8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/WorkspaceProvider.tsx @@ -0,0 +1,60 @@ +import type { SelectV2Workspace } from "@superset/db/schema"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; +import { createContext, type ReactNode, useContext } from "react"; +import { env } from "renderer/env.renderer"; +import { + getHostServiceHeaders, + getHostServiceWsToken, +} from "renderer/lib/host-service-auth"; +import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; +import { WorkspaceTrpcProvider } from "../WorkspaceTrpcProvider"; + +interface WorkspaceContextValue { + workspace: SelectV2Workspace; + hostUrl: string; +} + +const WorkspaceContext = createContext(null); + +export function WorkspaceProvider({ + workspace, + children, +}: { + workspace: SelectV2Workspace; + children: ReactNode; +}) { + const { machineId, activeHostUrl } = useLocalHostService(); + const hostUrl = + workspace.hostId === machineId + ? activeHostUrl + : `${env.RELAY_URL}/hosts/${buildHostRoutingKey( + workspace.organizationId, + workspace.hostId, + )}`; + + if (!hostUrl) { + return
; + } + + return ( + + getHostServiceHeaders(hostUrl)} + wsToken={() => getHostServiceWsToken(hostUrl)} + > + {children} + + + ); +} + +export function useWorkspace(): WorkspaceContextValue { + const ctx = useContext(WorkspaceContext); + if (!ctx) { + throw new Error("useWorkspace must be used within WorkspaceProvider"); + } + return ctx; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/index.ts new file mode 100644 index 00000000000..49fec6fbe16 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceProvider/index.ts @@ -0,0 +1 @@ +export { useWorkspace, WorkspaceProvider } from "./WorkspaceProvider";