Restore workbench clock, offline world map, and keep edge menus on-screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 00:39:06 +08:00
parent 99d180c501
commit 37dd43923f
6 changed files with 254 additions and 44 deletions

View file

@ -4,6 +4,7 @@ import {
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
@ -220,6 +221,9 @@ function isWorldFlatViewName(name: string | undefined | null): boolean {
return n === "世界地图" || n === "完整世界地图" || n === "World map";
}
/** Flat world-map canvas (LOD / scatter). Temporarily offline in the UI. */
const WORLD_MAP_ENABLED = false;
function isManualRootMapName(name: string | undefined | null): boolean {
const n = String(name || "").trim();
return n === "根图" || n === "Root map";
@ -1115,6 +1119,7 @@ export function TopologyPage() {
const [historyTick, setHistoryTick] = useState(0);
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
const [ctxMenu, setCtxMenu] = useState<CtxMenu | null>(null);
const ctxMenuRef = useRef<HTMLUListElement | null>(null);
const [hideIp, setHideIp] = useState(true);
const [hideVendor, setHideVendor] = useState(true);
const [hidePorts, setHidePorts] = useState(true);
@ -1293,8 +1298,12 @@ export function TopologyPage() {
const graphQuery = useQuery({
queryKey: queryKeys.topologyGraph(mapId),
queryFn: () =>
fetchTopologyGraph(mapId, treeFlatMap ? { lod: "overview" } : undefined),
enabled: Boolean(mapId),
fetchTopologyGraph(
mapId,
WORLD_MAP_ENABLED && treeFlatMap ? { lod: "overview" } : undefined,
),
// Offline: never fetch the flat world-map graph (LOD / scatter).
enabled: Boolean(mapId) && (WORLD_MAP_ENABLED || !treeFlatMap),
staleTime: liveSync ? 0 : 30_000,
refetchOnWindowFocus: liveSync && !treeFlatMap,
refetchInterval: liveSync && mapId && !treeFlatMap ? 3000 : false,
@ -1302,9 +1311,10 @@ export function TopologyPage() {
});
const isWorldFlatCanvas =
treeFlatMap ||
isWorldFlatViewName(graphQuery.data?.view?.name) ||
Boolean(graphQuery.data?.view?.filter?.world_flat);
WORLD_MAP_ENABLED &&
(treeFlatMap ||
isWorldFlatViewName(graphQuery.data?.view?.name) ||
Boolean(graphQuery.data?.view?.filter?.world_flat));
isWorldFlatCanvasRef.current = isWorldFlatCanvas;
mapIdRef.current = mapId;
@ -1360,7 +1370,7 @@ export function TopologyPage() {
}, [graphQuery.data?.world_transform]);
const refreshFlatViewport = useCallback(async () => {
if (!mapId || !isWorldFlatCanvas || dirtyRef.current) return;
if (!WORLD_MAP_ENABLED || !mapId || !isWorldFlatCanvas || dirtyRef.current) return;
const inst = rfRef.current;
if (!inst) return;
const zoom = inst.getZoom();
@ -1425,7 +1435,7 @@ export function TopologyPage() {
}, [mapId, isWorldFlatCanvas, queryClient]);
const scheduleFlatViewportRefresh = useCallback(() => {
if (!isWorldFlatCanvas) return;
if (!WORLD_MAP_ENABLED || !isWorldFlatCanvas) return;
if (flatLodTimerRef.current) window.clearTimeout(flatLodTimerRef.current);
flatLodTimerRef.current = window.setTimeout(() => {
flatLodTimerRef.current = null;
@ -1578,13 +1588,18 @@ export function TopologyPage() {
if (!activeRegion || !isUmeWorldContainer(activeRegion)) return null;
const drill =
(activeRegion.children || []).find((c) => isWorldDrillFolder(c)) || null;
const flatView =
(activeRegion.views || []).find((v) => isWorldFlatViewName(v.name)) || null;
const flatView = WORLD_MAP_ENABLED
? (activeRegion.views || []).find((v) => isWorldFlatViewName(v.name)) || null
: null;
return { drill, flatView };
}, [activeRegion]);
// Resolve World drill view id for shortcuts; do not auto-open canvas.
useEffect(() => {
if (!WORLD_MAP_ENABLED) {
setWorldViewId("");
return;
}
let cancelled = false;
fetchTopologyWorld()
.then((w) => {
@ -1625,7 +1640,23 @@ export function TopologyPage() {
clearDirty();
}, [confirmDiscardIfDirty, clearDirty, regions]);
// Leave flat world map if still open after the feature was taken offline.
useEffect(() => {
if (WORLD_MAP_ENABLED || !mapId || !treeFlatMap) return;
const container = regions.find(
(r) => r.external_ref === "ume:world" || r.name === "UME World",
);
setWorldFocusFolderId("");
if (container) {
setSelectedFolderId(container.id);
setExpandedIds((p) => ({ ...p, [container.id]: true }));
}
setMapId("");
clearDirty();
}, [mapId, treeFlatMap, regions, clearDirty]);
const goWorld = useCallback(() => {
if (!WORLD_MAP_ENABLED) return;
if (!confirmDiscardIfDirty()) return;
if (!worldViewId) return;
setWorldFocusFolderId("");
@ -1727,6 +1758,10 @@ export function TopologyPage() {
(viewId: string, folderId?: string) => {
if (!confirmDiscardIfDirty()) return;
const hit = findViewInRegion(regions, viewId);
if (!WORLD_MAP_ENABLED && isWorldFlatViewName(hit?.view?.name)) {
showError(t("topology.worldMapOffline"));
return;
}
const regionId = folderId || hit?.region.id || selectedFolderId;
if (regionId) {
setSelectedFolderId(regionId);
@ -1735,7 +1770,7 @@ export function TopologyPage() {
setWorldFocusFolderId("");
setMapId(viewId);
},
[confirmDiscardIfDirty, regions, selectedFolderId, expandFolderPath],
[confirmDiscardIfDirty, regions, selectedFolderId, expandFolderPath, showError, t],
);
const goBackBrowse = useCallback(() => {
@ -2187,13 +2222,15 @@ export function TopologyPage() {
const umeNav = isUmeWorldNavFolder(folder);
// Region === canvas: hide the physical primary row (region click opens it),
// but keep sibling custom canvases visible (e.g. MCP LPG-Full next to 根图).
// UME World container: only「世界地图」as a sibling view.
// UME World container: only「世界地图」as a sibling view (when enabled).
const canvasRegion = isRegionCanvasFolder(folder, String(treeRoot?.id || ""));
const visibleViews = canvasRegion
? (folder.views || []).filter((v) => String(v.kind) !== "physical")
: containerFolder
? (folder.views || []).filter((v) => isWorldFlatViewName(v.name))
: folder.views || [];
const visibleViews = (
canvasRegion
? (folder.views || []).filter((v) => String(v.kind) !== "physical")
: containerFolder
? (folder.views || []).filter((v) => isWorldFlatViewName(v.name))
: folder.views || []
).filter((v) => WORLD_MAP_ENABLED || !isWorldFlatViewName(v.name));
const hasKids = kids.length > 0 || visibleViews.length > 0;
const open = Boolean(expandedIds[folder.id]);
const viewActiveUnder =
@ -3301,7 +3338,11 @@ export function TopologyPage() {
useEffect(() => {
if (!ctxMenu) return;
const onScroll = () => closeCtxMenu();
const onScroll = (e: Event) => {
const t = e.target;
if (t instanceof Node && ctxMenuRef.current?.contains(t)) return;
closeCtxMenu();
};
const onPointerDown = (e: MouseEvent) => {
const el = e.target as HTMLElement | null;
if (el?.closest?.(".topo-ctx")) return;
@ -3315,6 +3356,25 @@ export function TopologyPage() {
};
}, [ctxMenu, closeCtxMenu]);
useLayoutEffect(() => {
if (!ctxMenu || !ctxMenuRef.current) return;
const el = ctxMenuRef.current;
const pad = 8;
const rect = el.getBoundingClientRect();
let x = ctxMenu.x;
let y = ctxMenu.y;
if (x + rect.width > window.innerWidth - pad) {
x = Math.max(pad, window.innerWidth - rect.width - pad);
}
if (y + rect.height > window.innerHeight - pad) {
y = Math.max(pad, window.innerHeight - rect.height - pad);
}
if (x !== ctxMenu.x || y !== ctxMenu.y) {
el.style.left = `${x}px`;
el.style.top = `${y}px`;
}
}, [ctxMenu]);
const removeNodeById = (nodeId: string) => {
if (!mapId) return;
pushHistory();
@ -3765,21 +3825,28 @@ export function TopologyPage() {
}
const target =
view ||
views.find((v) => isWorldFlatViewName(v.view_name)) ||
views.find((v) => String(v.kind) === "physical") ||
(WORLD_MAP_ENABLED
? views.find((v) => isWorldFlatViewName(v.view_name))
: undefined) ||
views.find((v) => !isWorldFlatViewName(v.view_name) && String(v.kind) === "physical") ||
views.find((v) => !isWorldFlatViewName(v.view_name)) ||
views[0] ||
null;
if (!target) {
showError(t("topology.treeSearchNotOnMap"));
return;
}
if (!WORLD_MAP_ENABLED && isWorldFlatViewName(target.view_name)) {
showError(t("topology.worldMapOffline"));
return;
}
if (!confirmDiscardIfDirty()) return;
const regionId = target.folder_id || findViewInRegion(regions, target.view_id)?.region.id;
if (regionId) {
setSelectedFolderId(regionId);
setExpandedIds((p) => ({ ...p, [regionId]: true }));
}
if (isWorldFlatViewName(target.view_name) && worldCoords) {
if (WORLD_MAP_ENABLED && isWorldFlatViewName(target.view_name) && worldCoords) {
pendingLocateWorldRef.current = { id: hit.id, ...worldCoords };
} else {
pendingLocateWorldRef.current = null;
@ -4226,7 +4293,12 @@ export function TopologyPage() {
</button>
{views.length > 0 ? (
<div className="topo-tree-search__views">
{views.map((v) => (
{views
.filter(
(v) =>
WORLD_MAP_ENABLED || !isWorldFlatViewName(v.view_name),
)
.map((v) => (
<button
key={`${hit.id}-${v.view_id}`}
type="button"
@ -6034,6 +6106,7 @@ export function TopologyPage() {
{ctxMenu
? createPortal(
<ul
ref={ctxMenuRef}
className={`topo-ctx${ctxMenu.kind === "edge" ? " topo-ctx--edge" : ""}`}
style={{ left: ctxMenu.x, top: ctxMenu.y }}
role="menu"

View file

@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useI18n } from "../i18n";
import { WorkbenchCardIcon } from "../components/WorkbenchCardIcon";
@ -84,7 +84,7 @@ function Gauge({
export function WorkbenchPage() {
const { t, locale } = useI18n();
const { user, isAdmin, hasScope } = useAuth();
const { isAdmin, hasScope } = useAuth();
const metricsQuery = useQuery({
queryKey: ["runtimeMetrics"],
@ -148,28 +148,50 @@ export function WorkbenchPage() {
const taskCap = Math.max(totalTasks, activeTasks, 8);
const taskPct = ratioPct(activeTasks, taskCap);
const todayLabel = useMemo(() => {
try {
return new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
weekday: "short",
}).format(new Date());
} catch {
return new Date().toLocaleDateString();
}
}, [locale]);
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(id);
}, []);
const displayName = user?.username || "admin";
const clockLocale = locale === "en" ? "en-US" : "zh-CN";
const clockDate = useMemo(() => {
try {
return new Intl.DateTimeFormat(clockLocale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
weekday: "short",
}).format(now);
} catch {
return now.toLocaleDateString();
}
}, [now, clockLocale]);
const hh = String(now.getHours()).padStart(2, "0");
const mm = String(now.getMinutes()).padStart(2, "0");
const ss = String(now.getSeconds()).padStart(2, "0");
const colonOn = now.getSeconds() % 2 === 0;
return (
<div className="workbench">
<section className="wb-hero" aria-label={t("workbench.title")}>
<div className="wb-hero__copy">
<p className="wb-hero__eyebrow">{t("workbench.welcomeEyebrow")}</p>
<h1 className="wb-hero__title">{t("workbench.welcomeTitle", { name: displayName })}</h1>
<p className="wb-hero__date">{t("workbench.today", { date: todayLabel })}</p>
<div className="wb-hero__clock" aria-live="polite">
<time className="wb-hero__clock-panel" dateTime={now.toISOString()}>
<span className="wb-hero__clock-digits" aria-hidden="true">
<span className="wb-hero__digit">{hh[0]}</span>
<span className="wb-hero__digit">{hh[1]}</span>
<span className={`wb-hero__colon${colonOn ? " is-on" : ""}`}>:</span>
<span className="wb-hero__digit">{mm[0]}</span>
<span className="wb-hero__digit">{mm[1]}</span>
<span className={`wb-hero__colon${colonOn ? " is-on" : ""}`}>:</span>
<span className="wb-hero__digit">{ss[0]}</span>
<span className="wb-hero__digit">{ss[1]}</span>
</span>
<span className="wb-hero__clock-sr">
{hh}:{mm}:{ss}
</span>
<span className="wb-hero__clock-date">{clockDate}</span>
</time>
</div>
<div className="wb-hero__status" aria-label={t("workbench.statusTitle")}>
<div className="wb-gauges wb-gauges--hero">

View file

@ -446,7 +446,10 @@ export default function TopologyReactFlowView(props: TopologyReactFlowViewProps)
onEdgeContextMenu={(e, edge) => {
e.preventDefault();
focusEdge(edge.id);
const pos = placeCtxMenu(e.clientX, e.clientY, { w: 260, h: 280 });
const pos = placeCtxMenu(e.clientX, e.clientY, {
w: 280,
h: Math.min(560, Math.max(280, window.innerHeight - 24)),
});
setCtxMenu({ kind: "edge", id: edge.id, ...pos });
}}
onSelectionContextMenu={(e) => {