feat(console-web): replace TanStack UI with Solid host pages
- move the management frontend under the wasmeld-console crate - use a persistent Host shell with disposable iframe page documents - version Host/Page postMessage state and command contracts - migrate runtime, service, WIT, invocation, and settings workflows - add Tailwind CSS v4, responsive layouts, and Host-owned dialogs - emit a dependency-free PWA worker with API cache exclusions - remove the superseded root TanStack Start application
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
Box,
|
||||
Braces,
|
||||
CircleStop,
|
||||
Play,
|
||||
RadioTower,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Search,
|
||||
SquareTerminal,
|
||||
} from "lucide-solid";
|
||||
import { For, Show } from "solid-js";
|
||||
import { serviceKey, type Service } from "../../lib/model";
|
||||
import type { HostSharedState, PageCommand } from "../../sdk/protocol";
|
||||
import { EmptyState } from "./empty-state";
|
||||
import { StatusBadge } from "./status-badge";
|
||||
|
||||
export function ServiceTable(props: {
|
||||
services: Service[];
|
||||
state: HostSharedState;
|
||||
command: (command: PageCommand) => void;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Show
|
||||
when={props.services.length > 0}
|
||||
fallback={
|
||||
<EmptyState icon={Search} title="没有匹配的服务" detail="调整搜索关键词或状态筛选。" />
|
||||
}
|
||||
>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>服务</th>
|
||||
<th>状态</th>
|
||||
<Show when={!props.compact}>
|
||||
<th>制品</th>
|
||||
<th>Host 能力</th>
|
||||
</Show>
|
||||
<th>调用</th>
|
||||
<th>延迟</th>
|
||||
<th>
|
||||
<span class="sr-only">操作</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={props.services}>
|
||||
{(service) => {
|
||||
const key = serviceKey(service);
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex min-w-48 items-center gap-3">
|
||||
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-brand-soft text-brand">
|
||||
<Box size={15} />
|
||||
</span>
|
||||
<span class="min-w-0">
|
||||
<span class="flex items-center gap-2">
|
||||
<strong class="truncate">{service.id}</strong>
|
||||
<Show when={service.active}>
|
||||
<span class="inline-flex items-center gap-1 rounded-sm bg-cyan-soft px-1.5 py-0.5 text-[10px] font-semibold text-cyan-strong">
|
||||
<RadioTower size={10} />
|
||||
对外
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
<small class="mt-0.5 block text-xs text-muted">{service.revision}</small>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<StatusBadge status={service.status} />
|
||||
</td>
|
||||
<Show when={!props.compact}>
|
||||
<td class="code max-w-48 truncate text-muted" title={service.artifact}>
|
||||
{service.artifact}
|
||||
</td>
|
||||
<td>
|
||||
<Show
|
||||
when={service.capabilities.length > 0}
|
||||
fallback={<span class="text-xs text-muted">无</span>}
|
||||
>
|
||||
<span
|
||||
class="inline-flex max-w-56 items-center gap-1.5 text-xs text-muted"
|
||||
title={service.capabilities
|
||||
.map((capability) => capability.interface)
|
||||
.join("\n")}
|
||||
>
|
||||
<Braces size={12} />
|
||||
<span class="truncate">
|
||||
{service.capabilities[0]?.package}/{service.capabilities[0]?.name}
|
||||
</span>
|
||||
<Show when={service.capabilities.length > 1}>
|
||||
<small>+{service.capabilities.length - 1}</small>
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
</td>
|
||||
</Show>
|
||||
<td class="font-mono text-xs">{service.calls.toLocaleString("zh-CN")}</td>
|
||||
<td class="font-mono text-xs">
|
||||
{service.latencyMs === null ? "—" : `${service.latencyMs} ms`}
|
||||
</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="设为对外版本"
|
||||
title={service.active ? "当前对外版本" : "设为对外版本"}
|
||||
disabled={
|
||||
service.active ||
|
||||
props.state.snapshot?.runtime.status !== "running" ||
|
||||
props.state.deploymentAction !== null
|
||||
}
|
||||
onClick={() => props.command({ type: "open-activate", serviceKey: key })}
|
||||
>
|
||||
{props.state.deploymentAction === key ? (
|
||||
<RefreshCw class="spin" size={15} />
|
||||
) : (
|
||||
<RadioTower size={15} />
|
||||
)}
|
||||
</button>
|
||||
<Show
|
||||
when={service.status === "running"}
|
||||
fallback={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`启动 ${service.id}`}
|
||||
title="启动"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "start",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Play size={15} />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`调用 ${service.id}`}
|
||||
title="测试调用"
|
||||
onClick={() => props.command({ type: "open-invoke", serviceKey: key })}
|
||||
>
|
||||
<SquareTerminal size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`重启 ${service.id}`}
|
||||
title="重启"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "restart",
|
||||
})
|
||||
}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`停止 ${service.id}`}
|
||||
title="停止"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "stop",
|
||||
})
|
||||
}
|
||||
>
|
||||
<CircleStop size={15} />
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user