feat(console-ui): show component host capabilities

- consume structured capability metadata from service responses
- add a stable-width Host Capability column to the full service table
- preserve compact overview density and horizontal overflow on narrow screens
- cover API typing and capability rendering wiring in SSR source tests
This commit is contained in:
Maofeng
2026-07-29 19:13:45 +08:00
parent 72a264e0d1
commit bdbf868f75
4 changed files with 98 additions and 1 deletions
+8
View File
@@ -4,11 +4,19 @@ const API_BASE_STORAGE_KEY = "wasmeld-api-base";
export type BackendServiceStatus = "running" | "stopped" | "faulted";
export type BackendCapability = {
interface: string;
package: string;
name: string;
version: string;
};
export type BackendService = {
id: string;
revision: string;
artifact: string;
world: string;
capabilities: BackendCapability[];
status: BackendServiceStatus;
updated_at_ms: number;
limits: {
+30 -1
View File
@@ -31,6 +31,7 @@ import {
} from "lucide-react";
import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import {
BackendCapability,
BackendEvent,
BackendDeployment,
BackendRuntime,
@@ -66,6 +67,7 @@ type Service = {
deadlineMs: number;
mailbox: number;
maxInputKb: number;
capabilities: BackendCapability[];
calls: number;
errors: number;
latencyMs: number | null;
@@ -120,6 +122,7 @@ function toService(service: BackendService, activeRevision?: string): Service {
deadlineMs: service.limits.deadline_ms,
mailbox: service.limits.mailbox_capacity,
maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
capabilities: service.capabilities,
calls: service.calls,
errors: service.errors,
latencyMs: service.last_latency_ms,
@@ -977,6 +980,26 @@ function StatusBadge({ status }: { status: ServiceStatus }) {
);
}
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,
@@ -1010,12 +1033,13 @@ function ServiceTable({
return (
<div className="table-scroll">
<table className="data-table">
<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>
@@ -1059,6 +1083,11 @@ function ServiceTable({
<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` : "—"}
+57
View File
@@ -768,6 +768,31 @@ button:disabled {
table-layout: fixed;
}
.service-table-full {
min-width: 980px;
}
.data-table.service-table-full th:first-child {
width: 24%;
}
.service-table-full th:nth-child(2) {
width: 90px;
}
.service-table-full th:nth-child(3) {
width: 150px;
}
.service-table-full th:nth-child(4) {
width: 220px;
}
.service-table-full th:nth-child(5),
.service-table-full th:nth-child(6) {
width: 72px;
}
.data-table th {
height: 34px;
padding: 0 12px;
@@ -891,6 +916,38 @@ button:disabled {
white-space: nowrap;
}
.capability-summary {
max-width: 100%;
height: 23px;
padding: 0 7px;
display: inline-flex;
align-items: center;
gap: 5px;
color: #315f52;
background: #edf5f2;
border: 1px solid #d2e4de;
border-radius: 4px;
font-family: var(--font-geist-mono), monospace;
font-size: 8px;
}
.capability-summary > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.capability-summary > small {
flex: 0 0 auto;
color: var(--ink-faint);
font-size: 7px;
}
.capability-empty {
color: var(--ink-faint);
font-size: 9px;
}
.numeric-cell {
color: var(--ink-soft);
font-family: var(--font-geist-mono), monospace;
+3
View File
@@ -97,7 +97,10 @@ test("uses TanStack Start routing and produces Node artifacts", async () => {
assert.match(rootRoute, /<Scripts \/>/);
assert.match(indexRoute, /createFileRoute\("\/"\)/);
assert.match(indexRoute, /切换对外版本/);
assert.match(indexRoute, /Host 能力/);
assert.match(indexRoute, /service\.capabilities/);
assert.match(apiClient, /\/api\/v1\/deployments/);
assert.match(apiClient, /capabilities: BackendCapability\[\]/);
assert.match(router, /createRouter/);
assert.match(packageJson, /"@tanstack\/react-start"/);
assert.match(packageJson, /"srvx"/);