Skip to main content

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: ScadaViewerlibs/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: nodeTypes and edgeTypes are imported from the Editor (@aquagen-mf-webapp/scadaEditor/components/nodes and .../edges) — ScadaViewerComponent.jsx.
  • View-mode flag: each node is decorated with data.isViewMode = true and an onClick handler — ScadaViewerComponent.jsx.
  • Edge Condition nodes are hidden (hidden: isEdgeCondition) and instead drive their connected edges' animation via a conditionValueScadaViewerComponent.jsx.
  • Auto fit-view excludes REMOTE_CONTROL and EDGE_CONDITION nodes 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 handleDateChangeloadFromApi(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 ScadaViewerProviderlibs/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 ScadaViewerProvider then ReactFlowProvider.
  • 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, plus loadDiagram, 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.writeRegistersScadaDataSource.writeRegistersscadaController.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 CircularProgressScadaViewer.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 finallyScadaViewer.jsx.

Null / Missing Handling

  • nodes/edges fall back to [] when a page has none — ScadaViewerStore.js.
  • Node click is a no-op when data.isViewMode === false or unitId is 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 RemoteControlPanel is rendered outside a ScadaViewerProvider, 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 falseScadaViewerStore.js.

Note: The Viewer store has no subscription/isDemo gating of its own and no WebSocket — live values are pulled via date-scoped scada/data fetches and explicit refreshes.


Dependencies

Shared Store / Services

DependencyImportUsed For
apiClient@aquagen-mf-webapp/shared/services/api/apiClientAll GET/POST calls
Urls@aquagen-mf-webapp/shared/services/api/urlsscadaData, plcWriteRegisters, plcEventHubData
AnalyticsService, AnalyticEvents@aquagen-mf-webapp/shared/services, .../enumsUWI_REMOTE_CONTROL_OPENED event
UwiDashboardController@aquagen-mf-webapp/uwi/controller/uwiControllerRemote-control logs
useNavigateToUnit@aquagen-mf-webapp/shared/hooks/useNavigateToUnitClick-through navigation
assets@aquagen-mf-webapp/shared/assets/assetsIcons, 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.
  • componentslogical/If, date-picker/AppDatePicker, helper/Expanded.
  • uwi — controller for control logs; UWI Plant View embeds ScadaPageViewer.

External npm Libraries

LibraryVersionUsage
reactflow^11.11.4ReactFlow, ReactFlowProvider, Background, Controls, MiniMap, useReactFlow
moment(shared)Date formatting for scada/data params
@mui/materialLayout, dialogs, switches

Verified: three.js (^0.179.1) and @dnd-kit/* are present in the root package.json but are not imported anywhere in libs/scadaViewer/. The Viewer's only diagram engine is ReactFlow.

Controllers / DataSources

  • ScadaControllerlibs/scadaViewer/src/controller/scadaController.js (writeRegisters, triggerEventHubRefresh).
  • ScadaDataSourcelibs/scadaViewer/src/dataSource/scadaDataSource.js (wraps apiClient).

Visibility & Access Control

Route Registration

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

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_VIEWERlibs/shared/src/enums/permissions.js.
  • Nav mapping: route /scadaViewer in the nav helper — navHelperInstance.js; listed under the EFFICIENCY submenu — navHelperInstance.js.

The sidebar entry uses access: SidebarAccess.STRICT_HIDDENnavHelperInstance.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 PermissionWrapper around 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; the demo app registers the route.

  • SCADA Editor — Authors the diagrams this Viewer renders (shared node/edge registries)
  • HMI Interface — Related device monitoring & control surface
  • PermissionsSCADA_VIEWER gating, ignoreSuperUser and STRICT_HIDDEN semantics
  • Routes — Where /scadaViewer is registered
  • Additional Features — High-level SCADA overview

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