refactor(console-web): centralize page lifecycle and UI composition
Declare navigation, page metadata, lazy loaders, and keep-alive policy in one registry. Retain primary iframe pages until a continuous inactive TTL expires and expose typed show, hide, and unload events through the Page SDK. Move business-aware components out of the UI primitive directory, migrate views and Host dialogs to variant primitives and local Tailwind classes, and remove the global component style layer. Keep native dialogs inside the overlay flex context so they remain centered. Cover iframe expiration, lifecycle dispatch, UI dependency direction, and global style ownership with focused tests.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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 "../feedback/empty-state";
|
||||
import { IconButton } from "../ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
|
||||
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="调整搜索关键词或状态筛选。" />
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>服务</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<Show when={!props.compact}>
|
||||
<TableHead>制品</TableHead>
|
||||
<TableHead>Host 能力</TableHead>
|
||||
</Show>
|
||||
<TableHead>调用</TableHead>
|
||||
<TableHead>延迟</TableHead>
|
||||
<TableHead>
|
||||
<span class="sr-only">操作</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<For each={props.services}>
|
||||
{(service) => {
|
||||
const key = serviceKey(service);
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<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>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={service.status} />
|
||||
</TableCell>
|
||||
<Show when={!props.compact}>
|
||||
<TableCell
|
||||
class="max-w-48 truncate font-mono text-xs text-muted"
|
||||
title={service.artifact}
|
||||
>
|
||||
{service.artifact}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<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>
|
||||
</TableCell>
|
||||
</Show>
|
||||
<TableCell class="font-mono text-xs">
|
||||
{service.calls.toLocaleString("zh-CN")}
|
||||
</TableCell>
|
||||
<TableCell class="font-mono text-xs">
|
||||
{service.latencyMs === null ? "—" : `${service.latencyMs} ms`}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<IconButton
|
||||
size="small"
|
||||
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="animate-spin" size={15} />
|
||||
) : (
|
||||
<RadioTower size={15} />
|
||||
)}
|
||||
</IconButton>
|
||||
<Show
|
||||
when={service.status === "running"}
|
||||
fallback={
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`启动 ${service.id}`}
|
||||
title="启动"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "start",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Play size={15} />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`调用 ${service.id}`}
|
||||
title="测试调用"
|
||||
onClick={() => props.command({ type: "open-invoke", serviceKey: key })}
|
||||
>
|
||||
<SquareTerminal size={15} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`重启 ${service.id}`}
|
||||
title="重启"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "restart",
|
||||
})
|
||||
}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
tone="danger"
|
||||
aria-label={`停止 ${service.id}`}
|
||||
title="停止"
|
||||
disabled={props.state.serviceAction !== null}
|
||||
onClick={() =>
|
||||
props.command({
|
||||
type: "service-action",
|
||||
serviceKey: key,
|
||||
action: "stop",
|
||||
})
|
||||
}
|
||||
>
|
||||
<CircleStop size={15} />
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import { STATUS_META, type ServiceStatus } from "../../lib/model";
|
||||
|
||||
const statusBadgeVariants = cva(
|
||||
"inline-flex items-center gap-1.5 whitespace-nowrap text-xs font-semibold before:size-1.5 before:rounded-full before:bg-current before:content-['']",
|
||||
{
|
||||
variants: {
|
||||
status: {
|
||||
running: "text-brand",
|
||||
stopped: "text-muted",
|
||||
faulted: "text-coral-strong",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function StatusBadge(props: { status: ServiceStatus }) {
|
||||
return (
|
||||
<span data-slot="service-status" class={statusBadgeVariants({ status: props.status })}>
|
||||
{STATUS_META[props.status].label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user