feat: show provider operation availability

This commit is contained in:
Codex
2026-05-05 04:57:23 +00:00
parent 6ffc06ed1f
commit e824634f32
8 changed files with 121 additions and 10 deletions
+55 -1
View File
@@ -333,6 +333,59 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
background: rgba(207, 106, 84, 0.1);
}
.node-availability-strip {
display: flex;
flex-wrap: wrap;
gap: 5px;
min-width: 220px;
margin: 5px 0 7px;
}
.capability-badge {
display: grid;
grid-template-columns: auto auto;
gap: 1px 6px;
min-width: 104px;
max-width: 190px;
padding: 4px 6px;
border: 1px solid var(--line);
background: rgba(0,0,0,0.16);
line-height: 1.22;
}
.capability-badge b {
color: var(--muted);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.capability-badge strong {
justify-self: end;
font-size: 11px;
}
.capability-badge small {
grid-column: 1 / -1;
min-width: 0;
overflow: hidden;
color: var(--faint);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.capability-badge.ok {
border-color: rgba(113, 191, 120, 0.42);
background: rgba(113, 191, 120, 0.07);
}
.capability-badge.ok strong { color: var(--ok); }
.capability-badge.warn {
border-color: rgba(215, 161, 58, 0.45);
background: rgba(215, 161, 58, 0.08);
}
.capability-badge.warn strong { color: var(--warn); }
.capability-badge.fail {
border-color: rgba(207, 106, 84, 0.45);
background: rgba(207, 106, 84, 0.08);
}
.capability-badge.fail strong { color: var(--danger); }
.status-badge {
display: inline-flex;
align-items: center;
@@ -693,7 +746,7 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
display: grid;
gap: 10px;
}
.gateway-version-table { min-width: 1120px; }
.gateway-version-table { min-width: 1280px; }
.gateway-version-table-wrap { max-height: 340px; }
.capability-row {
display: flex;
@@ -782,6 +835,7 @@ h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; }
.table-wrap { overflow: auto; max-height: calc(100vh - 174px); }
table { width: 100%; border-collapse: collapse; min-width: 760px; }
.node-list-table { min-width: 1180px; }
.task-history-table { min-width: 1080px; }
th, td {
padding: 7px 9px;
+50 -2
View File
@@ -179,9 +179,33 @@ function nodeGatewayStartedAt(node: any): string {
function nodeCapabilities(node: any): string[] {
const value = recordValue(node?.labels, "unideskCapabilities");
if (typeof value === "string") return value.split(",").map((item) => item.trim()).filter(Boolean);
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
}
function nodeHasCapability(node: any, capability: string): boolean {
return nodeCapabilities(node).includes(capability);
}
function labelBoolean(node: any, key: string): boolean {
const value = recordValue(node?.labels, key);
return value === true || value === "true" || value === "1";
}
function nodeSshAvailability(node: any): { tone: string; label: string; detail: string } {
if (!nodeHasCapability(node, "host.ssh")) return { tone: "fail", label: "不可用", detail: "未声明 host.ssh" };
if (!labelBoolean(node, "hostSshConfigured")) return { tone: "warn", label: "未配置", detail: "缺少 SSH 环境变量" };
if (!labelBoolean(node, "hostSshKeyPresent")) return { tone: "warn", label: "缺 key", detail: "私钥未挂载" };
return { tone: "ok", label: "可用", detail: labelString(node, "hostSshTarget", "host.ssh ready") };
}
function nodeUpgradeAvailability(node: any): { tone: string; label: string; detail: string } {
if (!nodeHasCapability(node, "provider.upgrade")) return { tone: "fail", label: "不可用", detail: "未声明 provider.upgrade" };
const policy = nodeGatewayPolicy(node);
if (policy !== "always-enabled") return { tone: "warn", label: "待确认", detail: `策略 ${policy}` };
return { tone: "ok", label: "可用", detail: "always-enabled" };
}
function fmtGatewayVersion(value: any): string {
const text = typeof value === "string" && value.length > 0 ? value : "未知";
if (text === "未知") return "版本未知";
@@ -336,6 +360,26 @@ function GatewayVersionBadge({ node }: AnyRecord) {
}, fmtGatewayVersion(version));
}
function CapabilityBadge({ title, state, testId }: AnyRecord) {
return h("span", {
className: `capability-badge ${state.tone}`,
title: state.detail,
"data-testid": testId,
},
h("b", null, title),
h("strong", null, state.label),
h("small", null, state.detail),
);
}
function NodeAvailabilityStrip({ node }: AnyRecord) {
const providerId = safeId(node?.providerId || "unknown");
return h("div", { className: "node-availability-strip" },
h(CapabilityBadge, { title: "SSH 透传", state: nodeSshAvailability(node), testId: `ssh-availability-${providerId}` }),
h(CapabilityBadge, { title: "远程更新", state: nodeUpgradeAvailability(node), testId: `upgrade-availability-${providerId}` }),
);
}
function DataSummary({ data, empty = "无数据" }: AnyRecord) {
if (data === null || data === undefined) return h("span", { className: "muted" }, empty);
if (typeof data !== "object") return h("span", { className: "summary-value" }, summarizeValue(data));
@@ -467,6 +511,7 @@ function NodeCard({ node, onRaw }: AnyRecord) {
h(GatewayVersionBadge, { node }),
h("span", null, `升级策略 ${nodeGatewayPolicy(node)}`),
),
h(NodeAvailabilityStrip, { node }),
h(LabelChips, { labels: node.labels, limit: 6 }),
h("div", { className: "node-card-foot" },
h("span", null, `心跳 ${fmtDate(node.lastHeartbeat)}`),
@@ -508,12 +553,13 @@ function LogsPage({ logs, onRaw }: AnyRecord) {
function NodeListPage({ nodes, onRaw }: AnyRecord) {
return h(Panel, { title: "节点清单", eyebrow: `${nodes.length} Providers` },
nodes.length === 0 ? h(EmptyState, { title: "暂无 Provider 节点", text: "确认 provider-gateway 已连接 provider ingress" }) :
h("div", { className: "table-wrap" }, h("table", null,
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "Provider"), h("th", null, "网关版本"), h("th", null, "资源标签"), h("th", null, "连接时间"), h("th", null, "最后心跳"), h("th", null, "操作"))),
h("div", { className: "table-wrap" }, h("table", { className: "node-list-table" },
h("thead", null, h("tr", null, h("th", null, "状态"), h("th", null, "Provider"), h("th", null, "网关版本"), h("th", null, "运维可用性"), h("th", null, "资源标签"), h("th", null, "连接时间"), h("th", null, "最后心跳"), h("th", null, "操作"))),
h("tbody", null, nodes.map((node: any) => h("tr", { key: node.providerId },
h("td", null, h(StatusBadge, { status: node.status })),
h("td", null, h("strong", null, node.name), h("code", null, node.providerId)),
h("td", null, h("div", { className: "gateway-cell" }, h(GatewayVersionBadge, { node }), h("span", null, nodeGatewayPolicy(node)))),
h("td", null, h(NodeAvailabilityStrip, { node })),
h("td", null, h(LabelChips, { labels: node.labels, limit: 5 })),
h("td", null, fmtDate(node.connectedAt)),
h("td", null, fmtDate(node.lastHeartbeat)),
@@ -766,6 +812,7 @@ function GatewayVersionPage({ nodes, tasks, onRaw }: AnyRecord) {
h("th", null, "Provider"),
h("th", null, "Gateway 版本"),
h("th", null, "升级策略"),
h("th", null, "运维可用性"),
h("th", null, "运行时间"),
h("th", null, "能力"),
h("th", null, "最近自动更新"),
@@ -776,6 +823,7 @@ function GatewayVersionPage({ nodes, tasks, onRaw }: AnyRecord) {
h("td", null, h("strong", null, row.node.name), h("code", null, row.node.providerId)),
h("td", null, h(GatewayVersionBadge, { node: row.node })),
h("td", null, nodeGatewayPolicy(row.node)),
h("td", null, h(NodeAvailabilityStrip, { node: row.node })),
h("td", null, nodeGatewayStartedAt(row.node) ? fmtDate(nodeGatewayStartedAt(row.node)) : "待新版上报"),
h("td", null, h("div", { className: "capability-row" },
row.capabilities.length === 0 ? h("span", { className: "muted" }, "未声明") :