refactor(console-ui): split dashboard responsibilities
Reduce the root TanStack route to orchestration and shell state. Move backend-to-view mapping and binary invocation formatting into console-model, presentation views into components/views, and modal workflows into components/dialogs. Update the rendered artifact test to assert behavior at the new module boundaries without changing the generated UI or routes.
This commit is contained in:
@@ -0,0 +1,986 @@
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Archive,
|
||||
ArrowRight,
|
||||
Box,
|
||||
Braces,
|
||||
Check,
|
||||
CircleStop,
|
||||
CloudUpload,
|
||||
Cpu,
|
||||
Download,
|
||||
FileCode2,
|
||||
Package,
|
||||
Play,
|
||||
RadioTower,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Server,
|
||||
SquareTerminal,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
BackendCapability,
|
||||
BackendRuntime,
|
||||
BackendWitPackage,
|
||||
witPackageDownloadUrl,
|
||||
} from "../api";
|
||||
import {
|
||||
EventTone,
|
||||
RuntimeEvent,
|
||||
Service,
|
||||
ServiceStatus,
|
||||
STATUS_META,
|
||||
formatDuration,
|
||||
serviceKey,
|
||||
} from "../console-model";
|
||||
|
||||
function PageHeading({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<span className="eyebrow">{eyebrow}</span>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
{actions && <div className="heading-actions">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Overview({
|
||||
services,
|
||||
filteredServices,
|
||||
events,
|
||||
running,
|
||||
faulted,
|
||||
totalCalls,
|
||||
deploymentCount,
|
||||
runtime,
|
||||
selectedService,
|
||||
onSelect,
|
||||
onDeploy,
|
||||
onInvoke,
|
||||
onStatus,
|
||||
onActivate,
|
||||
onViewAll,
|
||||
onRefresh,
|
||||
onRuntimeAction,
|
||||
runtimeAction,
|
||||
deploymentAction,
|
||||
}: {
|
||||
services: Service[];
|
||||
filteredServices: Service[];
|
||||
events: RuntimeEvent[];
|
||||
running: number;
|
||||
faulted: number;
|
||||
totalCalls: number;
|
||||
deploymentCount: number;
|
||||
runtime: BackendRuntime | null;
|
||||
selectedService: Service | null;
|
||||
onSelect: (service: Service) => void;
|
||||
onDeploy: () => void;
|
||||
onInvoke: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
onViewAll: () => void;
|
||||
onRefresh: () => void;
|
||||
onRuntimeAction: (action: "start" | "stop" | "restart") => void;
|
||||
runtimeAction: "start" | "stop" | "restart" | null;
|
||||
deploymentAction: string | null;
|
||||
}) {
|
||||
const errorCount = services.reduce((sum, service) => sum + service.errors, 0);
|
||||
const latencies = services
|
||||
.map((service) => service.latencyMs)
|
||||
.filter((latency): latency is number => latency !== null);
|
||||
const latestLatency = latencies.length ? Math.max(...latencies) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="LOCAL RUNTIME"
|
||||
title="运行概览"
|
||||
description={`${services.length} 个已注册版本,${running} 个 Actor 正在运行。`}
|
||||
actions={
|
||||
<>
|
||||
<button
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label="刷新"
|
||||
title="刷新"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
<RefreshCw size={17} />
|
||||
</button>
|
||||
<button className="primary-button" type="button" onClick={onDeploy}>
|
||||
<CloudUpload size={17} />
|
||||
注册组件
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="metric-grid" aria-label="关键指标">
|
||||
<Metric
|
||||
icon={<Package size={18} />}
|
||||
label="服务版本"
|
||||
value={services.length.toString()}
|
||||
note={`${deploymentCount} 个对外服务`}
|
||||
tone="cyan"
|
||||
/>
|
||||
<Metric
|
||||
icon={<Activity size={18} />}
|
||||
label="运行实例"
|
||||
value={running.toString()}
|
||||
note={`${services.length - running} 个未运行`}
|
||||
tone="green"
|
||||
/>
|
||||
<Metric
|
||||
icon={<Zap size={18} />}
|
||||
label="累计调用"
|
||||
value={totalCalls.toLocaleString("zh-CN")}
|
||||
note="持久化统计"
|
||||
tone="amber"
|
||||
/>
|
||||
<Metric
|
||||
icon={<AlertTriangle size={18} />}
|
||||
label="异常"
|
||||
value={(faulted + errorCount).toString()}
|
||||
note={faulted + errorCount ? "需要检查" : "无异常"}
|
||||
tone="coral"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="runtime-band">
|
||||
<div className="runtime-band-title">
|
||||
<div className="runtime-symbol">
|
||||
<Cpu size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<strong>Wasmeld Runtime</strong>
|
||||
<span>
|
||||
{runtime?.status === "running"
|
||||
? "Engine 已启用 fuel 与 epoch interruption"
|
||||
: "Engine 与所有 Actor 已释放"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="runtime-controls" aria-label="Runtime 生命周期">
|
||||
{runtime?.status === "stopped" ? (
|
||||
<button
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label="启动 Runtime"
|
||||
title="启动 Runtime"
|
||||
disabled={runtimeAction !== null}
|
||||
onClick={() => onRuntimeAction("start")}
|
||||
>
|
||||
<Play size={15} />
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="icon-button"
|
||||
type="button"
|
||||
aria-label="重启 Runtime"
|
||||
title="重启 Runtime"
|
||||
disabled={runtimeAction !== null || !runtime}
|
||||
onClick={() => onRuntimeAction("restart")}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button runtime-stop-button"
|
||||
type="button"
|
||||
aria-label="停止 Runtime"
|
||||
title="停止 Runtime"
|
||||
disabled={runtimeAction !== null || !runtime}
|
||||
onClick={() => onRuntimeAction("stop")}
|
||||
>
|
||||
<CircleStop size={15} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RuntimeStat
|
||||
label="已加载版本"
|
||||
value={(runtime?.registered_services ?? 0).toString()}
|
||||
detail={`${runtime?.managed_services ?? services.length} 个受管制品`}
|
||||
/>
|
||||
<RuntimeStat
|
||||
label="活跃 Store"
|
||||
value={(runtime?.running_services ?? running).toString()}
|
||||
detail="串行 Actor"
|
||||
/>
|
||||
<RuntimeStat
|
||||
label="最近延迟"
|
||||
value={latestLatency === null ? "—" : `${latestLatency} ms`}
|
||||
detail="各服务最近调用"
|
||||
/>
|
||||
<RuntimeStat
|
||||
label="运行时间"
|
||||
value={runtime?.status === "running" ? formatDuration(runtime.uptime_ms) : "—"}
|
||||
detail="当前 Runtime 实例"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className="overview-grid">
|
||||
<section className="panel service-panel">
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<h2>服务状态</h2>
|
||||
<p>已注册版本及当前 Actor 状态</p>
|
||||
</div>
|
||||
<button className="text-button" type="button" onClick={onViewAll}>
|
||||
查看全部
|
||||
<ArrowRight size={15} />
|
||||
</button>
|
||||
</div>
|
||||
<ServiceTable
|
||||
services={filteredServices.slice(0, 5)}
|
||||
selectedKey={selectedService ? serviceKey(selectedService) : ""}
|
||||
onSelect={onSelect}
|
||||
onInvoke={onInvoke}
|
||||
onStatus={onStatus}
|
||||
onActivate={onActivate}
|
||||
canActivate={runtime?.status === "running"}
|
||||
deploymentAction={deploymentAction}
|
||||
compact
|
||||
/>
|
||||
</section>
|
||||
|
||||
<aside className="panel event-panel">
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<h2>实时事件</h2>
|
||||
<p>Runtime 生命周期与调用结果</p>
|
||||
</div>
|
||||
<span className="live-label">
|
||||
<span />
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
<EventList events={events.slice(0, 5)} />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{selectedService && (
|
||||
<section className="detail-strip">
|
||||
<div className="detail-identity">
|
||||
<div className="artifact-icon">
|
||||
<FileCode2 size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<span>当前选中</span>
|
||||
<strong>
|
||||
{selectedService.id}
|
||||
<small>@{selectedService.revision}</small>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<DetailValue label="内存上限" value={`${selectedService.memoryMb} MB`} />
|
||||
<DetailValue label="Fuel / 调用" value={selectedService.fuel} />
|
||||
<DetailValue label="Deadline" value={`${selectedService.deadlineMs} ms`} />
|
||||
<DetailValue label="Mailbox" value={selectedService.mailbox.toString()} />
|
||||
<div className="detail-actions">
|
||||
{!selectedService.active && (
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
disabled={runtime?.status !== "running" || deploymentAction !== null}
|
||||
onClick={() => onActivate(selectedService)}
|
||||
>
|
||||
<RadioTower size={16} />
|
||||
设为对外
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="secondary-button"
|
||||
type="button"
|
||||
disabled={selectedService.status !== "running"}
|
||||
onClick={() => onInvoke(selectedService)}
|
||||
>
|
||||
<SquareTerminal size={16} />
|
||||
测试调用
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
tone,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
note: string;
|
||||
tone: string;
|
||||
}) {
|
||||
return (
|
||||
<article className="metric">
|
||||
<div className={`metric-icon metric-${tone}`}>{icon}</div>
|
||||
<div className="metric-copy">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
<small>{note}</small>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeStat({ label, value, detail }: { label: string; value: string; detail: string }) {
|
||||
return (
|
||||
<div className="runtime-stat">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
<small>{detail}</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailValue({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="detail-value">
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: ServiceStatus }) {
|
||||
const meta = STATUS_META[status];
|
||||
return (
|
||||
<span className={`status-badge ${meta.className}`}>
|
||||
<span />
|
||||
{meta.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CapabilitySummary({ capabilities }: { capabilities: BackendCapability[] }) {
|
||||
if (capabilities.length === 0) {
|
||||
return <span className="capability-empty">无</span>;
|
||||
}
|
||||
|
||||
const [first, ...remaining] = capabilities;
|
||||
return (
|
||||
<span
|
||||
className="capability-summary"
|
||||
title={capabilities.map((capability) => capability.interface).join("\n")}
|
||||
>
|
||||
<Braces size={12} />
|
||||
<span>
|
||||
{first.package}/{first.name} @{first.version}
|
||||
</span>
|
||||
{remaining.length > 0 && <small>+{remaining.length}</small>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ServiceTable({
|
||||
services,
|
||||
selectedKey,
|
||||
onSelect,
|
||||
onInvoke,
|
||||
onStatus,
|
||||
onActivate,
|
||||
canActivate,
|
||||
deploymentAction,
|
||||
compact = false,
|
||||
}: {
|
||||
services: Service[];
|
||||
selectedKey?: string;
|
||||
onSelect: (service: Service) => void;
|
||||
onInvoke: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
canActivate: boolean;
|
||||
deploymentAction: string | null;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
if (services.length === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<Search size={22} />
|
||||
<strong>没有匹配的服务</strong>
|
||||
<span>调整搜索关键词或状态筛选。</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="table-scroll">
|
||||
<table className={`data-table ${compact ? "service-table-compact" : "service-table-full"}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>服务</th>
|
||||
<th>状态</th>
|
||||
{!compact && <th>制品</th>}
|
||||
{!compact && <th>Host 能力</th>}
|
||||
<th>调用</th>
|
||||
<th>延迟</th>
|
||||
<th>
|
||||
<span className="sr-only">操作</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services.map((service) => (
|
||||
<tr
|
||||
key={`${service.id}-${service.revision}`}
|
||||
className={selectedKey === serviceKey(service) ? "selected" : ""}
|
||||
>
|
||||
<td>
|
||||
<button
|
||||
className="service-cell service-select"
|
||||
type="button"
|
||||
aria-label={`选择 ${service.id}@${service.revision}`}
|
||||
onClick={() => onSelect(service)}
|
||||
>
|
||||
<span className="service-glyph">
|
||||
<Box size={16} />
|
||||
</span>
|
||||
<span>
|
||||
<span className="service-title">
|
||||
<strong>{service.id}</strong>
|
||||
{service.active && (
|
||||
<span className="active-deployment-badge">
|
||||
<RadioTower size={10} />
|
||||
对外
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<small>
|
||||
{service.revision} · {service.description}
|
||||
</small>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<StatusBadge status={service.status} />
|
||||
</td>
|
||||
{!compact && <td className="artifact-cell">{service.artifact}</td>}
|
||||
{!compact && (
|
||||
<td>
|
||||
<CapabilitySummary capabilities={service.capabilities} />
|
||||
</td>
|
||||
)}
|
||||
<td className="numeric-cell">{service.calls.toLocaleString("zh-CN")}</td>
|
||||
<td className="numeric-cell">
|
||||
{service.latencyMs ? `${service.latencyMs} ms` : "—"}
|
||||
</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
className={service.active ? "active-deployment-button" : ""}
|
||||
type="button"
|
||||
aria-label={
|
||||
service.active
|
||||
? `${service.id}@${service.revision} 是当前对外版本`
|
||||
: `将 ${service.id}@${service.revision} 设为对外版本`
|
||||
}
|
||||
title={
|
||||
service.active
|
||||
? "当前对外版本"
|
||||
: canActivate
|
||||
? "设为对外版本"
|
||||
: "请先启动 Runtime"
|
||||
}
|
||||
disabled={service.active || !canActivate || deploymentAction !== null}
|
||||
onClick={() => onActivate(service)}
|
||||
>
|
||||
{deploymentAction === serviceKey(service) ? (
|
||||
<RefreshCw className="spin" size={15} />
|
||||
) : (
|
||||
<RadioTower size={15} />
|
||||
)}
|
||||
</button>
|
||||
{service.status === "running" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`调用 ${service.id}`}
|
||||
title="测试调用"
|
||||
onClick={() => onInvoke(service)}
|
||||
>
|
||||
<SquareTerminal size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`重启 ${service.id}`}
|
||||
title="重启"
|
||||
onClick={() => onStatus(service, "running")}
|
||||
>
|
||||
<RotateCcw size={15} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`停止 ${service.id}`}
|
||||
title="停止"
|
||||
onClick={() => onStatus(service, "stopped")}
|
||||
>
|
||||
<CircleStop size={15} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`启动 ${service.id}`}
|
||||
title="启动"
|
||||
onClick={() => onStatus(service, "running")}
|
||||
>
|
||||
<Play size={15} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventList({ events }: { events: RuntimeEvent[] }) {
|
||||
const icons: Record<EventTone, ReactNode> = {
|
||||
success: <Check size={14} />,
|
||||
warning: <AlertTriangle size={14} />,
|
||||
danger: <X size={14} />,
|
||||
neutral: <CircleStop size={14} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="event-list">
|
||||
{events.map((event) => (
|
||||
<div className="event-row" key={event.id}>
|
||||
<span className={`event-icon event-${event.tone}`}>{icons[event.tone]}</span>
|
||||
<div>
|
||||
<strong>{event.title}</strong>
|
||||
<span>{event.detail}</span>
|
||||
</div>
|
||||
<time>{event.time}</time>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServicesView({
|
||||
services,
|
||||
statusFilter,
|
||||
onFilter,
|
||||
onDeploy,
|
||||
onInvoke,
|
||||
onSelect,
|
||||
onStatus,
|
||||
onActivate,
|
||||
canActivate,
|
||||
deploymentAction,
|
||||
}: {
|
||||
services: Service[];
|
||||
statusFilter: "all" | ServiceStatus;
|
||||
onFilter: (status: "all" | ServiceStatus) => void;
|
||||
onDeploy: () => void;
|
||||
onInvoke: (service: Service) => void;
|
||||
onSelect: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onActivate: (service: Service) => void;
|
||||
canActivate: boolean;
|
||||
deploymentAction: string | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="REGISTRY"
|
||||
title="服务版本"
|
||||
description="管理不可变 Wasm Component 制品及当前对外版本。"
|
||||
actions={
|
||||
<button className="primary-button" type="button" onClick={onDeploy}>
|
||||
<CloudUpload size={17} />
|
||||
注册组件
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<section className="panel full-panel">
|
||||
<div className="filterbar">
|
||||
<div className="segmented" aria-label="状态筛选">
|
||||
{[
|
||||
["all", "全部"],
|
||||
["running", "运行中"],
|
||||
["stopped", "已停止"],
|
||||
["faulted", "故障"],
|
||||
].map(([value, label]) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className={statusFilter === value ? "active" : ""}
|
||||
onClick={() => onFilter(value as "all" | ServiceStatus)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="result-count">{services.length} 个版本</span>
|
||||
</div>
|
||||
<ServiceTable
|
||||
services={services}
|
||||
onSelect={onSelect}
|
||||
onInvoke={onInvoke}
|
||||
onStatus={onStatus}
|
||||
onActivate={onActivate}
|
||||
canActivate={canActivate}
|
||||
deploymentAction={deploymentAction}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WitPackagesView({
|
||||
apiBase,
|
||||
packages,
|
||||
onPublish,
|
||||
}: {
|
||||
apiBase: string;
|
||||
packages: BackendWitPackage[];
|
||||
onPublish: () => void;
|
||||
}) {
|
||||
function downloadPackage(packageMetadata: BackendWitPackage) {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = witPackageDownloadUrl(apiBase, packageMetadata);
|
||||
anchor.download = `${packageMetadata.name.replace(":", "-")}-${packageMetadata.version}.wasm`;
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="WIT REGISTRY"
|
||||
title="WIT 包"
|
||||
description="管理组件开发所依赖的不可变、版本化 WIT Package。"
|
||||
actions={
|
||||
<button className="primary-button" type="button" onClick={onPublish}>
|
||||
<CloudUpload size={17} />
|
||||
发布 WIT 包
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<section className="panel full-panel">
|
||||
<div className="filterbar">
|
||||
<span className="registry-note">发布后不可覆盖;包名、版本与依赖由制品自动解析。</span>
|
||||
<span className="result-count">{packages.length} 个包版本</span>
|
||||
</div>
|
||||
{packages.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<FileCode2 size={22} />
|
||||
<strong>没有匹配的 WIT 包</strong>
|
||||
<span>发布二进制 WIT Package,或调整搜索关键词。</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-scroll">
|
||||
<table className="data-table wit-package-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>依赖</th>
|
||||
<th>SHA-256</th>
|
||||
<th>
|
||||
<span className="sr-only">操作</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{packages.map((packageMetadata) => {
|
||||
const dependencies = packageMetadata.dependencies
|
||||
.map((dependency) => `${dependency.name}@${dependency.version}`)
|
||||
.join(", ");
|
||||
return (
|
||||
<tr key={`${packageMetadata.name}@${packageMetadata.version}`}>
|
||||
<td aria-label={`${packageMetadata.name}@${packageMetadata.version}`}>
|
||||
<div className="service-cell">
|
||||
<span className="service-glyph">
|
||||
<FileCode2 size={16} />
|
||||
</span>
|
||||
<span>
|
||||
<strong>{packageMetadata.name}</strong>
|
||||
<small>{packageMetadata.version}</small>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="artifact-cell" title={dependencies || "无直接依赖"}>
|
||||
{dependencies || "无直接依赖"}
|
||||
</td>
|
||||
<td className="artifact-cell" title={packageMetadata.sha256}>
|
||||
{packageMetadata.sha256}
|
||||
</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
title="下载 WIT 包"
|
||||
aria-label={`下载 ${packageMetadata.name}@${packageMetadata.version}`}
|
||||
onClick={() => downloadPackage(packageMetadata)}
|
||||
>
|
||||
<Download size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InstancesView({
|
||||
services,
|
||||
onInvoke,
|
||||
onStatus,
|
||||
onRefresh,
|
||||
}: {
|
||||
services: Service[];
|
||||
onInvoke: (service: Service) => void;
|
||||
onStatus: (service: Service, status: ServiceStatus) => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const instances = services.filter(
|
||||
(service) => service.status === "running" || service.status === "faulted",
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="ACTORS"
|
||||
title="运行实例"
|
||||
description={`${instances.length} 个 Actor 保持独立 Store 与 Instance。`}
|
||||
actions={
|
||||
<button
|
||||
className="icon-button"
|
||||
type="button"
|
||||
title="刷新"
|
||||
aria-label="刷新"
|
||||
onClick={onRefresh}
|
||||
>
|
||||
<RefreshCw size={17} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<section className="instance-list">
|
||||
{instances.length === 0 ? (
|
||||
<output className="instance-placeholder">
|
||||
<div className="instance-placeholder-icon">
|
||||
<Cpu size={22} />
|
||||
</div>
|
||||
<strong>暂无运行实例</strong>
|
||||
<span>0 ACTIVE ACTORS</span>
|
||||
</output>
|
||||
) : (
|
||||
instances.map((service) => (
|
||||
<article className="instance-row" key={service.id}>
|
||||
<div className="instance-main">
|
||||
<span className={`instance-indicator ${service.status}`} />
|
||||
<div className="artifact-icon">
|
||||
<Cpu size={19} />
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
{service.id}@{service.revision}
|
||||
</strong>
|
||||
<span>actor/{service.id}-01</span>
|
||||
</div>
|
||||
</div>
|
||||
<DetailValue
|
||||
label="Store 内存"
|
||||
value={
|
||||
service.status === "running"
|
||||
? `${Math.round(service.memoryMb * 0.42)} MB`
|
||||
: "已释放"
|
||||
}
|
||||
/>
|
||||
<DetailValue label="调用总数" value={service.calls.toLocaleString("zh-CN")} />
|
||||
<DetailValue
|
||||
label="最近延迟"
|
||||
value={service.latencyMs ? `${service.latencyMs} ms` : "—"}
|
||||
/>
|
||||
<div className="instance-status">
|
||||
<StatusBadge status={service.status} />
|
||||
<span>{service.updatedAt}</span>
|
||||
</div>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
title="测试调用"
|
||||
aria-label={`调用 ${service.id}`}
|
||||
disabled={service.status !== "running"}
|
||||
onClick={() => onInvoke(service)}
|
||||
>
|
||||
<SquareTerminal size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="重启"
|
||||
aria-label={`重启 ${service.id}`}
|
||||
onClick={() => onStatus(service, "running")}
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="停止"
|
||||
aria-label={`停止 ${service.id}`}
|
||||
disabled={service.status !== "running"}
|
||||
onClick={() => onStatus(service, "stopped")}
|
||||
>
|
||||
<CircleStop size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActivityView({
|
||||
events,
|
||||
services,
|
||||
}: {
|
||||
events: RuntimeEvent[];
|
||||
services: Service[];
|
||||
}) {
|
||||
const calls = services.reduce((sum, service) => sum + service.calls, 0);
|
||||
const errors = services.reduce((sum, service) => sum + service.errors, 0);
|
||||
const latencies = services
|
||||
.map((service) => service.latencyMs)
|
||||
.filter((latency): latency is number => latency !== null);
|
||||
const averageLatency = latencies.length
|
||||
? latencies.reduce((sum, latency) => sum + latency, 0) / latencies.length
|
||||
: null;
|
||||
|
||||
function exportEvents() {
|
||||
const blob = new Blob([JSON.stringify(events, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = "wasmeld-events.json";
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="EVENT STREAM"
|
||||
title="调用记录"
|
||||
description="Runtime 生命周期、调用结果与沙箱拒绝记录。"
|
||||
actions={
|
||||
<button className="secondary-button" type="button" onClick={exportEvents}>
|
||||
<Archive size={16} />
|
||||
导出记录
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<section className="panel full-panel">
|
||||
<div className="activity-summary">
|
||||
<div>
|
||||
<span>时间范围</span>
|
||||
<strong>当前进程</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>成功率</span>
|
||||
<strong>{calls ? `${(((calls - errors) / calls) * 100).toFixed(2)}%` : "—"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平均延迟</span>
|
||||
<strong>{averageLatency === null ? "—" : `${averageLatency.toFixed(1)} ms`}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>记录数</span>
|
||||
<strong>{events.length}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<EventList events={events} />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsView({
|
||||
endpoint,
|
||||
onSaveEndpoint,
|
||||
}: {
|
||||
endpoint: string;
|
||||
onSaveEndpoint: (value: string) => void;
|
||||
}) {
|
||||
const [value, setValue] = useState(endpoint);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading
|
||||
eyebrow="CONFIGURATION"
|
||||
title="运行设置"
|
||||
description="本地控制台连接与 Runtime 默认参数。"
|
||||
/>
|
||||
<form
|
||||
className="settings-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSaveEndpoint(value);
|
||||
}}
|
||||
>
|
||||
<section className="settings-section">
|
||||
<div className="settings-heading">
|
||||
<Server size={18} />
|
||||
<div>
|
||||
<h2>控制端连接</h2>
|
||||
<p>管理 API 的本地访问地址。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field wide-field">
|
||||
<label htmlFor="endpoint">API 地址</label>
|
||||
<input id="endpoint" value={value} onChange={(event) => setValue(event.target.value)} />
|
||||
<span>连接状态将在顶部工具栏显示。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="form-footer">
|
||||
<button className="primary-button" type="submit">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user