From fce6e2cc9e283885376b41e405cfe771636c4263 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 1 Aug 2026 17:47:08 +0800 Subject: [PATCH] Unify NetX module pages on a classic slate chrome UI. Align NE, UME, system, topology, and WebCRT with shared list styling, and set the browser tab title to NetX. Co-authored-by: Cursor --- web/index.html | 2 +- web/src/components/HeaderMenu.tsx | 2 +- web/src/index.css | 949 ++++++++++++++---- web/src/layout/AppLayout.tsx | 58 +- web/src/pages/ApiTokensPage.tsx | 217 ++-- web/src/pages/AuditPage.tsx | 188 ++-- web/src/pages/NePage.tsx | 447 +++++---- web/src/pages/UmePage.tsx | 114 ++- web/src/pages/UsersPage.tsx | 193 ++-- web/src/pages/WebcrtPage.tsx | 10 +- web/src/pages/network/PortTrafficWallPage.tsx | 2 +- 11 files changed, 1439 insertions(+), 743 deletions(-) diff --git a/web/index.html b/web/index.html index ff8be2a..c2ab048 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ - NetX · UME + NetX
diff --git a/web/src/components/HeaderMenu.tsx b/web/src/components/HeaderMenu.tsx index 321423d..f6baf01 100644 --- a/web/src/components/HeaderMenu.tsx +++ b/web/src/components/HeaderMenu.tsx @@ -31,7 +31,7 @@ export function HeaderMenu() {
- - {t("layout.netxApi")}: {connections.netxApi} - {netxSuffix} - - - {t("layout.oclawBridge")}: {connections.oclawBridge} - {oclawSuffix} - - {user ? ( - - {user.username} +
+ + {t("layout.netxApi")}: {connections.netxApi} + {netxSuffix} - ) : null} - {user ? ( - - ) : null} - + {t("layout.oclawBridge")}: {connections.oclawBridge} + {oclawSuffix} + +
+ {user ? ( + <> + +
+ + {user.username} + + + +
+ + ) : ( + + )}
diff --git a/web/src/pages/ApiTokensPage.tsx b/web/src/pages/ApiTokensPage.tsx index 9bb3de8..f12cf3c 100644 --- a/web/src/pages/ApiTokensPage.tsx +++ b/web/src/pages/ApiTokensPage.tsx @@ -4,6 +4,7 @@ import { useAuth } from "../auth/AuthContext"; import { useI18n } from "../i18n"; import { useToast } from "../hooks/useToast"; import { apiDelete, apiGet, apiPost } from "../services/api"; +import { formatSystemTime } from "../utils/time"; type TokenRow = { id: string; @@ -33,6 +34,12 @@ const EXPIRY_OPTIONS = [ { value: 0, labelKey: "auth.expireNever" }, ] as const; +function tokenStatusClass(row: TokenRow): string { + if (row.revoked) return "pt-list-status--failed"; + if (row.expired) return "pt-list-status--warning"; + return "pt-list-status--ok"; +} + export function ApiTokensPage() { const { t } = useI18n(); const { ready, user, isAdmin } = useAuth(); @@ -98,102 +105,130 @@ export function ApiTokensPage() { }; return ( -
-

{t("auth.apiKeysTitle")}

-

{t("auth.apiKeysHint")}

+
+
+
+

{t("auth.apiKeysTitle")}

+
+

{t("auth.apiKeysHint")}

-
- setName(e.target.value)} - required - /> - - {isAdmin ? ( - setName(e.target.value)} + required + /> + - ) : null} - -
+ + {isAdmin ? ( + + ) : null} + + - {createdPlain ? ( -
-
{t("auth.tokenOnceHint")}
- {createdPlain} - + {createdPlain ? ( +
+
+ {t("auth.tokenOnceHint")} +
+ {createdPlain} + +
+ ) : null} + + {tokensQuery.isLoading ?

{t("common.refreshing")}

: null} + + {!items.length && !tokensQuery.isLoading ? ( +
+

{t("common.empty")}

+
+ ) : ( +
+ + + + + + + + + + + + + + {items.map((row) => ( + + + + + + + + + + ))} + +
{t("auth.tokenName")}{t("auth.tokenOwner")}{t("auth.colTime")}{t("auth.expiresAt")}{t("auth.lastUsed")}{t("auth.status")}{t("auth.actions")}
{row.name}{row.username || row.user_id} + {row.created_at ? formatSystemTime(row.created_at) : t("common.empty")} + + {row.expires_at ? formatSystemTime(row.expires_at) : t("auth.expireNever")} + + {row.last_used_at ? formatSystemTime(row.last_used_at) : t("common.empty")} + + + {row.revoked + ? t("auth.tokenStatusRevoked") + : row.expired + ? t("auth.tokenStatusExpired") + : t("auth.tokenStatusActive")} + + +
+ +
+
+
+ )}
- ) : null} - - {tokensQuery.isLoading ?
{t("common.refreshing")}
: null} - - - - - - - - - - - - - - {items.map((row) => ( - - - - - - - - - - ))} - -
{t("auth.tokenName")}{t("auth.tokenOwner")}{t("auth.colTime")}{t("auth.expiresAt")}{t("auth.lastUsed")}{t("auth.status")}{t("auth.actions")}
{row.name}{row.username || row.user_id}{row.created_at || "-"}{row.expires_at || t("auth.expireNever")}{row.last_used_at || "-"} - {row.revoked - ? t("auth.tokenStatusRevoked") - : row.expired - ? t("auth.tokenStatusExpired") - : t("auth.tokenStatusActive")} - - -
+
); } diff --git a/web/src/pages/AuditPage.tsx b/web/src/pages/AuditPage.tsx index 5ae04f0..a30a052 100644 --- a/web/src/pages/AuditPage.tsx +++ b/web/src/pages/AuditPage.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { useAuth } from "../auth/AuthContext"; import { useI18n } from "../i18n"; import { apiGet } from "../services/api"; +import { formatSystemTime } from "../utils/time"; type AuditItem = { id: string; @@ -16,6 +17,13 @@ type AuditItem = { detail: Record; }; +function statusClass(code: number): string { + if (code >= 500) return "pt-list-status--failed"; + if (code >= 400) return "pt-list-status--warning"; + if (code >= 200 && code < 300) return "pt-list-status--ok"; + return "pt-list-status--other"; +} + export function AuditPage() { const { t } = useI18n(); const { ready, isAdmin } = useAuth(); @@ -41,79 +49,123 @@ export function AuditPage() { const items = useMemo(() => query.data?.items || [], [query.data]); const total = query.data?.total || 0; const pages = Math.max(1, Math.ceil(total / 50)); + const hasFilters = Boolean(username.trim() || action.trim()); return ( -
-

{t("auth.auditTitle")}

-

{isAdmin ? t("auth.auditHintAdmin") : t("auth.auditHintUser")}

+
+
+
+

{t("auth.auditTitle")}

+
+

{isAdmin ? t("auth.auditHintAdmin") : t("auth.auditHintUser")}

-
- {isAdmin ? ( - { - setPage(1); - setUsername(e.target.value); - }} - /> - ) : null} - { - setPage(1); - setAction(e.target.value); - }} - /> - -
+
+
+ {isAdmin ? ( + { + setPage(1); + setUsername(e.target.value); + }} + /> + ) : null} + { + setPage(1); + setAction(e.target.value); + }} + /> + + +
- {query.isLoading ?
{t("common.refreshing")}
: null} - - - - - - - - - - - - - - {items.map((row) => ( - - - - - - - - - - ))} - -
{t("auth.colTime")}{t("auth.colUser")}{t("auth.colAction")}{t("auth.colMethod")}{t("auth.colPath")}{t("auth.colStatus")}{t("auth.colIp")}
{row.ts || "-"}{row.actor_username || "-"}{row.action}{row.method}{row.path}{row.status_code}{row.client_ip || "-"}
+ {query.isLoading ?

{t("common.refreshing")}

: null} -
- - - {t("common.pagerMeta", { total, page, pages })} - - -
+ {!items.length && !query.isLoading ? ( +
+

{t("common.empty")}

+
+ ) : ( +
+ + + + + + + + + + + + + + {items.map((row) => ( + + + + + + + + + + ))} + +
{t("auth.colTime")}{t("auth.colUser")}{t("auth.colAction")}{t("auth.colMethod")}{t("auth.colPath")}{t("auth.colStatus")}{t("auth.colIp")}
+ {row.ts ? formatSystemTime(row.ts) : t("common.empty")} + {row.actor_username || t("common.empty")}{row.action}{row.method} + {row.path} + + + {row.status_code} + + {row.client_ip || t("common.empty")}
+
+ )} + +
+ + {t("common.pagerMeta", { + total: String(total), + page: String(page), + pages: String(pages), + })} + +
+ + +
+
+
+
); } diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx index ac779b1..641ef0f 100644 --- a/web/src/pages/NePage.tsx +++ b/web/src/pages/NePage.tsx @@ -111,11 +111,11 @@ function FormLabel({ children, required }: { children: ReactNode; required?: boo ); } -function connectPillLevel(status: string): "up" | "down" | "unknown" | "warn" { - if (status === "pass") return "up"; - if (status === "fail") return "down"; - if (status === "testing") return "warn"; - return "unknown"; +function connectStatusClass(status: string): string { + if (status === "pass") return "pt-list-status--ok"; + if (status === "fail") return "pt-list-status--failed"; + if (status === "testing") return "pt-list-status--running"; + return "pt-list-status--unknown"; } export function NePage() { @@ -491,53 +491,55 @@ export function NePage() { return items; }, [stats, perTag, t]); + const listItems = listQuery.data?.items || []; + const hasFilters = Boolean(keyword || vendorFilter || statusFilter); + return ( -
+
{!credsOk ? (

{t("managedNe.credsNotConfigured")}

) : null} - {/* ── statistics card ── */} -
-
-
- {t("managedNe.stats.title")} -
-
- - - +
+
+

{t("managedNe.stats.title")}

+
+
+ + + +
{stats ? ( @@ -548,7 +550,7 @@ export function NePage() {
{t("managedNe.stats.total", { n: x.total })}
{(["pass", "fail", "testing", "unknown"] as const).map((s) => ( - + {t(`managedNe.stats.${s}`)} {x.by_status[s] ?? 0} ))} @@ -671,170 +673,205 @@ export function NePage() {
-
- setKeyword(e.target.value)} - /> - - - - -
- - - - - - - - - - - - - - - - - - {(listQuery.data?.items || []).map((row) => ( - - - - - - - - - - - - - ))} - -
- - {t("managedNe.col.name")}{t("managedNe.col.vendor")}{t("managedNe.col.deviceType")}{t("managedNe.col.tags")}{t("managedNe.col.ip")}{t("managedNe.col.user")}{t("managedNe.col.connect")}{t("managedNe.col.testedAt")}{t("managedNe.col.actions")}
- toggleSelect(row.id)} - /> - {row.name || row.ip_address}{row.vendor}{row.device_type} - {row.tags - ? row.tags.split(/\s+/).map((tag) => ( - - {tag} - - )) - : t("common.empty")} - - {row.ip_address}:{row.port}/{row.protocol} - {row.hop_enabled ? ( - - {t( - `managedNe.hop.badge.${["linux", "huawei", "cisco", "zte", "bastion"].includes(row.hop_vendor) ? row.hop_vendor : "zte"}`, - )} - - ) : null} - {row.username} - - {row.connect_status} - - - {row.connect_tested_at - ? formatSystemTime(row.connect_tested_at, { assumeUtcNaive: true }) - : t("common.empty")} - - - - - -
- -
-
{t("common.pagerMeta", { total, page, pages })}
-
- - - { - setPageSize(Number(e.target.value) || 50); + setKeyword(e.target.value); + setPage(1); + }} + /> + + + +
+ + {listQuery.isLoading ?

{t("common.refreshing")}

: null} + + {!listItems.length && !listQuery.isLoading ? ( +
+

{t("common.empty")}

+
+ ) : ( +
+ + + + + + + + + + + + + + + + + {listItems.map((row) => ( + + + + + + + + + + + + + ))} + +
+ + {t("managedNe.col.name")}{t("managedNe.col.vendor")}{t("managedNe.col.deviceType")}{t("managedNe.col.tags")}{t("managedNe.col.ip")}{t("managedNe.col.user")}{t("managedNe.col.connect")}{t("managedNe.col.testedAt")}{t("managedNe.col.actions")}
+ toggleSelect(row.id)} + /> + {row.name || row.ip_address}{row.vendor}{row.device_type} + {row.tags + ? row.tags.split(/\s+/).map((tag) => ( + + {tag} + + )) + : t("common.empty")} + + {row.ip_address}:{row.port}/{row.protocol} + {row.hop_enabled ? ( + + {t( + `managedNe.hop.badge.${["linux", "huawei", "cisco", "zte", "bastion"].includes(row.hop_vendor) ? row.hop_vendor : "zte"}`, + )} + + ) : null} + {row.username} + + {row.connect_status} + + + {row.connect_tested_at + ? formatSystemTime(row.connect_tested_at, { assumeUtcNaive: true }) + : t("common.empty")} + +
+ + + + +
+
+
+ )} + +
+ + {t("common.pagerMeta", { + total: String(total), + page: String(page), + pages: String(pages), + })} + +
+ + + +
diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index f2ae06d..3c0e617 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -430,10 +430,12 @@ export function UmePage() { return ( <> -
-
-
-

{t("ume.token.title")}

+
+
+
+
+

{t("ume.token.title")}

+
-
-

{t("ume.tasks.currentTitle")}

-
- 0 ? "unknown" : "up"}`}> - {t("ume.tasks.running")}: {runningTasks.length} - - +
+
+

{t("ume.tasks.currentTitle")}

+
+ 0 ? "unknown" : "up"}`}> + {t("ume.tasks.running")}: {runningTasks.length} + + +
- +
+
+
@@ -549,6 +559,7 @@ export function UmePage() { )}
ID
+

{t("ume.tasks.runtimeTitle")}

{runtimeTaskError ? ( @@ -556,7 +567,8 @@ export function UmePage() { {t("ume.tasks.runtimeOpFailed")}: {runtimeTaskError}
) : null} - +
+
@@ -607,13 +619,17 @@ export function UmePage() { )}
task
+
+
-
-

- {t("ume.subscription.title")} - -

+
+
+

+ {t("ume.subscription.title")} + +

+
{t("ume.subscription.sub")}: {subscriptionActive ? t("ume.subscription.subActive") : t("ume.subscription.subInactive")} @@ -757,12 +773,12 @@ export function UmePage() {
-
-
-

+
+
+

{t("ume.cli.title")} -

+

@@ -770,12 +786,12 @@ export function UmePage() { {cliPanelOpen ? : null}
-
-
-

+
+
+

{t("ume.keyAlert.title")} -

+

@@ -974,7 +990,7 @@ export function UmePage() { {t("common.opFailed")}: {keyAlertOpError}
) : null} -
+
{t("ume.keyAlert.filterEnabledOff")}
- +
+
@@ -1093,11 +1110,12 @@ export function UmePage() { ) : null}
{t("ume.keyAlert.colMonitor")}
-
-
+
+
+
{t("common.pagerMeta", { total: keyAlertTotal, page: keyAlertPage, pages: keyAlertPages })}
-
+
{syncStatusPanelOpen ? ( - <> +
-
- +
+
@@ -1182,9 +1201,10 @@ export function UmePage() { )}
ID
-
-
{t("common.pagerMeta", { total: syncTotal, page: syncPage, pages: syncPages })}
-
+
+
+
{t("common.pagerMeta", { total: syncTotal, page: syncPage, pages: syncPages })}
+
@@ -1206,7 +1226,7 @@ export function UmePage() {
- +
) : null} @@ -1218,7 +1238,7 @@ export function UmePage() {
{nePanelOpen ? ( - <> +
setNeKeyword(e.target.value)} />
- +
+
@@ -1296,9 +1317,10 @@ export function UmePage() { ))}
ne_id
-
-
{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}
-
+
+
+
{t("common.pagerMeta", { total: neTotal, page: nePage, pages: nePages })}
+
@@ -1321,7 +1343,7 @@ export function UmePage() {
- +
) : null} diff --git a/web/src/pages/UsersPage.tsx b/web/src/pages/UsersPage.tsx index fa5d092..8a08875 100644 --- a/web/src/pages/UsersPage.tsx +++ b/web/src/pages/UsersPage.tsx @@ -62,91 +62,118 @@ export function UsersPage() { }; return ( -
-

{t("auth.usersTitle")}

-

{t("auth.usersHint")}

+
+
+
+

{t("auth.usersTitle")}

+
+

{t("auth.usersHint")}

-
- setUsername(e.target.value)} - required - /> - setPassword(e.target.value)} - required - minLength={6} - /> - - -
+
+
+ setUsername(e.target.value)} + required + /> + setPassword(e.target.value)} + required + minLength={6} + /> + + +
- {usersQuery.isLoading ?
{t("common.refreshing")}
: null} - - - - - - - - - - - {items.map((u) => ( - - - - - - - ))} - -
{t("auth.username")}{t("auth.role")}{t("auth.status")}{t("auth.actions")}
{u.username}{u.role === "admin" ? t("auth.roleAdmin") : t("auth.roleUser")}{u.is_active ? t("auth.active") : t("auth.disabled")} -
- - - setResetPwd((m) => ({ ...m, [u.id]: e.target.value }))} - style={{ width: 140 }} - /> - -
-
+ {usersQuery.isLoading ?

{t("common.refreshing")}

: null} + + {!items.length && !usersQuery.isLoading ? ( +
+

{t("common.empty")}

+
+ ) : ( +
+ + + + + + + + + + + {items.map((u) => ( + + + + + + + ))} + +
{t("auth.username")}{t("auth.role")}{t("auth.status")}{t("auth.actions")}
{u.username}{u.role === "admin" ? t("auth.roleAdmin") : t("auth.roleUser")} + + {u.is_active ? t("auth.active") : t("auth.disabled")} + + +
+ + + + setResetPwd((m) => ({ ...m, [u.id]: e.target.value })) + } + style={{ width: 140, minWidth: 140, flex: "0 0 auto" }} + /> + +
+
+
+ )} +
+
); } diff --git a/web/src/pages/WebcrtPage.tsx b/web/src/pages/WebcrtPage.tsx index 51380c2..29d7662 100644 --- a/web/src/pages/WebcrtPage.tsx +++ b/web/src/pages/WebcrtPage.tsx @@ -35,6 +35,14 @@ function deviceLabel(t: Pick): string { return String(t.name || t.ip_address || "").trim() || "-"; } +function ComputerIcon() { + return ( + + + + ); +} + function webcrtErrorMessage(err: unknown, t: (key: string, vars?: Record) => string): string { const raw = String(err); if (raw.includes("webcrt_session_limit")) return t("webcrt.err.sessionLimit"); @@ -336,7 +344,7 @@ export function WebcrtPage() { title={`${deviceLabel(row)}\n${row.ip_address}\n${row.source}`} > - ▣ + {deviceLabel(row)} diff --git a/web/src/pages/network/PortTrafficWallPage.tsx b/web/src/pages/network/PortTrafficWallPage.tsx index 05029d1..cf3e8ba 100644 --- a/web/src/pages/network/PortTrafficWallPage.tsx +++ b/web/src/pages/network/PortTrafficWallPage.tsx @@ -144,7 +144,7 @@ export function PortTrafficWallPage() { useEffect(() => { if (!board?.name) return; try { - document.title = `${board.name} · NetX`; + document.title = "NetX"; } catch { /* ignore */ }