mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 23:20:52 +08:00
Polish HeroUI chrome, topology toolbar, and session client IP.
Checkpoint before workbench facade redesign: list defaults, API key quota, toast portal, topology canvas editor toolbar with More menu, and trusted-proxy client IP for sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5096117ad4
commit
c4526e36d8
64 changed files with 6316 additions and 4521 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "@heroui/react";
|
||||
import type { Edge, Node } from "@xyflow/react";
|
||||
import { useI18n } from "../../i18n";
|
||||
import type { CtxMenu } from "./pageTypes";
|
||||
|
|
@ -372,16 +373,16 @@ export function TopologyCtxMenu({
|
|||
))}
|
||||
</ul>
|
||||
{!expandPhysicalLinks ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
onClick={() => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
onExpandPhysicalLinks();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("topology.expandPhysicalLinks")}
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Button } from "@heroui/react";
|
||||
import type { TopologyDiscoverOut } from "../../types";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { openOrFocusModule } from "../../utils/moduleWindows";
|
||||
|
|
@ -48,10 +49,10 @@ export function TopologyDiscoverPanel({
|
|||
<div className="topo-discover">
|
||||
<div className="topo-discover__head">
|
||||
<strong>{t("topology.discoverReport")}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
if (discovering) {
|
||||
void onCancelDiscover();
|
||||
return;
|
||||
|
|
@ -60,7 +61,7 @@ export function TopologyDiscoverPanel({
|
|||
}}
|
||||
>
|
||||
{discovering ? t("topology.discoverCancel") : t("topology.discoverClose")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{discovering && discoverProgress.total > 0 ? (
|
||||
<div className="topo-discover__progress" aria-live="polite">
|
||||
|
|
@ -96,10 +97,10 @@ export function TopologyDiscoverPanel({
|
|||
.replace("{{failed}}", String(discoverSummary.failed))}
|
||||
</p>
|
||||
<p className="panel__hint">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
const jobId = String(discoverReport?.job_id || "").trim();
|
||||
const qs = jobId ? `?job_id=${encodeURIComponent(jobId)}` : "";
|
||||
openOrFocusModule({
|
||||
|
|
@ -109,7 +110,7 @@ export function TopologyDiscoverPanel({
|
|||
}}
|
||||
>
|
||||
{t("topology.discoverGoLldp")}
|
||||
</button>
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Button } from "@heroui/react";
|
||||
import { Fragment } from "react";
|
||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
|
@ -125,17 +126,17 @@ export function TopologyHexBrowser({
|
|||
<p>{t("topology.treeLoadFailed")}</p>
|
||||
{treeError ? <span className="muted"> ({String(treeError)})</span> : null}
|
||||
<div className="topo-browser__empty-actions">
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onTreeRetry}>
|
||||
<Button size="sm" variant="ghost" onPress={onTreeRetry}>
|
||||
{t("topology.treeRetry")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
onClick={promptNewRegion}
|
||||
disabled={createRegionPending}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onPress={promptNewRegion}
|
||||
isDisabled={createRegionPending}
|
||||
>
|
||||
{t("topology.newRegion")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : regions.length === 0 ? (
|
||||
|
|
@ -144,14 +145,14 @@ export function TopologyHexBrowser({
|
|||
<RegionGlyph size={36} />
|
||||
</span>
|
||||
<p>{t("topology.emptyMaps")}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
onClick={promptNewRegion}
|
||||
disabled={createRegionPending}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onPress={promptNewRegion}
|
||||
isDisabled={createRegionPending}
|
||||
>
|
||||
{t("topology.newRegion")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="topo-browser__grid topo-browser__grid--regions">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
|
||||
import { Button, Input } from "@heroui/react";
|
||||
import type { FabricNodeSearchHit, TopologyTreeFolderItem } from "../../types";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { WORLD_MAP_ENABLED } from "./constants";
|
||||
|
|
@ -71,24 +72,24 @@ export function TopologySidebar({
|
|||
<aside className="topo-sidebar" aria-label={t("topology.maps")}>
|
||||
{collapsed ? (
|
||||
<div className="topo-sidebar__rail-wrap">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
className="topo-sidebar__rail"
|
||||
title={t("topology.expandSidebar")}
|
||||
aria-label={t("topology.expandSidebar")}
|
||||
onClick={onExpand}
|
||||
onPress={onExpand}
|
||||
>
|
||||
<span className="topo-sidebar__rail-icon" aria-hidden="true">
|
||||
<SidebarFoldIcon expand />
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="topo-sidebar__section">
|
||||
<div className="topo-tree-search" ref={treeSearchRef}>
|
||||
<div className="topo-tree-search__bar">
|
||||
<input
|
||||
<Input
|
||||
className="input"
|
||||
type="search"
|
||||
value={treeNeQuery}
|
||||
|
|
@ -100,25 +101,27 @@ export function TopologySidebar({
|
|||
}}
|
||||
onFocus={() => onTreeSearchOpen(true)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="topo-sidebar__icon-btn"
|
||||
onClick={promptNewRegion}
|
||||
disabled={createRegionPending}
|
||||
title={t("topology.newRegion")}
|
||||
onPress={promptNewRegion}
|
||||
isDisabled={createRegionPending}
|
||||
aria-label={t("topology.newRegion")}
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="topo-sidebar__icon-btn"
|
||||
title={t("topology.collapseSidebar")}
|
||||
aria-label={t("topology.collapseSidebar")}
|
||||
onClick={onCollapse}
|
||||
onPress={onCollapse}
|
||||
>
|
||||
<SidebarFoldIcon />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{treeSearchOpen && treeNeQuery.trim() ? (
|
||||
<div className="topo-tree-search__panel" role="listbox">
|
||||
|
|
@ -206,9 +209,9 @@ export function TopologySidebar({
|
|||
<>
|
||||
{t("topology.treeLoadFailed")}
|
||||
{treeError ? <span className="muted"> ({String(treeError)})</span> : null}{" "}
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onTreeRetry}>
|
||||
<Button size="sm" variant="ghost" onPress={onTreeRetry}>
|
||||
{t("topology.treeRetry")}
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
t("topology.emptyMaps")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Fragment, type RefObject } from "react";
|
||||
import { Button, Dropdown } from "@heroui/react";
|
||||
import { Fragment, type Key, type RefObject, useMemo } from "react";
|
||||
import type { Edge, Node, ReactFlowInstance } from "@xyflow/react";
|
||||
import type { TopologyTreeFolderItem, TopologyTreeViewItem } from "../../types";
|
||||
import { HelpHint } from "../../components/HelpHint";
|
||||
|
|
@ -31,7 +32,6 @@ export type TopologyToolbarProps = {
|
|||
savePending: boolean;
|
||||
onSave: () => void;
|
||||
onFit: () => void;
|
||||
exportMenuRef: RefObject<HTMLDetailsElement | null>;
|
||||
exporting: boolean;
|
||||
onExport: (kind: "svg" | "xml") => void;
|
||||
mapId: string;
|
||||
|
|
@ -51,6 +51,16 @@ export type TopologyToolbarProps = {
|
|||
) => TopologyTreeViewItem | null | undefined;
|
||||
};
|
||||
|
||||
type MoreActionKey =
|
||||
| "live-sync"
|
||||
| "back"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "fit"
|
||||
| "export-svg"
|
||||
| "export-xml"
|
||||
| "remove-stale";
|
||||
|
||||
export function TopologyToolbar({
|
||||
readOnly = false,
|
||||
breadcrumbFolders,
|
||||
|
|
@ -75,7 +85,6 @@ export function TopologyToolbar({
|
|||
savePending,
|
||||
onSave,
|
||||
onFit,
|
||||
exportMenuRef,
|
||||
exporting,
|
||||
onExport,
|
||||
mapId,
|
||||
|
|
@ -94,6 +103,27 @@ export function TopologyToolbar({
|
|||
}: TopologyToolbarProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const canvasEmpty = !isWorldFlatCanvas && nodes.length === 0;
|
||||
|
||||
const moreDisabledKeys = useMemo(() => {
|
||||
const keys: MoreActionKey[] = [];
|
||||
if (readOnly || !canUndo) keys.push("undo");
|
||||
if (readOnly || !canRedo) keys.push("redo");
|
||||
if (canvasEmpty) keys.push("fit");
|
||||
if (exporting || canvasEmpty) keys.push("export-svg");
|
||||
if (exporting || !mapId) keys.push("export-xml");
|
||||
if (readOnly || !staleEdgeCount) keys.push("remove-stale");
|
||||
return keys;
|
||||
}, [
|
||||
readOnly,
|
||||
canUndo,
|
||||
canRedo,
|
||||
canvasEmpty,
|
||||
exporting,
|
||||
mapId,
|
||||
staleEdgeCount,
|
||||
]);
|
||||
|
||||
const handleCreateNe = () => {
|
||||
let flowX = 80 + nodes.length * 24;
|
||||
let flowY = 80 + nodes.length * 24;
|
||||
|
|
@ -112,14 +142,51 @@ export function TopologyToolbar({
|
|||
onCreateNe(flowX, flowY);
|
||||
};
|
||||
|
||||
const handleMoreAction = (key: Key) => {
|
||||
switch (key as MoreActionKey) {
|
||||
case "live-sync":
|
||||
onLiveSyncToggle();
|
||||
break;
|
||||
case "back":
|
||||
onBackUp();
|
||||
break;
|
||||
case "undo":
|
||||
onUndo();
|
||||
break;
|
||||
case "redo":
|
||||
onRedo();
|
||||
break;
|
||||
case "fit":
|
||||
onFit();
|
||||
break;
|
||||
case "export-svg":
|
||||
void onExport("svg");
|
||||
break;
|
||||
case "export-xml":
|
||||
void onExport("xml");
|
||||
break;
|
||||
case "remove-stale":
|
||||
void onRemoveStale();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="topo-toolbar">
|
||||
<div className="topo-toolbar__row">
|
||||
<div className="topo-toolbar__title">
|
||||
<div className="topo-breadcrumb">
|
||||
<button type="button" className="topo-breadcrumb__link" onClick={() => goRoot()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="topo-breadcrumb__link"
|
||||
onPress={() => goRoot()}
|
||||
>
|
||||
{t("topology.rootName")}
|
||||
</button>
|
||||
</Button>
|
||||
{breadcrumbFolders.map((folder, idx) => {
|
||||
const isLast = idx === breadcrumbFolders.length - 1;
|
||||
const primaryView = primaryViewOfFolder(folder);
|
||||
|
|
@ -140,13 +207,15 @@ export function TopologyToolbar({
|
|||
{dirty ? " *" : ""}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="topo-breadcrumb__link"
|
||||
onClick={() => goRegion(folder.id)}
|
||||
onPress={() => goRegion(folder.id)}
|
||||
>
|
||||
{regionDisplayName(folder, t)}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
@ -175,116 +244,81 @@ export function TopologyToolbar({
|
|||
) : null}
|
||||
<HelpHint text={t("topology.canvasHint")} ariaLabel={t("common.help")} />
|
||||
</div>
|
||||
<div className="topo-toolbar__actions">
|
||||
<button
|
||||
<div className="topo-toolbar__actions topo-toolbar__primary">
|
||||
<Button
|
||||
type="button"
|
||||
className={`btn btn--sm${liveSync ? "" : " btn--ghost"}`}
|
||||
aria-pressed={liveSync}
|
||||
title={t("topology.liveSyncHint")}
|
||||
onClick={onLiveSyncToggle}
|
||||
>
|
||||
{liveSync ? t("topology.liveSyncOn") : t("topology.liveSync")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={readOnly || isWorldFlatCanvas}
|
||||
title={
|
||||
readOnly
|
||||
? t("topology.readOnlyHint")
|
||||
: isWorldFlatCanvas
|
||||
? t("topology.worldMapNoDirectNes")
|
||||
: undefined
|
||||
}
|
||||
onClick={onAddNe}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={readOnly || isWorldFlatCanvas}
|
||||
onPress={onAddNe}
|
||||
>
|
||||
{t("topology.addNe")}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={readOnly || isWorldFlatCanvas}
|
||||
title={
|
||||
readOnly
|
||||
? t("topology.readOnlyHint")
|
||||
: isWorldFlatCanvas
|
||||
? t("topology.worldMapNoDirectNes")
|
||||
: undefined
|
||||
}
|
||||
onClick={handleCreateNe}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={readOnly || isWorldFlatCanvas}
|
||||
onPress={handleCreateNe}
|
||||
>
|
||||
{t("topology.createNe")}
|
||||
</button>
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onBackUp}>
|
||||
{t("topology.backUp")}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={readOnly || !canUndo}
|
||||
onClick={onUndo}
|
||||
title="Ctrl+Z"
|
||||
>
|
||||
{t("topology.undo")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={readOnly || !canRedo}
|
||||
onClick={onRedo}
|
||||
title="Ctrl+Y"
|
||||
>
|
||||
{t("topology.redo")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn--sm${dirty ? "" : " btn--ghost"}`}
|
||||
disabled={readOnly || savePending || !dirty}
|
||||
onClick={onSave}
|
||||
title={readOnly ? t("topology.readOnlyHint") : "Ctrl+S"}
|
||||
size="sm"
|
||||
className={dirty ? "topo-toolbar__save topo-toolbar__save--dirty" : "topo-toolbar__save"}
|
||||
variant={dirty ? "primary" : "ghost"}
|
||||
isDisabled={readOnly || savePending || !dirty}
|
||||
onPress={onSave}
|
||||
>
|
||||
{savePending ? t("topology.saving") : dirty ? t("topology.saveDirty") : t("topology.save")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={!isWorldFlatCanvas && nodes.length === 0}
|
||||
onClick={onFit}
|
||||
>
|
||||
{t("topology.fit")}
|
||||
</button>
|
||||
<details className="topo-toolbar__export" ref={exportMenuRef}>
|
||||
<summary>{exporting ? t("topology.exporting") : t("topology.export")}</summary>
|
||||
<div className="topo-export-menu" role="menu" aria-label={t("topology.export")}>
|
||||
<button
|
||||
type="button"
|
||||
className="topo-export-menu__btn"
|
||||
role="menuitem"
|
||||
disabled={exporting || (!isWorldFlatCanvas && nodes.length === 0)}
|
||||
onClick={() => void onExport("svg")}
|
||||
</Button>
|
||||
<Dropdown>
|
||||
<Button type="button" size="sm" variant="ghost" aria-label={t("topology.more")}>
|
||||
{t("topology.more")}
|
||||
</Button>
|
||||
<Dropdown.Popover className="topo-toolbar__more-popover" placement="bottom end">
|
||||
<Dropdown.Menu
|
||||
aria-label={t("topology.more")}
|
||||
disabledKeys={moreDisabledKeys}
|
||||
onAction={handleMoreAction}
|
||||
>
|
||||
{t("topology.exportSvg")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="topo-export-menu__btn"
|
||||
role="menuitem"
|
||||
disabled={exporting || !mapId}
|
||||
onClick={() => void onExport("xml")}
|
||||
>
|
||||
{t("topology.exportXml")}
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={readOnly || !staleEdgeCount}
|
||||
title={readOnly ? t("topology.readOnlyHint") : t("topology.removeStaleHint")}
|
||||
onClick={() => void onRemoveStale()}
|
||||
>
|
||||
{t("topology.removeStale").replace("{{count}}", String(staleEdgeCount))}
|
||||
</button>
|
||||
<Dropdown.Item id="live-sync" textValue={liveSync ? t("topology.liveSyncOn") : t("topology.liveSync")}>
|
||||
{liveSync ? t("topology.liveSyncOn") : t("topology.liveSync")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="back" textValue={t("topology.backUp")}>
|
||||
{t("topology.backUp")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="undo" textValue={t("topology.undo")}>
|
||||
{t("topology.undo")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="redo" textValue={t("topology.redo")}>
|
||||
{t("topology.redo")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id="fit" textValue={t("topology.fit")}>
|
||||
{t("topology.fit")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
id="export-svg"
|
||||
textValue={exporting ? t("topology.exporting") : t("topology.exportSvg")}
|
||||
>
|
||||
{exporting ? t("topology.exporting") : t("topology.exportSvg")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
id="export-xml"
|
||||
textValue={exporting ? t("topology.exporting") : t("topology.exportXml")}
|
||||
>
|
||||
{exporting ? t("topology.exporting") : t("topology.exportXml")}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
id="remove-stale"
|
||||
textValue={t("topology.removeStale").replace("{{count}}", String(staleEdgeCount))}
|
||||
>
|
||||
{t("topology.removeStale").replace("{{count}}", String(staleEdgeCount))}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown.Popover>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topo-toolbar__row topo-toolbar__row--tools">
|
||||
|
|
@ -295,23 +329,27 @@ export function TopologyToolbar({
|
|||
["pan", t("topology.toolPan"), "H"],
|
||||
["connect", t("topology.toolConnect"), "C"],
|
||||
] as const
|
||||
).map(([mode, label, key]) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={`topo-tools__btn${toolMode === mode ? " is-active" : ""}`}
|
||||
title={`${label} (${key})${readOnly && mode === "connect" ? ` — ${t("topology.readOnlyHint")}` : ""}`}
|
||||
aria-pressed={toolMode === mode}
|
||||
disabled={readOnly && mode === "connect"}
|
||||
onClick={() => {
|
||||
onToolModeChange(mode);
|
||||
onConnectClickReset();
|
||||
}}
|
||||
>
|
||||
<span className="topo-tools__label">{label}</span>
|
||||
<kbd className="topo-tools__key">{key}</kbd>
|
||||
</button>
|
||||
))}
|
||||
).map(([mode, label, key]) => {
|
||||
const active = toolMode === mode;
|
||||
return (
|
||||
<Button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={`topo-tools__btn${active ? " is-active" : ""}`}
|
||||
variant={active ? "primary" : "ghost"}
|
||||
size="sm"
|
||||
aria-pressed={active}
|
||||
isDisabled={readOnly && mode === "connect"}
|
||||
onPress={() => {
|
||||
onToolModeChange(mode);
|
||||
onConnectClickReset();
|
||||
}}
|
||||
>
|
||||
<span className="topo-tools__label">{label}</span>
|
||||
<kbd className="topo-tools__key">{key}</kbd>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!fullscreen ? (
|
||||
<div
|
||||
|
|
@ -322,14 +360,15 @@ export function TopologyToolbar({
|
|||
/>
|
||||
) : null}
|
||||
{outsidePeerCount > 0 ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost topo-toolbar__outside-peers"
|
||||
title={t("topology.outsidePeers").replace("{{count}}", String(outsidePeerCount))}
|
||||
onClick={onOpenOutsidePeers}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="topo-toolbar__outside-peers"
|
||||
onPress={onOpenOutsidePeers}
|
||||
>
|
||||
{t("topology.outsidePeersView").replace("{{count}}", String(outsidePeerCount))}
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Button, Input } from "@heroui/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { RefObject } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
|
@ -299,16 +300,17 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
<div className="topo-display-defaults topo-display-defaults--canvas-bg topo-display-defaults--colors">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.canvasBg")}</strong>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
onCanvasBgChange(DEFAULT_CANVAS_BG);
|
||||
persistCanvasBg(DEFAULT_CANVAS_BG);
|
||||
}}
|
||||
>
|
||||
{t("topology.canvasBgReset")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="topo-display-defaults__row">
|
||||
<span className="topo-display-defaults__name">{t("topology.canvasBgColor")}</span>
|
||||
|
|
@ -342,16 +344,17 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
<div className="topo-display-defaults topo-display-defaults--canvas-bg topo-display-defaults--colors">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.textColors")}</strong>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
onLabelColorsChange({ ...DEFAULT_LABEL_COLORS });
|
||||
persistLabelColors({ ...DEFAULT_LABEL_COLORS });
|
||||
}}
|
||||
>
|
||||
{t("topology.textColorsReset")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{(
|
||||
[
|
||||
|
|
@ -393,16 +396,17 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
<div className="topo-display-defaults topo-display-defaults--canvas-bg topo-display-defaults--colors">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.vendorColors")}</strong>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
onVendorColorsChange({ ...DEFAULT_VENDOR_COLORS });
|
||||
persistVendorColors({ ...DEFAULT_VENDOR_COLORS });
|
||||
}}
|
||||
>
|
||||
{t("topology.vendorColorsReset")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{VENDOR_TONE_KEYS.map((key) => {
|
||||
const label = t(`topology.vendorTone.${key}`);
|
||||
|
|
@ -444,9 +448,9 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
<div className="topo-display-defaults">
|
||||
<div className="topo-display-defaults__head">
|
||||
<strong>{t("topology.edgeDefaults")}</strong>
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onResetEdgeDefaults}>
|
||||
<Button type="button" size="sm" variant="ghost" onPress={onResetEdgeDefaults}>
|
||||
{t("topology.edgeDefaultsReset")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{(
|
||||
[
|
||||
|
|
@ -528,7 +532,7 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
aria-label={t("topology.findNode")}
|
||||
ref={findBoxRef}
|
||||
>
|
||||
<input
|
||||
<Input
|
||||
className="topo-toolbar__find"
|
||||
value={canvasQuery}
|
||||
onChange={(e) => onCanvasQueryChange(e.target.value)}
|
||||
|
|
@ -571,20 +575,20 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
<div className="topo-find-suggest__empty">{t("topology.findNoMatch")}</div>
|
||||
) : (
|
||||
canvasHits.slice(0, 12).map((n, idx) => (
|
||||
<button
|
||||
<Button
|
||||
key={n.id}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={idx === findActiveIdx}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`topo-find-suggest__item${idx === findActiveIdx ? " is-active" : ""}`}
|
||||
onMouseEnter={() => onFindActiveIdxChange(idx)}
|
||||
onClick={() => onFindOnCanvas(n.id)}
|
||||
onHoverStart={() => onFindActiveIdxChange(idx)}
|
||||
onPress={() => onFindOnCanvas(n.id)}
|
||||
>
|
||||
<span className="topo-find-suggest__name">{n.data.label || n.id}</span>
|
||||
<span className="topo-find-suggest__meta">
|
||||
{[n.data.ne_ip, n.data.vendor].filter(Boolean).join(SEP)}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
{canvasHits.length > 12 ? (
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import type { ManagedNeFormState } from "../../managedNe/formState";
|
|||
import { isWorldFlatViewName } from "../treeUtils";
|
||||
import type { NeNodeData } from "../TopologyReactFlowView";
|
||||
|
||||
type FocusNode = (fabricNodeId: string, fit?: boolean) => void;
|
||||
type FocusNode = (fabricNodeId: string, fit: boolean) => void;
|
||||
|
||||
type Args = {
|
||||
mapId: string | null | undefined;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { DragEvent } from "react";
|
||||
import { Button, Chip, Input, Modal } from "@heroui/react";
|
||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
||||
import { useI18n } from "../../../i18n";
|
||||
import type { PaletteItem, PaletteSource } from "../pageTypes";
|
||||
|
||||
|
|
@ -34,54 +36,37 @@ export function AddNePaletteDialog({
|
|||
onAddSelected,
|
||||
}: AddNePaletteDialogProps) {
|
||||
const { t } = useI18n();
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="topo-modal" role="dialog" aria-modal="true" aria-label={t("topology.addNe")}>
|
||||
<div
|
||||
className="topo-modal__backdrop"
|
||||
onClick={() => {
|
||||
if (paletteAdding) return;
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<div className="topo-modal__panel">
|
||||
<div className="topo-modal__head">
|
||||
<strong>{t("topology.addNe")}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={paletteAdding}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="panel__hint topo-modal__hint">{t("topology.paletteHint")}</p>
|
||||
<div className="topo-palette-source" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={paletteSource === "managed"}
|
||||
className={`topo-palette-source__btn${paletteSource === "managed" ? " is-active" : ""}`}
|
||||
disabled={paletteAdding}
|
||||
onClick={() => onPaletteSourceChange("managed")}
|
||||
<TopoModalShell open={open} onClose={onClose} dismissible={!paletteAdding} size="md">
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("topology.addNe")}</Modal.Heading>
|
||||
<Modal.CloseTrigger isDisabled={paletteAdding} />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted">{t("topology.paletteHint")}</p>
|
||||
<div className="topo-palette-source flex gap-2" role="tablist">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={paletteSource === "managed" ? "primary" : "secondary"}
|
||||
isDisabled={paletteAdding}
|
||||
className="flex-1"
|
||||
onPress={() => onPaletteSourceChange("managed")}
|
||||
>
|
||||
{t("topology.paletteManaged")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={paletteSource === "ume"}
|
||||
className={`topo-palette-source__btn${paletteSource === "ume" ? " is-active" : ""}`}
|
||||
disabled={paletteAdding}
|
||||
onClick={() => onPaletteSourceChange("ume")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={paletteSource === "ume" ? "primary" : "secondary"}
|
||||
isDisabled={paletteAdding}
|
||||
className="flex-1"
|
||||
onPress={() => onPaletteSourceChange("ume")}
|
||||
>
|
||||
{t("topology.paletteUme")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<input
|
||||
className="input"
|
||||
<Input
|
||||
className="w-full"
|
||||
value={keyword}
|
||||
onChange={(e) => onKeywordChange(e.target.value)}
|
||||
placeholder={t("topology.filterPh")}
|
||||
|
|
@ -109,9 +94,11 @@ export function AddNePaletteDialog({
|
|||
/>
|
||||
<span>{t("topology.selectAllVisible")}</span>
|
||||
</label>
|
||||
<span className="panel__hint">
|
||||
{t("topology.selectedCount").replace("{{count}}", String(paletteSelectedKeys.length))}
|
||||
</span>
|
||||
<Chip size="sm" variant="soft">
|
||||
<Chip.Label>
|
||||
{t("topology.selectedCount").replace("{{count}}", String(paletteSelectedKeys.length))}
|
||||
</Chip.Label>
|
||||
</Chip>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="topo-palette topo-modal__list">
|
||||
|
|
@ -168,27 +155,22 @@ export function AddNePaletteDialog({
|
|||
})
|
||||
)}
|
||||
</ul>
|
||||
<div className="topo-modal__foot">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={paletteAdding}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={paletteSelectedKeys.length === 0 || paletteAdding}
|
||||
onClick={() => void onAddSelected()}
|
||||
>
|
||||
{paletteAdding
|
||||
? t("topology.addingNe")
|
||||
: t("topology.addSelected").replace("{{count}}", String(paletteSelectedKeys.length))}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="tertiary" size="sm" isDisabled={paletteAdding} onPress={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
isDisabled={paletteSelectedKeys.length === 0 || paletteAdding}
|
||||
onPress={() => void onAddSelected()}
|
||||
>
|
||||
{paletteAdding
|
||||
? t("topology.addingNe")
|
||||
: t("topology.addSelected").replace("{{count}}", String(paletteSelectedKeys.length))}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</TopoModalShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Button, Modal } from "@heroui/react";
|
||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
||||
import { useI18n } from "../../../i18n";
|
||||
|
||||
export type CreateNeModeDialogProps = {
|
||||
|
|
@ -15,34 +17,34 @@ export function CreateNeModeDialog({
|
|||
onPickPlaceholder,
|
||||
}: CreateNeModeDialogProps) {
|
||||
const { t } = useI18n();
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="topo-modal" role="dialog" aria-modal="true" aria-labelledby="topo-create-ne-mode-title">
|
||||
<div className="topo-modal__backdrop" onClick={onClose} />
|
||||
<div className="topo-modal__panel" style={{ maxWidth: 440 }}>
|
||||
<div className="topo-modal__head">
|
||||
<strong id="topo-create-ne-mode-title">{t("topology.createNe")}</strong>
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
<TopoModalShell open={open} onClose={onClose} size="sm">
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("topology.createNe")}</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted">{t("topology.createNeModeHint")}</p>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-auto flex-col items-start gap-1 whitespace-normal px-4 py-3 text-left"
|
||||
onPress={onPickManaged}
|
||||
>
|
||||
<span className="font-semibold">{t("topology.createNeManaged")}</span>
|
||||
<span className="text-sm font-normal opacity-90">{t("topology.createNeManagedHint")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-auto flex-col items-start gap-1 whitespace-normal px-4 py-3 text-left"
|
||||
onPress={onPickPlaceholder}
|
||||
>
|
||||
<span className="font-semibold">{t("topology.createNePlaceholder")}</span>
|
||||
<span className="text-sm font-normal text-muted">{t("topology.createNePlaceholderHint")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="panel__hint topo-modal__hint">{t("topology.createNeModeHint")}</p>
|
||||
<div className="topo-create-ne-mode" style={{ padding: "0 16px 16px", display: "grid", gap: 10 }}>
|
||||
<button type="button" className="btn" onClick={onPickManaged}>
|
||||
<span style={{ display: "block", fontWeight: 600 }}>{t("topology.createNeManaged")}</span>
|
||||
<span className="panel__hint" style={{ display: "block", marginTop: 4 }}>
|
||||
{t("topology.createNeManagedHint")}
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" className="btn btn--ghost" onClick={onPickPlaceholder}>
|
||||
<span style={{ display: "block", fontWeight: 600 }}>{t("topology.createNePlaceholder")}</span>
|
||||
<span className="panel__hint" style={{ display: "block", marginTop: 4 }}>
|
||||
{t("topology.createNePlaceholderHint")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</TopoModalShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
||||
import { useI18n } from "../../../i18n";
|
||||
|
||||
export type NewRootDialogProps = {
|
||||
|
|
@ -10,57 +12,43 @@ export type NewRootDialogProps = {
|
|||
|
||||
export function NewRootDialog({ dialog, pending, onNameChange, onClose, onSubmit }: NewRootDialogProps) {
|
||||
const { t } = useI18n();
|
||||
if (!dialog) return null;
|
||||
const open = Boolean(dialog);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="topo-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="topo-new-root-title"
|
||||
>
|
||||
<div className="topo-modal__backdrop" onClick={onClose} />
|
||||
<div className="topo-modal__panel" style={{ maxWidth: 420 }}>
|
||||
<div className="topo-modal__head">
|
||||
<strong id="topo-new-root-title">{t("topology.newRegion")}</strong>
|
||||
<button type="button" className="btn btn--sm btn--ghost" onClick={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="panel__hint topo-modal__hint">{t("topology.folderHint")}</p>
|
||||
<div className="form-grid" style={{ padding: "0 16px 8px" }}>
|
||||
<label>
|
||||
<span className="form-label">{t("topology.newRegionPrompt")}</span>
|
||||
<input
|
||||
autoFocus
|
||||
value={dialog.name}
|
||||
disabled={pending}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onSubmit();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="topo-modal__foot">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={pending}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={pending || !String(dialog.name || "").trim()}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{pending ? "…" : t("topology.newRegion")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopoModalShell open={open} onClose={onClose} dismissible={!pending} size="sm">
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("topology.newRegion")}</Modal.Heading>
|
||||
<Modal.CloseTrigger isDisabled={pending} />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted">{t("topology.folderHint")}</p>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
value={dialog?.name ?? ""}
|
||||
onChange={onNameChange}
|
||||
isDisabled={pending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onSubmit();
|
||||
}}
|
||||
>
|
||||
<Label>{t("topology.newRegionPrompt")}</Label>
|
||||
<Input />
|
||||
</TextField>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="tertiary" size="sm" isDisabled={pending} onPress={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
isDisabled={pending || !String(dialog?.name || "").trim()}
|
||||
onPress={onSubmit}
|
||||
>
|
||||
{pending ? "…" : t("topology.newRegion")}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</TopoModalShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Button, Chip, Input, Modal } from "@heroui/react";
|
||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
||||
import type { TopologyOutsidePeer } from "../../../types";
|
||||
import { useI18n } from "../../../i18n";
|
||||
|
||||
|
|
@ -29,40 +31,20 @@ export function OutsidePeersDialog({
|
|||
onAddOutsidePeers,
|
||||
}: OutsidePeersDialogProps) {
|
||||
const { t } = useI18n();
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="topo-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("topology.outsidePeersTitle")}
|
||||
>
|
||||
<div
|
||||
className="topo-modal__backdrop"
|
||||
onClick={() => {
|
||||
if (outsidePeersAdding) return;
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<div className="topo-modal__panel topo-modal__panel--wide">
|
||||
<div className="topo-modal__head">
|
||||
<strong>
|
||||
{t("topology.outsidePeersTitle")}
|
||||
<span className="topo-modal__count"> · {outsidePeers.length}</span>
|
||||
</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={outsidePeersAdding}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="panel__hint topo-modal__hint">{t("topology.outsidePeersHint")}</p>
|
||||
<input
|
||||
className="input"
|
||||
<TopoModalShell open={open} onClose={onClose} dismissible={!outsidePeersAdding} size="lg">
|
||||
<Modal.Header>
|
||||
<Modal.Heading>
|
||||
{t("topology.outsidePeersTitle")}
|
||||
<span className="topo-modal__count"> · {outsidePeers.length}</span>
|
||||
</Modal.Heading>
|
||||
<Modal.CloseTrigger isDisabled={outsidePeersAdding} />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted">{t("topology.outsidePeersHint")}</p>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={outsidePeerQuery}
|
||||
onChange={(e) => onOutsidePeerQueryChange(e.target.value)}
|
||||
placeholder={t("topology.outsidePeersFilterPh")}
|
||||
|
|
@ -95,12 +77,14 @@ export function OutsidePeersDialog({
|
|||
/>
|
||||
<span>{t("topology.selectAllVisible")}</span>
|
||||
</label>
|
||||
<span className="panel__hint">
|
||||
{t("topology.selectedCount").replace(
|
||||
"{{count}}",
|
||||
String(outsidePeerSelectedIds.length),
|
||||
)}
|
||||
</span>
|
||||
<Chip size="sm" variant="soft">
|
||||
<Chip.Label>
|
||||
{t("topology.selectedCount").replace(
|
||||
"{{count}}",
|
||||
String(outsidePeerSelectedIds.length),
|
||||
)}
|
||||
</Chip.Label>
|
||||
</Chip>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="topo-palette topo-modal__list">
|
||||
|
|
@ -157,30 +141,25 @@ export function OutsidePeersDialog({
|
|||
})
|
||||
)}
|
||||
</ul>
|
||||
<div className="topo-modal__foot">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={outsidePeersAdding}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={outsidePeerSelectedIds.length === 0 || outsidePeersAdding}
|
||||
onClick={() => void onAddOutsidePeers(outsidePeerSelectedIds)}
|
||||
>
|
||||
{outsidePeersAdding
|
||||
? t("topology.addingNe")
|
||||
: t("topology.addSelected").replace(
|
||||
"{{count}}",
|
||||
String(outsidePeerSelectedIds.length),
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="tertiary" size="sm" isDisabled={outsidePeersAdding} onPress={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
isDisabled={outsidePeerSelectedIds.length === 0 || outsidePeersAdding}
|
||||
onPress={() => void onAddOutsidePeers(outsidePeerSelectedIds)}
|
||||
>
|
||||
{outsidePeersAdding
|
||||
? t("topology.addingNe")
|
||||
: t("topology.addSelected").replace(
|
||||
"{{count}}",
|
||||
String(outsidePeerSelectedIds.length),
|
||||
)}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</TopoModalShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Button, Input, Label, Modal, TextField } from "@heroui/react";
|
||||
import { TopoModalShell } from "../../../components/ui/TopoModalShell";
|
||||
import { useI18n } from "../../../i18n";
|
||||
|
||||
export type PlaceholderCreateDialogState = {
|
||||
|
|
@ -23,82 +25,56 @@ export function PlaceholderCreateDialog({
|
|||
onSubmit,
|
||||
}: PlaceholderCreateDialogProps) {
|
||||
const { t } = useI18n();
|
||||
if (!dialog) return null;
|
||||
const open = Boolean(dialog);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="topo-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="topo-create-ne-title"
|
||||
<TopoModalShell
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
dismissible={!busy}
|
||||
size="sm"
|
||||
>
|
||||
<div
|
||||
className="topo-modal__backdrop"
|
||||
onClick={() => {
|
||||
if (busy) return;
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<div className="topo-modal__panel" style={{ maxWidth: 420 }}>
|
||||
<div className="topo-modal__head">
|
||||
<strong id="topo-create-ne-title">{t("topology.createNeTitle")}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={busy}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="panel__hint topo-modal__hint">{t("topology.createNeHint")}</p>
|
||||
<div className="form-grid" style={{ padding: "0 16px 8px" }}>
|
||||
<label>
|
||||
<span className="form-label">
|
||||
{t("topology.createNeName")}
|
||||
<span className="form-label__required" aria-hidden="true">
|
||||
{" "}
|
||||
*
|
||||
</span>
|
||||
</span>
|
||||
<input
|
||||
autoFocus
|
||||
value={dialog.name}
|
||||
placeholder={t("topology.createNeNamePh")}
|
||||
disabled={busy}
|
||||
onChange={(e) => onChange({ name: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void onSubmit();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="form-label">{t("topology.createNeIp")}</span>
|
||||
<input
|
||||
value={dialog.ip_address}
|
||||
placeholder={t("topology.createNeIpPh")}
|
||||
disabled={busy}
|
||||
onChange={(e) => onChange({ ip_address: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void onSubmit();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="topo-modal__foot">
|
||||
<button type="button" className="btn btn--ghost" disabled={busy} onClick={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--primary"
|
||||
disabled={busy}
|
||||
onClick={() => void onSubmit()}
|
||||
>
|
||||
{busy ? t("topology.createNeBusy") : t("topology.createNePlaceholder")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("topology.createNeTitle")}</Modal.Heading>
|
||||
<Modal.CloseTrigger isDisabled={busy} />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted">{t("topology.createNeHint")}</p>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
isRequired
|
||||
value={dialog?.name ?? ""}
|
||||
onChange={(name) => onChange({ name })}
|
||||
isDisabled={busy}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void onSubmit();
|
||||
}}
|
||||
>
|
||||
<Label>{t("topology.createNeName")}</Label>
|
||||
<Input placeholder={t("topology.createNeNamePh")} />
|
||||
</TextField>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={dialog?.ip_address ?? ""}
|
||||
onChange={(ip_address) => onChange({ ip_address })}
|
||||
isDisabled={busy}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void onSubmit();
|
||||
}}
|
||||
>
|
||||
<Label>{t("topology.createNeIp")}</Label>
|
||||
<Input placeholder={t("topology.createNeIpPh")} />
|
||||
</TextField>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="tertiary" isDisabled={busy} onPress={onClose}>
|
||||
{t("topology.discoverClose")}
|
||||
</Button>
|
||||
<Button variant="primary" isDisabled={busy} onPress={() => void onSubmit()}>
|
||||
{busy ? t("topology.createNeBusy") : t("topology.createNePlaceholder")}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</TopoModalShell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/** Fixed box for onlyRenderVisibleElements (xyflow skips off-screen mount when sized + handles set). */
|
||||
export const TOPO_NODE_W = 80;
|
||||
export const TOPO_NODE_H = 25;
|
||||
export const TOPO_ICON = 25;
|
||||
export const TOPO_NODE_W = 88;
|
||||
export const TOPO_NODE_H = 28;
|
||||
export const TOPO_ICON = 28;
|
||||
/** region-building.png is 195×133 — keep aspect so icon center == handle. */
|
||||
export const TOPO_REGION_ICON_H = Math.round((TOPO_ICON * 133) / 195);
|
||||
export const TOPO_HANDLE_X = TOPO_NODE_W / 2;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue