feat(frontend): add HWLAB external entry
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user