- 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]>
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
import { LoaderCircle } from "lucide-react";
|
|
import { type ReactNode, useState } from "react";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "~/components/ui/alert-dialog.tsx";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
|
|
/**
|
|
* 确认弹窗。onConfirm 可返回 Promise:执行中按钮禁用并转圈,resolve 后自动关闭,reject 时保持打开
|
|
* (错误提示由调用方 toast)。受控(open/onOpenChange)或非受控(trigger)两种用法。
|
|
* <ConfirmDialog trigger={<Button variant="destructive">删除</Button>} title="删除实例?"
|
|
* description="…" confirmLabel="删除" destructive onConfirm={() => del.mutateAsync(id)} />
|
|
*/
|
|
export function ConfirmDialog({
|
|
trigger,
|
|
open: openProp,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
confirmLabel = "确认",
|
|
cancelLabel = "取消",
|
|
destructive = false,
|
|
onConfirm,
|
|
children,
|
|
}: {
|
|
trigger?: ReactNode;
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
title: ReactNode;
|
|
description?: ReactNode;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
destructive?: boolean;
|
|
onConfirm: () => unknown | Promise<unknown>;
|
|
/** 描述下方的额外内容(输入确认等) */
|
|
children?: ReactNode;
|
|
}) {
|
|
const [inner, setInner] = useState(false);
|
|
const [pending, setPending] = useState(false);
|
|
const open = openProp ?? inner;
|
|
const setOpen = (o: boolean) => {
|
|
if (pending) return;
|
|
setInner(o);
|
|
onOpenChange?.(o);
|
|
};
|
|
|
|
const run = async () => {
|
|
setPending(true);
|
|
try {
|
|
await onConfirm();
|
|
setPending(false);
|
|
setInner(false);
|
|
onOpenChange?.(false);
|
|
} catch {
|
|
setPending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
{trigger ? <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger> : null}
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
{description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}
|
|
</AlertDialogHeader>
|
|
{children}
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={pending}>{cancelLabel}</AlertDialogCancel>
|
|
<Button variant={destructive ? "destructive" : "default"} onClick={run} disabled={pending}>
|
|
{pending ? <LoaderCircle className="animate-spin" /> : null}
|
|
{confirmLabel}
|
|
</Button>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|