196 lines
7.9 KiB
TypeScript
196 lines
7.9 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|