Skip to content

Commit 539e163

Browse files
committed
Fix hosted folder browsing and sync feedback
1 parent ed0e31d commit 539e163

6 files changed

Lines changed: 486 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/src/lib/dashboard-api.ts

Lines changed: 143 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,31 @@ type DeviceWire = {
110110
display_name: string
111111
}
112112

113+
type SharedFolderTreeWire = {
114+
revision_id: string | null
115+
file_count: number
116+
entries: SharedFolderTreeEntryWire[]
117+
revisions: SharedFolderRevisionWire[]
118+
}
119+
120+
type SharedFolderTreeEntryWire = {
121+
path: string
122+
name: string
123+
kind: "directory" | "file" | "symlink" | "unsupported"
124+
parent_path: string | null
125+
size_bytes: number | null
126+
updated_at: string
127+
object_id: string | null
128+
}
129+
130+
type SharedFolderRevisionWire = {
131+
id: string
132+
parent_id: string | null
133+
boundary: string
134+
created_at: string
135+
changed_files: number
136+
}
137+
113138
type BindhubSessionWire = {
114139
account_id: string
115140
session_id: string
@@ -315,12 +340,20 @@ async function dataFromApi(
315340
fetchJson<SharedFolderWire[]>(`${baseUrl}/v1/shared-folders`, headers),
316341
fetchJson<DeviceWire[]>(`${baseUrl}/v1/devices`, headers),
317342
])
343+
const trees = await Promise.all(
344+
folders.map((folder) =>
345+
fetchJson<SharedFolderTreeWire>(
346+
`${baseUrl}/v1/loom/shared-folders/${encodeURIComponent(folder.id)}/tree`,
347+
headers
348+
).catch(() => emptyHostedTree())
349+
)
350+
)
318351

319352
return {
320353
identity,
321354
source,
322355
overview: overview(folders, machines),
323-
folders: folders.map((folder) => ({
356+
folders: folders.map((folder, index) => ({
324357
id: folder.id,
325358
displayName: folder.display_name,
326359
description: "Hosted shared folder",
@@ -330,25 +363,26 @@ async function dataFromApi(
330363
visibility: folder.role === "viewer" ? "team" : "private",
331364
syncStatus: "synced",
332365
hydrationState: "fully-local",
333-
lastCheckpoint: "Synced from Bindhub API",
334-
updatedAt: "Known to Bindhub API",
366+
lastCheckpoint: trees[index].revision_id ?? "No synced revision yet",
367+
updatedAt: latestTreeTimestamp(trees[index]) ?? "Known to Bindhub API",
335368
sizeLabel: "Remote metadata",
336-
fileCount: 0,
369+
fileCount: trees[index].file_count,
337370
machineCount: machines.length,
338-
entries: hostedPlaceholderEntries(folder.display_name),
339-
revisions: hostedPlaceholderRevisions(),
371+
entries: hostedEntriesFromTree(trees[index]),
372+
revisions: hostedRevisionsFromTree(trees[index]),
340373
settings: hostedPlaceholderSettings(folder.role === "viewer" ? "team" : "private"),
341374
recentActivity: [
342375
{
343376
id: `${folder.id}-api-sync`,
344377
title: "Synced from hosted API",
345378
detail:
346-
"Folder membership is live. File-tree metadata will come from Loom revisions.",
379+
trees[index].revision_id
380+
? `${trees[index].file_count} files are visible from the latest Loom revision.`
381+
: "Folder membership is live. No Loom revision has synced yet.",
347382
timestamp: "Now",
348383
},
349384
],
350-
readme:
351-
"This folder is backed by hosted bindhub metadata. File browsing will become exact once the API exposes Loom folder revision trees.",
385+
readme: hostedReadmeFromTree(folder.display_name, trees[index]),
352386
})),
353387
machines: machines.map((machine) => ({
354388
id: machine.id,
@@ -732,37 +766,109 @@ function fixtureDashboardData(
732766
}
733767
}
734768

735-
function hostedPlaceholderEntries(displayName: string): SharedFolderEntry[] {
736-
return [
737-
{
738-
path: "README.md",
739-
name: "README.md",
740-
kind: "file",
741-
parentPath: null,
742-
sizeLabel: "remote",
743-
updatedAt: "Awaiting Loom tree",
769+
function emptyHostedTree(): SharedFolderTreeWire {
770+
return {
771+
revision_id: null,
772+
file_count: 0,
773+
entries: [],
774+
revisions: [],
775+
}
776+
}
777+
778+
function hostedEntriesFromTree(tree: SharedFolderTreeWire): SharedFolderEntry[] {
779+
return tree.entries
780+
.filter(
781+
(
782+
entry
783+
): entry is SharedFolderTreeEntryWire & { kind: "file" | "directory" } =>
784+
entry.kind === "file" || entry.kind === "directory"
785+
)
786+
.map((entry) => ({
787+
path: entry.path,
788+
name: entry.name,
789+
kind: entry.kind,
790+
parentPath: entry.parent_path,
791+
sizeLabel:
792+
entry.kind === "directory" ? "folder" : formatBytes(entry.size_bytes ?? 0),
793+
updatedAt: entry.updated_at,
744794
hydrationState: "remote-only",
745-
language: "Markdown",
746-
summary: `${displayName} file tree metadata is not exposed by the API yet.`,
747-
content:
748-
"This hosted folder exists, but the hosted API does not expose Loom file-tree metadata yet.",
749-
},
750-
]
795+
language: languageForPath(entry.path),
796+
summary:
797+
entry.kind === "directory"
798+
? "Folder in the latest hosted revision."
799+
: "File in the latest hosted revision.",
800+
}))
751801
}
752802

753-
function hostedPlaceholderRevisions(): FolderRevision[] {
754-
return [
755-
{
756-
id: "hosted-current",
757-
label: "Hosted cursor",
758-
message: "Folder membership is live; Loom revision history is pending API support.",
759-
kind: "auto",
760-
createdAt: "Known to Bindhub API",
761-
author: "Bindhub hosted",
762-
changedFiles: 0,
763-
pinned: false,
764-
},
765-
]
803+
function hostedRevisionsFromTree(tree: SharedFolderTreeWire): FolderRevision[] {
804+
return tree.revisions.map((revision) => ({
805+
id: revision.id,
806+
label: revision.id,
807+
message: `Loom ${revision.boundary} revision`,
808+
kind: "auto",
809+
createdAt: revision.created_at,
810+
author: "Bindhub hosted",
811+
changedFiles: revision.changed_files,
812+
pinned: false,
813+
}))
814+
}
815+
816+
function hostedReadmeFromTree(displayName: string, tree: SharedFolderTreeWire) {
817+
if (!tree.revision_id) {
818+
return `${displayName} is known to Bindhub, but no synced folder revision is available yet.`
819+
}
820+
return `${displayName} is backed by hosted Loom revision ${tree.revision_id}.`
821+
}
822+
823+
function latestTreeTimestamp(tree: SharedFolderTreeWire) {
824+
return tree.revisions.at(-1)?.created_at ?? tree.entries[0]?.updated_at ?? null
825+
}
826+
827+
function formatBytes(bytes: number) {
828+
if (bytes >= 1024 * 1024 * 1024) {
829+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`
830+
}
831+
if (bytes >= 1024 * 1024) {
832+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
833+
}
834+
if (bytes >= 1024) {
835+
return `${(bytes / 1024).toFixed(1)} KiB`
836+
}
837+
return `${bytes} bytes`
838+
}
839+
840+
function languageForPath(path: string) {
841+
const extension = path.split(".").pop()?.toLowerCase()
842+
switch (extension) {
843+
case "astro":
844+
return "Astro"
845+
case "css":
846+
return "CSS"
847+
case "go":
848+
return "Go"
849+
case "html":
850+
return "HTML"
851+
case "js":
852+
case "jsx":
853+
return "JavaScript"
854+
case "json":
855+
return "JSON"
856+
case "md":
857+
case "mdx":
858+
return "Markdown"
859+
case "rs":
860+
return "Rust"
861+
case "ts":
862+
case "tsx":
863+
return "TypeScript"
864+
case "toml":
865+
return "TOML"
866+
case "yml":
867+
case "yaml":
868+
return "YAML"
869+
default:
870+
return undefined
871+
}
766872
}
767873

768874
function hostedPlaceholderSettings(

bindhub/crates/bindhub-api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ blake3 = "1"
1111
bindhub-platform = { path = "../bindhub-platform" }
1212
bindhub-sync = { path = "../bindhub-sync" }
1313
loom-core = { path = "../../../loom/crates/loom-core" }
14+
loom-pack = { path = "../../../loom/crates/loom-pack" }
1415
postgres = "0.19"
1516
serde = { version = "1", features = ["derive"] }
1617
serde_json = "1"

0 commit comments

Comments
 (0)