feat(web): W3-1 shared console routes, nav, api/sse/query-keys and shared components
- routes.ts registers all workspaces under /console (+ detail routes, engine, audit, login) - api.ts: cookie session first (credentials include, CSRF header), Bearer only with stored key, 401 -> /login - sse.ts: shared EventSource with ?after=seq catch-up, backoff reconnect, per-type subscribe - shared components: data-table, filter-bar, detail-sheet, timeline, json-view, confirm-dialog, form-field, kv-list, stat-tile, error/loading states; ui tabs and textarea Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a24bda952b
commit
e5e42b1f0d
32 files changed
+1286
-71
No files matched your search
@@ -0,0 +1,86 @@
|
||||
import { Check, ChevronRight, Copy } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
/**
|
||||
* 只读 JSON 展示:等宽、自动换行、限高滚动,右上角复制。
|
||||
* label 存在时可折叠(defaultOpen 控制初始展开)。null/undefined 显示"无"。
|
||||
*/
|
||||
export function JsonView({
|
||||
value,
|
||||
label,
|
||||
defaultOpen = true,
|
||||
maxHeight = "20rem",
|
||||
tone = "neutral",
|
||||
className,
|
||||
}: {
|
||||
value: unknown;
|
||||
label?: string;
|
||||
defaultOpen?: boolean;
|
||||
maxHeight?: string;
|
||||
/** destructive:错误负载 */
|
||||
tone?: "neutral" | "destructive";
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const empty = value === null || value === undefined;
|
||||
const text = empty ? "" : typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
} catch {
|
||||
toast.error("复制失败");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("min-w-0", className)}>
|
||||
{label ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
className="mb-1.5 flex cursor-pointer items-center gap-1 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase hover:text-foreground"
|
||||
>
|
||||
<ChevronRight className={cn("h-3.5 w-3.5 transition-transform duration-base", open && "rotate-90")} />
|
||||
{label}
|
||||
</button>
|
||||
) : null}
|
||||
{open || !label ? (
|
||||
empty ? (
|
||||
<p className="text-caption text-muted-foreground">无</p>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-lg border bg-foreground/[0.03]",
|
||||
tone === "destructive" ? "border-destructive/30" : "border-border",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
aria-label="复制"
|
||||
className="absolute top-1.5 right-1.5 grid h-7 w-7 cursor-pointer place-items-center rounded-full text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<pre
|
||||
className={cn(
|
||||
"overflow-auto p-3 pr-10 font-mono text-meta leading-relaxed break-all whitespace-pre-wrap",
|
||||
tone === "destructive" ? "text-destructive" : "text-foreground",
|
||||
)}
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user