Skip to content

Network Mapper Developer Guide

Ed Mozley edited this page Oct 25, 2026 · 1 revision

Network Mapper - Developer Guide

What it does for the analyst: Network Mapper Β· on a phone: Mobile-Friendly: Network Mapper Β· over the API: REST API: Network Mapper

In plain words

A diagram is a set of nodes, each one a pointer to a real CMDB object (cmdb_objects), plus connectors between them. The editor is one hand-written JavaScript module (window.NM) that keeps the whole diagram in memory, draws nodes as positioned <div>s and connectors in an SVG layer, and saves by sending the whole diagram back. The server deletes the old nodes and connectors and inserts the new ones, so every save gives every node a new id. Versions are a linked list of network_diagrams rows; only the newest one (the leaf) can be written to.

The user page already covers every feature. This page is about how the code is put together.

Files

File What it is
network-mapper/index.php The diagram list - cards, filter, New modal, Delete. Inline script; calls list_diagrams.php, create_diagram.php, delete_diagram.php
network-mapper/diagram.php The editor shell (?id=N, optional ?fit=1). Mostly CSS and markup; the inline script at the bottom calls NM.init(), wires Ctrl+S, Escape (closes every modal and dropdown), click-outside on the modals and the beforeunload warning
network-mapper/help.php The in-app guide
network-mapper/includes/header.php Module header (waffle menu, one nav tab: Diagrams)
assets/js/network-mapper.js The editor - everything live. One IIFE, public surface on window.NM
assets/js/network-mapper-icons.js window.NM_ICONS, NM_ICON_META, NM_ICON_CATEGORIES and nmRenderIcon(key, size, extra)
assets/js/vendor/html2canvas.min.js, jspdf.umd.min.js PNG / PDF export, vendored
includes/services/network_mapper.php NetworkMapperService - the write rules, shared by the UI endpoints, the REST API and the CMDB blast-radius handoff
api/network-mapper/*.php Eight UI endpoints (below)
api/v1/resources/network_mapper.php The REST resource, including the API-only incremental node / connector endpoints
api/cmdb/create_impact_diagram.php "Open as diagram" from the CMDB blast radius - creates a diagram through NetworkMapperService::createDiagram()

The tables

Three tables, all install-wide (no tenant_id). Columns from includes/db_verify_schema.php; foreign keys from database/freeitsm.sql.

Table Columns
network_diagrams id, parent_diagram_id (self-FK, ON DELETE SET NULL), title, description, version_label (50), created_by_analyst_id, created_datetime, updated_datetime, paper_size, paper_orientation, header_left / header_center / header_right / footer_left / footer_center / footer_right (200 each, NULL = inherit the org default, '' = explicit blank), is_demo
network_diagram_nodes id, diagram_id (cascade), cmdb_object_id, x, y, size (small / medium / large), icon_override, is_demo
network_diagram_connectors id, diagram_id (cascade), from_node_id / to_node_id (cascade from the node), cmdb_relationship_id (ON DELETE SET NULL to cmdb_object_relationships), label, line_style (solid / dashed), is_demo

The node holds no name, class or icon - those are read from the CMDB on every load. That is the whole point: rename the CI and the diagram follows.

The data model in the browser

get_diagram.php?id=N returns this (field names are exact, values illustrative):

{
  "success": true,
  "diagram": {
    "id": 12, "parent_diagram_id": 9, "title": "Core network", "description": null,
    "version_label": "v2", "created_by_analyst_id": 3, "author_name": "Ed",
    "created_datetime": "2026-10-01 09:00:00", "updated_datetime": "2026-10-02 14:10:00",
    "paper_size": "A3", "paper_orientation": "landscape",
    "header_left": null, "header_center": null, "header_right": null,
    "footer_left": null, "footer_center": null, "footer_right": null,
    "child_count": 0, "is_current": true
  },
  "nodes": [
    { "id": 301, "cmdb_object_id": 41, "x": 160, "y": 80, "size": "medium", "icon_override": null,
      "name": "core-sw-01", "is_planned": false,
      "class_id": 5, "class_name": "Switch", "class_icon": "switch" }
  ],
  "connectors": [
    { "id": 77, "from_node_id": 301, "to_node_id": 302,
      "cmdb_relationship_id": 18, "label": "connects to", "line_style": "solid" }
  ]
}

loadDiagram() puts these straight into three module variables: diagram, nodes, connectors. Everything the editor does is a change to those arrays followed by a re-render.

Unsaved things get negative ids. A node placed from the picker (commitPickerSelection()) or from Add related objects (commitRelatedSelections()) has id: null and tempId: nextTempId-- (-1, -2, ...). A connector's from_node_id / to_node_id hold whichever the node has - nodeRef(n) returns n.id || n.tempId, and findNodeByRef() matches either. A new connector also gets a _tempKey that is only used for selection and is never sent.

Two kinds of key, do not mix them up:

Function Gives Used for
nodeRef(n) a number - real id or negative tempId connector ends, and what the server maps
nodeKey(n) a string - 'r301' or 't-1' selection (selectedNodeKey) and the node element's data-key
connectorKey(c) 'cr77' or 'ct-3' connector selection (selectedConnectorKey)

Rendering

HTML for nodes, SVG for lines. Not a canvas element.

  • #canvas (.nm-canvas) is the scroll viewport and carries the dot grid. It is not zoomed.
  • #canvasInner (.nm-canvas-inner) holds everything that is: the SVG layer, the node <div>s, the page outline and the branding strips. applyZoom() sets transform: scale(zoom) on it and grows #canvasSpacer to 3000 Γ— zoom px so the viewport can still scroll. Zoom steps are ZOOM_LEVELS (0.25 to 3); setZoom() snaps to the nearest one.
  • ensureSvgLayer() creates one 5000 Γ— 5000 <svg class="nm-svg-layer"> as the first child of #canvasInner, with two arrowhead markers (nm-arrow, and cyan nm-arrow-sel for the selected line).
  • renderNodes() removes every .nm-node and rebuilds them with buildNodeEl(): the icon (nmRenderIcon(icon_override || class_icon || 'box')), the PLANNED pill, the label and - on the current version only - four .nm-edge-handle dots. It always ends by calling renderConnectors().
  • renderConnectors() removes every .nm-connector-group and draws each connector as a <g>: a wide invisible .nm-connector-hit path (the click target), the visible .nm-connector-line path, and if labelled a background <rect> and <text> at the midpoint.
  • calcConnectorPoints() picks the side of each icon box (nodeIconBBox(), not the whole node with its label) that faces the other node, on whichever axis has the bigger gap. Lines are straight.

All coordinates are model coordinates at 1Γ—. canvasModelCoords(e) turns a mouse event into them: offset from #canvas, plus its scroll, divided by zoom. Every drop, drag and hit test goes through it, which is why zoom never breaks dragging.

Interaction at the code level

Gesture Code
Drag a class from the palette HTML5 drag. onTileDragStart() puts {kind: 'nm-class', class_id} on dataTransfer; onCanvasDrop() snaps the drop point to the 20px GRID and calls openObjectPicker()
Pick the object fetchPickerResults() calls api/cmdb/get_objects.php?class_id= (with &search= after a 200ms debounce). renderPickerResults() hides objects already on the canvas. onPickerKeyDown() handles arrows / Enter / Escape
Select selectNode(key) / selectConnector(key) - mutually exclusive. Selecting a node opens the detail panel (openDetailForNode()), which lazy-loads api/cmdb/get_object.php; currentPropertiesObjectId makes a slow reply for the previous node be ignored
Move onNodeMouseDown() records nodeDrag, then document mousemove / mouseup. onNodeMouseMove() snaps, moves the element and redraws all connectors. onNodeMouseUp() calls markDirty() only if nodeDrag.moved
Draw a connector startConnectDrag() from an edge handle (stops propagation so the node does not also start moving). onConnectDragMove() draws a .nm-temp-connector line. onConnectDragUp() hit-tests with findNodeAt() (icon box), refuses a self-loop and an identical connector in the same direction, then commitConnectorDrag()
Label a connector Double-click the hit path, the label rect or the text: promptConnectorLabel()
Delete A document keydown in bindCanvasEvents(): Delete / Backspace, ignored while typing or while a .nm-modal-overlay.active is open. deleteSelectedNode() also drops every connector touching the node
Add related objects fetchRelatedRows() calls get_related_objects.php; commitRelatedSelections() places the new nodes in a ring (radius 180px, growing past six) and adds one connector per ticked path - outgoing goes source to other, incoming and property paths go other to source

There is no undo. Nothing in network-mapper.js records history; the way back from a wrong node is Delete. The phone layer (assets/js/mobile.js) changes none of this file - its place / move / delete gestures end by dispatching the same drop, mouse and keydown events these handlers already listen for. See the mobile page.

Read-only (historical) versions are enforced in the browser in three places: applyReadOnlyState() disables the buttons, the drop / drag / connect / delete handlers return early unless diagram.is_current, and markDirty() does nothing. The server refuses anyway.

Saving

markDirty()  ->  setStatus('unsaved')  ->  scheduleAutosave()   (2s debounce, only if autosave is on)
save()       ->  POST save_diagram.php (everything)  ->  GET get_diagram.php  ->  re-render
  • save() sends the metadata, the paper settings, all six branding slots (so going back to "inherit" also saves), every node as {id, tempId, cmdb_object_id, x, y, size, icon_override} and every connector as {from_node_id, to_node_id, cmdb_relationship_id, label, line_style}.
  • api/network-mapper/save_diagram.php is a thin adapter. It turns each node's id ?? tempId into a ref, each connector's ends into from_ref / to_ref, and the flat branding keys into branding.{header|footer}.{left|center|right}, then calls NetworkMapperService::saveDiagram().
  • The service checks the diagram is a leaf (requireLeaf()), updates the row, and in one transaction replaceContents() deletes the old connectors and nodes and inserts the new ones, resolving each connector through the ref map. It is strict: an unknown CI, size, line style or icon key, a dangling ref or a self-link throws and the whole save rolls back.
  • Because the ids are new, save() reloads the diagram, throws away the connector selection, and re-selects the node by cmdb_object_id.
  • scheduleAutosave() waits while nodeDrag is set, because the reload would destroy the element being dragged.
  • Autosave on / off is the user preference network_mapper_autosave (api/system/get_user_preference.php / set_user_preference.php).
  • The beforeunload warning in diagram.php has no getter to ask - it checks whether #saveStatus has the class nm-status-unsaved.

Every save, autosave included, ends with WorkflowEngine::emitCrud('network_diagram', 'updated', ...); create and delete emit created / deleted.

⚠️ Comments in network-mapper.js still talk about save_diagram.php's "nodeIdMap" and "silently skipped" rows. That was the old endpoint. Today the map is the service's $refMap, and bad rows are an error, not skipped.

Versions

A version is a network_diagrams row whose parent_diagram_id points at the one before it. The leaf is the row nobody points at - list_diagrams.php finds them with WHERE d.id NOT IN (SELECT parent_diagram_id ...), and get_diagram.php returns is_current = (child_count === 0).

  • New version: openNewVersionModal() saves first if dirty (the server copies what is stored, not what is on screen), suggests the next label with suggestNextVersionLabel(), then createNewVersion() posts to create_version.php and navigates to the new id. NetworkMapperService::createVersion() refuses a non-leaf parent - that is what keeps the chain linear - and copies the paper and branding settings, every node and every connector, mapping old node ids to new ones.
  • Versions dropdown: fetchVersionList() calls list_versions.php, which walks up to the root then down, and returns the chain ordered by created_datetime. Rows are real links.
  • Delete (from the list page): deleteDiagram() with $chain = false, so only a leaf can go; its parent becomes the leaf again. The REST API can delete a whole chain with ?chain=true.

The UI endpoints

All api/network-mapper/, all check the session and requireModuleAccessJson('network-mapper'), all reply {success, ...}.

Endpoint Does
list_diagrams.php Leaves only, with node_count, connector_count and version_count
get_diagram.php?id= One version, nodes hydrated from the CMDB
list_versions.php?id= The chain, each with is_current, plus root_id
create_diagram.php NetworkMapperService::createDiagram()
save_diagram.php NetworkMapperService::saveDiagram() (above)
create_version.php NetworkMapperService::createVersion(), parent in parent_diagram_id
delete_diagram.php NetworkMapperService::deleteDiagram(), leaf only
get_related_objects.php?object_id= One row per (object, path), kind = outgoing / incoming / property

The REST resource uses the same service for create / save / delete / version, and has its own handlers for the incremental node and connector routes and for suggestions. See REST API: Network Mapper.

The CMDB link

  • Reading: names, classes, icons and the planned flag come from joins in get_diagram.php (cmdb_objects, cmdb_classes, cmdb_icons). A node never stores them.
  • Placing: the palette is api/cmdb/get_classes.php (active classes only), the picker is api/cmdb/get_objects.php.
  • Related objects: get_related_objects.php reads cmdb_object_relationships both ways (the verb outward, the inverse_verb inward) and cmdb_object_properties.value_object_id for object-ref properties. A relationship path carries relationship_id, which becomes the connector's cmdb_relationship_id; a property path has none and uses the property label as the connector label.
  • The other direction: the CMDB blast radius can create a diagram - CMDB impact analysis - Developer Guide, section 10. That is why diagram.php honours ?fit=1 once on load.

Permissions and company scope

  • Pages use requireModuleAccess('network-mapper'), endpoints requireModuleAccessJson('network-mapper'). There are no finer capabilities in this module.
  • Diagrams themselves have no company. What is scoped is the CIs:
    • writes - NetworkMapperService::validateObjectExists() refuses a cmdb_object_id outside the actor's companyScope, with the same "Unknown cmdb_object_id" message as a missing one, so it gives nothing away;
    • placing - get_objects.php and get_related_objects.php filter by company (activeTenantFilter(), and analystCanAccessCmdbObject() for the source object);
    • reading - get_diagram.php deliberately does not filter nodes by company. Its comment explains why: filtering would make nodes silently vanish from existing diagrams. The residual is a diagram drawn before CMDB companies existed. Giving diagrams a company is noted there as the first job of a future Network Mapper multi-tenancy slice.
  • Write endpoints carry no explicit CSRF call; the central check in CSRF protection covers them.

Traps and known gaps

There are no TRAP: comments in this module's code. The things that catch people out:

  • πŸ”΄ Node and connector ids change on every save. Never keep a node id across a save - in the browser, in a test or in an integration. The browser re-finds by cmdb_object_id; the REST API's incremental endpoints exist precisely to give stable ids.
  • πŸ”΄ A connector end is a number that may be negative. Compare with nodeRef() and findNodeByRef(), never only n.id. deleteSelectedNode() checks both id and tempId for this reason.
  • ⚠️ Icon overrides must exist in cmdb_icons. The comment above openIconPicker() says any key from NM_ICONS works because the column is free text. The column is, but NetworkMapperService::validateIcon() now refuses a key that is not in cmdb_icons - add a new icon there as well as to network-mapper-icons.js.
  • ⚠️ Export CSS names a class that does not exist. The .is-exporting rules in diagram.php hide .nm-node-edge-handle; the handles are .nm-edge-handle. Exports come out clean anyway, because captureCanvas() clears the selection first and handles only show on hover or selection.
  • The picker hides objects already drawn, but the service accepts the same CI twice in a save (a connector naming a duplicated CI by *_object_id is refused; by ref it works).

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally