diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index ce9b80d49..f4c5c03ac 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -73,6 +73,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer"; import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -99,6 +100,32 @@ const NON_DELETABLE_DOCUMENT_TYPES: readonly string[] = ["SURFSENSE_DOCS"]; const LOCAL_FILESYSTEM_TRUST_KEY = "surfsense.local-filesystem-trust.v1"; const MAX_LOCAL_FILESYSTEM_ROOTS = 5; +function CloudDocumentsSkeleton() { + const rows = [ + { id: "row-1", widthClass: "w-44" }, + { id: "row-2", widthClass: "w-32" }, + { id: "row-3", widthClass: "w-32" }, + { id: "row-4", widthClass: "w-44" }, + { id: "row-5", widthClass: "w-32" }, + { id: "row-6", widthClass: "w-32" }, + { id: "row-7", widthClass: "w-44" }, + { id: "row-8", widthClass: "w-32" }, + ]; + + return ( +
+
+ {rows.map((row) => ( +
+ + +
+ ))} +
+
+ ); +} + type FilesystemSettings = { mode: "cloud" | "desktop_local_folder"; localRootPaths: string[]; @@ -407,8 +434,8 @@ function AuthenticatedDocumentsSidebar({ ); // Zero queries for tree data - const [zeroFolders] = useQuery(queries.folders.bySpace({ searchSpaceId })); - const [zeroAllDocs] = useQuery(queries.documents.bySpace({ searchSpaceId })); + const [zeroFolders, zeroFoldersResult] = useQuery(queries.folders.bySpace({ searchSpaceId })); + const [zeroAllDocs, zeroAllDocsResult] = useQuery(queries.documents.bySpace({ searchSpaceId })); const [agentCreatedDocs, setAgentCreatedDocs] = useAtom(agentCreatedDocumentsAtom); const treeFolders: FolderDisplay[] = useMemo( @@ -989,6 +1016,9 @@ function AuthenticatedDocumentsSidebar({ const showFilesystemTabs = !isMobile && !!electronAPI && !!filesystemSettings; const currentFilesystemTab = filesystemSettings?.mode === "desktop_local_folder" ? "local" : "cloud"; + const showCloudSkeleton = + currentFilesystemTab === "cloud" && + (zeroFoldersResult.type !== "complete" || zeroAllDocsResult.type !== "complete"); const cloudContent = ( <> @@ -1101,45 +1131,49 @@ function AuthenticatedDocumentsSidebar({ )} - { - openEditorPanel({ - documentId: doc.id, - searchSpaceId, - title: doc.title, - }); - }} - onEditDocument={(doc) => { - openEditorPanel({ - documentId: doc.id, - searchSpaceId, - title: doc.title, - }); - }} - onDeleteDocument={(doc) => handleDeleteDocument(doc.id)} - onMoveDocument={handleMoveDocument} - onExportDocument={handleExportDocument} - onVersionHistory={(doc) => setVersionDocId(doc.id)} - activeTypes={activeTypes} - onDropIntoFolder={handleDropIntoFolder} - onReorderFolder={handleReorderFolder} - watchedFolderIds={watchedFolderIds} - onRescanFolder={handleRescanFolder} - onStopWatchingFolder={handleStopWatching} - onExportFolder={handleExportFolder} - /> + {showCloudSkeleton ? ( + + ) : ( + { + openEditorPanel({ + documentId: doc.id, + searchSpaceId, + title: doc.title, + }); + }} + onEditDocument={(doc) => { + openEditorPanel({ + documentId: doc.id, + searchSpaceId, + title: doc.title, + }); + }} + onDeleteDocument={(doc) => handleDeleteDocument(doc.id)} + onMoveDocument={handleMoveDocument} + onExportDocument={handleExportDocument} + onVersionHistory={(doc) => setVersionDocId(doc.id)} + activeTypes={activeTypes} + onDropIntoFolder={handleDropIntoFolder} + onReorderFolder={handleReorderFolder} + watchedFolderIds={watchedFolderIds} + onRescanFolder={handleRescanFolder} + onStopWatchingFolder={handleStopWatching} + onExportFolder={handleExportFolder} + /> + )} diff --git a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx index 5b08f2e37..30e532896 100644 --- a/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx +++ b/surfsense_web/components/layout/ui/sidebar/LocalFilesystemBrowser.tsx @@ -1,8 +1,9 @@ "use client"; import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog"; +import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { useElectronAPI } from "@/hooks/use-platform"; import { getSupportedExtensionsSet } from "@/lib/supported-extensions"; @@ -39,6 +40,8 @@ type LocalRootMount = { rootPath: string; }; +type MountLoadStatus = "idle" | "loading" | "complete" | "error"; + const getFolderDisplayName = (rootPath: string): string => rootPath.split(/[\\/]/).at(-1) || rootPath; @@ -79,6 +82,10 @@ export function LocalFilesystemBrowser({ const [rootStateMap, setRootStateMap] = useState>({}); const [expandedFolderKeys, setExpandedFolderKeys] = useState>(new Set()); const [mountByRootKey, setMountByRootKey] = useState>(new Map()); + const [mountStatus, setMountStatus] = useState("idle"); + const [mountRefreshInFlight, setMountRefreshInFlight] = useState(false); + const hasLoadedMountsOnceRef = useRef(false); + const hasResolvedAtLeastOneRootRef = useRef(false); const supportedExtensions = useMemo(() => Array.from(getSupportedExtensionsSet()), []); const isWindowsPlatform = electronAPI?.versions.platform === "win32"; @@ -139,23 +146,44 @@ export function LocalFilesystemBrowser({ useEffect(() => { if (!electronAPI?.getAgentFilesystemMounts) { + setMountStatus("error"); setMountByRootKey(new Map()); return; } let cancelled = false; + const isInitialMountLoad = !hasLoadedMountsOnceRef.current; + if (isInitialMountLoad) { + setMountStatus("loading"); + } else { + setMountRefreshInFlight(true); + } void electronAPI .getAgentFilesystemMounts() .then((mounts: LocalRootMount[]) => { if (cancelled) return; + const knownRootKeys = new Set( + rootPaths.map((rootPath) => normalizeRootPathForLookup(rootPath, isWindowsPlatform)) + ); const next = new Map(); for (const entry of mounts) { - next.set(normalizeRootPathForLookup(entry.rootPath, isWindowsPlatform), entry.mount); + const normalizedRootKey = normalizeRootPathForLookup(entry.rootPath, isWindowsPlatform); + if (!knownRootKeys.has(normalizedRootKey)) continue; + next.set(normalizedRootKey, entry.mount); } setMountByRootKey(next); + setMountStatus("complete"); + hasLoadedMountsOnceRef.current = true; }) .catch(() => { if (cancelled) return; - setMountByRootKey(new Map()); + if (isInitialMountLoad) { + setMountByRootKey(new Map()); + setMountStatus("error"); + } + }) + .finally(() => { + if (cancelled) return; + setMountRefreshInFlight(false); }); return () => { cancelled = true; @@ -265,6 +293,43 @@ export function LocalFilesystemBrowser({ ); } + const allRootsLoaded = rootPaths.every((rootPath) => { + const state = rootStateMap[rootPath]; + return !!state && !state.loading; + }); + const mountsSettled = mountStatus === "complete" || mountStatus === "error"; + if (allRootsLoaded && mountsSettled && rootPaths.length > 0) { + hasResolvedAtLeastOneRootRef.current = true; + } + const showInitialLoading = + !hasResolvedAtLeastOneRootRef.current && (!allRootsLoaded || !mountsSettled); + + if (showInitialLoading) { + const rows = [ + { id: "local-row-1", widthClass: "w-44" }, + { id: "local-row-2", widthClass: "w-32" }, + { id: "local-row-3", widthClass: "w-32" }, + { id: "local-row-4", widthClass: "w-44" }, + { id: "local-row-5", widthClass: "w-32" }, + { id: "local-row-6", widthClass: "w-32" }, + { id: "local-row-7", widthClass: "w-44" }, + { id: "local-row-8", widthClass: "w-32" }, + ]; + + return ( +
+
+ {rows.map((row) => ( +
+ + +
+ ))} +
+
+ ); + } + return (
{treeByRoot.map(({ rootPath, rootNode, matchCount, totalCount }) => { @@ -273,9 +338,11 @@ export function LocalFilesystemBrowser({ const mount = mountByRootKey.get(rootKey); if (!state || state.loading) { return ( -
- - Loading {getFolderDisplayName(rootPath)}... +
+
+ + Loading {getFolderDisplayName(rootPath)}... +
); } @@ -291,11 +358,24 @@ export function LocalFilesystemBrowser({ return (
{mount ? renderFolder(rootNode, 0, mount) : null} - {!mount && ( + {!mount && (mountRefreshInFlight || mountStatus === "loading") && ( +
+
+ + Loading {getFolderDisplayName(rootPath)}... +
+
+ )} + {!mount && mountStatus === "complete" && !mountRefreshInFlight && (
Unable to resolve mounted root for this folder.
)} + {!mount && mountStatus === "error" && ( +
+ Failed to resolve local folder mounts. +
+ )} {isEmpty && (
No supported files found in this folder.