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:
@@ -4,11 +4,19 @@ const API_BASE_STORAGE_KEY = "wasmeld-api-base";
|
|||||||
|
|
||||||
export type BackendServiceStatus = "running" | "stopped" | "faulted";
|
export type BackendServiceStatus = "running" | "stopped" | "faulted";
|
||||||
|
|
||||||
|
export type BackendCapability = {
|
||||||
|
interface: string;
|
||||||
|
package: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BackendService = {
|
export type BackendService = {
|
||||||
id: string;
|
id: string;
|
||||||
revision: string;
|
revision: string;
|
||||||
artifact: string;
|
artifact: string;
|
||||||
world: string;
|
world: string;
|
||||||
|
capabilities: BackendCapability[];
|
||||||
status: BackendServiceStatus;
|
status: BackendServiceStatus;
|
||||||
updated_at_ms: number;
|
updated_at_ms: number;
|
||||||
limits: {
|
limits: {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
import { FormEvent, ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
BackendCapability,
|
||||||
BackendEvent,
|
BackendEvent,
|
||||||
BackendDeployment,
|
BackendDeployment,
|
||||||
BackendRuntime,
|
BackendRuntime,
|
||||||
@@ -66,6 +67,7 @@ type Service = {
|
|||||||
deadlineMs: number;
|
deadlineMs: number;
|
||||||
mailbox: number;
|
mailbox: number;
|
||||||
maxInputKb: number;
|
maxInputKb: number;
|
||||||
|
capabilities: BackendCapability[];
|
||||||
calls: number;
|
calls: number;
|
||||||
errors: number;
|
errors: number;
|
||||||
latencyMs: number | null;
|
latencyMs: number | null;
|
||||||
@@ -120,6 +122,7 @@ function toService(service: BackendService, activeRevision?: string): Service {
|
|||||||
deadlineMs: service.limits.deadline_ms,
|
deadlineMs: service.limits.deadline_ms,
|
||||||
mailbox: service.limits.mailbox_capacity,
|
mailbox: service.limits.mailbox_capacity,
|
||||||
maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
|
maxInputKb: Math.round(service.limits.max_input_bytes / 1024),
|
||||||
|
capabilities: service.capabilities,
|
||||||
calls: service.calls,
|
calls: service.calls,
|
||||||
errors: service.errors,
|
errors: service.errors,
|
||||||
latencyMs: service.last_latency_ms,
|
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({
|
function ServiceTable({
|
||||||
services,
|
services,
|
||||||
selectedKey,
|
selectedKey,
|
||||||
@@ -1010,12 +1033,13 @@ function ServiceTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="data-table">
|
<table className={`data-table ${compact ? "service-table-compact" : "service-table-full"}`}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>服务</th>
|
<th>服务</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
{!compact && <th>制品</th>}
|
{!compact && <th>制品</th>}
|
||||||
|
{!compact && <th>Host 能力</th>}
|
||||||
<th>调用</th>
|
<th>调用</th>
|
||||||
<th>延迟</th>
|
<th>延迟</th>
|
||||||
<th>
|
<th>
|
||||||
@@ -1059,6 +1083,11 @@ function ServiceTable({
|
|||||||
<StatusBadge status={service.status} />
|
<StatusBadge status={service.status} />
|
||||||
</td>
|
</td>
|
||||||
{!compact && <td className="artifact-cell">{service.artifact}</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.calls.toLocaleString("zh-CN")}</td>
|
||||||
<td className="numeric-cell">
|
<td className="numeric-cell">
|
||||||
{service.latencyMs ? `${service.latencyMs} ms` : "—"}
|
{service.latencyMs ? `${service.latencyMs} ms` : "—"}
|
||||||
|
|||||||
@@ -768,6 +768,31 @@ button:disabled {
|
|||||||
table-layout: fixed;
|
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 {
|
.data-table th {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
@@ -891,6 +916,38 @@ button:disabled {
|
|||||||
white-space: nowrap;
|
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 {
|
.numeric-cell {
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
font-family: var(--font-geist-mono), monospace;
|
font-family: var(--font-geist-mono), monospace;
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ test("uses TanStack Start routing and produces Node artifacts", async () => {
|
|||||||
assert.match(rootRoute, /<Scripts \/>/);
|
assert.match(rootRoute, /<Scripts \/>/);
|
||||||
assert.match(indexRoute, /createFileRoute\("\/"\)/);
|
assert.match(indexRoute, /createFileRoute\("\/"\)/);
|
||||||
assert.match(indexRoute, /切换对外版本/);
|
assert.match(indexRoute, /切换对外版本/);
|
||||||
|
assert.match(indexRoute, /Host 能力/);
|
||||||
|
assert.match(indexRoute, /service\.capabilities/);
|
||||||
assert.match(apiClient, /\/api\/v1\/deployments/);
|
assert.match(apiClient, /\/api\/v1\/deployments/);
|
||||||
|
assert.match(apiClient, /capabilities: BackendCapability\[\]/);
|
||||||
assert.match(router, /createRouter/);
|
assert.match(router, /createRouter/);
|
||||||
assert.match(packageJson, /"@tanstack\/react-start"/);
|
assert.match(packageJson, /"@tanstack\/react-start"/);
|
||||||
assert.match(packageJson, /"srvx"/);
|
assert.match(packageJson, /"srvx"/);
|
||||||
|
|||||||
Reference in New Issue
Block a user