mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 15:53:20 +08:00
Refactor web UI: feature-first structure, tokenized styles, modern blue theme.
Structure
- pages/ -> features/<domain>/{Page, components, hooks, lib}; 95 files moved,
333 imports rewritten, verified with tsc.
- services/api.ts -> services/api/<domain>.ts (250 exports preserved),
types.ts -> types/<domain>.ts, i18n zh/en -> locales/<lang>/<namespace>.ts
(2696 messages deep-equal before/after).
- ESLint rule: shared layers must not import features/.
Styles
- index.css (16k lines) split losslessly into styles/{base,layout,components,
pages,legacy,theme}; compiled CSS was byte-identical before tokenizing.
- ~930 hard-coded colors replaced by --nx-* tokens (theme/tokens.css); WebCRT
off-palette colors unified; fixed dark-on-dark text introduced by mapping.
- theme/theme.css: modern blue design language (rounded shapes, gradients,
unified buttons/fields/tables/modals), form-grid alignment fix, flat nav list
without tree indent, soft selected state instead of left accent bar.
Docs: WEB.md documents code structure and style rules.
Not touched: the large Biz*/Webcrt/Topology page components (to be split later).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b45d6abe3
commit
0d894efe5e
196 changed files with 26947 additions and 28091 deletions
214
web/src/features/topology/lib/linkDisplay.ts
Normal file
214
web/src/features/topology/lib/linkDisplay.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import type { Edge } from "@xyflow/react";
|
||||
|
||||
export type LinkMember = {
|
||||
id: string;
|
||||
a_port: string;
|
||||
b_port: string;
|
||||
source: string;
|
||||
display_label?: string;
|
||||
};
|
||||
|
||||
export type LinkEdgeData = {
|
||||
source?: string;
|
||||
source_port?: string;
|
||||
target_port?: string;
|
||||
stroke_color?: string;
|
||||
stroke_width?: number;
|
||||
line_style?: string;
|
||||
discovered_at?: string | null;
|
||||
/** UME userLabel when ports are unavailable (no EQ+PTP). */
|
||||
display_label?: string;
|
||||
/** Logical bundle of parallel physical links between the same NE pair. */
|
||||
aggregated?: boolean;
|
||||
member_count?: number;
|
||||
members?: LinkMember[];
|
||||
parallelIndex?: number;
|
||||
parallelCount?: number;
|
||||
};
|
||||
|
||||
export function pairKey(a: string, b: string): string {
|
||||
return a < b ? `${a}::${b}` : `${b}::${a}`;
|
||||
}
|
||||
|
||||
export function isAggregateEdgeId(id: string): boolean {
|
||||
return String(id || "").startsWith("agg:");
|
||||
}
|
||||
|
||||
export function aggregateIdForPair(a: string, b: string): string {
|
||||
return `agg:${pairKey(a, b)}`;
|
||||
}
|
||||
|
||||
function isLabelPlaceholderPort(port: string): boolean {
|
||||
return String(port || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith("label:");
|
||||
}
|
||||
|
||||
/** Clear A↔B port label (not ambiguous "local/remote"). */
|
||||
export function formatPortPairLabel(aPort: string, bPort: string, displayLabel?: string): string {
|
||||
const shown = String(displayLabel || "").trim();
|
||||
if (shown) return shown;
|
||||
const a = String(aPort || "").trim();
|
||||
const b = String(bPort || "").trim();
|
||||
if (isLabelPlaceholderPort(a) || isLabelPlaceholderPort(b)) return "";
|
||||
if (a && b) return `${a} ↔ ${b}`;
|
||||
return a || b || "";
|
||||
}
|
||||
|
||||
export function formatBundleLabel(
|
||||
count: number,
|
||||
sample?: { a_port?: string; b_port?: string; display_label?: string },
|
||||
opts?: { hidePorts?: boolean },
|
||||
): string {
|
||||
if (count <= 1) {
|
||||
return opts?.hidePorts
|
||||
? ""
|
||||
: formatPortPairLabel(sample?.a_port || "", sample?.b_port || "", sample?.display_label);
|
||||
}
|
||||
if (opts?.hidePorts) return `×${count}`;
|
||||
const sampleLabel = formatPortPairLabel(
|
||||
sample?.a_port || "",
|
||||
sample?.b_port || "",
|
||||
sample?.display_label,
|
||||
);
|
||||
return sampleLabel ? `×${count} · ${sampleLabel}` : `×${count}`;
|
||||
}
|
||||
|
||||
function memberFrom(edge: Edge): LinkMember {
|
||||
const d = (edge.data || {}) as LinkEdgeData;
|
||||
return {
|
||||
id: edge.id,
|
||||
a_port: String(d.source_port || "").trim(),
|
||||
b_port: String(d.target_port || "").trim(),
|
||||
source: String(d.source || "manual"),
|
||||
display_label: String(d.display_label || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function statusRank(source: string): number {
|
||||
if (source === "stale") return 2;
|
||||
if (source === "manual") return 1;
|
||||
return 0; // discovered / lldp preferred
|
||||
}
|
||||
|
||||
/** Pick representative physical edge for an aggregate (prefer live discovered). */
|
||||
function pickPrimary(list: Edge[]): Edge {
|
||||
return [...list].sort((a, b) => {
|
||||
const da = (a.data || {}) as LinkEdgeData;
|
||||
const db = (b.data || {}) as LinkEdgeData;
|
||||
return statusRank(String(da.source || "")) - statusRank(String(db.source || ""));
|
||||
})[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Default: one logical edge per NE pair (bundle parallel links).
|
||||
* Expanded: all physical edges, offset when count > 1.
|
||||
*/
|
||||
export function buildLinkDisplayEdges(
|
||||
edges: Edge[],
|
||||
expandPhysical: boolean,
|
||||
hidePorts = false,
|
||||
scaleBundleWidth = false,
|
||||
): Edge[] {
|
||||
const groups = new Map<string, Edge[]>();
|
||||
for (const e of edges) {
|
||||
const k = pairKey(e.source, e.target);
|
||||
const list = groups.get(k);
|
||||
if (list) list.push(e);
|
||||
else groups.set(k, [e]);
|
||||
}
|
||||
|
||||
const out: Edge[] = [];
|
||||
for (const [, list] of groups) {
|
||||
if (expandPhysical) {
|
||||
const count = list.length;
|
||||
list.forEach((e, i) => {
|
||||
const prev = (e.data || {}) as LinkEdgeData;
|
||||
const data: LinkEdgeData = {
|
||||
...prev,
|
||||
aggregated: false,
|
||||
member_count: count,
|
||||
members: list.map(memberFrom),
|
||||
parallelIndex: i,
|
||||
parallelCount: count,
|
||||
};
|
||||
const portLabel = formatPortPairLabel(
|
||||
data.source_port || "",
|
||||
data.target_port || "",
|
||||
data.display_label,
|
||||
);
|
||||
out.push({
|
||||
...e,
|
||||
type: count > 1 ? "topoParallel" : e.type || "straight",
|
||||
// Never fall back to a stale e.label — it may still contain ports.
|
||||
label: hidePorts ? undefined : portLabel || undefined,
|
||||
data,
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (list.length === 1) {
|
||||
const e = list[0];
|
||||
const prev = (e.data || {}) as LinkEdgeData;
|
||||
const data: LinkEdgeData = {
|
||||
...prev,
|
||||
aggregated: false,
|
||||
member_count: 1,
|
||||
members: [memberFrom(e)],
|
||||
};
|
||||
const portLabel = formatPortPairLabel(
|
||||
data.source_port || "",
|
||||
data.target_port || "",
|
||||
data.display_label,
|
||||
);
|
||||
out.push({
|
||||
...e,
|
||||
type: "straight",
|
||||
label: hidePorts ? undefined : portLabel || undefined,
|
||||
data,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const primary = pickPrimary(list);
|
||||
const prev = (primary.data || {}) as LinkEdgeData;
|
||||
const members = list.map(memberFrom);
|
||||
const anyStale = members.some((m) => m.source === "stale");
|
||||
const data: LinkEdgeData = {
|
||||
...prev,
|
||||
source: anyStale ? "stale" : prev.source,
|
||||
aggregated: true,
|
||||
member_count: list.length,
|
||||
members,
|
||||
};
|
||||
// Optional: thicken logical bundles by member count (default keeps single-line width).
|
||||
if (scaleBundleWidth) {
|
||||
data.stroke_width = Math.max(Number(prev.stroke_width || 0), Math.min(8, 2 + list.length));
|
||||
}
|
||||
out.push({
|
||||
...primary,
|
||||
id: aggregateIdForPair(primary.source, primary.target),
|
||||
type: "straight",
|
||||
label: formatBundleLabel(
|
||||
list.length,
|
||||
{ a_port: data.source_port, b_port: data.target_port },
|
||||
{ hidePorts },
|
||||
) || undefined,
|
||||
data,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function physicalIdsForDisplayEdge(edge: Edge | null | undefined, allEdges: Edge[]): string[] {
|
||||
if (!edge) return [];
|
||||
const d = (edge.data || {}) as LinkEdgeData;
|
||||
if (d.members?.length) return d.members.map((m) => m.id);
|
||||
if (isAggregateEdgeId(edge.id)) {
|
||||
const k = edge.id.slice(4);
|
||||
return allEdges.filter((e) => pairKey(e.source, e.target) === k).map((e) => e.id);
|
||||
}
|
||||
return [edge.id];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue