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: ScadaEditorPage — libs/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:
onDropreads the dataTransfer type, converts screen → flow coordinates withscreenToFlowPosition, and callsaddNode(nodeType, position)—ScadaEditor.jsx. - Connect to add edges:
onConnectcreates arawSewageedge by default —ScadaEditor.jsx. - Selection: node/edge/pane clicks set
selectedNodeId/selectedEdgeId—ScadaEditor.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,FormulaEditorDialogundercomponents/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).
| Category | Node types (id) |
|---|---|
| Containers | CIRCULAR_TANK, CLARIFIER, AERATION_TANK, RECT_TANK, SLUDGE_WELL, SLUDGE_BEDS |
| Equipment | SAND_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 & Labels | STATUS_BADGE, QUALITY_PARAMS, LABEL_NODE |
| General | CARD_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 NodeDimensions — scadaEnums.js. Per-type style/meta/calculation keys live in ScadaKeys — libs/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 acontrolsmeta array consumed by the Viewer's Remote Control panel —nodeStyleKeys.js.EDGE_CONDITION— hidden node whosevaluecalculation animates connected edges.
Edge Types (6)
ScadaEdgeType — scadaEnums.js:
| id | Label | Animated |
|---|---|---|
rawSewage | Raw Sewage | yes (default on connect) |
cleanWater | Clean Water | yes |
air | Air Lines | yes |
sludge | Sludge Lines | yes |
backWash | Back Wash | yes |
electrical | Electrical Lines | no |
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. Thescada/dataendpoint 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/saveToApicatch errors, log them, and return{ success: false, message }—ScadaEditorStore.js,ScadaEditorStore.js.JsonExportersurfaces the returned message in a Snackbar withseveritysuccess/error—JsonExporter.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
addNodereturnsnullfor an unknownnodeType(if (!nodeConfig) return null;) —ScadaEditorStore.js.onDropignores payloads whose type is not inScadaNodeType—ScadaEditor.jsx.nodes/edgesdefault 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 Function — FormulaEditorDialog.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
isDemogating — access is controlled entirely by route registration and the sidebar permission (see Visibility & Access Control).
Dependencies
Shared Store / Services
| Dependency | Import | Used For |
|---|---|---|
apiClient | @aquagen-mf-webapp/shared/services/api/apiClient | Load/save diagram |
Urls | @aquagen-mf-webapp/shared/services/api/urls | scadaGraphEditor |
LocalDBInstance, LocalDBKeys | @aquagen-mf-webapp/shared/services/localdb/localdb | Read categoryUnits for the formula editor |
Other Libs
- components —
logical/If,logical/IfNot. - scadaViewer (consumer relationship): the Viewer imports this lib's
nodeTypes,edgeTypes,ScadaNodeType, andnodeStyleKeyshelpers to render authored diagrams. See SCADA Viewer.
External npm Libraries
| Library | Version | Usage |
|---|---|---|
reactflow | ^11.11.4 | ReactFlow, ReactFlowProvider, Background, Controls, MiniMap, useReactFlow, applyNodeChanges, applyEdgeChanges, ConnectionMode |
@mui/material | — | Toolbar, dialogs, property panel UI |
Verified: Drag-and-drop uses the native HTML5 DnD API (
event.dataTransfer), not@dnd-kit.@dnd-kit/*andthree.jsexist in the rootpackage.jsonbut are not imported anywhere inlibs/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).
Nav Helpers
navHelperInstance.jsmapsSCADA_EDITOR → /scadaEditor;:113places it in theEFFICIENCYsubmenu.
Visibility & Access Control
Route Registration
| App | /scadaEditor registered? | Reference |
|---|---|---|
production | Yes | apps/production/src/routes/routes.js |
demo | Yes | apps/demo/src/routes/routes.js |
rwi | No | — |
uwms | No | — |
lakepulse | No | — |
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_EDITOR—libs/shared/src/enums/permissions.js. - Nav mapping:
navHelperInstance.js, submenu at:113.
Sidebar Lock State
The sidebar entry uses access: SidebarAccess.STRICT_HIDDEN — navHelperInstance.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; thedemoapp registers the route.
Related Documentation
- SCADA Viewer — Renders the diagrams authored here (shared node/edge registries and
nodeStyleKeyshelpers) - HMI Interface — Related device monitoring & control surface
- Permissions —
SCADA_EDITORgating,ignoreSuperUserand STRICT_HIDDEN semantics - Routes — Where
/scadaEditoris registered - Additional Features — High-level SCADA overview
Last Updated: July 2026
Module Location: libs/scadaEditor/src/