feat(frontend): add HWLAB external entry

This commit is contained in:
Codex
2026-05-21 12:19:38 +00:00
parent 92dff00028
commit 7b9aa4261c
7 changed files with 297 additions and 91 deletions
File diff suppressed because one or more lines are too long
+62
View File
@@ -1456,6 +1456,68 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); }
display: grid;
gap: 10px;
}
.hwlab-page {
grid-template-columns: 1fr;
}
.hwlab-page .panel-body {
display: grid;
gap: 10px;
}
.hwlab-hero {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr);
gap: 10px;
align-items: stretch;
}
.hwlab-hero-copy {
display: grid;
gap: 4px;
min-width: 0;
}
.hwlab-hero-copy h2 {
font-size: 22px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hwlab-hero-copy .paragraph {
max-width: 60ch;
}
.hwlab-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: start;
align-content: start;
}
.hwlab-actions .ghost-btn,
.hwlab-actions .primary-btn {
width: max-content;
}
.hwlab-entry-list {
grid-template-columns: 1fr;
}
.hwlab-entry-card {
display: grid;
gap: 4px;
min-width: 0;
padding: 8px;
border: 1px solid var(--line-soft);
background: var(--panel-3);
}
.hwlab-entry-card b {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hwlab-entry-card span {
color: var(--muted);
}
.hwlab-entry-actions {
margin-top: 6px;
}
.hwlab-url {
word-break: break-all;
}
.microservice-actions, .inline-actions {
display: flex;
flex-wrap: wrap;
+27
View File
@@ -7,6 +7,7 @@ import { CodeQueuePage } from "./code-queue";
import { DecisionCenterPage } from "./decision-center";
import { FileBrowserPage } from "./filebrowser";
import { FindJobPage } from "./findjob";
import { HWLAB_URL, HwlabPage } from "./hwlab";
import { MetNonlinearPage } from "./met-nonlinear";
import { MdtodoPage } from "./mdtodo";
import { canonicalizeKnownRoute, createRouteRegistry, DEFAULT_ACTIVE_TABS, MODULES, pathForTarget, resolveRouteTarget } from "./navigation";
@@ -1654,6 +1655,31 @@ function MicroserviceCatalogPage({ microservices, onRaw, onNavigate }: AnyRecord
h(MetricCard, { label: "集成前端", value: microservices.filter((service: any) => service.frontend?.integrated).length, hint: "UniDesk React 页面" }),
),
),
h(Panel, { title: "外部静态入口", eyebrow: "External Link" },
h("div", { className: "endpoint-list hwlab-entry-list" },
h("article", { className: "hwlab-entry-card", "data-testid": "hwlab-entry-card" },
h("b", null, "HWLAB"),
h("span", null, HWLAB_URL),
h("span", null, "external static link"),
h("div", { className: "microservice-actions hwlab-entry-actions" },
h("button", {
type: "button",
className: "ghost-btn",
"data-testid": "hwlab-open-current-from-catalog",
onClick: () => window.location.assign(HWLAB_URL),
}, "当前窗口"),
h("a", {
className: "ghost-btn",
href: HWLAB_URL,
target: "_blank",
rel: "noreferrer",
"data-testid": "hwlab-open-new-from-catalog",
}, "新窗口"),
),
),
),
h("p", { className: "muted paragraph" }, "该入口仅用于外部静态跳转,不会进入 microservice health、proxy 或运行态探测。"),
),
h(Panel, { title: "服务映射", eyebrow: "Repo Reference + Runtime" },
microservices.length === 0 ? h(EmptyState, { title: "暂无用户服务", text: "在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射" }) :
h("div", { className: "table-wrap" }, h("table", { className: "microservice-table" },
@@ -2189,6 +2215,7 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa
if (activeModule === "tasks" && activeTab === "history") return h(TaskHistoryPage, { tasks: data.tasks, onRaw });
if (activeModule === "tasks" && activeTab === "results") return h(TaskResultsPage, { tasks: data.tasks, onRaw });
if (activeModule === "apps" && activeTab === "catalog") return h(MicroserviceCatalogPage, { microservices: data.microservices, onRaw, onNavigate });
if (activeModule === "apps" && activeTab === "hwlab") return h(HwlabPage, {});
if (activeModule === "apps" && activeTab === "todo-note") return h(TodoNotePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
if (activeModule === "apps" && activeTab === "findjob") return h(FindJobPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
if (activeModule === "apps" && activeTab === "pipeline") return h(PipelinePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl });
+103
View File
@@ -0,0 +1,103 @@
import React from "react";
import { LoadingTitle } from "./loading-indicator";
type AnyRecord = Record<string, any>;
const h = React.createElement;
export const HWLAB_URL = "http://74.48.78.17:6666";
function StatusBadge({ status, children }: AnyRecord) {
const normalized = String(status || "unknown").toLowerCase();
return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown");
}
function Panel({ title, eyebrow, actions, children, className }: AnyRecord) {
return h("section", { className: `panel ${className || ""}` },
h("div", { className: "panel-head" },
h("div", null,
eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null,
h(LoadingTitle, { title, loading: false }),
),
actions ? h("div", { className: "panel-actions" }, actions) : null,
),
h("div", { className: "panel-body" }, children),
);
}
function EmptyState({ title, text }: AnyRecord) {
return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text));
}
function ActionLink({ href, children, testId, primary = false }: AnyRecord) {
return h("a", {
className: primary ? "primary-btn compact" : "ghost-btn compact",
href,
target: "_blank",
rel: "noreferrer",
"data-testid": testId,
}, children);
}
function ActionButton({ onClick, children, testId, primary = false }: AnyRecord) {
return h("button", {
type: "button",
className: primary ? "primary-btn compact" : "ghost-btn compact",
onClick,
"data-testid": testId,
}, children);
}
export function HwlabPage(): any {
return h("div", { className: "microservice-page hwlab-page", "data-testid": "hwlab-page" },
h(Panel, {
title: "HWLAB",
eyebrow: "External Static Entry",
actions: h("span", { className: "panel-link-badge" },
h(StatusBadge, { status: "external" }, "external"),
),
},
h("div", { className: "hwlab-hero" },
h("div", { className: "hwlab-hero-copy" },
h("p", { className: "panel-eyebrow" }, "Static Link"),
h("h2", null, "HWLAB"),
h("p", { className: "muted paragraph" }, "这是一个外部静态入口,不经过 UniDesk 后端代理,不纳入 microservice health。"),
),
h("div", { className: "hwlab-actions" },
h(ActionButton, {
primary: true,
testId: "hwlab-open-current",
onClick: () => window.location.assign(HWLAB_URL),
}, "当前窗口打开"),
h(ActionLink, { href: HWLAB_URL, testId: "hwlab-open-new" }, "新窗口打开"),
),
),
h("div", { className: "metric-grid hwlab-metrics" },
h("article", { className: "metric-card" },
h("div", { className: "metric-label" }, "入口类型"),
h("div", { className: "metric-value" }, "external"),
h("div", { className: "metric-hint" }, "static link only"),
),
h("article", { className: "metric-card" },
h("div", { className: "metric-label" }, "目标 URL"),
h("div", { className: "metric-value hwlab-url" }, HWLAB_URL),
h("div", { className: "metric-hint" }, "fixed destination"),
),
h("article", { className: "metric-card" },
h("div", { className: "metric-label" }, "后端依赖"),
h("div", { className: "metric-value" }, "none"),
h("div", { className: "metric-hint" }, "no proxy / no health"),
),
h("article", { className: "metric-card" },
h("div", { className: "metric-label" }, "打开方式"),
h("div", { className: "metric-value" }, "current or new tab"),
h("div", { className: "metric-hint" }, "same style as catalog"),
),
),
h(EmptyState, {
title: "外部入口说明",
text: "HWLAB 只作为静态跳转目标展示在前端,不会登记到用户服务 health、proxy 或运行态数据中。",
}),
),
);
}
@@ -62,6 +62,7 @@ export const MODULES: UniDeskModuleDefinition[] = [
] },
{ id: "apps", label: "用户服务", code: "APP", routeSegment: "app", tabs: [
{ id: "catalog", label: "服务目录" },
{ id: "hwlab", label: "HWLAB" },
{ id: "todo-note", label: "Todo Note" },
{ id: "findjob", label: "FindJob" },
{ id: "pipeline", label: "Pipeline" },