SPB Git

Repository Public MIT

file-explorer.tsx 4.1 KB 91 lines tsx

1"use client";2import { FileCode2, FileText, Lock } from "lucide-react";3import { useState } from "react";4import { bytes } from "@/lib/format";56interface F {7  path: string;8  sizeBytes: number;9  sha256: string;10  executable: boolean;11  previewable: boolean;12}1314/** Read-only browser for release files. Content is fetched as text and rendered as text — never as HTML. */15export function FileExplorer({ namespace, slug, version, files }: { namespace: string; slug: string; version: string; files: F[] }) {16  const [open, setOpen] = useState<string | null>(null);17  const [content, setContent] = useState<string>("");18  const [state, setState] = useState<"idle" | "loading" | "error">("idle");1920  async function show(f: F) {21    setOpen(f.path);22    // Binary or large files are never fetched or rendered: only their size and checksum are shown.23    if (!f.previewable) {24      setContent("");25      setState("idle");26      return;27    }28    setState("loading");29    try {30      const res = await fetch(`/api/v1/artifacts/${namespace}/${slug}/versions/${encodeURIComponent(version)}/files?path=${encodeURIComponent(f.path)}`);31      if (!res.ok) throw new Error(String(res.status));32      setContent(((await res.json()) as { content: string }).content);33      setState("idle");34    } catch {35      setState("error");36    }37  }38  const current = files.find((f) => f.path === open);3940  return (41    <div className="grid gap-4 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]">42      <ul className="card max-h-[28rem] overflow-y-auto p-1.5" aria-label="Files in this release">43        {files.map((f) => (44          <li key={f.path}>45            <button46              type="button"47              onClick={() => show(f)}48              aria-current={open === f.path}49              title={f.previewable ? f.path : `${f.path} — binary or large file, not previewed`}50              className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left font-mono text-[12px] ${open === f.path ? "bg-raised text-text" : "text-muted hover:bg-raised hover:text-text"}`}51            >52              {f.previewable ? f.executable ? <FileCode2 size={13} aria-hidden className="shrink-0 text-warn" /> : <FileText size={13} aria-hidden className="shrink-0" /> : <Lock size={13} aria-hidden className="shrink-0" />}53              <span className="min-w-0 flex-1 truncate">{f.path}</span>54              <span className="shrink-0 text-[10px] text-faint">{bytes(f.sizeBytes)}</span>55            </button>56          </li>57        ))}58      </ul>59      <div className="card min-h-40 overflow-hidden">60        {!current ? (61          <p className="p-5 text-sm text-muted">Select a file to preview it. Files are shown as plain text and never executed.</p>62        ) : (63          <>64            <div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-line px-4 py-2 font-mono text-[11px] text-faint">65              <span className="text-text">{current.path}</span>66              <span>{bytes(current.sizeBytes)}</span>67              {current.executable && <span className="text-warn">executable</span>}68              <span className="truncate" title={current.sha256}>sha256 {current.sha256.slice(0, 16)}…</span>69            </div>70            {state === "loading" ? (71              <div className="space-y-2 p-4" aria-busy="true" aria-label="Loading file">72                <div className="skeleton h-3 w-2/3" />73                <div className="skeleton h-3 w-1/2" />74                <div className="skeleton h-3 w-3/5" />75              </div>76            ) : state === "error" ? (77              <p className="p-5 text-sm text-danger" role="alert">This file could not be loaded.</p>78            ) : !current.previewable ? (79              <p className="p-5 text-sm text-muted" data-testid="binary-file">Binary file, {bytes(current.sizeBytes)}. It is not previewed here; download the archive and check its SHA-256 to inspect it.</p>80            ) : (81              <pre className="max-h-[32rem] overflow-auto bg-code p-4 font-mono text-[12.5px] leading-relaxed text-text" tabIndex={0}>82                <code>{content}</code>83              </pre>84            )}85          </>86        )}87      </div>88    </div>89  );90}91
Files main
Menu

Theme