@mp70/react-networks
v0.9.102
Published
React components for network diagrams using React Flow
Maintainers
Readme
@mp70/react-networks
A React library for creating interactive network diagrams with support for rack management, fibre networks, and power distribution. Built on React Flow (@xyflow/react v12) with TypeScript support.
Features
- Rack Management: Full support for 19" rack units with exact U positioning
- Device Placement: Drag and drop devices with automatic U position snapping
- Fibre Networks: Visual representation of fibre connections and patch panels
- Power Distribution: Vertical PDU support with power connections
- Face Switching: Front/rear face switching for devices and racks
- Device Images: Front/rear device images supported (PNG, SVG, JPEG, etc.) via
frontImageUrl/rearImageUrl(https://,blob:,data:image/*, or relative URLs) - Rack Alignment: Align rack bottoms to the same horizontal plane
- Data Integration: Built-in converters for generic inventory/CMDB data and NetBox, on server-safe subpath entries
- Server-safe model entry: Import the data model, adapters, and handle-id grammar in Node/SSR without pulling in React or React Flow
- Customizable:
--rn-*CSS custom properties and theme class hooks - Responsive: Works on desktop and mobile devices
- TypeScript: Full TypeScript support with comprehensive type definitions
Quick Start
Installation
Install the library and its peer dependencies:
npm install @mp70/react-networks react react-dom @xyflow/react @xyflow/system
# or
yarn add @mp70/react-networks react react-dom @xyflow/react @xyflow/system
# or
pnpm add @mp70/react-networks react react-dom @xyflow/react @xyflow/systemPeer dependencies
@mp70/react-networks renders on React Flow v12 (@xyflow/react), not the
legacy reactflow v11 package. The required peers are:
| Peer | Range | Notes |
| --- | --- | --- |
| react | ^18 \|\| ^19 | |
| react-dom | ^18 \|\| ^19 | |
| @xyflow/react | ^12 | The React Flow v12 engine. Do not install the old reactflow package. |
| @xyflow/system | 0.0.x | Required transitively by the type surface; install it explicitly. |
You must import two stylesheets — React Flow's own base styles and this library's styles — or the diagram renders unstyled:
import '@xyflow/react/dist/style.css';
import '@mp70/react-networks/index.css';The library's CSS is emitted as a standalone file (it is not injected by
importing the JS entry), so the @mp70/react-networks/index.css import is
required, not optional.
Basic Usage
import React from 'react';
import '@xyflow/react/dist/style.css';
import '@mp70/react-networks/index.css';
import { NetworkDiagram } from '@mp70/react-networks';
import type { NetworkNode, NetworkEdge } from '@mp70/react-networks';
const nodes: NetworkNode[] = [
{
id: 'rack-1',
type: 'rack',
position: { x: 100, y: 100 },
data: {
label: 'Main Rack',
uHeight: 42
}
},
{
id: 'server-1',
type: 'server',
position: { x: 120, y: 150 },
data: {
label: 'Web Server',
uPosition: 1,
ports: [
{ id: 'eth0', label: 'eth0', group: 'Network Ports', connected: true, type: 'ethernet' },
{ id: 'eth1', label: 'eth1', group: 'Network Ports', connected: false, type: 'ethernet' }
]
}
}
];
const edges: NetworkEdge[] = [];
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<NetworkDiagram
nodes={nodes}
edges={edges}
alignRacksToBottom={true}
onNodeClick={(node) => console.log('Node clicked:', node)}
onEdgeClick={(edge) => console.log('Edge clicked:', edge)}
/>
</div>
);
}
export default App;Entry points
The package ships four entry points. Import from the narrowest one that fits —
the subpaths below . are server-safe and pull in zero React/React Flow
runtime code, so Node scripts, SSR, and API routes can use them without the
canvas stack.
| Import | What it is | Server-safe |
| --- | --- | --- |
| @mp70/react-networks (.) | The React components (NetworkDiagram, FibreFlowMap, DiagramErrorBoundary), plus the diagram utilities, hooks, and types. | No — pulls React + React Flow. |
| @mp70/react-networks/model | The pure data model: core types, the handle-id grammar, port-contract, port-position, document adapters, path-layout, and the fibre connection policy. | Yes — zero React/React Flow at runtime. |
| @mp70/react-networks/netbox | NetBox → diagram converters (netboxToDiagramDocument, netboxToNetworkDiagram, …) and the NetBox* DTO types. | Yes. |
| @mp70/react-networks/inventory | Generic CMDB/inventory converters (inventoryToDiagramDocument, inventoryToNetworkDiagram, …) and the Inventory*DTO types. | Yes. |
| @mp70/react-networks/index.css | The stylesheet (see Custom styling). | n/a |
// In a Node/SSR API route — no React, no React Flow pulled in.
import { networkGraphToDiagramDocument } from '@mp70/react-networks/model';
import { netboxToDiagramDocument } from '@mp70/react-networks/netbox';
import { inventoryToDiagramDocument } from '@mp70/react-networks/inventory';Server-side usage
Import pure helpers and types from @mp70/react-networks/model (or
/netbox / /inventory), not from the root @mp70/react-networks.
The root entry carries the 'use client' directive and pulls in React and the
React Flow runtime. Server code — API routes, SSR loaders, cron jobs, migration
scripts — that only needs the data model, adapters, handle-id/node-id grammar,
or pure geometry should import from the server-safe /model subpath so none of
the canvas stack lands in the server bundle. Everything on /model is
tree-shakeable and framework-free.
// ✅ server-safe — zero React / React Flow at runtime
import {
networkGraphToDiagramDocument,
buildPortHandleId,
handlesMatch,
calculateSplicePositionFromNumber,
} from '@mp70/react-networks/model';
// ❌ in server code this drags in React + React Flow
import { networkGraphToDiagramDocument } from '@mp70/react-networks';Enforce it with ESLint
Add a no-restricted-imports rule scoped to your server globs so a stray root
import fails lint instead of bloating the server bundle. Copy-paste (flat
config):
// eslint.config.js (flat config)
export default [
{
files: ['**/server/**', '**/api/**', '**/*.server.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@mp70/react-networks',
message:
'Server code must import from @mp70/react-networks/model (or /netbox, /inventory) — the root entry pulls in React + React Flow.',
},
],
},
],
},
},
];The same rule in legacy .eslintrc form, an overrides variant, and a
typescript-eslint note live in
docs/eslint-server-imports.md.
Documentation
Data Model
@mp70/react-networks supports two control layers:
document/initialDocument/onDocumentChangeuseDiagramDocument, the canonical persisted/editor schema. Prefer this for saved diagrams, import/export, and external integrations.DiagramDocumentnow carries a requiredversion: 1discriminator (see CHANGELOG).nodes/edgesremain the lower-level React Flow-shaped control surface when you want direct control of rendered nodes and edges.
For device-style nodes, prefer the flat ports: DevicePort[] input. Use
portGroups and rearPortGroups only when you need explicit grouped front/rear
layout control.
Rendering primitives
You render diagrams through NetworkDiagram and FibreFlowMap. The individual
node/edge components (rack, device, PDU, splice, tube, cable, coupler, patch
panel, closure, fibre-split, fibre edge, power edge, …) are registered
internally by those components and are not part of the public surface — do
not mount them directly.
API Reference
Components
NetworkDiagram— main diagram componentFibreFlowMap— standalone fibre-flow diagram (closures, cables, tubes, splices, circuits)DiagramErrorBoundary— error boundary for diagram errors
Utilities (selected)
Rack schema:
buildNodesFromRackConfig/createRackConfigFromNodesaddDeviceToRack/removeDeviceFromRack/updateDeviceUPositionvalidateRackDevicePlacements
Splice schema:
buildNodesFromSpliceConfig/createSpliceConfigFromNodesaddSpliceToTray/removeSpliceFromTray/updateSpliceHolderPositionvalidateSplicePlacements
U positioning: snapToUPosition, isUPositionAvailable, findNextAvailableUPosition, calculateDevicePositionFromU, validateAndSnapDevice
Data integration (on the server-safe subpaths — see Entry points):
inventoryToDiagramDocument/inventoryToNetworkDiagram(/inventory)netboxToDiagramDocument/netboxToNetworkDiagram(/netbox)
Power: getPowerPortStyle, getConnectorConfig, getDeviceConnectorType, getPDUPortType, isHighPowerConnector, POWER_CONNECTORS
Fibre colours: FIBRE_COLORS_12, baseColorFor, isStriped, fibreSolidOrStriped, getFibreColor
Rack geometry: getRackBounds, isPointInRack, findNearestRack
Constants: U_HEIGHT_PX, RACK_HEADER_HEIGHT, RACK_WIDTH_PX, HANDLE_EXTENSION_PX
Status: getStatusColor
Edges: replaceEdge
Connection validation (pure functions): validateNetworkConnection, computeEdgeZIndex, createNodeMap, getHandleSide
Handle & node identity (pure; on /model): handlesMatch, findEdgesForPortHandle, encodeHandle / decodeHandle (structured Handle type), buildPortHandleId / buildFibreHandleId / buildTubeHandleId / buildSpliceHandleId (+ siblings), buildRackNodeId / buildDeviceNodeId / parseRackChildNodeId / isRackNode / isDeviceNode / getNodeKind. See API.md § Handle & node identity. sanitizeHandleIdForMatching is deprecated for matching — use handlesMatch.
Fibre scene geometry (pure; on /model): calculateSplicePositionFromNumber plus the HOLDER_HEIGHT_PX / TRAY_HEADER_HEIGHT / TRAY_* layout constants, for hand-composing closure/tray/splice scenes that line up with rendered nodes.
The full type-level surface is enforced by API Extractor reports (
etc/*.api.md) generated per entry point. Those reports are the source of truth for what is public.
NetworkDiagram Props
For new code, prefer the document-model props. The graph props and React Flow escape hatches remain available for advanced integrations and compatibility with older code.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| document | DiagramDocument | - | Controlled canonical diagram document. Takes precedence over nodes / edges. |
| initialDocument | DiagramDocument | - | Initial canonical document for uncontrolled document mode. |
| onDocumentChange | (document: DiagramDocument) => void | - | Callback when the canonical document changes. |
| alignRacksToBottom | boolean | false | Align rack bottoms to baseline |
| onNodeClick | (node: NetworkNode \| null) => void | - | Node click / selection handler |
| onEdgeClick | (edge: NetworkEdge) => void | - | Edge click handler |
| onFaceChange | (nodeId: string, face: 'front' \| 'rear') => void | - | Device face change handler |
| onRackFaceChange | (rackId: string, face: 'front' \| 'rear') => void | - | Rack face change handler |
| className | string | - | CSS class name |
| style | React.CSSProperties | - | Inline styles |
| readOnly | boolean | false | Force lock diagram (no drag/connect/delete/reconnect/pan/zoom) |
| interaction | NetworkDiagramInteractionOptions | - | Fine-grained interaction controls for editing, pan, zoom, minimap, and controls |
| actionPanel | NetworkDiagramActionPanelOptions | - | Configure action panel (enabled, showRearToggle, showDeRack) |
| nodes | NetworkNode[] | [] | Advanced: controlled lower-level graph nodes |
| edges | NetworkEdge[] | [] | Advanced: controlled lower-level graph edges |
| onNodesChange | (nodes: NetworkNode[]) => void | - | Advanced: callback when lower-level nodes change |
| onEdgesChange | (edges: NetworkEdge[]) => void | - | Advanced: callback when lower-level edges change |
| initialNodes | NetworkNode[] | - | Advanced: initial lower-level nodes |
| initialEdges | NetworkEdge[] | - | Advanced: initial lower-level edges |
| nodeTypes | NodeTypes | - | Custom React Flow node types |
| edgeTypes | EdgeTypes | - | Custom React Flow edge types |
| onConnect | (connection: Connection) => void | - | Connection handler override |
| onEdgeUpdate | (oldEdge: Edge, newConnection: Connection) => void | - | Edge update (reconnect) override |
| isValidConnection | (connection: Connection) => boolean | - | Connection validation override |
| connectionMode | ConnectionMode | - | React Flow connection mode |
| connectionRadius | number | React Flow default | Pointer snap distance (px) for handles. Raise to make small ports easier to target; lower to avoid mis-targeting on dense devices |
| onConnectRejected | (payload: NetworkConnectionRejection) => void | - | Fired when a connect/reconnect attempt is rejected, so you can surface feedback instead of failing silently |
| reAssignable | boolean | true | Allow devices to move between racks / ungroup on drag |
| colorMode | 'light' \| 'dark' \| 'system' | - | Diagram color mode |
| onInit | (instance: ReactFlowInstance) => void | - | Called when React Flow is ready |
| onFlowMethods | (methods: NetworkDiagramFlowMethods) => void | - | Receive toObject/setViewport/toImage/fitView etc. |
| fitViewOnInit | boolean | true | Run React Flow's fit-on-init once dimensions are measured. Pass false to keep a restored viewport. |
| showDownloadButton | boolean | - | Show export image button |
| downloadButtonPosition | 'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left' | - | Download button position |
| useSmoothstepEdgesForTubes | boolean | false | Use smoothstep edges for tube connections |
| debug | boolean | false | Enable debug mode |
| fitViewOptions | FitViewOptions | - | React Flow fitView options |
| highlightGroups | HighlightGroup[] | - | Declarative multi-group emphasis: colour sets of node/edge IDs and (with dimUnhighlighted) dim the rest. See Highlight groups |
| highlightedNodeIds | string[] | - | Node IDs kept at full opacity when focus dimming is active |
| highlightedEdgeIds | string[] | - | Edge IDs kept at full opacity when focus dimming is active |
| dimUnhighlighted | boolean | false | Dim nodes/edges not in the focus ID lists |
| dimOpacity | number | 0.25 | Opacity applied to dimmed elements (0-1) |
Edge highlighting
Per-edge emphasis uses NetworkEdge.data:
{
id: 'edge-1',
source: 'switch-1',
target: 'server-1',
type: 'fiber',
data: {
highlight: true,
highlightColor: '#dc2626',
highlightWidth: 5,
label: 'Cable A',
},
}Diagram-level focus (trace, DRC row click):
<NetworkDiagram
nodes={nodes}
edges={edges}
highlightedEdgeIds={focusedEdgeIds}
highlightedNodeIds={focusedNodeIds}
dimUnhighlighted
/>For multi-severity emphasis (several colours at once, dim the rest) prefer the
declarative highlightGroups prop over hand-maintaining per-edge highlight*
data. Pass groups highest-severity first; when an element is in more than one
group the earliest wins:
<NetworkDiagram
nodes={nodes}
edges={edges}
dimUnhighlighted
highlightGroups={[
{ edgeIds: errorEdgeIds, color: '#dc2626', width: 5, label: 'Error' },
{ edgeIds: warnEdgeIds, color: '#d97706', width: 3, label: 'Warning' },
]}
/>The library does not define severity enums. Apps pick highlightColor / group
color hex values. Common conventions:
| Meaning | Suggested highlightColor |
|---|---|
| Error | #dc2626 |
| Warning | #d97706 |
| Complete | #16a34a |
| Installer power step | #f97316 |
| Installer network step | #ef4444 |
Focus dimming survives a diagramDocumentToNetworkGraph round-trip: the
exported applyDiagramFocusToNodes / applyDiagramFocusToEdges helpers write
opacity to node/edge style and, for edges, to data.opacity as well, so a
dimmed edge stays dimmed after conversion. Use the library helpers rather than
app-local copies.
Pass showDeviceImages={false} on NetworkDiagram to hide rack photos; status border colors from
getStatusColor(status) still apply. Use onPortClick and highlightedPortHandles for port trace UX.
NetworkNode Types
type NetworkNodeType =
| 'rack'
| 'switch'
| 'router'
| 'server'
| 'fiber'
| 'patch-panel'
| 'device'
| 'vertical-pdu'
| 'splice'
| 'splice-tray'
| 'tube'
| 'cable'
| 'multi-tube-cable'
| 'coupler'
| 'closure'
| 'fibre-split'
| 'fibre-flow-cable'
| 'fibre-flow-closure';Note: the
'fiber'node-type string and edge-data keys such asfiberId/ribbonFiberIdsare serialized wire values and are deliberately unchanged by theFiber→Fibresymbol rename (see CHANGELOG).
NetworkEdge Types
type NetworkEdgeType = 'fiber' | 'ethernet' | 'power' | 'smoothstep' | 'step' | 'thick-cable' | 'fibre-flow' | 'fibre-flow-link';Advanced Usage
Rack Configuration
import { RackConfig, buildNodesFromRackConfig, Width } from '@mp70/react-networks';
const rackSchema: RackConfig[] = [
{
id: 'rack-1',
name: 'Main Rack',
position: { x: 100, y: 100 },
units: 42,
devices: [
{
id: 'server-1',
name: 'Web Server',
unit: 1,
height: 1,
type: 'server',
width: Width.FULL,
ports: [
{ id: 'eth0', label: 'eth0', group: 'Network Ports', connected: true, type: 'ethernet' },
{ id: 'eth1', label: 'eth1', group: 'Network Ports', connected: false, type: 'ethernet' }
]
},
{
id: 'switch-1',
name: 'Core Switch',
unit: 3,
height: 1,
type: 'switch',
width: Width.FULL,
ports: [
{ id: 'port-1', label: 'port-1', group: 'Uplink Ports', connected: true, type: 'fiber' },
{ id: 'port-2', label: 'port-2', group: 'Uplink Ports', connected: false, type: 'fiber' }
]
}
]
}
];
const nodes = buildNodesFromRackConfig(rackSchema);For the simplest device API, pass a flat ports: DevicePort[] array and set group / face per port. Use portGroups and rearPortGroups only when you need explicit grouped front/rear control.
Device images (e.g. server front/rear photos or SVGs) use frontImageUrl and rearImageUrl on each device; supported formats include PNG, SVG, JPEG, GIF, and WebP.
Inventory Integration
The inventory and NetBox converters live on server-safe subpaths, so you can run them in a Node/SSR import pipeline without loading React or React Flow:
import { inventoryToNetworkDiagram } from '@mp70/react-networks/inventory';
const inventoryData = {
racks: [...],
devices: [...],
cables: [...]
};
const { nodes, edges } = inventoryToNetworkDiagram(inventoryData);To produce the canonical persisted document instead, use
inventoryToDiagramDocument (or netboxToDiagramDocument from
@mp70/react-networks/netbox).
Custom styling
The stylesheet is emitted as a separate file (it is not injected by importing the JS entry), so a consumer must import it once, alongside React Flow's own stylesheet:
import '@xyflow/react/dist/style.css';
import '@mp70/react-networks/index.css';Without both imports the diagram renders unstyled.
Theming
The supported theming API is (1) two theme class hooks you place on a wrapper
element and (2) a set of --rn-* CSS custom properties. These names are
stable and covered by semver from 1.0 onward; nothing else in the stylesheet is
a public contract.
Theme classes
| Class | Purpose |
| --- | --- |
| react-flow-dark | Selects the dark palette for the canvas, controls, and minimap. |
| react-flow-light | Selects the light palette. |
| react-networks-attribution | Hook for styling the attribution badge. |
Apply react-flow-dark or react-flow-light to an ancestor of the diagram to
switch palettes; each class sets the --rn-flow-* variables below.
:root override points
These variables have a built-in fallback and are not written as inline
styles by the components, so a plain :root (or scoped) override takes effect:
/* Canvas / controls / minimap palette (set by the theme classes; override to
retint). */
.react-flow-dark,
.react-flow-light {
--rn-flow-background: #1a1a1a;
--rn-flow-text: #ffffff;
--rn-flow-border: #404040;
--rn-flow-controls-bg: #2d2d2d;
--rn-flow-controls-border: #404040;
--rn-flow-minimap-bg: #2d2d2d;
--rn-flow-minimap-border: #404040;
}
/* Device node base colours (override to restyle). */
:root {
--rn-device-bg-color: #1f2937;
--rn-device-text-color: white;
}Set by the components — not :root override points
The following variables are written unconditionally as inline styles by the
components (per device, per edge, or per handle) from node/edge data. An inline
custom property shadows any :root declaration, so overriding these in a
:root block has no effect — style the source data (device status,
images, edge highlight*, etc.) or target the elements directly instead:
| Variable(s) | Set by | Driven from |
| --- | --- | --- |
| --rn-device-bg-image, --rn-device-bg-position, --rn-device-bg-repeat, --rn-device-bg-size | Device node | frontImageUrl / rearImageUrl + imageFit/imagePosition/imageRepeat |
| --rn-device-border-color, --rn-device-status-color | Device node | getStatusColor(status) |
| --rn-device-box-shadow | Device node | selection state |
| --rn-edge-highlight-from, --rn-edge-highlight-to | Edge components | per-edge highlight stroke width |
| --rn-hit-expand-top / -right / -bottom / -left | Device node handles | per-port hit-area expansion |
| --rn-fibre-bg, --rn-fibre-bg-image | Fibre/tube handles | per-handle cable/tube fibre colour |
Performance
- Large canvases: enable
onlyRenderVisibleElements. React Flow viewport culling keeps multi-rack floor and ODF-scale surfaces responsive. It is safe to leave on for image export — the library force-disables culling for the duration of atoImagerender so off-screen nodes still appear in the PNG, then restores the setting (see Export-safe culling). Leave it off for a single-rack editor, where everything is on-screen anyway. - Document-mode drag cost is memoized. When you drive
NetworkDiagramwithdocument/onDocumentChange, endpoint derivation is WeakMap-memoized onnode.dataidentity, so a position-only change during a drag frame skips re-deriving (and deep-cloning) ports. If your host already holdsNetworkNode[]/NetworkEdge[], you can also rendernodes/edgeswithonNodesChangedirectly and skip the document round-trip entirely.
Public API & stability
From 1.0 the following are the public, semver-covered surface:
- JavaScript / TypeScript exports of the four entry points:
.(components + diagram utilities),./model,./netbox, and./inventory. What each exports is pinned by committed API Extractor reports (etc/*.api.md). - CSS hooks: the
--rn-*custom properties and thereact-flow-dark,react-flow-light, andreact-networks-attributionclass names. - Handle-id strings are a frozen wire format (used inside React Flow
handles). Build/parse them only through the handle-id grammar exports
(
buildPortHandleId,parseHandleId,encodeHandle/decodeHandle, …); do not hand-construct or hand-parse them. To test whether two handle strings denote the same handle, usehandlesMatch(orfindEdgesForPortHandlefor the edge lookup);sanitizeHandleIdForMatchingis deprecated for matching, intentionally many-to-one, and must never be persisted. For persisted identity, referenceDiagramEndpoint.id(an opaque, relabel-stable key) — the handle string is derived from the endpoint at the rendering boundary, so persist the endpoint id, not the handle.
Anything not listed above (internal node/edge components, other CSS selectors, DOM structure) is an implementation detail and may change without a major bump.
Development
Prerequisites
- Node.js 22+
- npm, yarn, or pnpm
Setup
# Clone the repository, then:
cd react-networks/packages/network-diagrams
npm installBuild
npm run buildDevelopment
npm run devTesting
npm testLinting
npm run lintDistribution
The library is distributed as:
- ES Modules:
dist/index.mjs - CommonJS:
dist/index.js - TypeScript:
dist/index.d.ts(and.d.mtsfor ESM) - CSS:
dist/index.css(imported via@mp70/react-networks/index.css)
Server-safe subpath entries (./model, ./netbox, ./inventory) ship the same
.mjs / .js / .d.mts / .d.ts set.
Contributing
We welcome contributions! See CONTRIBUTING.md for details.
Development Workflow
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Run the test suite
- Submit a pull request
License
Dual-licensed under:
- AGPL-3.0-only (Free) - For open source, personal, and educational use. Copyleft applies.
- Commercial (
LicenseRef-Commercial) - For proprietary/commercial use without copyleft obligations.
The SPDX license expression is AGPL-3.0-only OR LicenseRef-Commercial. See
../../LICENSE for details. By installing, you agree to use under either license.
Acknowledgments
- React Flow for the diagram foundation
Made by matt from rackout.net.
