feat: show provider operation availability
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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" }, "未声明") :
|
||||
|
||||
Reference in New Issue
Block a user