Spaces:
Running
Running
import { useState } from "react"; | |
// The directory browser. | |
import { Link, useNavigate, useParams } from "react-router"; | |
import useSWR from "swr"; | |
import type { DirectoryEntry } from "./apiTypes.ts"; | |
// @ts-ignore | |
import File from "~icons/tabler/file"; | |
// @ts-ignore | |
import FilePlus from "~icons/tabler/file-plus"; | |
// @ts-ignore | |
import Folder from "~icons/tabler/folder"; | |
// @ts-ignore | |
import FolderPlus from "~icons/tabler/folder-plus"; | |
// @ts-ignore | |
import Home from "~icons/tabler/home"; | |
// @ts-ignore | |
import Trash from "~icons/tabler/trash"; | |
import logo from "./assets/logo.png"; | |
const fetcher = (url: string) => fetch(url).then((res) => res.json()); | |
export default function () { | |
const { path } = useParams(); | |
const encodedPath = encodeURIComponent(path || ""); | |
const list = useSWR(`/api/dir/list?path=${encodedPath}`, fetcher, { | |
dedupingInterval: 0, | |
}); | |
const navigate = useNavigate(); | |
const [isCreatingDir, setIsCreatingDir] = useState(false); | |
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); | |
function link(item: DirectoryEntry) { | |
if (item.type === "directory") { | |
return `/dir/${item.name}`; | |
} | |
return `/edit/${item.name}`; | |
} | |
function shortName(item: DirectoryEntry) { | |
return item.name.split("/").pop(); | |
} | |
function newName(list: DirectoryEntry[], baseName = "Untitled") { | |
let i = 0; | |
while (true) { | |
const name = `${baseName}${i ? ` ${i}` : ""}`; | |
if (!list.find((item) => item.name === name)) { | |
return name; | |
} | |
i++; | |
} | |
} | |
function newWorkspaceIn( | |
path: string, | |
list: DirectoryEntry[], | |
workspaceName?: string, | |
) { | |
const pathSlash = path ? `${path}/` : ""; | |
const name = workspaceName || newName(list); | |
navigate(`/edit/${pathSlash}${name}`, { replace: true }); | |
} | |
async function newFolderIn( | |
path: string, | |
list: DirectoryEntry[], | |
folderName?: string, | |
) { | |
const name = folderName || newName(list, "New Folder"); | |
const pathSlash = path ? `${path}/` : ""; | |
const res = await fetch("/api/dir/mkdir", { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ path: pathSlash + name }), | |
}); | |
if (res.ok) { | |
navigate(`/dir/${pathSlash}${name}`); | |
} else { | |
alert("Failed to create folder."); | |
} | |
} | |
async function deleteItem(item: DirectoryEntry) { | |
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) | |
return; | |
const pathSlash = path ? `${path}/` : ""; | |
const apiPath = | |
item.type === "directory" ? "/api/dir/delete" : "/api/delete"; | |
await fetch(apiPath, { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify({ path: pathSlash + item.name }), | |
}); | |
} | |
return ( | |
<div className="directory"> | |
<div className="logo"> | |
<a href="https://lynxkite.com/"> | |
<img src={logo} className="logo-image" alt="LynxKite logo" /> | |
</a> | |
<div className="tagline">The Complete Graph Data Science Platform</div> | |
</div> | |
<div className="entry-list"> | |
{list.error && <p className="error">{list.error.message}</p>} | |
{list.isLoading && ( | |
<output className="loading spinner-border"> | |
<span className="visually-hidden">Loading...</span> | |
</output> | |
)} | |
{list.data && ( | |
<> | |
<div className="actions"> | |
<div className="new-workspace"> | |
{isCreatingWorkspace && ( | |
// @ts-ignore | |
<form | |
onSubmit={(e) => { | |
e.preventDefault(); | |
newWorkspaceIn( | |
path || "", | |
list.data, | |
( | |
e.target as HTMLFormElement | |
).workspaceName.value.trim(), | |
); | |
}} | |
> | |
<input | |
type="text" | |
name="workspaceName" | |
defaultValue={newName(list.data)} | |
placeholder={newName(list.data)} | |
/> | |
</form> | |
)} | |
<button | |
type="button" | |
onClick={() => setIsCreatingWorkspace(true)} | |
> | |
<FolderPlus /> New workspace | |
</button> | |
</div> | |
<div className="new-folder"> | |
{isCreatingDir && ( | |
// @ts-ignore | |
<form | |
onSubmit={(e) => { | |
e.preventDefault(); | |
newFolderIn( | |
path || "", | |
list.data, | |
(e.target as HTMLFormElement).folderName.value.trim(), | |
); | |
}} | |
> | |
<input | |
type="text" | |
name="folderName" | |
defaultValue={newName(list.data)} | |
placeholder={newName(list.data)} | |
/> | |
</form> | |
)} | |
<button type="button" onClick={() => setIsCreatingDir(true)}> | |
<FolderPlus /> New folder | |
</button> | |
</div> | |
</div> | |
{path && ( | |
<div className="breadcrumbs"> | |
<Link to="/dir/"> | |
<Home /> | |
</Link>{" "} | |
<span className="current-folder">{path}</span> | |
</div> | |
)} | |
{list.data.map((item: DirectoryEntry) => ( | |
<div key={item.name} className="entry"> | |
<Link key={link(item)} to={link(item)}> | |
{item.type === "directory" ? <Folder /> : <File />} | |
{shortName(item)} | |
</Link> | |
<button | |
type="button" | |
onClick={() => { | |
deleteItem(item); | |
}} | |
> | |
<Trash /> | |
</button> | |
</div> | |
))} | |
</> | |
)} | |
</div>{" "} | |
</div> | |
); | |
} | |