diff --git a/console/src/components/dialogs.tsx b/console/src/components/dialogs.tsx new file mode 100644 index 0000000..65c79c6 --- /dev/null +++ b/console/src/components/dialogs.tsx @@ -0,0 +1,379 @@ +import { + ArrowRight, + Clock3, + CloudUpload, + FileCode2, + Package, + Play, + RadioTower, + RefreshCw, + ShieldCheck, + SquareTerminal, + X, +} from "lucide-react"; +import { FormEvent, ReactNode, useState } from "react"; +import { InvokeResult, RegisterComponentInput } from "../api"; +import { + FormattedInvocationOutput, + Service, + formatInvocationOutput, + invocationInputSize, + parseInvocationInput, +} from "../console-model"; + +function DialogFrame({ + title, + description, + icon, + onClose, + closeDisabled = false, + children, +}: { + title: string; + description: string; + icon: ReactNode; + onClose: () => void; + closeDisabled?: boolean; + children: ReactNode; +}) { + return ( +
+ { + if (closeDisabled) event.preventDefault(); + else onClose(); + }} + onKeyDown={(event) => { + if (event.key === "Escape" && !closeDisabled) onClose(); + }} + > +
+
+ {icon} +
+

{title}

+

{description}

+
+
+ +
+ {children} +
+
+ ); +} + +export function ActivateDeploymentDialog({ + service, + currentRevision, + submitting, + onClose, + onConfirm, +}: { + service: Service; + currentRevision: string | null; + submitting: boolean; + onClose: () => void; + onConfirm: () => void; +}) { + return ( + } + onClose={onClose} + closeDisabled={submitting} + > +
+
+
+ 当前版本 + {currentRevision ?? "尚未部署"} +
+ +
+ 目标版本 + {service.revision} +
+
+

+ Wasmeld 将先确认 {service.id}@{service.revision} 的 Actor 可用,再更新对外路由。 +

+
+
+ + +
+
+ ); +} + +export function DeployDialog({ + onClose, + onRegister, +}: { + onClose: () => void; + onRegister: (input: RegisterComponentInput) => Promise; +}) { + const [file, setFile] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + const fileName = file?.name ?? ""; + + async function submit(event: FormEvent) { + event.preventDefault(); + if (!file) return; + if (!file.name.toLowerCase().endsWith(".wasmpkg")) { + setError("请选择 .wasmpkg 组件包"); + return; + } + setSubmitting(true); + setError(""); + try { + await onRegister({ file }); + } catch (submitError) { + setError(submitError instanceof Error ? submitError.message : "组件注册失败"); + setSubmitting(false); + } + } + + return ( + } + onClose={onClose} + > +
+ + + {error &&
{error}
} + +
+ + +
+
+
+ ); +} + +export function WitPackageDialog({ + onClose, + onPublish, +}: { + onClose: () => void; + onPublish: (file: File) => Promise; +}) { + const [file, setFile] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + const fileName = file?.name ?? ""; + + async function submit(event: FormEvent) { + event.preventDefault(); + if (!file) return; + if (!file.name.toLowerCase().endsWith(".wasm")) { + setError("请选择由 wasmeld wit build 生成的 .wasm 文件"); + return; + } + if (file.size > 4 * 1024 * 1024) { + setError("WIT 包不能超过 4 MB"); + return; + } + setSubmitting(true); + setError(""); + try { + await onPublish(file); + } catch (submitError) { + setError(submitError instanceof Error ? submitError.message : "WIT 包发布失败"); + setSubmitting(false); + } + } + + return ( + } + onClose={onClose} + > +
+ + + {error &&
{error}
} + +
+ + +
+
+
+ ); +} + +export function InvokeDialog({ + service, + onClose, + onInvoke, +}: { + service: Service; + onClose: () => void; + onInvoke: (service: Service, input: Uint8Array) => Promise; +}) { + const [input, setInput] = useState(service.id === "echo" ? "hello wasm" : ""); + const [format, setFormat] = useState<"utf8" | "hex">("utf8"); + const [output, setOutput] = useState(""); + const [outputBytes, setOutputBytes] = useState(null); + const [outputFormat, setOutputFormat] = useState(null); + const [running, setRunning] = useState(false); + const [failed, setFailed] = useState(false); + const [latency, setLatency] = useState(null); + + async function invoke() { + setRunning(true); + setOutput(""); + setOutputBytes(null); + setOutputFormat(null); + setFailed(false); + setLatency(null); + try { + const result = await onInvoke(service, parseInvocationInput(input, format)); + const formatted = formatInvocationOutput(service, result.output, format); + setOutput(formatted.text); + setOutputBytes(result.outputBytes); + setOutputFormat(formatted); + setLatency(result.latencyMs); + } catch (invokeError) { + setFailed(true); + setOutput(invokeError instanceof Error ? invokeError.message : "组件调用失败"); + } finally { + setRunning(false); + } + } + + return ( + } + onClose={onClose} + > +
+
+
+ + +
+ {invocationInputSize(input, format)} B +
+