Skip to main content

SCADA Editor

A drag-and-drop visual graph editor for building SCADA schematics. Authors drop typed nodes (tanks, pumps, panel rooms, motors, status badges, labels) onto a ReactFlow canvas, connect them with typed pipe edges, bind live values through unit-referencing formulas, organize the diagram into pages, and persist it to the backend. The saved graph is what the SCADA Viewer renders live.


Overview

The Editor is a three-pane layout: a Toolbar node palette on the left, the ScadaEditor ReactFlow canvas in the center, and a context-sensitive PropertyPanel on the right. A PageManager tab strip sits above the canvas, and a JsonExporter action bar (Save / Reload / Export / Copy / Import) sits in the header. All diagram state lives in ScadaEditorStore, which also handles JSON import/export and API load/save.

Location: libs/scadaEditor/src/

Route: /scadaEditor (registered in the demo and production apps only)

Permission Required: SCADA_EDITOR

Entry component: ScadaEditorPagelibs/scadaEditor/src/ScadaEditorPage.jsx


Key Features

1. Drag-and-Drop Node Palette

Toolbar lists all node types grouped by category (Containers, Equipment, Status & Labels, General) with live scaled-down previews, grid/list view toggle, search, and a resizable sidebar — libs/scadaEditor/src/components/Toolbar.jsx. Dragging sets a native HTML5 dataTransfer payload:

event.dataTransfer.setData('application/reactflow-nodetype', nodeType);

Toolbar.jsx

2. Canvas Editing

ScadaEditor is the interactive ReactFlow surface — libs/scadaEditor/src/components/ScadaEditor.jsx:

  • Drop to add: onDrop reads the dataTransfer type, converts screen → flow coordinates with screenToFlowPosition, and calls addNode(nodeType, position)ScadaEditor.jsx.
  • Connect to add edges: onConnect creates a rawSewage edge by default — ScadaEditor.jsx.
  • Selection: node/edge/pane clicks set selectedNodeId / selectedEdgeIdScadaEditor.jsx.
  • Delete key: deleteKeyCode={['Backspace', 'Delete']}, multi-select with Shift, loose connection mode, snap-to-grid — ScadaEditor.jsx.

3. Property Panel

PropertyPanel switches on selection — libs/scadaEditor/src/components/PropertyPanel.jsx:

  • Nothing selected → "Select a node or edge to edit its properties" placeholder — PropertyPanel.jsx.
  • Node selected → NodePropertyPanel (label, style, meta, calculations/formulas, delete).
  • Edge selected → EdgePropertyPanel (pipe type, delete).
  • Sub-editors: ColorPicker, KeyValueEditor, NodeStyleKeysDialog, FormulaEditorDialog under components/propertyPanel/.

4. Formula Editor

FormulaEditorDialog lets authors bind values using {unitId} and {unitId -> param} references plus arithmetic/comparison/logical operators — libs/scadaEditor/src/components/propertyPanel/FormulaEditorDialog.jsx. Units are read from the logged-in user's categoryUnits in local DB; the dialog validates and auto-formats formulas before saving.

5. Multi-Page Diagrams

PageManager provides add / rename / delete pages as a tab strip — libs/scadaEditor/src/components/PageManager.jsx. Each node/edge is tagged with editMeta.graphTag = pageId so a single export/save round-trips all pages.

6. JSON & API Persistence

JsonExporter exposes Save to API, Reload from API, Export (download .json), Copy to clipboard, and Import (paste or upload file) — libs/scadaEditor/src/components/JsonExporter.jsx.


Node & Edge Taxonomy

Node Types (28)

Defined in libs/scadaEditor/src/enum/scadaEnums.js (ScadaNodeType) and mapped to React components in libs/scadaEditor/src/components/nodes/index.js (nodeTypes).

CategoryNode types (id)
ContainersCIRCULAR_TANK, CLARIFIER, AERATION_TANK, RECT_TANK, SLUDGE_WELL, SLUDGE_BEDS
EquipmentSAND_CARBON_FILTER_NODE, SAND_FILTER_NODE, CARBON_FILTER_NODE, PANEL_ROOM, PANEL_ROOM_V2, MOTOR_NODE, CHLORINE_DOSING_NODE, FILTER_PRESS_NODE, PUMP_TYPE_A_NODE, PUMP_TYPE_B_NODE, PROCESS_BOX, REMOTE_CONTROL
Status & LabelsSTATUS_BADGE, QUALITY_PARAMS, LABEL_NODE
GeneralCARD_NODE, ROUND_NODE, VALUE_DISPLAY, LABEL_WITH_CONNECTOR, GROUP_BOX, JUNCTION, EDGE_CONDITION

Each entry carries id, label, category, description, and defaultStyle (some also defaultMeta, e.g. PANEL_ROOM_V2.defaultMeta.plcTableMarkdown) — scadaEnums.js. Per-type default sizes live in NodeDimensionsscadaEnums.js. Per-type style/meta/calculation keys live in ScadaKeyslibs/scadaEditor/src/components/nodes/nodeStyleKeys.js.

Notable equipment nodes:

  • PANEL_ROOM / PANEL_ROOM_V2 — control panels exposing MCB/VFD/Blower/pump status & trip calculation keys (e.g. vfd1Status, blower1Trip, stpm1Status) — nodeStyleKeys.js.
  • MOTOR_NODE — ON/OFF/TRIP indicator.
  • REMOTE_CONTROL — carries a controls meta array consumed by the Viewer's Remote Control panel — nodeStyleKeys.js.
  • EDGE_CONDITION — hidden node whose value calculation animates connected edges.

Edge Types (6)

ScadaEdgeTypescadaEnums.js:

idLabelAnimated
rawSewageRaw Sewageyes (default on connect)
cleanWaterClean Wateryes
airAir Linesyes
sludgeSludge Linesyes
backWashBack Washyes
electricalElectrical Linesno

Architecture

Data Flow

ScadaEditorPage (ScadaEditorProvider + ReactFlowProvider)

ScadaEditorPageContent.useEffect → loadFromApi()

ScadaEditorStore.loadFromApi()

apiClient.get(Urls.scadaGraphEditor) // 'scada/graphEditor'

Parse data.metadata.pages + data.nodes/edges → nodesByPage / edgesByPage (keyed by editMeta.graphTag)

Toolbar (drag) → ScadaEditor.onDrop → addNode → setNodes
PropertyPanel edits → updateNode / updateEdge

JsonExporter.Save → saveToApi() → apiClient.post(Urls.scadaGraphEditor)

Key Components

1. ScadaEditorPage — provider wrapper + three-pane layout — ScadaEditorPage.jsx.

2. ScadaEditorProvider / useScadaEditor — all diagram state and actions — libs/scadaEditor/src/store/ScadaEditorStore.js. useScadaEditor() throws if used outside the provider — ScadaEditorStore.js.

3. Toolbar — node palette (Toolbar.jsx).

4. ScadaEditor — interactive ReactFlow canvas (ScadaEditor.jsx, wrapped in React.memo).

5. PropertyPanel — selection-driven editor (PropertyPanel.jsx).

6. PageManager — page tabs + CRUD dialogs (PageManager.jsx).

7. JsonExporter — persistence action bar (JsonExporter.jsx).


Store API

ScadaEditorStore maintains pages, currentPageId, nodesByPage, edgesByPage, selectedNodeId, selectedEdgeId, and metadata. nodes/edges are derived for the current page — ScadaEditorStore.js.

Node Actions

addNode(nodeType, position)   // creates node with default data + editMeta.graphTag = currentPageId
updateNode(nodeId, updates) // shallow-merges data / editMeta
deleteNode(nodeId) // also removes connected edges

New nodes get a default value FORMULA calculation and config { showLabel, showValue, showStatus }ScadaEditorStore.js.

Edge Actions

addEdge(source, target, sourceHandle, targetHandle, edgeType = 'rawSewage')
updateEdge(edgeId, updates) // merges data / style / markerEnd
deleteEdge(edgeId)

ScadaEditorStore.js

Page Actions

addPage(displayName)   // slugifies name → unique id, seeds empty nodes/edges
updatePage(pageId, updates)
deletePage(pageId) // guarded: no-op when only one page remains

ScadaEditorStore.js

Persistence

exportToJson()          // { id, type:'scada', metadata:{pages,...}, nodes:[], edges:[] } — graphTag stamped per item
importFromJson(json) // string|object; returns false on parse failure
clearAll() // reset to initial single-page state
saveToApi()POST Urls.scadaGraphEditor
loadFromApi()GET Urls.scadaGraphEditor

ScadaEditorStore.js


API Integration

Both endpoints resolve from libs/shared/src/services/api/urls.js.

Load Diagram

GET scada/graphEditor          // Urls.scadaGraphEditor — urls.js

Response: {
metadata: { version: '1.0', pages: [ { id, displayName, order, icon }, ... ] },
nodes: [ { id, type, position, data, editMeta: { graphTag, units, calculations } }, ... ],
edges: [ { id, source, target, type, data, style, editMeta: { graphTag } }, ... ]
}

ScadaEditorStore.js

Save Diagram

POST scada/graphEditor          // Urls.scadaGraphEditor
Request Body: {
metadata: { ...metadata, pages, lastSaved: <ISO> },
nodes: [ ... ], // flattened across all pages, each with editMeta.graphTag
edges: [ ... ]
}

ScadaEditorStore.js

Endpoint correction: the Editor both loads and saves through scada/graphEditor. The scada/data endpoint is the Viewer's live-values feed only.


Usage Examples

1. Mount the Editor (route element)

import ScadaEditorPage from '@aquagen-mf-webapp/scadaEditor/ScadaEditorPage';

// apps/*/src/routes/routes.js
{ path: 'scadaEditor', element: <ScadaEditorPage /> }

2. Programmatic Add + Save

import { useScadaEditor } from '@aquagen-mf-webapp/scadaEditor';

function AddTankButton() {
const { addNode, saveToApi } = useScadaEditor();

const handle = async () => {
addNode('CIRCULAR_TANK', { x: 200, y: 120 });
const res = await saveToApi();
if (!res.success) console.error(res.message);
};

return <button onClick={handle}>Add tank & save</button>;
}

3. Formula Binding

// Inserted from the FormulaEditorDialog unit list:
{UNIT1} + {UNIT2}
{UNIT1 -> kwh}
({UNIT1} > 0) and ({UNIT2} > 0)

FormulaEditorDialog.jsx


Edge Cases & Error Handling

Loading

ScadaEditorPageContent shows a centered CircularProgress until loadFromApi() resolves; the call is wrapped in try/catch with finally { setLoading(false) }ScadaEditorPage.jsx.

Empty / No-Config Diagram

When the API returns no data, the store falls back to a single default page (createDefaultPage() → id main) rather than an empty state — ScadaEditorStore.js, ScadaEditorStore.js. clearAll() resets to the same single empty page — ScadaEditorStore.js.

API Error (load/save)

  • loadFromApi / saveToApi catch errors, log them, and return { success: false, message }ScadaEditorStore.js, ScadaEditorStore.js.
  • JsonExporter surfaces the returned message in a Snackbar with severity success/errorJsonExporter.jsx.

Invalid JSON Import

importFromJson parses inside try/catch and returns false on failure; JsonExporter.handleImport then shows "Import failed: Invalid JSON format"ScadaEditorStore.js, JsonExporter.jsx. The Import button is disabled while the textarea is empty (disabled={!importJson.trim()}) — JsonExporter.jsx. It accepts both a normalized nodes array and the Viewer's { physicalNodes, virtualNodes } shape — ScadaEditorStore.js.

Destructive Actions (guards)

  • Clear all requires a window.confirm("...This cannot be undone.")JsonExporter.jsx.
  • Delete page is blocked when only one page remains (if (pages.length <= 1) return;), and the Delete menu item is hidden in that case — ScadaEditorStore.js, PageManager.jsx. Deleting a page also warns that all its nodes/edges will be removed — PageManager.jsx.
  • Delete node cascades to remove connected edges — ScadaEditorStore.js.

Unknown / Null Handling

  • addNode returns null for an unknown nodeType (if (!nodeConfig) return null;) — ScadaEditorStore.js.
  • onDrop ignores payloads whose type is not in ScadaNodeTypeScadaEditor.jsx.
  • nodes/edges default to [] for a page with no entries — ScadaEditorStore.js.

Formula Errors

validateFormula checks balanced {} and (), non-empty and single-word {unitId} / {unitId -> param} references, and compiles a test expression via new FunctionFormulaEditorDialog.jsx. Any error is shown as helperText and disables Save (disabled={!!formulaError}) — FormulaEditorDialog.jsx. If the user's categoryUnits can't be read from local DB, the unit list falls back to []FormulaEditorDialog.jsx.

Unsaved Changes

State is in-memory only; there is no dirty-tracking guard on navigation. Reload from API discards local edits without a prompt (it directly re-fetches and replaces state) — JsonExporter.jsx. Persist explicitly via Save, Export, or Copy before leaving.

Provider Misuse

useScadaEditor() throws "useScadaEditor must be used within a ScadaEditorProvider" if called outside the provider — ScadaEditorStore.js.

Note: The Editor store has no built-in permission, subscription, or isDemo gating — access is controlled entirely by route registration and the sidebar permission (see Visibility & Access Control).


Dependencies

Shared Store / Services

DependencyImportUsed For
apiClient@aquagen-mf-webapp/shared/services/api/apiClientLoad/save diagram
Urls@aquagen-mf-webapp/shared/services/api/urlsscadaGraphEditor
LocalDBInstance, LocalDBKeys@aquagen-mf-webapp/shared/services/localdb/localdbRead categoryUnits for the formula editor

Other Libs

  • componentslogical/If, logical/IfNot.
  • scadaViewer (consumer relationship): the Viewer imports this lib's nodeTypes, edgeTypes, ScadaNodeType, and nodeStyleKeys helpers to render authored diagrams. See SCADA Viewer.

External npm Libraries

LibraryVersionUsage
reactflow^11.11.4ReactFlow, ReactFlowProvider, Background, Controls, MiniMap, useReactFlow, applyNodeChanges, applyEdgeChanges, ConnectionMode
@mui/materialToolbar, dialogs, property panel UI

Verified: Drag-and-drop uses the native HTML5 DnD API (event.dataTransfer), not @dnd-kit. @dnd-kit/* and three.js exist in the root package.json but are not imported anywhere in libs/scadaEditor/.

Controllers / DataSources

The Editor talks to the backend directly from its store via apiClient + Urls.scadaGraphEditor; it has no dedicated controller/dataSource module (unlike the Viewer's ScadaController/ScadaDataSource).

  • navHelperInstance.js maps SCADA_EDITOR → /scadaEditor; :113 places it in the EFFICIENCY submenu.

Visibility & Access Control

Route Registration

App/scadaEditor registered?Reference
productionYesapps/production/src/routes/routes.js
demoYesapps/demo/src/routes/routes.js
rwiNo
uwmsNo
lakepulseNo

The component is imported as ScadaEditorPage from @aquagen-mf-webapp/scadaEditor/ScadaEditorPage in both apps — routes.js. All apps alias the lib in build config, but only production and demo mount the route.

Permission Tag

  • Permission: SCADA_EDITORlibs/shared/src/enums/permissions.js.
  • Nav mapping: navHelperInstance.js, submenu at :113.

The sidebar entry uses access: SidebarAccess.STRICT_HIDDENnavHelperInstance.js:

SCADA_EDITOR: {
permissionId: 'SCADA_EDITOR',
access: SidebarAccess.STRICT_HIDDEN,
displayName: 'Scada Editor',
...
}

Per the SidebarAccess contract (navHelperInstance.js):

STRICT_HIDDEN — hide entirely if the user lacks the permission, no super-user bypass.

The SCADA Editor is a diagram-authoring surface, so this strict gating is deliberate: only users explicitly granted SCADA_EDITOR see or reach it — super users included. The parallel PermissionWrapper idiom would be <PermissionWrapper tag={Permissions.allPermission.SCADA_EDITOR} ignoreSuperUser> (see Permissions).

Super-User & Demo Notes

  • Super user: does not unlock the Editor's sidebar entry (STRICT_HIDDEN, no bypass).
  • Demo: isDemoOption: false — not stripped from demo builds; the demo app registers the route.

  • SCADA Viewer — Renders the diagrams authored here (shared node/edge registries and nodeStyleKeys helpers)
  • HMI Interface — Related device monitoring & control surface
  • PermissionsSCADA_EDITOR gating, ignoreSuperUser and STRICT_HIDDEN semantics
  • Routes — Where /scadaEditor is registered
  • Additional Features — High-level SCADA overview

Last Updated: July 2026 Module Location: libs/scadaEditor/src/