SCADA Viewer
Render live SCADA (Supervisory Control and Data Acquisition) diagrams as a read-only ReactFlow canvas: multi-page schematics, live device/parameter values driven by backend calculations, click-through to unit pages, and an optional Remote Control panel that writes to PLC registers.
Overview
The SCADA Viewer loads a saved diagram (authored in the SCADA Editor) and its live values for a selected date, then draws every node and edge in a non-interactive ReactFlow surface. Nodes are supplied by the SCADA Editor's node/edge component registries, so the Viewer and Editor always render the same shapes. On top of the read-only canvas it layers a legend, page tabs, a date picker, and — when the diagram contains a REMOTE_CONTROL node — a slide-in panel for toggling STP equipment via PLC writes.
Location: libs/scadaViewer/src/
Route: /scadaViewer (registered in the demo and production apps only)
Permission Required: SCADA_VIEWER
Entry component: ScadaViewer — libs/scadaViewer/src/ScadaViewer.jsx
Key Features
1. Read-Only Live Diagram
ScadaViewerComponent wraps ReactFlow with every interactive affordance disabled (nodesDraggable={false}, nodesConnectable={false}, elementsSelectable={false}), so users can pan/zoom but never mutate the graph.
- Node registry reuse:
nodeTypesandedgeTypesare imported from the Editor (@aquagen-mf-webapp/scadaEditor/components/nodesand.../edges) —ScadaViewerComponent.jsx. - View-mode flag: each node is decorated with
data.isViewMode = trueand anonClickhandler —ScadaViewerComponent.jsx. - Edge Condition nodes are hidden (
hidden: isEdgeCondition) and instead drive their connected edges' animation via aconditionValue—ScadaViewerComponent.jsx. - Auto fit-view excludes
REMOTE_CONTROLandEDGE_CONDITIONnodes when framing the diagram —ScadaViewerComponent.jsx.
2. Click-Through to Unit Pages
Clicking a node that carries a unitId (via COMMON_META.UNIT_ID) navigates to that unit using the shared useNavigateToUnit hook — ScadaViewerComponent.jsx.
3. Multi-Page Navigation
Diagrams are organized into pages. ViewerPageTabs renders a scrollable tab bar; it returns null when there is only one page — libs/scadaViewer/src/components/ViewerPageTabs.jsx.
4. Date Selection
An AppDatePicker in the header re-fetches live values for the chosen date via handleDateChange → loadFromApi(date) — ScadaViewer.jsx, ScadaViewerStore.js.
5. Remote Control (PLC Write)
When a diagram contains a REMOTE_CONTROL node, a floating Remote Control tab (RemoteControlToggleButton) opens the RemoteControlPanel side panel. Each control is a Switch that, on toggle, opens a confirmation dialog and — only on confirm — writes to the PLC. See Remote Control Flow.
6. Embeddable Page Viewer
ScadaPageViewer renders a single page's diagram in isolation (used inside the UWI/STP Plant View at libs/uwi/src/components/UwiPlantViewPage.jsx). It manages its own fetch via externalNodes/externalEdges passed into ScadaViewerProvider — libs/scadaViewer/src/components/ScadaPageViewer.jsx.
Architecture
Data Flow
ScadaViewer (ScadaViewerProvider + ReactFlowProvider)
↓
ScadaViewerPageContent.useEffect → loadFromApi() / loadDiagram(json)
↓
ScadaViewerStore.loadFromApi()
↓
apiClient.get(Urls.scadaData, { date, type: 'DAY' }) // 'scada/data'
↓
Parse response.pages → nodesByPage / edgesByPage
↓
ScadaViewerComponent renders ReactFlow (read-only)
Remote control write path:
RemoteControlPanel (confirm dialog)
↓
ScadaController.writeRegisters(payload)
↓
ScadaDataSource.writeRegisters → apiClient.post(Urls.plcWriteRegisters) // 'plc/write-registers'
↓ (on ack success, after 2s)
ScadaController.triggerEventHubRefresh → apiClient.get(Urls.plcEventHubData)
↓
refreshScadaData({ silent: true }) → re-fetch live values
Key Components
1. ScadaViewer
- Purpose: Top-level provider wrapper.
- Location:
libs/scadaViewer/src/ScadaViewer.jsx - Wraps content in
ScadaViewerProviderthenReactFlowProvider. - Props:
diagramData(optional JSON to render instead of fetching),title.
2. ScadaViewerProvider / useScadaViewer
- Purpose: State + actions for the viewer.
- Location:
libs/scadaViewer/src/store/ScadaViewerStore.js - Exposes
pages,currentPage,nodes,edges,loading,error,lastUpdated,selectedDate,sidePanelView,overlayView, plusloadDiagram,loadFromApi,handleDateChange,updateNodesData,fetchControlLogs, and remote-control panel helpers. - Safe fallback:
useScadaViewer()returns a benign default object (no-op setters) when called outside the provider so embedded panels never crash —ScadaViewerStore.js.
3. ScadaViewerComponent
- Purpose: The read-only ReactFlow canvas.
- Location:
libs/scadaViewer/src/components/ScadaViewerComponent.jsx
4. RemoteControlPanel
- Purpose: STP equipment ON/OFF control via PLC writes.
- Location:
libs/scadaViewer/src/components/RemoteControlPanel.jsx
5. ScadaPageViewer
- Purpose: Standalone single-page viewer for embedding.
- Location:
libs/scadaViewer/src/components/ScadaPageViewer.jsx
Supporting components: ViewerPageTabs, Legend, RemoteControlToggleButton, RemoteControlLogsDialog, PanelRoomDetailPanel, PanelRoomDetailPanelV2, AutoShowControlPanel (all under libs/scadaViewer/src/components/).
API Integration
All endpoints resolve from libs/shared/src/services/api/urls.js.
Get Live Diagram Data
GET scada/data // Urls.scadaData — urls.js
Params: { date: 'DD/MM/YYYY', type: 'DAY' }
Response: {
pages: {
<pageId>: {
id: 'main',
displayName: 'Main Diagram',
order: 1,
icon: 'EditIcon',
diagram: {
nodes: [ { id, type, position, nodeType, data }, ... ],
edges: [ { id, source, target, type, data }, ... ]
}
}
}
}
Parsed in ScadaViewerStore.loadFromApi() — ScadaViewerStore.js.
Write PLC Registers (Remote Control)
POST plc/write-registers // Urls.plcWriteRegisters — urls.js
Request Body: { ...controlPayload, name: 'Blower 1' }
Response: {
response: {
frames: [ { status: 'success', raw_ack_data: '...0001' } ]
}
}
Called via ScadaController.writeRegisters → ScadaDataSource.writeRegisters — scadaController.js, scadaDataSource.js.
Trigger Event Hub Refresh
GET https://prod-aquagen.azurewebsites.net/api/admin/plcEventHub/data
// Urls.plcEventHubData — urls.js (absolute URL)
Called after a successful write (and by the manual Refresh button) — scadaDataSource.js.
Control Logs
Remote-control logs are fetched through the UWI controller, not a SCADA-specific endpoint: UwiDashboardController.getUwmsControlLogs(date) — ScadaViewerStore.js.
Remote Control Flow
RemoteControlPanel builds its items list from the REMOTE_CONTROL node's controls meta and each control's calculated ON/OFF state — RemoteControlPanel.jsx.
1. Confirmation Guard
Toggling a switch never writes immediately. It sets a pending action and opens an in-panel confirm dialog ("Are you sure you want to turn this ON/OFF?"):
const handleToggle = (id) => {
setNotification(null);
setPendingToggleId(id);
const item = items.find((i) => i.id === id);
setPendingToggleAction(item?.isOn ? 'OFF' : 'ON');
setShowConfirmDialog(true);
};
RemoteControlPanel.jsx
2. Write + ACK Verification
On confirm, runToggle posts the ON/OFF payload and verifies the PLC acknowledgement frame before treating the write as successful:
const result = await ScadaController.writeRegisters(payload);
const frame = result?.response?.frames?.[0];
const expectedAckSuffix = action === 'ON' ? '0001' : '0000';
frameOk =
frame?.status === 'success' &&
!!frame?.raw_ack_data?.endsWith(expectedAckSuffix);
RemoteControlPanel.jsx
If the frame is missing, not success, or the ack suffix doesn't match, the write is treated as failed: an error notification with a Retry action is shown and no refresh occurs.
3. Settle + Refresh
On success it waits 2 seconds, calls triggerEventHubRefresh(), then refreshScadaData({ silent: true }) to pull fresh values, and shows a success toast that auto-dismisses after 4s — RemoteControlPanel.jsx, RemoteControlPanel.jsx.
4. Analytics
Opening the panel fires AnalyticsService.sendEvent(AnalyticEvents.UWI_REMOTE_CONTROL_OPENED, {}) — RemoteControlPanel.jsx.
Usage Examples
1. Render the Live Viewer (route element)
import ScadaViewer from '@aquagen-mf-webapp/scadaViewer/ScadaViewer';
// apps/*/src/routes/routes.js
{ path: 'scadaViewer', element: <ScadaViewer /> }
2. Render a Diagram from JSON (no fetch)
// When diagramData is provided, loadDiagram(json) is used instead of loadFromApi()
<ScadaViewer diagramData={savedJson} title="Main Facility" />
ScadaViewer.jsx
3. Embed a Single Page
import { ScadaPageViewer } from '@aquagen-mf-webapp/scadaViewer';
<ScadaPageViewer
pageId="stp_overview"
date={selectedDate}
showMiniMap={false}
showControlPanel // mounts AutoShowControlPanel to auto-clear the side panel on unmount
/>
ScadaPageViewer.jsx
4. Access Viewer State
import { useScadaViewer } from '@aquagen-mf-webapp/scadaViewer';
function DiagramMeta() {
const { currentPage, lastUpdated, loading } = useScadaViewer();
if (loading) return null;
return <span>{currentPage?.displayName} · {new Date(lastUpdated).toLocaleString()}</span>;
}
Edge Cases & Error Handling
Loading
Two loading gates exist: an initialLoading local flag on first mount and the store loading flag on every fetch. Either shows a centered CircularProgress — ScadaViewer.jsx. ScadaViewerComponent also shows an inline "Loading diagram..." spinner — ScadaViewerComponent.jsx.
Empty Diagram / No Config
When nodes.length === 0, the canvas shows "No diagram data loaded" rather than a blank surface — ScadaViewerComponent.jsx. ScadaPageViewer shows "No diagram data for this page" for an empty embedded page — ScadaPageViewer.jsx. If the API returns no page for the requested pageId, nodes/edges are set to empty arrays — ScadaPageViewer.jsx.
API Error
Fetch failures are caught, logged, and stored in error; loadFromApi returns { success: false, message } — ScadaViewerStore.js. The canvas renders an "Error loading diagram" panel with the message — ScadaViewerComponent.jsx. The top-level useEffect also wraps loadData in try/catch and always clears initialLoading in finally — ScadaViewer.jsx.
Null / Missing Handling
nodes/edgesfall back to[]when a page has none —ScadaViewerStore.js.- Node click is a no-op when
data.isViewMode === falseorunitIdis empty —ScadaViewerComponent.jsx. - Response is only parsed when
response.status === 200 && response.data; otherwise{ success: false, message: 'No data found' }—ScadaViewerStore.js.
Remote Control — Write Confirmation & Guards
- No write happens without an explicit confirm (see Remote Control Flow).
- Failed/unacknowledged writes surface an error toast with a Retry button and skip the refresh —
RemoteControlPanel.jsx,RemoteControlPanel.jsx. - While a write is in flight (
isConfirming), the Yes/No buttons and Retry are disabled —RemoteControlPanel.jsx. - The event-hub refresh is itself wrapped in try/catch so a refresh failure never blocks the success path —
RemoteControlPanel.jsx. - Empty controls render "No equipment data available" —
RemoteControlPanel.jsx.
Permission-Denied / Missing Context
- The route is only mounted where registered and where the user holds
SCADA_VIEWER(see Visibility & Access Control). - If
RemoteControlPanelis rendered outside aScadaViewerProvider, it renders an "ERROR: No SCADA Context" warning box instead of throwing —RemoteControlPanel.jsx.
Invalid JSON (diagramData / loadDiagram)
loadDiagram accepts a string or object; it JSON.parses strings inside try/catch, sets error on failure, and returns false — ScadaViewerStore.js.
Note: The Viewer store has no subscription/
isDemogating of its own and no WebSocket — live values are pulled via date-scopedscada/datafetches and explicit refreshes.
Dependencies
Shared Store / Services
| Dependency | Import | Used For |
|---|---|---|
apiClient | @aquagen-mf-webapp/shared/services/api/apiClient | All GET/POST calls |
Urls | @aquagen-mf-webapp/shared/services/api/urls | scadaData, plcWriteRegisters, plcEventHubData |
AnalyticsService, AnalyticEvents | @aquagen-mf-webapp/shared/services, .../enums | UWI_REMOTE_CONTROL_OPENED event |
UwiDashboardController | @aquagen-mf-webapp/uwi/controller/uwiController | Remote-control logs |
useNavigateToUnit | @aquagen-mf-webapp/shared/hooks/useNavigateToUnit | Click-through navigation |
assets | @aquagen-mf-webapp/shared/assets/assets | Icons, colors |
Other Libs
- scadaEditor (tight coupling): the Viewer imports
nodeTypes,edgeTypes,ScadaNodeType, and node-style helpers (COMMON_META,getMetaValue,getCalculationValue,isOne,ScadaKeys) from@aquagen-mf-webapp/scadaEditor/*. The Editor authors diagrams; the Viewer renders them. See SCADA Editor. - components —
logical/If,date-picker/AppDatePicker,helper/Expanded. - uwi — controller for control logs; UWI Plant View embeds
ScadaPageViewer.
External npm Libraries
| Library | Version | Usage |
|---|---|---|
reactflow | ^11.11.4 | ReactFlow, ReactFlowProvider, Background, Controls, MiniMap, useReactFlow |
moment | (shared) | Date formatting for scada/data params |
@mui/material | — | Layout, dialogs, switches |
Verified:
three.js(^0.179.1) and@dnd-kit/*are present in the rootpackage.jsonbut are not imported anywhere inlibs/scadaViewer/. The Viewer's only diagram engine is ReactFlow.
Controllers / DataSources
ScadaController—libs/scadaViewer/src/controller/scadaController.js(writeRegisters,triggerEventHubRefresh).ScadaDataSource—libs/scadaViewer/src/dataSource/scadaDataSource.js(wrapsapiClient).
Visibility & Access Control
Route Registration
| App | /scadaViewer registered? | Reference |
|---|---|---|
production | Yes | apps/production/src/routes/routes.js |
demo | Yes | apps/demo/src/routes/routes.js |
rwi | No | — |
uwms | No | — |
lakepulse | No | — |
All five apps alias the scadaViewer lib in their rspack.config*.js/tsconfig.json, but only production and demo mount the route.
Permission Tag
- Permission:
SCADA_VIEWER—libs/shared/src/enums/permissions.js. - Nav mapping: route
/scadaViewerin the nav helper —navHelperInstance.js; listed under theEFFICIENCYsubmenu —navHelperInstance.js.
Sidebar Lock State
The sidebar entry uses access: SidebarAccess.STRICT_HIDDEN — navHelperInstance.js:
SCADA_VIEWER: {
permissionId: 'SCADA_VIEWER',
access: SidebarAccess.STRICT_HIDDEN,
displayName: 'Scada Viewer',
...
}
Per the SidebarAccess contract (navHelperInstance.js, enum at libs/shared/src/enums/sidebarAccess.js):
STRICT_HIDDEN — hide entirely if the user lacks the permission, no super-user bypass.
So the SCADA Viewer nav item is invisible to any user without an explicit SCADA_VIEWER permission — including super users. This mirrors the ignoreSuperUser pattern documented in Permissions: a SUPER_USER does not automatically gain SCADA access.
Super-User & Demo Notes
- Super user: does not unlock the SCADA Viewer sidebar entry (STRICT_HIDDEN). If you wanted a
PermissionWrapperaround the route element, the equivalent would be<PermissionWrapper tag={Permissions.allPermission.SCADA_VIEWER} ignoreSuperUser>. - Demo:
isDemoOption: false— the entry is not removed in demo builds; thedemoapp registers the route.
Related Documentation
- SCADA Editor — Authors the diagrams this Viewer renders (shared node/edge registries)
- HMI Interface — Related device monitoring & control surface
- Permissions —
SCADA_VIEWERgating,ignoreSuperUserand STRICT_HIDDEN semantics - Routes — Where
/scadaVieweris registered - Additional Features — High-level SCADA overview
Last Updated: July 2026
Module Location: libs/scadaViewer/src/