Skip to content

Commit 27c9d40

Browse files
committed
feat(workbench): converge IEC 61850 browser workflow
1 parent 9e70a49 commit 27c9d40

17 files changed

Lines changed: 1295 additions & 372 deletions

‎.github/workflows/ied-simulator-qt.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,19 @@ jobs:
132132
QT_QPA_PLATFORM: offscreen
133133
run: ./build-ied-simulator-qt/ied_product_hardening_qa
134134

135-
- name: IEDScout-style File/Home workflow contract
135+
- name: File/Home workflow contract
136136
run: >-
137137
python3 apps/ied_simulator/test_file_home_workflow.py
138138
apps/ied_simulator/qml/FileWorkspace.qml
139139
apps/ied_simulator/qml/Main.qml
140140
141-
- name: IEDScout-style Browser convergence contract
141+
- name: Browser convergence contract
142142
run: >-
143143
python3 apps/ied_simulator/test_browser_convergence.py
144144
apps/ied_simulator/qml/IedBrowserWorkspace.qml
145145
apps/ied_simulator/qml/IedBrowserNavigation.qml
146146
apps/ied_simulator/qml/BrowserStatusConsole.qml
147+
apps/ied_simulator/qml/BrowserDataModelPane.qml
147148
148149
- name: Canonical Browser service reuse contract
149150
run: >-

‎apps/ied_simulator/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ qt_add_qml_module(arstack_ied_simulator
102102
qml/IedBrowserWorkspace.qml
103103
qml/IedBrowserNavigation.qml
104104
qml/BrowserStatusConsole.qml
105+
qml/BrowserDataModelPane.qml
105106
qml/BrowserDataSetPane.qml
106107
qml/BrowserGoosePane.qml
107108
qml/BrowserGlobalDataPane.qml

‎apps/ied_simulator/design-qa.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# IED Browser workflow visual QA
2+
3+
Date: 2026-10-14
4+
Result: PASS
5+
6+
## Scope
7+
8+
The Browser presentation was compared against eight user-provided commercial-tool screenshots. The implementation reproduces the useful interaction model and information hierarchy without copying vendor branding, trademarks, icons, or packaged assets.
9+
10+
## Verified states
11+
12+
- Browser shell with a compact grouped ribbon for application, endpoint, session, data, and service actions.
13+
- Persistent dark engineering explorer with one active canonical IED context.
14+
- Data Model hierarchy with LD, LN, DO, and DA rows.
15+
- Dense right-pane Data Model projection with bounded direct-child rows, Back navigation, and double-click drill-down using the canonical tree.
16+
- DataSet overview-to-member drill-down with live values resolved from the same canonical model when available.
17+
- Reports inventory with URCB/BRCB identity, domain, logical node, selection, and contextual actions.
18+
- RCB detail as a dense property grid, with explicit engineering/runtime evidence state.
19+
- Received-report rows with bounded expandable value lists.
20+
- GOOSE overview and item drill-down with control-block, addressing, VLAN, timing, and DataSet-member details.
21+
- DataSets, Files, Setting Groups, Global Data, status history, and multi-IED tabs remain reachable.
22+
23+
## Intentional product differences
24+
25+
- ARStack identity and the existing teal/graphite design system are retained.
26+
- Multi-IED workspace tabs remain because they are an ARStack capability.
27+
- Unsupported or unavailable live values render as unknown (`—`); the UI does not invent false/default protocol state.
28+
- Engineering-model GOOSE evidence remains distinct from live Ethernet capture and publication.
29+
- Report activation remains gated by a live association; an offline SCL view does not pretend that an RCB is enabled.
30+
31+
## Evidence reviewed
32+
33+
- Side-by-side RCB detail comparison: hierarchy, grouped commands, property-grid density, report-data region, and status placement.
34+
- Side-by-side GOOSE detail comparison: explorer drill-down, breadcrumb, attribute table, and DataSet members.
35+
- Native rendered screenshots completed with no changed-pane QML runtime diagnostics on stderr; pre-existing diagnostics in unrelated dialogs/workspaces remain outside this change.
36+
- Final Data Model render retained as local QA evidence outside the repository.
37+
- The 60,106-node large-model gate remained within bounds after the presentation changes.
38+
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
import QtQuick
3+
import QtQuick.Controls
4+
import QtQuick.Layouts
5+
6+
Rectangle {
7+
id: root
8+
9+
required property var theme
10+
required property var client
11+
required property var modelProvider
12+
13+
readonly property var treeModel: modelProvider.treeModel
14+
readonly property var currentNode: treeModel.presentationNode
15+
property int highlightedRow: -1
16+
17+
color: theme.background
18+
19+
function text(value) {
20+
return value === undefined || value === null || String(value).length === 0
21+
? "—" : String(value)
22+
}
23+
24+
function drillInto(row) {
25+
if (!treeModel.selectChild(row))
26+
return
27+
highlightedRow = -1
28+
if (treeModel.presentationNode.hasChildren !== true
29+
&& client.connected && !client.operationBusy
30+
&& treeModel.presentationNode.readable === true)
31+
client.readEngineeringSelected()
32+
}
33+
34+
Connections {
35+
target: root.treeModel
36+
function onSelectionChanged() { root.highlightedRow = -1 }
37+
}
38+
39+
ColumnLayout {
40+
anchors.fill: parent
41+
spacing: 0
42+
43+
Rectangle {
44+
Layout.fillWidth: true
45+
Layout.preferredHeight: 30
46+
color: theme.surface
47+
border.width: 1
48+
border.color: theme.lineSoft
49+
50+
RowLayout {
51+
anchors.fill: parent
52+
anchors.leftMargin: 8
53+
anchors.rightMargin: 8
54+
spacing: 7
55+
56+
Button {
57+
objectName: "browserDataModelBack"
58+
visible: Boolean(root.currentNode.kind) && root.currentNode.kind !== "IED"
59+
implicitWidth: 24
60+
implicitHeight: 22
61+
text: "‹"
62+
onClicked: root.treeModel.selectParent()
63+
ToolTip.visible: hovered
64+
ToolTip.text: "Up one model level"
65+
}
66+
67+
Rectangle {
68+
implicitWidth: Math.max(24, nodeKind.implicitWidth + 10)
69+
implicitHeight: 18
70+
radius: 2
71+
color: theme.accent
72+
Label {
73+
id: nodeKind
74+
anchors.centerIn: parent
75+
text: root.currentNode.kind || "MODEL"
76+
color: "#ffffff"
77+
font.pixelSize: 7
78+
font.weight: Font.Bold
79+
}
80+
}
81+
82+
Label {
83+
Layout.fillWidth: true
84+
text: root.currentNode.label || "Data Model"
85+
color: theme.text
86+
font.pixelSize: 10
87+
font.weight: Font.DemiBold
88+
elide: Text.ElideMiddle
89+
}
90+
91+
Label {
92+
visible: root.treeModel.selectedChildCount > 0
93+
text: root.treeModel.selectedChildCount + " item(s)"
94+
color: theme.muted
95+
font.pixelSize: 8
96+
}
97+
}
98+
}
99+
100+
Rectangle {
101+
visible: root.treeModel.selectedChildCount > 0
102+
Layout.fillWidth: true
103+
Layout.preferredHeight: 25
104+
color: theme.chrome
105+
border.width: 1
106+
border.color: theme.lineSoft
107+
108+
RowLayout {
109+
anchors.fill: parent
110+
anchors.leftMargin: 8
111+
anchors.rightMargin: 8
112+
spacing: 8
113+
Label {
114+
Layout.fillWidth: true
115+
text: "Name"
116+
color: theme.textSoft
117+
font.pixelSize: 8
118+
font.weight: Font.DemiBold
119+
}
120+
Label {
121+
Layout.preferredWidth: Math.max(180, root.width * 0.42)
122+
text: "Value / description"
123+
color: theme.textSoft
124+
font.pixelSize: 8
125+
font.weight: Font.DemiBold
126+
}
127+
}
128+
}
129+
130+
ListView {
131+
id: childList
132+
objectName: "browserDataModelChildren"
133+
visible: root.treeModel.selectedChildCount > 0
134+
Layout.fillWidth: true
135+
Layout.fillHeight: true
136+
clip: true
137+
reuseItems: true
138+
cacheBuffer: 0
139+
model: root.treeModel.selectedChildren
140+
ScrollBar.vertical: ScrollBar {}
141+
142+
delegate: Rectangle {
143+
id: childRow
144+
required property int index
145+
required property var modelData
146+
width: childList.width
147+
height: 25
148+
color: root.highlightedRow === index ? theme.accent
149+
: childMouse.containsMouse ? theme.accentSoft
150+
: index % 2 ? theme.surfaceSoft : theme.surface
151+
border.width: 1
152+
border.color: theme.lineSoft
153+
154+
RowLayout {
155+
anchors.fill: parent
156+
anchors.leftMargin: 7
157+
anchors.rightMargin: 7
158+
spacing: 6
159+
160+
Label {
161+
Layout.preferredWidth: 10
162+
text: childRow.modelData.hasChildren === true ? "›" : ""
163+
color: root.highlightedRow === childRow.index ? "#ffffff" : theme.muted
164+
font.pixelSize: 9
165+
}
166+
Rectangle {
167+
Layout.preferredWidth: 20
168+
Layout.preferredHeight: 17
169+
radius: 2
170+
color: root.highlightedRow === childRow.index ? "#ffffff" : theme.accent
171+
Label {
172+
anchors.centerIn: parent
173+
text: childRow.modelData.kind || ""
174+
color: root.highlightedRow === childRow.index ? theme.accent : "#ffffff"
175+
font.pixelSize: 7
176+
font.weight: Font.Bold
177+
}
178+
}
179+
Label {
180+
Layout.fillWidth: true
181+
text: childRow.modelData.label || childRow.modelData.reference || "—"
182+
color: root.highlightedRow === childRow.index ? "#ffffff" : theme.text
183+
font.pixelSize: 9
184+
elide: Text.ElideMiddle
185+
}
186+
Label {
187+
visible: childRow.modelData.functionalConstraint
188+
&& String(childRow.modelData.functionalConstraint).length > 0
189+
text: "[" + childRow.modelData.functionalConstraint + "]"
190+
color: root.highlightedRow === childRow.index ? "#dff7f3" : theme.muted
191+
font.pixelSize: 8
192+
}
193+
Label {
194+
Layout.preferredWidth: Math.max(180, root.width * 0.42)
195+
text: childRow.modelData.value
196+
|| childRow.modelData.description
197+
|| (childRow.modelData.childCount > 0
198+
? childRow.modelData.childCount + " item(s)" : "—")
199+
color: root.highlightedRow === childRow.index ? "#ffffff" : theme.textSoft
200+
font.pixelSize: 9
201+
elide: Text.ElideRight
202+
}
203+
}
204+
205+
MouseArea {
206+
id: childMouse
207+
anchors.fill: parent
208+
hoverEnabled: true
209+
onClicked: root.highlightedRow = childRow.index
210+
onDoubleClicked: root.drillInto(childRow.index)
211+
}
212+
}
213+
}
214+
215+
ScrollView {
216+
visible: root.treeModel.selectedChildCount === 0
217+
Layout.fillWidth: true
218+
Layout.fillHeight: true
219+
contentWidth: availableWidth
220+
221+
ColumnLayout {
222+
width: Math.max(0, parent.width - 24)
223+
x: 12
224+
spacing: 0
225+
226+
Repeater {
227+
model: [
228+
{label: "Reference", value: root.currentNode.reference},
229+
{label: "Value", value: root.currentNode.value},
230+
{label: "Quality", value: root.currentNode.quality},
231+
{label: "Timestamp", value: root.currentNode.timestamp},
232+
{label: "Functional constraint", value: root.currentNode.functionalConstraint},
233+
{label: "MMS type", value: root.currentNode.mmsType},
234+
{label: "SCL type", value: root.currentNode.sclType},
235+
{label: "Type evidence", value: root.currentNode.typeStatus}
236+
]
237+
delegate: Rectangle {
238+
required property int index
239+
required property var modelData
240+
visible: modelData.value !== undefined && modelData.value !== null
241+
&& String(modelData.value).length > 0
242+
Layout.fillWidth: true
243+
Layout.preferredHeight: visible ? 27 : 0
244+
color: index % 2 ? theme.surfaceSoft : theme.surface
245+
border.width: 1
246+
border.color: theme.lineSoft
247+
RowLayout {
248+
anchors.fill: parent
249+
anchors.leftMargin: 8
250+
anchors.rightMargin: 8
251+
Label {
252+
Layout.preferredWidth: Math.max(150, root.width * 0.34)
253+
text: modelData.label
254+
color: theme.muted
255+
font.pixelSize: 8
256+
}
257+
Label {
258+
Layout.fillWidth: true
259+
text: root.text(modelData.value)
260+
color: theme.text
261+
font.pixelSize: 9
262+
elide: Text.ElideMiddle
263+
}
264+
}
265+
}
266+
}
267+
268+
Label {
269+
visible: !root.currentNode.kind
270+
Layout.fillWidth: true
271+
Layout.topMargin: 32
272+
text: "No canonical model is loaded."
273+
horizontalAlignment: Text.AlignHCenter
274+
color: theme.muted
275+
font.pixelSize: 10
276+
}
277+
Item { Layout.fillHeight: true }
278+
}
279+
}
280+
281+
Label {
282+
visible: root.treeModel.selectedChildrenTruncated
283+
Layout.fillWidth: true
284+
Layout.preferredHeight: 24
285+
text: "Showing the first 4096 direct children. Refine the canonical tree selection to continue."
286+
color: theme.amber
287+
font.pixelSize: 8
288+
horizontalAlignment: Text.AlignHCenter
289+
verticalAlignment: Text.AlignVCenter
290+
}
291+
}
292+
}

0 commit comments

Comments
 (0)