import { ArrowRight, Clock3, CloudUpload, FileCode2, Package, Play, RadioTower, RefreshCw, ShieldCheck, SquareTerminal, X, } from "lucide-solid"; import { createMemo, createSignal, onCleanup, onMount, Show, type JSXElement } from "solid-js"; import type { InvokeResult } from "../lib/api"; import { formatInvocationOutput, invocationInputSize, parseInvocationInput, type Service, } from "../lib/model"; export type DialogState = | { type: "register" } | { type: "wit-publish" } | { type: "invoke"; service: Service } | { type: "activate"; service: Service; currentRevision: string | null }; export function DialogLayer(props: { dialog: DialogState; busy: boolean; onClose: () => void; onRegister: (file: File) => Promise; onPublishWit: (file: File) => Promise; onInvoke: (service: Service, input: Uint8Array) => Promise; onActivate: (service: Service) => Promise; }) { return ( <> ).service} onClose={props.onClose} onInvoke={props.onInvoke} /> } busy={props.busy} onClose={props.onClose} onActivate={props.onActivate} /> ); } function DialogFrame(props: { title: string; description: string; icon: JSXElement; closeDisabled?: boolean; onClose: () => void; children: JSXElement; footer: JSXElement; }) { const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape" && !props.closeDisabled) props.onClose(); }; onMount(() => window.addEventListener("keydown", onKeyDown)); onCleanup(() => window.removeEventListener("keydown", onKeyDown)); return ( ); } function UploadDialog(props: { kind: "component" | "wit"; busy: boolean; onClose: () => void; onSubmit: (file: File) => Promise; }) { const [file, setFile] = createSignal(null); const [error, setError] = createSignal(""); const isWit = () => props.kind === "wit"; const title = () => (isWit() ? "发布 WIT 包" : "注册组件包"); async function submit() { const selected = file(); if (!selected || props.busy) return; const extension = isWit() ? ".wasm" : ".wasmpkg"; const limit = isWit() ? 4 * 1024 * 1024 : 64 * 1024 * 1024; if (!selected.name.toLowerCase().endsWith(extension)) { setError(`请选择 ${extension} 文件`); return; } if (selected.size > limit) { setError(`文件不能超过 ${isWit() ? "4 MB" : "64 MB"}`); return; } setError(""); await props.onSubmit(selected).catch((submitError: unknown) => { setError(submitError instanceof Error ? submitError.message : `${title()}失败`); }); } return ( : } closeDisabled={props.busy} onClose={props.onClose} footer={ <> } >
{error()}
); } function ActivateDialog(props: { dialog: Extract; busy: boolean; onClose: () => void; onActivate: (service: Service) => Promise; }) { return ( } closeDisabled={props.busy} onClose={props.onClose} footer={ <> } >
当前版本 {props.dialog.currentRevision ?? "尚未部署"}
目标版本 {props.dialog.service.revision}

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

); } function InvokeDialog(props: { service: Service; onClose: () => void; onInvoke: (service: Service, input: Uint8Array) => Promise; }) { const [input, setInput] = createSignal(props.service.id === "echo" ? "hello wasm" : ""); const [format, setFormat] = createSignal<"utf8" | "hex">("utf8"); const [output, setOutput] = createSignal(""); const [outputMeta, setOutputMeta] = createSignal(""); const [running, setRunning] = createSignal(false); const [failed, setFailed] = createSignal(false); const inputBytes = createMemo(() => invocationInputSize(input(), format())); async function invoke() { setRunning(true); setOutput(""); setOutputMeta(""); setFailed(false); try { const result = await props.onInvoke(props.service, parseInvocationInput(input(), format())); const formatted = formatInvocationOutput(props.service, result.output, format()); setOutput(formatted.text); setOutputMeta( `${result.outputBytes} B · ${formatted.format}${formatted.automatic ? " · 自动" : ""} · ${result.latencyMs} ms`, ); } catch (error) { setFailed(true); setOutput(error instanceof Error ? error.message : "组件调用失败"); } finally { setRunning(false); } } return ( } closeDisabled={running()} onClose={props.onClose} footer={ <> } >
{inputBytes()} B