@typeonce/effect-machine-devtools 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/README.md +11 -11
- package/dist/DevToolsProtocol.d.ts +66 -630
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +0 -190
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +38 -18
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +14 -12
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +22 -2
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineWalkthrough.d.ts +171 -0
- package/dist/MachineWalkthrough.d.ts.map +1 -0
- package/dist/MachineWalkthrough.js +98 -0
- package/dist/MachineWalkthrough.js.map +1 -0
- package/dist/ProjectInspector.d.ts +0 -2
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-BTOAhezX.js +37 -0
- package/dist/client/assets/index-Cg7xzSkz.css +1 -0
- package/dist/client/index.html +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +4 -64
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +7 -263
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +3 -1
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/machineWalkthrough.d.ts +26 -0
- package/dist/internal/machineWalkthrough.d.ts.map +1 -0
- package/dist/internal/machineWalkthrough.js +256 -0
- package/dist/internal/machineWalkthrough.js.map +1 -0
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +1 -11
- package/dist/internal/projectInspector.js.map +1 -1
- package/index.html +2 -2
- package/package.json +4 -3
- package/src/DevToolsProtocol.ts +0 -285
- package/src/MachineDocument.ts +21 -19
- package/src/MachineWalkthrough.ts +199 -0
- package/src/ProjectInspector.ts +0 -5
- package/src/index.ts +2 -2
- package/src/internal/browser/chart-layout.ts +433 -0
- package/src/internal/browser/chart-model.ts +234 -0
- package/src/internal/browser/chart-renderer.ts +661 -0
- package/src/internal/browser/example-machine.ts +15 -6
- package/src/internal/browser/input-form.ts +1 -418
- package/src/internal/browser/invoke-outcomes-example.ts +231 -0
- package/src/internal/browser/parallel-completion-example.ts +199 -0
- package/src/internal/browser/planner-example.ts +2 -0
- package/src/internal/browser/protocol-events-example.ts +200 -0
- package/src/internal/browser/styles.css +795 -341
- package/src/internal/browser/transition-semantics-example.ts +236 -0
- package/src/internal/browser/visualizer-app.ts +781 -713
- package/src/internal/browser/visualizer-model.ts +104 -0
- package/src/internal/devServer.ts +4 -82
- package/src/internal/evaluationWorker.ts +7 -371
- package/src/internal/machineDocument.ts +3 -1
- package/src/internal/machineWalkthrough.ts +355 -0
- package/src/internal/projectInspector.ts +3 -31
- package/dist/MachineSimulator.d.ts +0 -169
- package/dist/MachineSimulator.d.ts.map +0 -1
- package/dist/MachineSimulator.js +0 -98
- package/dist/MachineSimulator.js.map +0 -1
- package/dist/client/assets/index-B49Tjawc.js +0 -14
- package/dist/client/assets/index-BKH0cOG2.css +0 -1
- package/dist/internal/machineSimulator.d.ts +0 -5
- package/dist/internal/machineSimulator.d.ts.map +0 -1
- package/dist/internal/machineSimulator.js +0 -156
- package/dist/internal/machineSimulator.js.map +0 -1
- package/src/MachineSimulator.ts +0 -154
- package/src/internal/browser/simulation-client.ts +0 -20
- package/src/internal/machineSimulator.ts +0 -199
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type SimulationFrame,
|
|
5
|
-
type SimulationReady,
|
|
6
|
-
type SimulationRequest
|
|
7
|
-
} from "../../DevToolsProtocol.js"
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Result from "effect/Result"
|
|
3
|
+
import type { Diagnostic } from "../../DevToolsProtocol.js"
|
|
8
4
|
import type {
|
|
9
5
|
Activity as VisualizationActivity,
|
|
10
6
|
Branch as VisualizationBranch,
|
|
7
|
+
InputSchema,
|
|
11
8
|
MachineDocument as VisualizationDocument,
|
|
12
|
-
Transition as VisualizationTransition
|
|
9
|
+
Transition as VisualizationTransition,
|
|
10
|
+
Trigger
|
|
13
11
|
} from "../../MachineDocument.js"
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
12
|
+
import * as MachineWalkthrough from "../../MachineWalkthrough.js"
|
|
13
|
+
import {
|
|
14
|
+
type ChartInteractionAnchor,
|
|
15
|
+
type ChartPresentation,
|
|
16
|
+
type ChartView,
|
|
17
|
+
maximumChartZoom,
|
|
18
|
+
minimumChartZoom,
|
|
19
|
+
renderChart
|
|
20
|
+
} from "./chart-renderer.js"
|
|
21
|
+
import { type InputField, projectInputSchema } from "./input-form.js"
|
|
16
22
|
import {
|
|
17
|
-
|
|
23
|
+
branchTargetApi,
|
|
18
24
|
type IncomingTransition,
|
|
19
25
|
makeVisualizerModel,
|
|
20
26
|
type StateInspection,
|
|
21
|
-
type TopologyNode,
|
|
22
27
|
triggerLabel
|
|
23
28
|
} from "./visualizer-model.js"
|
|
24
29
|
|
|
@@ -33,16 +38,33 @@ const createElement = <Tag extends keyof HTMLElementTagNameMap>(
|
|
|
33
38
|
return element
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
type MetadataValue = string | Node | null | undefined
|
|
42
|
+
|
|
43
|
+
const metadata = (items: ReadonlyArray<readonly [string, MetadataValue]>): HTMLDListElement => {
|
|
37
44
|
const list = createElement("dl", "metadata")
|
|
38
45
|
for (const [label, value] of items) {
|
|
39
|
-
|
|
46
|
+
if (value === null || value === undefined || value === "" || value === "none") continue
|
|
47
|
+
const description = createElement("dd")
|
|
48
|
+
description.append(value)
|
|
49
|
+
list.append(createElement("dt", undefined, label), description)
|
|
40
50
|
}
|
|
41
51
|
return list
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
const badge = (text: string, kind = "neutral"): HTMLSpanElement => createElement("span", `badge badge-${kind}`, text)
|
|
45
55
|
|
|
56
|
+
const stateStatus = (active: boolean, initial: boolean): HTMLSpanElement => {
|
|
57
|
+
const status = createElement(
|
|
58
|
+
"span",
|
|
59
|
+
`chart-state-status${active ? " is-active" : ""}${initial ? " is-initial" : ""}`
|
|
60
|
+
)
|
|
61
|
+
status.setAttribute(
|
|
62
|
+
"aria-label",
|
|
63
|
+
active && initial ? "active, initial state" : active ? "active" : initial ? "initial state" : "inactive"
|
|
64
|
+
)
|
|
65
|
+
return status
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
type StateNavigator = (path: string) => void
|
|
47
69
|
|
|
48
70
|
const stateLink = (path: string, label: string, navigate: StateNavigator): HTMLButtonElement => {
|
|
@@ -52,26 +74,35 @@ const stateLink = (path: string, label: string, navigate: StateNavigator): HTMLB
|
|
|
52
74
|
return link
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
const renderBranch = (
|
|
77
|
+
const renderBranch = (
|
|
78
|
+
document: VisualizationDocument,
|
|
79
|
+
source: string,
|
|
80
|
+
branch: VisualizationBranch,
|
|
81
|
+
navigate: StateNavigator
|
|
82
|
+
): HTMLElement => {
|
|
56
83
|
const row = createElement("div", "branch-row")
|
|
57
84
|
const main = createElement("div", "branch-main")
|
|
58
85
|
if (branch.type === "branch") main.append(badge(branch.title, "condition"))
|
|
59
|
-
main.append(createElement("span", "branch-arrow", "→"))
|
|
60
86
|
if (branch.target !== null) {
|
|
61
|
-
main.append(stateLink(branch.target, branch.target, navigate))
|
|
87
|
+
main.append(createElement("span", "branch-arrow", "→"), stateLink(branch.target, branch.target, navigate))
|
|
88
|
+
} else if (branch.selection.kind === "update") {
|
|
89
|
+
main.append(
|
|
90
|
+
badge(branch.selection.scope === "local" ? "to.local.update" : "value update", "update"),
|
|
91
|
+
createElement("span", "branch-target", "Updates the owner value")
|
|
92
|
+
)
|
|
62
93
|
} else {
|
|
63
|
-
main.append(createElement("span", "branch-target",
|
|
94
|
+
main.append(createElement("span", "branch-target", "No target state"))
|
|
64
95
|
}
|
|
65
96
|
row.append(main)
|
|
66
97
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
["
|
|
70
|
-
|
|
98
|
+
const api = branchTargetApi(document, source, branch)
|
|
99
|
+
const details: Array<readonly [string, MetadataValue]> = api === undefined
|
|
100
|
+
? [["Selection", branch.selection.kind], ["Scope", branch.selection.scope]]
|
|
101
|
+
: [["API", api]]
|
|
71
102
|
row.append(metadata(details))
|
|
72
103
|
if (branch.updates.length > 0) {
|
|
73
104
|
const updates = createElement("div", "branch-updates")
|
|
74
|
-
updates.append(createElement("span", "branch-updates-label", "
|
|
105
|
+
updates.append(createElement("span", "branch-updates-label", "Value owner"))
|
|
75
106
|
branch.updates.forEach((path) => updates.append(stateLink(path, path, navigate)))
|
|
76
107
|
row.append(updates)
|
|
77
108
|
}
|
|
@@ -79,6 +110,7 @@ const renderBranch = (branch: VisualizationBranch, navigate: StateNavigator): HT
|
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
const renderTransition = (
|
|
113
|
+
document: VisualizationDocument,
|
|
82
114
|
transition: VisualizationTransition,
|
|
83
115
|
navigate: StateNavigator,
|
|
84
116
|
showSource = false
|
|
@@ -86,44 +118,52 @@ const renderTransition = (
|
|
|
86
118
|
const card = createElement("article", "inspection-card transition-card")
|
|
87
119
|
const header = createElement("div", "card-header")
|
|
88
120
|
const title = createElement("div", "card-title")
|
|
89
|
-
title.append(
|
|
121
|
+
title.append(createElement("strong", undefined, triggerLabel(transition)))
|
|
90
122
|
const flags = createElement("div", "card-flags")
|
|
123
|
+
flags.append(badge(transition.trigger.type, "trigger"))
|
|
91
124
|
if (transition.reenter) flags.append(badge("reenter"))
|
|
92
125
|
if (transition.acceptance === "declinable") flags.append(badge("declinable"))
|
|
93
126
|
header.append(title, flags)
|
|
94
127
|
card.append(header)
|
|
95
128
|
if (showSource) {
|
|
96
|
-
const source =
|
|
97
|
-
source.
|
|
129
|
+
const source = metadata([["Source", stateLink(transition.source, transition.source, navigate)]])
|
|
130
|
+
source.classList.add("card-metadata")
|
|
98
131
|
card.append(source)
|
|
99
132
|
}
|
|
100
133
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
branches.
|
|
104
|
-
|
|
105
|
-
|
|
134
|
+
if (transition.branches.length > 0) {
|
|
135
|
+
const branches = createElement("div", "branch-list")
|
|
136
|
+
transition.branches.forEach((branch) =>
|
|
137
|
+
branches.append(renderBranch(document, transition.source, branch, navigate))
|
|
138
|
+
)
|
|
139
|
+
card.append(branches)
|
|
106
140
|
}
|
|
107
|
-
card.append(branches)
|
|
108
141
|
return card
|
|
109
142
|
}
|
|
110
143
|
|
|
111
|
-
const renderIncomingTransition = (
|
|
144
|
+
const renderIncomingTransition = (
|
|
145
|
+
document: VisualizationDocument,
|
|
146
|
+
incoming: IncomingTransition,
|
|
147
|
+
navigate: StateNavigator
|
|
148
|
+
): HTMLElement => {
|
|
112
149
|
const card = createElement("article", "inspection-card incoming-card")
|
|
113
150
|
const header = createElement("div", "card-header")
|
|
114
151
|
const title = createElement("div", "card-title")
|
|
115
|
-
title.append(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
header.append(title, stateLink(incoming.transition.source, incoming.transition.source, navigate))
|
|
152
|
+
title.append(createElement("strong", undefined, triggerLabel(incoming.transition)))
|
|
153
|
+
const flags = createElement("div", "card-flags")
|
|
154
|
+
flags.append(badge(incoming.transition.trigger.type, "trigger"))
|
|
155
|
+
header.append(title, flags)
|
|
120
156
|
card.append(header)
|
|
121
157
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
["
|
|
158
|
+
const api = branchTargetApi(document, incoming.transition.source, incoming.branch)
|
|
159
|
+
const details: Array<readonly [string, MetadataValue]> = [
|
|
160
|
+
["Source", stateLink(incoming.transition.source, incoming.transition.source, navigate)],
|
|
161
|
+
["API", api]
|
|
125
162
|
]
|
|
126
|
-
if (
|
|
163
|
+
if (api === undefined) {
|
|
164
|
+
details.push(["Selection", incoming.branch.selection.kind], ["Scope", incoming.branch.selection.scope])
|
|
165
|
+
}
|
|
166
|
+
if (incoming.branch.type === "branch") details.unshift(["Title", incoming.branch.title])
|
|
127
167
|
card.append(metadata(details))
|
|
128
168
|
return card
|
|
129
169
|
}
|
|
@@ -140,15 +180,19 @@ const activityTitle = (activity: VisualizationActivity): string => {
|
|
|
140
180
|
}
|
|
141
181
|
}
|
|
142
182
|
|
|
143
|
-
const renderActivity = (activity: VisualizationActivity): HTMLElement => {
|
|
183
|
+
const renderActivity = (activity: VisualizationActivity, navigate: StateNavigator): HTMLElement => {
|
|
144
184
|
const card = createElement("article", "inspection-card activity-card")
|
|
145
185
|
const header = createElement("div", "card-header")
|
|
146
186
|
const title = createElement("div", "card-title")
|
|
147
|
-
title.append(
|
|
148
|
-
|
|
187
|
+
title.append(createElement("strong", undefined, activityTitle(activity)))
|
|
188
|
+
const flags = createElement("div", "card-flags")
|
|
189
|
+
flags.append(badge(activity.type, "activity"))
|
|
190
|
+
header.append(title, flags)
|
|
149
191
|
card.append(header)
|
|
150
192
|
|
|
151
|
-
const details: Array<readonly [string,
|
|
193
|
+
const details: Array<readonly [string, MetadataValue]> = [
|
|
194
|
+
["Owner", stateLink(activity.source, activity.source, navigate)]
|
|
195
|
+
]
|
|
152
196
|
if (activity.type === "timer") details.push(["Duration", activity.duration])
|
|
153
197
|
if (activity.type === "effect") {
|
|
154
198
|
details.push(["Success", activity.outcomes.success], ["Failure", activity.outcomes.failure])
|
|
@@ -160,24 +204,183 @@ const renderActivity = (activity: VisualizationActivity): HTMLElement => {
|
|
|
160
204
|
return card
|
|
161
205
|
}
|
|
162
206
|
|
|
163
|
-
const inspectionSection = (title: string, count
|
|
207
|
+
const inspectionSection = (title: string, count?: number): HTMLElement => {
|
|
164
208
|
const header = createElement("div", "section-heading")
|
|
165
|
-
header.append(createElement("h3", undefined, title)
|
|
209
|
+
header.append(createElement("h3", undefined, title))
|
|
210
|
+
if (count !== undefined) header.append(createElement("span", "section-count", String(count)))
|
|
166
211
|
return header
|
|
167
212
|
}
|
|
168
213
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
const inputType = (field: InputField): string => {
|
|
215
|
+
switch (field._tag) {
|
|
216
|
+
case "String":
|
|
217
|
+
return field.format ?? "string"
|
|
218
|
+
case "Number":
|
|
219
|
+
return field.integer ? "integer" : "number"
|
|
220
|
+
case "Boolean":
|
|
221
|
+
return "boolean"
|
|
222
|
+
case "Enum":
|
|
223
|
+
return "enum"
|
|
224
|
+
case "Literal":
|
|
225
|
+
return "literal"
|
|
226
|
+
case "Object":
|
|
227
|
+
return "object"
|
|
228
|
+
case "Array":
|
|
229
|
+
return `${inputType(field.item)}[]`
|
|
230
|
+
case "Union":
|
|
231
|
+
return "union"
|
|
232
|
+
case "Unsupported":
|
|
233
|
+
return "unknown"
|
|
234
|
+
}
|
|
173
235
|
}
|
|
174
236
|
|
|
175
|
-
const
|
|
237
|
+
const inputConstraints = (field: InputField): ReadonlyArray<string> => {
|
|
238
|
+
switch (field._tag) {
|
|
239
|
+
case "String":
|
|
240
|
+
return [
|
|
241
|
+
field.minLength === undefined ? undefined : `min ${field.minLength}`,
|
|
242
|
+
field.maxLength === undefined ? undefined : `max ${field.maxLength}`,
|
|
243
|
+
field.pattern === undefined ? undefined : `pattern ${field.pattern}`
|
|
244
|
+
].filter((value): value is string => value !== undefined)
|
|
245
|
+
case "Number":
|
|
246
|
+
return [
|
|
247
|
+
field.minimum === undefined ? undefined : `≥ ${field.minimum}`,
|
|
248
|
+
field.maximum === undefined ? undefined : `≤ ${field.maximum}`
|
|
249
|
+
].filter((value): value is string => value !== undefined)
|
|
250
|
+
case "Enum":
|
|
251
|
+
return field.values.map((value) => JSON.stringify(value))
|
|
252
|
+
case "Literal":
|
|
253
|
+
return [JSON.stringify(field.value)]
|
|
254
|
+
case "Array":
|
|
255
|
+
return [
|
|
256
|
+
field.minItems > 0 ? `min ${field.minItems}` : undefined,
|
|
257
|
+
field.maxItems === undefined ? undefined : `max ${field.maxItems}`
|
|
258
|
+
].filter((value): value is string => value !== undefined)
|
|
259
|
+
default:
|
|
260
|
+
return []
|
|
261
|
+
}
|
|
262
|
+
}
|
|
176
263
|
|
|
177
|
-
const
|
|
264
|
+
const renderContractField = (
|
|
265
|
+
label: string,
|
|
266
|
+
required: boolean,
|
|
267
|
+
field: InputField,
|
|
268
|
+
omit: ReadonlySet<string>
|
|
269
|
+
): HTMLElement | null => {
|
|
270
|
+
if (omit.has(label)) return null
|
|
271
|
+
const row = createElement("div", "input-contract-field")
|
|
272
|
+
const heading = createElement("div", "input-field-heading")
|
|
273
|
+
const identity = createElement("div", "input-field-identity")
|
|
274
|
+
identity.append(
|
|
275
|
+
createElement("span", "input-label", label),
|
|
276
|
+
createElement("span", "input-field-type", inputType(field)),
|
|
277
|
+
createElement("span", required ? "input-required" : "input-optional", required ? "required" : "optional")
|
|
278
|
+
)
|
|
279
|
+
heading.append(identity)
|
|
280
|
+
row.append(heading)
|
|
281
|
+
const constraints = inputConstraints(field)
|
|
282
|
+
if (constraints.length > 0) {
|
|
283
|
+
const values = createElement("div", "input-constraints")
|
|
284
|
+
constraints.forEach((value) => values.append(createElement("span", undefined, value)))
|
|
285
|
+
row.append(values)
|
|
286
|
+
}
|
|
287
|
+
if (field.description !== undefined) row.append(createElement("p", "input-description", field.description))
|
|
288
|
+
if (field._tag === "Object") {
|
|
289
|
+
const children = createElement("div", "input-contract-children")
|
|
290
|
+
field.fields.forEach(({ key, required, field }) => {
|
|
291
|
+
const child = renderContractField(key, required, field, omit)
|
|
292
|
+
if (child !== null) children.append(child)
|
|
293
|
+
})
|
|
294
|
+
if (children.childElementCount > 0) row.append(children)
|
|
295
|
+
} else if (field._tag === "Array" && field.item._tag === "Object") {
|
|
296
|
+
const children = createElement("div", "input-contract-children")
|
|
297
|
+
field.item.fields.forEach(({ key, required, field }) => {
|
|
298
|
+
const child = renderContractField(key, required, field, omit)
|
|
299
|
+
if (child !== null) children.append(child)
|
|
300
|
+
})
|
|
301
|
+
if (children.childElementCount > 0) row.append(children)
|
|
302
|
+
} else if (field._tag === "Union") {
|
|
303
|
+
const alternatives = createElement("div", "input-contract-alternatives")
|
|
304
|
+
field.alternatives.forEach((alternative, index) => {
|
|
305
|
+
const item = renderContractField(`Option ${index + 1}`, true, alternative, omit)
|
|
306
|
+
if (item !== null) alternatives.append(item)
|
|
307
|
+
})
|
|
308
|
+
row.append(alternatives)
|
|
309
|
+
} else if (field._tag === "Unsupported") {
|
|
310
|
+
row.append(createElement("p", "input-unsupported", field.reason))
|
|
311
|
+
}
|
|
312
|
+
return row
|
|
313
|
+
}
|
|
178
314
|
|
|
179
|
-
const
|
|
180
|
-
|
|
315
|
+
const renderInputContract = (
|
|
316
|
+
title: string,
|
|
317
|
+
schema: InputSchema,
|
|
318
|
+
omit: ReadonlyArray<string> = []
|
|
319
|
+
): HTMLElement => {
|
|
320
|
+
const section = createElement("section", "inspector-section input-contract")
|
|
321
|
+
section.append(inspectionSection(title))
|
|
322
|
+
const projected = projectInputSchema(schema)
|
|
323
|
+
const fields = createElement("div", "input-contract-fields")
|
|
324
|
+
const omitted = new Set(omit)
|
|
325
|
+
if (projected._tag === "Object") {
|
|
326
|
+
projected.fields.forEach(({ key, required, field }) => {
|
|
327
|
+
const child = renderContractField(key, required, field, omitted)
|
|
328
|
+
if (child !== null) fields.append(child)
|
|
329
|
+
})
|
|
330
|
+
} else {
|
|
331
|
+
const child = renderContractField("value", true, projected, omitted)
|
|
332
|
+
if (child !== null) fields.append(child)
|
|
333
|
+
}
|
|
334
|
+
if (fields.childElementCount === 0) {
|
|
335
|
+
fields.append(createElement("p", "section-empty", "No additional data fields."))
|
|
336
|
+
}
|
|
337
|
+
section.append(fields)
|
|
338
|
+
return section
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const contractSummary = (schema: InputSchema | null): string | null => {
|
|
342
|
+
if (schema === null) return null
|
|
343
|
+
const field = projectInputSchema(schema)
|
|
344
|
+
if (field._tag !== "Object") return inputType(field)
|
|
345
|
+
const fields = field.fields.filter(({ key }) => key !== "_tag")
|
|
346
|
+
if (fields.length === 0) return null
|
|
347
|
+
return fields.slice(0, 3).map(({ key, required, field }) => `${key}${required ? "" : "?"}: ${inputType(field)}`).join(
|
|
348
|
+
" · "
|
|
349
|
+
) + (fields.length > 3 ? ` · +${fields.length - 3}` : "")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const triggerName = (trigger: Trigger): string => {
|
|
353
|
+
switch (trigger.type) {
|
|
354
|
+
case "event":
|
|
355
|
+
return trigger.event
|
|
356
|
+
case "always":
|
|
357
|
+
return "Always"
|
|
358
|
+
case "done":
|
|
359
|
+
return "Completion"
|
|
360
|
+
case "choice":
|
|
361
|
+
return "Choice"
|
|
362
|
+
case "invoke":
|
|
363
|
+
return `${trigger.id} · ${trigger.outcome}`
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const decisionLabel = (decision: MachineWalkthrough.Decision): string => {
|
|
368
|
+
switch (decision) {
|
|
369
|
+
case "conditional-branch":
|
|
370
|
+
return "Choose branch"
|
|
371
|
+
case "declinable-transition":
|
|
372
|
+
return "Assume accepted"
|
|
373
|
+
case "automatic-trigger":
|
|
374
|
+
return "Advance automatic trigger"
|
|
375
|
+
case "invoke-outcome":
|
|
376
|
+
return "Choose invoke outcome"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const unavailableLabel = (reason: MachineWalkthrough.UnavailableReason): string =>
|
|
381
|
+
reason === "history-unavailable"
|
|
382
|
+
? "No history has been recorded for this target yet"
|
|
383
|
+
: "The target is resolved only at runtime"
|
|
181
384
|
|
|
182
385
|
const activeTopology = (paths: ReadonlyArray<string>): string => {
|
|
183
386
|
const leaves = paths.filter((path) => !paths.some((candidate) => candidate.startsWith(`${path}.`)))
|
|
@@ -186,68 +389,85 @@ const activeTopology = (paths: ReadonlyArray<string>): string => {
|
|
|
186
389
|
|
|
187
390
|
export const renderVisualizer = (
|
|
188
391
|
root: HTMLElement,
|
|
189
|
-
|
|
392
|
+
_machineKey: string,
|
|
190
393
|
visualization: VisualizationDocument,
|
|
191
394
|
diagnostics: ReadonlyArray<Diagnostic> = []
|
|
192
395
|
): void => {
|
|
193
396
|
const model = makeVisualizerModel(visualization)
|
|
194
|
-
const
|
|
195
|
-
const nodes = new Map<string, HTMLElement>()
|
|
196
|
-
const statuses = new Map<string, HTMLElement>()
|
|
197
|
-
const eventButtons = new Map<string, HTMLElement>()
|
|
397
|
+
const transitionsById = new Map(visualization.transitions.map((transition) => [transition.id, transition]))
|
|
198
398
|
const eventSchemas = new Map(visualization.inputs.events.map(({ event, schema }) => [event, schema]))
|
|
199
|
-
const
|
|
399
|
+
const relatedFrom = new Set<string>()
|
|
400
|
+
const relatedTo = new Set<string>()
|
|
401
|
+
const incomingTransitions = new Set<string>()
|
|
402
|
+
const outgoingTransitions = new Set<string>()
|
|
200
403
|
let selectedPath: string | undefined
|
|
201
|
-
let
|
|
202
|
-
let selectedFrame:
|
|
203
|
-
let
|
|
204
|
-
let
|
|
205
|
-
let activeInputForm: InputForm | undefined
|
|
404
|
+
let selectedTransition: string | undefined
|
|
405
|
+
let selectedFrame: MachineWalkthrough.Frame | undefined
|
|
406
|
+
let walkthrough: MachineWalkthrough.Session | undefined
|
|
407
|
+
let chartView: ChartView | undefined
|
|
206
408
|
|
|
207
|
-
const activePaths = (): ReadonlyArray<string> =>
|
|
208
|
-
|
|
409
|
+
const activePaths = (): ReadonlyArray<string> =>
|
|
410
|
+
walkthrough === undefined ? model.activePaths : MachineWalkthrough.current(walkthrough).after.activePaths
|
|
411
|
+
const availableChoices = (): ReadonlyArray<MachineWalkthrough.Choice> =>
|
|
412
|
+
walkthrough === undefined ? [] : MachineWalkthrough.choices(walkthrough)
|
|
209
413
|
|
|
210
414
|
const shell = createElement("main", "app-shell")
|
|
211
415
|
const workspace = createElement("section", "workspace")
|
|
212
|
-
const
|
|
213
|
-
|
|
416
|
+
const chartPanel = createElement("section", "chart-panel")
|
|
417
|
+
chartPanel.setAttribute("aria-label", `${model.machineId} topology`)
|
|
214
418
|
const inspector = createElement("aside", "inspector")
|
|
215
419
|
inspector.setAttribute("aria-live", "polite")
|
|
420
|
+
inspector.setAttribute("aria-label", "Selection details")
|
|
421
|
+
inspector.hidden = true
|
|
422
|
+
const inspectorClose = createElement("button", "inspector-close", "Close")
|
|
423
|
+
inspectorClose.type = "button"
|
|
424
|
+
inspectorClose.setAttribute("aria-label", "Close details")
|
|
425
|
+
const inspectorContent = createElement("div", "inspector-content")
|
|
426
|
+
inspector.append(inspectorClose, inspectorContent)
|
|
427
|
+
const choicePicker = createElement("section", "transition-picker")
|
|
428
|
+
choicePicker.setAttribute("role", "dialog")
|
|
429
|
+
choicePicker.setAttribute("aria-label", "Available transitions")
|
|
430
|
+
choicePicker.hidden = true
|
|
431
|
+
const choicePickerHeader = createElement("div", "transition-picker-header")
|
|
432
|
+
const choicePickerTitle = createElement("strong")
|
|
433
|
+
const choicePickerClose = createElement("button", "transition-picker-close", "Close")
|
|
434
|
+
choicePickerClose.type = "button"
|
|
435
|
+
const choicePickerContent = createElement("div", "transition-picker-content")
|
|
436
|
+
choicePickerHeader.append(choicePickerTitle, choicePickerClose)
|
|
437
|
+
choicePicker.append(choicePickerHeader, choicePickerContent)
|
|
216
438
|
|
|
217
439
|
const clearButton = createElement("button", "toolbar-button", "Clear selection")
|
|
218
440
|
clearButton.type = "button"
|
|
219
441
|
clearButton.disabled = true
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
collapseButton.type = "button"
|
|
442
|
+
const detailsButton = createElement("button", "toolbar-button", "View details")
|
|
443
|
+
detailsButton.type = "button"
|
|
444
|
+
detailsButton.disabled = true
|
|
224
445
|
const revealActiveButton = createElement("button", "toolbar-button", "Reveal active")
|
|
225
446
|
revealActiveButton.type = "button"
|
|
226
447
|
revealActiveButton.disabled = model.activePaths.length === 0
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
inspector.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
inspector.append(summary)
|
|
448
|
+
const walkthroughButton = createElement("button", "toolbar-button", "Start simulation")
|
|
449
|
+
walkthroughButton.type = "button"
|
|
450
|
+
walkthroughButton.disabled = model.roots.length === 0
|
|
451
|
+
|
|
452
|
+
const hideInspector = (): void => {
|
|
453
|
+
inspectorContent.replaceChildren()
|
|
454
|
+
inspector.hidden = true
|
|
455
|
+
detailsButton.textContent = "View details"
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const showInspector = (): void => {
|
|
459
|
+
inspector.hidden = false
|
|
460
|
+
detailsButton.textContent = "Hide details"
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const closeChoicePicker = (): void => {
|
|
464
|
+
choicePicker.hidden = true
|
|
465
|
+
choicePickerContent.replaceChildren()
|
|
246
466
|
}
|
|
247
467
|
|
|
248
468
|
const renderInspection = (inspection: StateInspection): void => {
|
|
249
|
-
|
|
250
|
-
|
|
469
|
+
inspectorContent.replaceChildren()
|
|
470
|
+
showInspector()
|
|
251
471
|
const header = createElement("header", "inspector-header")
|
|
252
472
|
const breadcrumbs = createElement("nav", "breadcrumbs")
|
|
253
473
|
breadcrumbs.setAttribute("aria-label", "State path")
|
|
@@ -255,17 +475,20 @@ export const renderVisualizer = (
|
|
|
255
475
|
if (index > 0) breadcrumbs.append(createElement("span", "breadcrumb-separator", "/"))
|
|
256
476
|
breadcrumbs.append(stateLink(item.path, item.label, navigateToState))
|
|
257
477
|
})
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
478
|
+
const titleRow = createElement("div", "inspector-title-row")
|
|
479
|
+
const title = createElement("div", "inspector-state-title")
|
|
480
|
+
title.append(
|
|
481
|
+
stateStatus(activePaths().includes(inspection.state.path), inspection.initial),
|
|
482
|
+
createElement("h2", undefined, inspection.label)
|
|
483
|
+
)
|
|
484
|
+
titleRow.append(title, badge(inspection.state.type, "state"))
|
|
485
|
+
header.append(breadcrumbs, titleRow)
|
|
263
486
|
header.append(metadata([
|
|
264
487
|
["Path", inspection.state.path],
|
|
265
|
-
["Parent", inspection.state.parent
|
|
266
|
-
["Children", String(inspection.state.children.length)],
|
|
267
|
-
["Initial child", inspection.state.initial
|
|
268
|
-
["History", inspection.state.history
|
|
488
|
+
["Parent", inspection.state.parent],
|
|
489
|
+
["Children", inspection.state.children.length > 0 ? String(inspection.state.children.length) : null],
|
|
490
|
+
["Initial child", inspection.state.initial],
|
|
491
|
+
["History", inspection.state.history]
|
|
269
492
|
]))
|
|
270
493
|
if (inspection.state.description !== null || inspection.state.documentation !== null) {
|
|
271
494
|
const annotations = createElement("div", "state-annotations")
|
|
@@ -277,401 +500,200 @@ export const renderVisualizer = (
|
|
|
277
500
|
}
|
|
278
501
|
header.append(annotations)
|
|
279
502
|
}
|
|
280
|
-
|
|
503
|
+
inspectorContent.append(header)
|
|
281
504
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
505
|
+
if (inspection.outgoing.length > 0) {
|
|
506
|
+
const transitions = createElement("section", "inspector-section")
|
|
507
|
+
transitions.append(inspectionSection("Transitions", inspection.outgoing.length))
|
|
508
|
+
inspection.outgoing.forEach((transition) =>
|
|
509
|
+
transitions.append(renderTransition(visualization, transition, navigateToState))
|
|
510
|
+
)
|
|
511
|
+
inspectorContent.append(transitions)
|
|
288
512
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
incoming.append(inspectionSection("Entered by", inspection.incoming.length))
|
|
293
|
-
if (inspection.incoming.length === 0) {
|
|
294
|
-
incoming.append(createElement("p", "section-empty", "No transitions target this state."))
|
|
295
|
-
} else {
|
|
513
|
+
if (inspection.incoming.length > 0) {
|
|
514
|
+
const incoming = createElement("section", "inspector-section")
|
|
515
|
+
incoming.append(inspectionSection("Entered by", inspection.incoming.length))
|
|
296
516
|
inspection.incoming.forEach((transition) =>
|
|
297
|
-
incoming.append(renderIncomingTransition(transition, navigateToState))
|
|
517
|
+
incoming.append(renderIncomingTransition(visualization, transition, navigateToState))
|
|
298
518
|
)
|
|
519
|
+
inspectorContent.append(incoming)
|
|
299
520
|
}
|
|
300
|
-
inspector.append(incoming)
|
|
301
|
-
|
|
302
521
|
if (inspection.activities.length > 0) {
|
|
303
522
|
const activities = createElement("section", "inspector-section")
|
|
304
|
-
activities.append(inspectionSection("
|
|
305
|
-
inspection.activities.forEach((activity) => activities.append(renderActivity(activity)))
|
|
306
|
-
|
|
523
|
+
activities.append(inspectionSection("Invoked", inspection.activities.length))
|
|
524
|
+
inspection.activities.forEach((activity) => activities.append(renderActivity(activity, navigateToState)))
|
|
525
|
+
inspectorContent.append(activities)
|
|
307
526
|
}
|
|
308
527
|
}
|
|
309
528
|
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
529
|
+
const renderTransitionInspection = (transition: VisualizationTransition): void => {
|
|
530
|
+
inspectorContent.replaceChildren()
|
|
531
|
+
showInspector()
|
|
313
532
|
const header = createElement("header", "inspector-header")
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
533
|
+
const titleRow = createElement("div", "inspector-title-row")
|
|
534
|
+
const flags = createElement("div", "card-flags")
|
|
535
|
+
flags.append(badge(transition.trigger.type, "trigger"))
|
|
536
|
+
if (transition.reenter) flags.append(badge("reenter"))
|
|
537
|
+
if (transition.acceptance === "declinable") flags.append(badge("declinable"))
|
|
538
|
+
titleRow.append(createElement("h2", undefined, triggerLabel(transition)), flags)
|
|
539
|
+
header.append(titleRow)
|
|
319
540
|
header.append(metadata([
|
|
320
|
-
["
|
|
321
|
-
["
|
|
541
|
+
["Source", transition.source],
|
|
542
|
+
["Branches", transition.branches.length > 1 ? String(transition.branches.length) : null],
|
|
543
|
+
["Acceptance", transition.acceptance === "declinable" ? transition.acceptance : null],
|
|
544
|
+
["Reenter", transition.reenter ? "yes" : null]
|
|
322
545
|
]))
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
name: inspection.event,
|
|
338
|
-
fixed: { _tag: inspection.event },
|
|
339
|
-
omit: ["_tag"]
|
|
340
|
-
})
|
|
341
|
-
activeInputForm = input
|
|
342
|
-
const send = createElement("button", "simulation-action", `Send ${inspection.event}`)
|
|
343
|
-
send.type = "submit"
|
|
344
|
-
send.disabled = simulationPending || !input.supported
|
|
345
|
-
input.element.addEventListener("submit", (event) => {
|
|
346
|
-
event.preventDefault()
|
|
347
|
-
const result = input.read()
|
|
348
|
-
const source = visualization.source
|
|
349
|
-
if (!result.ok || source === null || simulation === undefined) return
|
|
350
|
-
void runSimulation({
|
|
351
|
-
_tag: "SendSimulationEvent",
|
|
352
|
-
protocolVersion,
|
|
353
|
-
key: machineKey,
|
|
354
|
-
revision: visualization.revision,
|
|
355
|
-
source,
|
|
356
|
-
step: simulation.step,
|
|
357
|
-
snapshot: simulation.snapshot,
|
|
358
|
-
event: result.value as never
|
|
359
|
-
})
|
|
360
|
-
})
|
|
361
|
-
input.element.append(send)
|
|
362
|
-
composer.append(
|
|
363
|
-
input.element,
|
|
364
|
-
createElement(
|
|
365
|
-
"p",
|
|
366
|
-
"simulation-note",
|
|
367
|
-
"The real planner validates this event and shows every synchronous transition it selects."
|
|
368
|
-
)
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
inspector.append(composer)
|
|
546
|
+
inspectorContent.append(header)
|
|
547
|
+
if (transition.trigger.type === "event") {
|
|
548
|
+
const schema = eventSchemas.get(transition.trigger.event)
|
|
549
|
+
if (schema !== undefined) inspectorContent.append(renderInputContract("Data contract", schema, ["_tag"]))
|
|
550
|
+
}
|
|
551
|
+
if (transition.branches.length > 0) {
|
|
552
|
+
const details = createElement("section", "inspector-section")
|
|
553
|
+
details.append(inspectionSection("Branches", transition.branches.length))
|
|
554
|
+
const branches = createElement("div", "inspection-card branch-list")
|
|
555
|
+
transition.branches.forEach((branch) =>
|
|
556
|
+
branches.append(renderBranch(visualization, transition.source, branch, navigateToState))
|
|
557
|
+
)
|
|
558
|
+
details.append(branches)
|
|
559
|
+
inspectorContent.append(details)
|
|
372
560
|
}
|
|
373
|
-
|
|
374
|
-
const transitions = createElement("section", "inspector-section")
|
|
375
|
-
transitions.append(inspectionSection("Transitions", inspection.transitions.length))
|
|
376
|
-
inspection.transitions.forEach((transition) =>
|
|
377
|
-
transitions.append(renderTransition(transition, navigateToState, true))
|
|
378
|
-
)
|
|
379
|
-
inspector.append(transitions)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const renderSimulationFailure = (failureDiagnostics: ReadonlyArray<Diagnostic>): void => {
|
|
383
|
-
activeInputForm = undefined
|
|
384
|
-
inspector.replaceChildren()
|
|
385
|
-
const header = createElement("header", "inspector-header")
|
|
386
|
-
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
387
|
-
eyebrow.append(badge("planner error", "error"))
|
|
388
|
-
header.append(eyebrow, createElement("h2", undefined, "Simulation could not continue"))
|
|
389
|
-
inspector.append(header)
|
|
390
|
-
const list = createElement("section", "inspector-section trace-list")
|
|
391
|
-
failureDiagnostics.forEach((item) => {
|
|
392
|
-
const card = createElement("article", "inspection-card trace-card")
|
|
393
|
-
card.append(createElement("strong", undefined, item.code), createElement("pre", "trace-error", item.message))
|
|
394
|
-
list.append(card)
|
|
395
|
-
})
|
|
396
|
-
inspector.append(list)
|
|
397
561
|
}
|
|
398
562
|
|
|
399
563
|
const renderPathGroup = (label: string, paths: ReadonlyArray<string>): HTMLElement => {
|
|
400
564
|
const group = createElement("div", "trace-path-group")
|
|
401
565
|
group.append(createElement("span", "trace-label", label))
|
|
402
566
|
const values = createElement("div", "trace-paths")
|
|
403
|
-
if (paths.length === 0) values.append(createElement("span", "section-empty", "none"))
|
|
404
567
|
paths.forEach((path) => values.append(stateLink(path, path, navigateToState)))
|
|
568
|
+
if (paths.length === 0) values.append(createElement("span", "section-empty", "none"))
|
|
405
569
|
group.append(values)
|
|
406
570
|
return group
|
|
407
571
|
}
|
|
408
572
|
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (values.length === 0) {
|
|
413
|
-
section.append(createElement("span", "section-empty", "none"))
|
|
414
|
-
} else {
|
|
415
|
-
values.forEach((value) => section.append(jsonBlock(value)))
|
|
416
|
-
}
|
|
417
|
-
return section
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const renderSimulationTrace = (frame: SimulationFrame): void => {
|
|
421
|
-
activeInputForm = undefined
|
|
422
|
-
inspector.replaceChildren()
|
|
573
|
+
const renderWalkthroughTrace = (frame: MachineWalkthrough.Frame): void => {
|
|
574
|
+
inspectorContent.replaceChildren()
|
|
575
|
+
showInspector()
|
|
423
576
|
const header = createElement("header", "inspector-header trace-header")
|
|
424
577
|
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
425
|
-
eyebrow.append(badge("
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
: `Event · ${eventName(frame.trigger.event)}`
|
|
578
|
+
eyebrow.append(badge("walkthrough", "active"))
|
|
579
|
+
const title = frame.choice === null
|
|
580
|
+
? "Initial configuration"
|
|
581
|
+
: frame.choice.title ?? triggerName(frame.choice.trigger)
|
|
430
582
|
header.append(eyebrow, createElement("h2", undefined, title))
|
|
431
583
|
header.append(metadata([
|
|
432
584
|
["Step", String(frame.step)],
|
|
433
|
-
[
|
|
434
|
-
|
|
435
|
-
|
|
585
|
+
[
|
|
586
|
+
"Result",
|
|
587
|
+
frame.changed
|
|
588
|
+
? `${activeTopology(frame.before.activePaths)} → ${activeTopology(frame.after.activePaths)}`
|
|
589
|
+
: "Topology unchanged"
|
|
590
|
+
]
|
|
436
591
|
]))
|
|
437
|
-
|
|
438
|
-
if (received !== undefined) header.append(jsonBlock(received))
|
|
439
|
-
inspector.append(header)
|
|
592
|
+
inspectorContent.append(header)
|
|
440
593
|
|
|
441
594
|
const topology = createElement("section", "inspector-section trace-topology")
|
|
442
|
-
topology.append(inspectionSection("Topology change", frame.microsteps.length))
|
|
443
595
|
topology.append(
|
|
596
|
+
inspectionSection("Topology"),
|
|
444
597
|
renderPathGroup("Before", frame.before.activePaths),
|
|
598
|
+
renderPathGroup("Exit", frame.exitPaths),
|
|
599
|
+
renderPathGroup("Entry", frame.entryPaths),
|
|
445
600
|
renderPathGroup("After", frame.after.activePaths)
|
|
446
601
|
)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const steps = createElement("section", "inspector-section trace-list")
|
|
450
|
-
steps.append(inspectionSection("Microsteps", frame.microsteps.length))
|
|
451
|
-
if (frame.microsteps.length === 0) {
|
|
452
|
-
steps.append(createElement(
|
|
453
|
-
"p",
|
|
454
|
-
"section-empty",
|
|
455
|
-
frame.trigger._tag === "Initial"
|
|
456
|
-
? "No automatic microsteps were needed."
|
|
457
|
-
: "No transition accepted this event."
|
|
458
|
-
))
|
|
459
|
-
}
|
|
460
|
-
frame.microsteps.forEach((step) => {
|
|
461
|
-
const card = createElement("article", "inspection-card trace-card")
|
|
462
|
-
const cardHeader = createElement("div", "card-header")
|
|
463
|
-
const cardTitle = createElement("div", "card-title")
|
|
464
|
-
cardTitle.append(
|
|
465
|
-
badge(`#${step.index + 1}`, "count"),
|
|
466
|
-
createElement("strong", undefined, eventName(step.event))
|
|
467
|
-
)
|
|
468
|
-
cardHeader.append(cardTitle, badge(step.changed ? "changed" : "unchanged", step.changed ? "active" : "neutral"))
|
|
469
|
-
card.append(cardHeader)
|
|
470
|
-
if (step.transitions.length > 0) {
|
|
471
|
-
const selected = createElement("div", "trace-transitions")
|
|
472
|
-
selected.append(createElement("span", "trace-label", "Selected transitions"))
|
|
473
|
-
step.transitions.forEach((transition) => {
|
|
474
|
-
const row = createElement("div", "trace-transition")
|
|
475
|
-
row.append(stateLink(transition.source, transition.source, navigateToState))
|
|
476
|
-
row.append(createElement("span", "branch-arrow", "→"))
|
|
477
|
-
const target = transition.resolvedTarget ?? transition.target
|
|
478
|
-
if (target === null) row.append(createElement("span", "branch-target", "No target"))
|
|
479
|
-
else row.append(stateLink(target, target, navigateToState))
|
|
480
|
-
if (transition.branchKey !== null) row.append(badge(transition.branchKey, "condition"))
|
|
481
|
-
if (transition.reenter) row.append(badge("reenter"))
|
|
482
|
-
selected.append(row)
|
|
483
|
-
if (transition.updates.length > 0) selected.append(renderPathGroup("Updates", transition.updates))
|
|
484
|
-
})
|
|
485
|
-
card.append(selected)
|
|
486
|
-
}
|
|
487
|
-
card.append(renderPathGroup("Exit", step.exitPaths), renderPathGroup("Entry", step.entryPaths))
|
|
488
|
-
if (step.raisedEvents.length > 0) card.append(renderTraceValues("Raised", step.raisedEvents))
|
|
489
|
-
if (step.emittedEvents.length > 0) card.append(renderTraceValues("Emitted", step.emittedEvents))
|
|
490
|
-
if (step.commands.length > 0) card.append(renderTraceValues("Commands", step.commands))
|
|
491
|
-
card.append(renderPathGroup("Active after", step.activePaths))
|
|
492
|
-
steps.append(card)
|
|
493
|
-
})
|
|
494
|
-
inspector.append(steps)
|
|
495
|
-
|
|
496
|
-
if (frame.commands.length > 0 || frame.emittedEvents.length > 0 || frame.output !== undefined) {
|
|
497
|
-
const results = createElement("section", "inspector-section trace-results")
|
|
498
|
-
results.append(inspectionSection("Plan result", frame.commands.length + frame.emittedEvents.length))
|
|
499
|
-
if (frame.commands.length > 0) results.append(renderTraceValues("Planned commands", frame.commands))
|
|
500
|
-
if (frame.emittedEvents.length > 0) results.append(renderTraceValues("Emitted events", frame.emittedEvents))
|
|
501
|
-
if (frame.output !== undefined) results.append(renderTraceValues("Output", [frame.output]))
|
|
502
|
-
inspector.append(results)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
602
|
+
inspectorContent.append(topology)
|
|
505
603
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const header = createElement("header", "inspector-header")
|
|
510
|
-
const eyebrow = createElement("div", "inspector-eyebrow")
|
|
511
|
-
eyebrow.append(badge("planner", "active"))
|
|
512
|
-
header.append(eyebrow, createElement("h2", undefined, "Start simulation"))
|
|
513
|
-
inspector.append(header)
|
|
514
|
-
const composer = createElement("section", "inspector-section simulation-composer")
|
|
515
|
-
composer.append(formSection("Machine input"))
|
|
516
|
-
const input = visualization.inputs.machine
|
|
517
|
-
if (input === null) {
|
|
518
|
-
composer.append(createElement("p", "section-empty", "This machine does not declare startup input."))
|
|
519
|
-
inspector.append(composer)
|
|
520
|
-
return
|
|
521
|
-
}
|
|
522
|
-
const form = renderInputForm(input, { name: "Machine input" })
|
|
523
|
-
activeInputForm = form
|
|
524
|
-
const start = createElement("button", "simulation-action", "Start simulation")
|
|
525
|
-
start.type = "submit"
|
|
526
|
-
start.disabled = simulationPending || visualization.source === null || !form.supported
|
|
527
|
-
form.element.addEventListener("submit", (event) => {
|
|
528
|
-
event.preventDefault()
|
|
529
|
-
const source = visualization.source
|
|
530
|
-
if (source === null) return
|
|
531
|
-
const result = form.read()
|
|
532
|
-
if (!result.ok) return
|
|
533
|
-
const request: SimulationRequest = {
|
|
534
|
-
_tag: "StartSimulation",
|
|
535
|
-
protocolVersion,
|
|
536
|
-
key: machineKey,
|
|
537
|
-
revision: visualization.revision,
|
|
538
|
-
source,
|
|
539
|
-
input: result.value as never
|
|
540
|
-
}
|
|
541
|
-
void runSimulation(request)
|
|
542
|
-
})
|
|
543
|
-
form.element.append(start)
|
|
544
|
-
composer.append(
|
|
545
|
-
form.element,
|
|
546
|
-
createElement(
|
|
547
|
-
"p",
|
|
548
|
-
"simulation-note",
|
|
549
|
-
"Initialization and synchronous callbacks run in isolation. Runtime activities and planned commands are not started."
|
|
550
|
-
)
|
|
551
|
-
)
|
|
552
|
-
inspector.append(composer)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function runSimulation(request: SimulationRequest): Promise<void> {
|
|
556
|
-
if (simulationPending) return
|
|
557
|
-
simulationPending = true
|
|
558
|
-
activeInputForm?.clearIssues()
|
|
559
|
-
activeInputForm?.setPending(true)
|
|
560
|
-
simulationFeedback.textContent = "Planning in an isolated worker…"
|
|
561
|
-
simulationFeedback.dataset.status = "pending"
|
|
562
|
-
updateSimulationUi()
|
|
563
|
-
try {
|
|
564
|
-
const result = await requestSimulation(request)
|
|
565
|
-
if (result._tag === "SimulationFailed") {
|
|
566
|
-
if (result.inputIssues.length > 0 && activeInputForm !== undefined) {
|
|
567
|
-
simulationFeedback.textContent = "Some input fields are invalid"
|
|
568
|
-
simulationFeedback.dataset.status = "error"
|
|
569
|
-
activeInputForm.setIssues(result.inputIssues)
|
|
570
|
-
return
|
|
571
|
-
}
|
|
572
|
-
selectedFrame = undefined
|
|
573
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
574
|
-
selectedEvent = undefined
|
|
575
|
-
simulationFeedback.textContent = result.diagnostics[0]?.message ?? "Simulation failed"
|
|
576
|
-
simulationFeedback.dataset.status = "error"
|
|
577
|
-
renderSimulationFailure(result.diagnostics)
|
|
578
|
-
return
|
|
579
|
-
}
|
|
580
|
-
simulation = result
|
|
581
|
-
activeInputForm = undefined
|
|
582
|
-
if (selectedPath !== undefined) {
|
|
583
|
-
nodes.get(selectedPath)?.classList.remove("is-selected")
|
|
584
|
-
rows.get(selectedPath)?.setAttribute("aria-selected", "false")
|
|
585
|
-
}
|
|
586
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
587
|
-
selectedPath = undefined
|
|
588
|
-
selectedEvent = undefined
|
|
589
|
-
selectedFrame = result.frame
|
|
590
|
-
clearRelations()
|
|
591
|
-
result.frame.microsteps.forEach((step) => {
|
|
592
|
-
step.transitions.forEach((transition) => {
|
|
593
|
-
relatedPaths.add(transition.source)
|
|
594
|
-
nodes.get(transition.source)?.classList.add("is-related-source")
|
|
595
|
-
})
|
|
596
|
-
step.entryPaths.forEach((path) => {
|
|
597
|
-
relatedPaths.add(path)
|
|
598
|
-
nodes.get(path)?.classList.add("is-related-target")
|
|
599
|
-
})
|
|
600
|
-
step.transitions.flatMap((transition) => transition.updates).forEach((path) => {
|
|
601
|
-
relatedPaths.add(path)
|
|
602
|
-
nodes.get(path)?.classList.add("is-related-update")
|
|
603
|
-
})
|
|
604
|
-
})
|
|
605
|
-
clearButton.disabled = false
|
|
606
|
-
const before = activeTopology(result.frame.before.activePaths)
|
|
607
|
-
const after = activeTopology(result.frame.after.activePaths)
|
|
608
|
-
simulationFeedback.textContent = result.frame.trigger._tag === "Initial"
|
|
609
|
-
? `Started in ${after}`
|
|
610
|
-
: result.frame.microsteps.length === 0
|
|
611
|
-
? `${eventName(result.frame.trigger.event)} was not accepted · remained in ${after}`
|
|
612
|
-
: before === after
|
|
613
|
-
? `${eventName(result.frame.trigger.event)} handled · remained in ${after}`
|
|
614
|
-
: `${before} → ${after} · ${result.frame.microsteps.length} microstep${
|
|
615
|
-
result.frame.microsteps.length === 1 ? "" : "s"
|
|
616
|
-
}`
|
|
617
|
-
simulationFeedback.dataset.status = "applied"
|
|
618
|
-
} catch (cause) {
|
|
619
|
-
selectedFrame = undefined
|
|
620
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
621
|
-
selectedEvent = undefined
|
|
622
|
-
const failure: Diagnostic = {
|
|
623
|
-
severity: "error",
|
|
624
|
-
code: "simulation-request-failed",
|
|
625
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
626
|
-
location: visualization.source === null
|
|
627
|
-
? null
|
|
628
|
-
: { file: visualization.source.file, line: null, column: null },
|
|
629
|
-
statePath: null
|
|
604
|
+
if (frame.choice === null) {
|
|
605
|
+
if (visualization.inputs.machine !== null) {
|
|
606
|
+
inspectorContent.append(renderInputContract("Machine input contract", visualization.inputs.machine))
|
|
630
607
|
}
|
|
631
|
-
|
|
632
|
-
simulationFeedback.dataset.status = "error"
|
|
633
|
-
renderSimulationFailure([failure])
|
|
634
|
-
} finally {
|
|
635
|
-
simulationPending = false
|
|
636
|
-
activeInputForm?.setPending(false)
|
|
637
|
-
updateSimulationUi()
|
|
608
|
+
return
|
|
638
609
|
}
|
|
610
|
+
|
|
611
|
+
const choice = frame.choice
|
|
612
|
+
const transition = transitionsById.get(choice.transitionId)
|
|
613
|
+
const branch = transition?.branches.find(({ id }) => id === choice.branchId)
|
|
614
|
+
const api = branch === undefined ? undefined : branchTargetApi(visualization, choice.source, branch)
|
|
615
|
+
const selection = createElement("section", "inspector-section")
|
|
616
|
+
selection.append(inspectionSection("Selected branch"))
|
|
617
|
+
const card = createElement("article", "inspection-card trace-card")
|
|
618
|
+
const cardHeader = createElement("div", "card-header")
|
|
619
|
+
const cardTitle = createElement("div", "card-title")
|
|
620
|
+
cardTitle.append(createElement("strong", undefined, triggerName(choice.trigger)))
|
|
621
|
+
const flags = createElement("div", "card-flags")
|
|
622
|
+
choice.decisions.forEach((decision) => flags.append(badge(decisionLabel(decision), "condition")))
|
|
623
|
+
cardHeader.append(cardTitle, flags)
|
|
624
|
+
card.append(cardHeader)
|
|
625
|
+
card.append(metadata([
|
|
626
|
+
["Title", choice.title],
|
|
627
|
+
["Source", stateLink(choice.source, choice.source, navigateToState)],
|
|
628
|
+
["Target", choice.target === null ? "No target" : stateLink(choice.target, choice.target, navigateToState)],
|
|
629
|
+
["API", api]
|
|
630
|
+
]))
|
|
631
|
+
if (choice.updates.length > 0) card.append(renderPathGroup("Updates", choice.updates))
|
|
632
|
+
selection.append(card)
|
|
633
|
+
inspectorContent.append(selection)
|
|
634
|
+
if (choice.input !== null) inspectorContent.append(renderInputContract("Data contract", choice.input, ["_tag"]))
|
|
639
635
|
}
|
|
640
636
|
|
|
641
637
|
const clearRelations = (): void => {
|
|
642
|
-
|
|
643
|
-
|
|
638
|
+
relatedFrom.clear()
|
|
639
|
+
relatedTo.clear()
|
|
640
|
+
incomingTransitions.clear()
|
|
641
|
+
outgoingTransitions.clear()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const markFrame = (frame: MachineWalkthrough.Frame): void => {
|
|
645
|
+
clearRelations()
|
|
646
|
+
if (frame.choice === null) return
|
|
647
|
+
relatedFrom.add(frame.choice.source)
|
|
648
|
+
outgoingTransitions.add(frame.choice.transitionId)
|
|
649
|
+
if (frame.choice.target !== null) relatedTo.add(frame.choice.target)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const chartPresentation = (): ChartPresentation => {
|
|
653
|
+
const choices = availableChoices()
|
|
654
|
+
const usable = choices.filter(({ unavailableReason }) => unavailableReason === null)
|
|
655
|
+
return {
|
|
656
|
+
simulationMode: walkthrough !== undefined,
|
|
657
|
+
activePaths: activePaths(),
|
|
658
|
+
selectedState: selectedPath ?? null,
|
|
659
|
+
selectedTransition: selectedTransition ?? null,
|
|
660
|
+
fromPaths: [...relatedFrom],
|
|
661
|
+
toPaths: [...relatedTo],
|
|
662
|
+
incomingTransitionIds: [...incomingTransitions],
|
|
663
|
+
outgoingTransitionIds: [...outgoingTransitions],
|
|
664
|
+
availableBranchIds: usable.map(({ branchId }) => branchId),
|
|
665
|
+
unavailableBranchIds: choices.filter(({ unavailableReason }) => unavailableReason !== null).map((
|
|
666
|
+
{ branchId }
|
|
667
|
+
) => branchId)
|
|
644
668
|
}
|
|
645
|
-
relatedPaths.clear()
|
|
646
669
|
}
|
|
647
670
|
|
|
671
|
+
const updateChartPresentation = (): void => chartView?.update(chartPresentation())
|
|
672
|
+
|
|
648
673
|
const clearSelection = (): void => {
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
674
|
+
if (walkthrough !== undefined) {
|
|
675
|
+
closeChoicePicker()
|
|
676
|
+
return
|
|
652
677
|
}
|
|
653
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
654
678
|
clearRelations()
|
|
655
679
|
selectedPath = undefined
|
|
656
|
-
|
|
680
|
+
selectedTransition = undefined
|
|
657
681
|
selectedFrame = undefined
|
|
658
682
|
clearButton.disabled = true
|
|
659
|
-
|
|
683
|
+
detailsButton.disabled = true
|
|
684
|
+
hideInspector()
|
|
685
|
+
updateChartPresentation()
|
|
660
686
|
}
|
|
661
687
|
|
|
662
|
-
const markTransitions = (
|
|
688
|
+
const markTransitions = (
|
|
689
|
+
transitions: ReadonlyArray<VisualizationTransition>,
|
|
690
|
+
includeSource = true
|
|
691
|
+
): void => {
|
|
663
692
|
for (const transition of transitions) {
|
|
664
|
-
|
|
665
|
-
|
|
693
|
+
if (includeSource) relatedFrom.add(transition.source)
|
|
694
|
+
outgoingTransitions.add(transition.id)
|
|
666
695
|
for (const branch of transition.branches) {
|
|
667
|
-
if (branch.target !== null)
|
|
668
|
-
relatedPaths.add(branch.target)
|
|
669
|
-
nodes.get(branch.target)?.classList.add("is-related-target")
|
|
670
|
-
}
|
|
671
|
-
for (const update of branch.updates) {
|
|
672
|
-
relatedPaths.add(update)
|
|
673
|
-
nodes.get(update)?.classList.add("is-related-update")
|
|
674
|
-
}
|
|
696
|
+
if (branch.target !== null) relatedTo.add(branch.target)
|
|
675
697
|
}
|
|
676
698
|
}
|
|
677
699
|
}
|
|
@@ -679,160 +701,269 @@ export const renderVisualizer = (
|
|
|
679
701
|
const markRelatedStates = (inspection: StateInspection): void => {
|
|
680
702
|
clearRelations()
|
|
681
703
|
for (const incoming of inspection.incoming) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
markTransitions(inspection.outgoing)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const expandAncestors = (inspection: StateInspection): void => {
|
|
689
|
-
for (const ancestor of inspection.breadcrumbs.slice(0, -1)) {
|
|
690
|
-
const row = rows.get(ancestor.path)
|
|
691
|
-
const children = nodes.get(ancestor.path)?.querySelector<HTMLElement>(":scope > .topology-children")
|
|
692
|
-
if (row === undefined || children === null || children === undefined) continue
|
|
693
|
-
row.setAttribute("aria-expanded", "true")
|
|
694
|
-
children.hidden = false
|
|
695
|
-
const disclosure = row.querySelector<HTMLElement>(".state-disclosure")
|
|
696
|
-
if (disclosure !== null) disclosure.textContent = "▾"
|
|
704
|
+
relatedFrom.add(incoming.transition.source)
|
|
705
|
+
incomingTransitions.add(incoming.transition.id)
|
|
697
706
|
}
|
|
707
|
+
markTransitions(inspection.outgoing, false)
|
|
698
708
|
}
|
|
699
709
|
|
|
700
710
|
const selectState = (path: string, focus: boolean): void => {
|
|
711
|
+
if (walkthrough !== undefined) return
|
|
701
712
|
const inspection = model.inspectState(path)
|
|
702
713
|
if (inspection === undefined) return
|
|
703
|
-
expandAncestors(inspection)
|
|
704
|
-
if (selectedPath !== undefined) {
|
|
705
|
-
nodes.get(selectedPath)?.classList.remove("is-selected")
|
|
706
|
-
rows.get(selectedPath)?.setAttribute("aria-selected", "false")
|
|
707
|
-
}
|
|
708
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
709
714
|
selectedPath = path
|
|
710
|
-
|
|
715
|
+
selectedTransition = undefined
|
|
711
716
|
selectedFrame = undefined
|
|
712
|
-
nodes.get(path)?.classList.add("is-selected")
|
|
713
|
-
rows.get(path)?.setAttribute("aria-selected", "true")
|
|
714
717
|
markRelatedStates(inspection)
|
|
715
718
|
clearButton.disabled = false
|
|
716
|
-
|
|
717
|
-
if (
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
719
|
+
detailsButton.disabled = false
|
|
720
|
+
if (!inspector.hidden) renderInspection(inspection)
|
|
721
|
+
updateChartPresentation()
|
|
722
|
+
if (focus) chartView?.focusState(path)
|
|
721
723
|
}
|
|
722
724
|
|
|
723
725
|
function navigateToState(path: string): void {
|
|
724
726
|
selectState(path, true)
|
|
725
727
|
}
|
|
726
728
|
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
rows.get(selectedPath)?.setAttribute("aria-selected", "false")
|
|
732
|
-
}
|
|
733
|
-
if (selectedEvent !== undefined) eventButtons.get(selectedEvent)?.classList.remove("is-selected")
|
|
729
|
+
const selectTransition = (transitionId: string): void => {
|
|
730
|
+
if (walkthrough !== undefined) return
|
|
731
|
+
const transition = transitionsById.get(transitionId)
|
|
732
|
+
if (transition === undefined) return
|
|
734
733
|
selectedPath = undefined
|
|
735
|
-
|
|
734
|
+
selectedTransition = transitionId
|
|
736
735
|
selectedFrame = undefined
|
|
737
|
-
eventButtons.get(event)?.classList.add("is-selected")
|
|
738
736
|
clearRelations()
|
|
739
|
-
markTransitions(
|
|
737
|
+
markTransitions([transition])
|
|
740
738
|
clearButton.disabled = false
|
|
741
|
-
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
739
|
+
detailsButton.disabled = false
|
|
740
|
+
if (!inspector.hidden) renderTransitionInspection(transition)
|
|
741
|
+
updateChartPresentation()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const openStateDetails = (path: string): void => {
|
|
745
|
+
if (walkthrough !== undefined) return
|
|
746
|
+
selectState(path, false)
|
|
747
|
+
const inspection = model.inspectState(path)
|
|
748
|
+
if (inspection !== undefined) renderInspection(inspection)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const openTransitionDetails = (transitionId: string): void => {
|
|
752
|
+
if (walkthrough !== undefined) return
|
|
753
|
+
selectTransition(transitionId)
|
|
754
|
+
const transition = transitionsById.get(transitionId)
|
|
755
|
+
if (transition !== undefined) renderTransitionInspection(transition)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const walkthroughDock = createElement("section", "walkthrough-dock")
|
|
759
|
+
walkthroughDock.hidden = true
|
|
760
|
+
const walkthroughHeading = createElement("div", "walkthrough-heading")
|
|
761
|
+
const walkthroughIdentity = createElement("div", "walkthrough-identity")
|
|
762
|
+
const walkthroughHint = createElement("span", "walkthrough-hint")
|
|
763
|
+
const walkthroughStatus = createElement("span", "walkthrough-status")
|
|
764
|
+
const walkthroughTimeline = createElement("div", "walkthrough-timeline")
|
|
765
|
+
walkthroughTimeline.setAttribute("aria-label", "Simulation timeline")
|
|
766
|
+
walkthroughIdentity.append(createElement("strong", undefined, "Simulation"), walkthroughHint)
|
|
767
|
+
walkthroughHeading.append(walkthroughIdentity, walkthroughStatus)
|
|
768
|
+
walkthroughDock.append(walkthroughHeading, walkthroughTimeline)
|
|
769
|
+
|
|
770
|
+
const selectFrame = (frame: MachineWalkthrough.Frame): void => {
|
|
771
|
+
selectedPath = undefined
|
|
772
|
+
selectedTransition = undefined
|
|
773
|
+
selectedFrame = frame
|
|
774
|
+
markFrame(frame)
|
|
775
|
+
clearButton.disabled = false
|
|
776
|
+
detailsButton.disabled = false
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const choiceRoute = (choice: MachineWalkthrough.Choice): string =>
|
|
780
|
+
`${choice.source} → ${choice.target ?? "no target"}`
|
|
781
|
+
|
|
782
|
+
const takeChoice = (choice: MachineWalkthrough.Choice): void => {
|
|
783
|
+
if (walkthrough === undefined || choice.unavailableReason !== null) return
|
|
784
|
+
const result = MachineWalkthrough.take(walkthrough, choice.id)
|
|
785
|
+
if (Result.isFailure(result)) return
|
|
786
|
+
walkthrough = result.success
|
|
787
|
+
selectFrame(MachineWalkthrough.current(walkthrough))
|
|
788
|
+
closeChoicePicker()
|
|
789
|
+
updateWalkthroughUi()
|
|
790
|
+
chartView?.revealStates(activePaths())
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const renderPickerChoice = (choice: MachineWalkthrough.Choice): HTMLButtonElement => {
|
|
794
|
+
const button = createElement(
|
|
795
|
+
"button",
|
|
796
|
+
`transition-picker-choice${choice.unavailableReason === null ? "" : " is-unavailable"}`
|
|
797
|
+
)
|
|
798
|
+
button.type = "button"
|
|
799
|
+
button.disabled = choice.unavailableReason !== null
|
|
800
|
+
const main = createElement("span", "transition-picker-choice-main")
|
|
801
|
+
main.append(createElement("strong", undefined, triggerName(choice.trigger)))
|
|
802
|
+
if (choice.title !== null) main.append(createElement("span", "transition-picker-choice-title", choice.title))
|
|
803
|
+
button.append(main, createElement("span", "transition-picker-choice-route", choiceRoute(choice)))
|
|
804
|
+
const contract = contractSummary(choice.input)
|
|
805
|
+
if (contract !== null) button.append(createElement("span", "transition-picker-choice-contract", contract))
|
|
806
|
+
if (choice.decisions.length > 0) {
|
|
807
|
+
button.append(createElement(
|
|
808
|
+
"span",
|
|
809
|
+
"transition-picker-choice-decision",
|
|
810
|
+
choice.decisions.map(decisionLabel).join(" · ")
|
|
811
|
+
))
|
|
761
812
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (node.transitionCount > 0) markers.append(badge(`${node.transitionCount}t`, "count"))
|
|
799
|
-
if (node.activityCount > 0) markers.append(badge(`${node.activityCount}a`, "count"))
|
|
800
|
-
row.append(disclosure, status, label, markers)
|
|
801
|
-
container.append(row)
|
|
802
|
-
row.addEventListener("focus", () => {
|
|
803
|
-
rows.forEach((candidate) => candidate.tabIndex = candidate === row ? 0 : -1)
|
|
813
|
+
if (choice.unavailableReason !== null) {
|
|
814
|
+
button.title = unavailableLabel(choice.unavailableReason)
|
|
815
|
+
button.append(createElement(
|
|
816
|
+
"span",
|
|
817
|
+
"transition-picker-choice-unavailable",
|
|
818
|
+
unavailableLabel(choice.unavailableReason)
|
|
819
|
+
))
|
|
820
|
+
} else {
|
|
821
|
+
button.title = choiceRoute(choice)
|
|
822
|
+
button.addEventListener("click", () => takeChoice(choice))
|
|
823
|
+
}
|
|
824
|
+
return button
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const showChoicePicker = (
|
|
828
|
+
title: string,
|
|
829
|
+
choices: ReadonlyArray<MachineWalkthrough.Choice>,
|
|
830
|
+
anchor: ChartInteractionAnchor
|
|
831
|
+
): void => {
|
|
832
|
+
if (choices.length === 1 && choices[0]?.unavailableReason === null) {
|
|
833
|
+
takeChoice(choices[0])
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
if (choices.length === 0) {
|
|
837
|
+
closeChoicePicker()
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
choicePickerTitle.textContent = title
|
|
841
|
+
choicePickerContent.replaceChildren(...choices.map(renderPickerChoice))
|
|
842
|
+
choicePicker.style.left = `${anchor.x + 12}px`
|
|
843
|
+
choicePicker.style.top = `${anchor.y + 12}px`
|
|
844
|
+
choicePicker.hidden = false
|
|
845
|
+
requestAnimationFrame(() => {
|
|
846
|
+
const bounds = choicePicker.getBoundingClientRect()
|
|
847
|
+
choicePicker.style.left = `${Math.max(10, Math.min(anchor.x + 12, window.innerWidth - bounds.width - 10))}px`
|
|
848
|
+
choicePicker.style.top = `${Math.max(58, Math.min(anchor.y + 12, window.innerHeight - bounds.height - 10))}px`
|
|
804
849
|
})
|
|
850
|
+
}
|
|
805
851
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
852
|
+
const handleStateClick = (path: string, _anchor: ChartInteractionAnchor): void => {
|
|
853
|
+
if (walkthrough === undefined) {
|
|
854
|
+
selectState(path, false)
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
closeChoicePicker()
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const handleTransitionClick = (
|
|
861
|
+
transitionId: string,
|
|
862
|
+
branchIds: ReadonlyArray<string>,
|
|
863
|
+
anchor: ChartInteractionAnchor
|
|
864
|
+
): void => {
|
|
865
|
+
if (walkthrough === undefined) {
|
|
866
|
+
selectTransition(transitionId)
|
|
867
|
+
return
|
|
819
868
|
}
|
|
820
|
-
|
|
869
|
+
const transition = transitionsById.get(transitionId)
|
|
870
|
+
if (transition === undefined) return
|
|
871
|
+
showChoicePicker(
|
|
872
|
+
triggerLabel(transition),
|
|
873
|
+
MachineWalkthrough.choices(walkthrough).filter((choice) => branchIds.includes(choice.branchId)),
|
|
874
|
+
anchor
|
|
875
|
+
)
|
|
821
876
|
}
|
|
822
877
|
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
878
|
+
const renderWalkthroughDock = (): void => {
|
|
879
|
+
walkthroughDock.hidden = walkthrough === undefined
|
|
880
|
+
chartPanel.classList.toggle("has-walkthrough", walkthrough !== undefined)
|
|
881
|
+
if (walkthrough === undefined) {
|
|
882
|
+
walkthroughTimeline.replaceChildren()
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
const cursor = MachineWalkthrough.cursor(walkthrough)
|
|
886
|
+
const timeline = MachineWalkthrough.timeline(walkthrough)
|
|
887
|
+
const choices = MachineWalkthrough.choices(walkthrough)
|
|
888
|
+
walkthroughStatus.textContent = `Step ${cursor} · ${activeTopology(activePaths())}`
|
|
889
|
+
walkthroughHint.textContent = choices.length === 0
|
|
890
|
+
? "No outgoing transitions"
|
|
891
|
+
: "Click a highlighted transition to advance"
|
|
892
|
+
walkthroughTimeline.replaceChildren()
|
|
893
|
+
timeline.forEach((frame) => {
|
|
894
|
+
const label = frame.choice === null ? "Initial" : frame.choice.title ?? triggerName(frame.choice.trigger)
|
|
895
|
+
const button = createElement(
|
|
896
|
+
"button",
|
|
897
|
+
`walkthrough-step${frame.step === cursor ? " is-current" : ""}`,
|
|
898
|
+
`${frame.step} · ${label}`
|
|
899
|
+
)
|
|
900
|
+
button.type = "button"
|
|
901
|
+
button.addEventListener("click", () => {
|
|
902
|
+
if (walkthrough === undefined) return
|
|
903
|
+
const result = MachineWalkthrough.seek(walkthrough, frame.step)
|
|
904
|
+
if (Result.isFailure(result)) return
|
|
905
|
+
walkthrough = result.success
|
|
906
|
+
selectFrame(MachineWalkthrough.current(walkthrough))
|
|
907
|
+
closeChoicePicker()
|
|
908
|
+
updateWalkthroughUi()
|
|
909
|
+
chartView?.revealStates(activePaths())
|
|
910
|
+
})
|
|
911
|
+
walkthroughTimeline.append(button)
|
|
826
912
|
})
|
|
827
913
|
}
|
|
828
914
|
|
|
829
915
|
clearButton.addEventListener("click", clearSelection)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
916
|
+
inspectorClose.addEventListener("click", hideInspector)
|
|
917
|
+
choicePickerClose.addEventListener("click", closeChoicePicker)
|
|
918
|
+
detailsButton.addEventListener("click", () => {
|
|
919
|
+
if (!inspector.hidden) {
|
|
920
|
+
hideInspector()
|
|
921
|
+
} else if (selectedFrame !== undefined) {
|
|
922
|
+
renderWalkthroughTrace(selectedFrame)
|
|
923
|
+
} else if (selectedPath !== undefined) {
|
|
924
|
+
const inspection = model.inspectState(selectedPath)
|
|
925
|
+
if (inspection !== undefined) renderInspection(inspection)
|
|
926
|
+
} else if (selectedTransition !== undefined) {
|
|
927
|
+
const transition = transitionsById.get(selectedTransition)
|
|
928
|
+
if (transition !== undefined) renderTransitionInspection(transition)
|
|
929
|
+
}
|
|
835
930
|
})
|
|
931
|
+
revealActiveButton.addEventListener("click", () => chartView?.revealStates(activePaths()))
|
|
932
|
+
|
|
933
|
+
const zoomOutButton = createElement("button", "toolbar-button zoom-button", "−")
|
|
934
|
+
zoomOutButton.type = "button"
|
|
935
|
+
zoomOutButton.setAttribute("aria-label", "Zoom out")
|
|
936
|
+
const zoomResetButton = createElement("button", "toolbar-button zoom-value", "100%")
|
|
937
|
+
zoomResetButton.type = "button"
|
|
938
|
+
zoomResetButton.setAttribute("aria-label", "Reset zoom")
|
|
939
|
+
const zoomInButton = createElement("button", "toolbar-button zoom-button", "+")
|
|
940
|
+
zoomInButton.type = "button"
|
|
941
|
+
zoomInButton.setAttribute("aria-label", "Zoom in")
|
|
942
|
+
const zoomFitButton = createElement("button", "toolbar-button zoom-button", "Fit")
|
|
943
|
+
zoomFitButton.type = "button"
|
|
944
|
+
zoomFitButton.setAttribute("aria-label", "Fit chart")
|
|
945
|
+
const zoomButtons = [zoomOutButton, zoomResetButton, zoomInButton, zoomFitButton]
|
|
946
|
+
zoomButtons.forEach((button) => button.disabled = true)
|
|
947
|
+
|
|
948
|
+
const updateZoomControls = (): void => {
|
|
949
|
+
const zoom = chartView?.getZoom()
|
|
950
|
+
zoomResetButton.textContent = `${Math.round((zoom ?? 1) * 100)}%`
|
|
951
|
+
zoomOutButton.disabled = zoom === undefined || zoom <= minimumChartZoom
|
|
952
|
+
zoomResetButton.disabled = zoom === undefined || zoom === 1
|
|
953
|
+
zoomInButton.disabled = zoom === undefined || zoom >= maximumChartZoom
|
|
954
|
+
zoomFitButton.disabled = zoom === undefined
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const changeZoom = (next: (view: ChartView) => number): void => {
|
|
958
|
+
if (chartView === undefined) return
|
|
959
|
+
next(chartView)
|
|
960
|
+
updateZoomControls()
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
zoomOutButton.addEventListener("click", () => changeZoom((view) => view.setZoom(view.getZoom() - 0.1)))
|
|
964
|
+
zoomResetButton.addEventListener("click", () => changeZoom((view) => view.setZoom(1)))
|
|
965
|
+
zoomInButton.addEventListener("click", () => changeZoom((view) => view.setZoom(view.getZoom() + 0.1)))
|
|
966
|
+
zoomFitButton.addEventListener("click", () => changeZoom((view) => view.fit()))
|
|
836
967
|
|
|
837
968
|
const toolbar = createElement("div", "toolbar")
|
|
838
969
|
const runtime = createElement("div", "runtime-summary")
|
|
@@ -840,164 +971,69 @@ export const renderVisualizer = (
|
|
|
840
971
|
const runtimeText = createElement("span")
|
|
841
972
|
runtime.append(runtimeDot, runtimeText)
|
|
842
973
|
const toolbarActions = createElement("div", "toolbar-actions")
|
|
843
|
-
toolbarActions.append(clearButton,
|
|
974
|
+
toolbarActions.append(clearButton, detailsButton, walkthroughButton, revealActiveButton)
|
|
975
|
+
const zoomControls = createElement("div", "zoom-controls")
|
|
976
|
+
zoomControls.setAttribute("role", "group")
|
|
977
|
+
zoomControls.setAttribute("aria-label", "Chart zoom")
|
|
978
|
+
zoomControls.append(zoomOutButton, zoomResetButton, zoomInButton, zoomFitButton)
|
|
844
979
|
toolbar.append(runtime, toolbarActions)
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
simulationFeedback.setAttribute("role", "status")
|
|
852
|
-
tree.append(events, simulationFeedback)
|
|
853
|
-
|
|
854
|
-
const renderEventButtons = (): void => {
|
|
855
|
-
events.replaceChildren(createElement("span", "enabled-events-label", "Enabled"))
|
|
856
|
-
eventButtons.clear()
|
|
857
|
-
const candidates = candidateEvents()
|
|
858
|
-
if (candidates.length === 0) {
|
|
859
|
-
events.append(createElement("span", "enabled-events-empty", "none"))
|
|
860
|
-
return
|
|
861
|
-
}
|
|
862
|
-
candidates.forEach((event) => {
|
|
863
|
-
const button = createElement("button", `event-button${event === selectedEvent ? " is-selected" : ""}`, event)
|
|
864
|
-
button.type = "button"
|
|
865
|
-
button.disabled = simulationPending
|
|
866
|
-
button.addEventListener("click", () => selectEvent(event))
|
|
867
|
-
eventButtons.set(event, button)
|
|
868
|
-
events.append(button)
|
|
869
|
-
})
|
|
870
|
-
}
|
|
980
|
+
const chart = createElement("div", "topology-chart")
|
|
981
|
+
chart.setAttribute("role", "region")
|
|
982
|
+
chart.setAttribute("aria-label", `${model.machineId} states`)
|
|
983
|
+
const chartHost = createElement("div", "chart-host")
|
|
984
|
+
chartHost.append(createElement("div", "chart-loading", "Computing layout…"))
|
|
985
|
+
chart.append(chartHost)
|
|
871
986
|
|
|
872
|
-
const
|
|
987
|
+
const updateWalkthroughUi = (): void => {
|
|
873
988
|
const active = new Set(activePaths())
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
status.setAttribute("aria-label", isActive ? "active" : "inactive")
|
|
878
|
-
})
|
|
879
|
-
const hasRuntimeState = simulation !== undefined || model.hasSnapshot
|
|
989
|
+
const simulating = walkthrough !== undefined
|
|
990
|
+
updateChartPresentation()
|
|
991
|
+
const hasRuntimeState = simulating || model.hasSnapshot
|
|
880
992
|
runtimeDot.classList.toggle("has-snapshot", hasRuntimeState)
|
|
881
|
-
runtimeText.textContent =
|
|
882
|
-
?
|
|
993
|
+
runtimeText.textContent = walkthrough !== undefined
|
|
994
|
+
? `Simulation · ${active.size} active · step ${MachineWalkthrough.cursor(walkthrough)}`
|
|
883
995
|
: diagnostics.length > 0
|
|
884
996
|
? "Partial"
|
|
885
997
|
: model.hasSnapshot
|
|
886
998
|
? `${active.size} active`
|
|
887
999
|
: "Structure only"
|
|
888
|
-
|
|
889
|
-
|
|
1000
|
+
walkthroughButton.textContent = simulating ? "Exit simulation" : "Start simulation"
|
|
1001
|
+
walkthroughButton.disabled = model.roots.length === 0
|
|
890
1002
|
revealActiveButton.disabled = active.size === 0
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1003
|
+
clearButton.hidden = simulating
|
|
1004
|
+
detailsButton.hidden = simulating
|
|
1005
|
+
chartPanel.classList.toggle("is-simulating", simulating)
|
|
1006
|
+
if (simulating) hideInspector()
|
|
1007
|
+
renderWalkthroughDock()
|
|
1008
|
+
if (!simulating && !inspector.hidden) {
|
|
1009
|
+
if (selectedFrame !== undefined) {
|
|
1010
|
+
renderWalkthroughTrace(selectedFrame)
|
|
1011
|
+
} else if (selectedPath !== undefined) {
|
|
1012
|
+
const inspection = model.inspectState(selectedPath)
|
|
1013
|
+
if (inspection !== undefined) renderInspection(inspection)
|
|
1014
|
+
} else if (selectedTransition !== undefined) {
|
|
1015
|
+
const transition = transitionsById.get(selectedTransition)
|
|
1016
|
+
if (transition !== undefined) renderTransitionInspection(transition)
|
|
1017
|
+
}
|
|
901
1018
|
}
|
|
902
1019
|
}
|
|
903
1020
|
|
|
904
|
-
|
|
905
|
-
if (
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (visualization.inputs.machine === null && visualization.source !== null) {
|
|
912
|
-
void runSimulation({
|
|
913
|
-
_tag: "StartSimulation",
|
|
914
|
-
protocolVersion,
|
|
915
|
-
key: machineKey,
|
|
916
|
-
revision: visualization.revision,
|
|
917
|
-
source: visualization.source
|
|
918
|
-
})
|
|
919
|
-
} else {
|
|
920
|
-
renderStartSimulation()
|
|
921
|
-
}
|
|
1021
|
+
walkthroughButton.addEventListener("click", () => {
|
|
1022
|
+
if (walkthrough === undefined) {
|
|
1023
|
+
walkthrough = MachineWalkthrough.start(visualization)
|
|
1024
|
+
selectFrame(MachineWalkthrough.current(walkthrough))
|
|
1025
|
+
hideInspector()
|
|
1026
|
+
closeChoicePicker()
|
|
1027
|
+
requestAnimationFrame(() => chartView?.revealStates(activePaths()))
|
|
922
1028
|
} else {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
simulationFeedback.textContent = ""
|
|
926
|
-
delete simulationFeedback.dataset.status
|
|
1029
|
+
walkthrough = undefined
|
|
1030
|
+
closeChoicePicker()
|
|
927
1031
|
clearSelection()
|
|
928
1032
|
}
|
|
929
|
-
|
|
930
|
-
})
|
|
931
|
-
if (model.roots.length === 0) {
|
|
932
|
-
const empty = createElement("div", "topology-empty")
|
|
933
|
-
empty.append(
|
|
934
|
-
createElement("strong", undefined, "No states yet"),
|
|
935
|
-
createElement("span", undefined, "The topology will appear as the machine definition becomes available.")
|
|
936
|
-
)
|
|
937
|
-
tree.append(empty)
|
|
938
|
-
} else {
|
|
939
|
-
model.roots.forEach((node) => tree.append(renderNode(node, 0)))
|
|
940
|
-
}
|
|
941
|
-
updateSimulationUi()
|
|
942
|
-
rows.values().next().value?.setAttribute("tabindex", "0")
|
|
943
|
-
tree.addEventListener("keydown", (event) => {
|
|
944
|
-
const current = event.target instanceof HTMLElement ? event.target.closest<HTMLElement>(".state-row") : null
|
|
945
|
-
if (current === null) return
|
|
946
|
-
const visible = [...rows.values()].filter((row) => row.getClientRects().length > 0)
|
|
947
|
-
const index = visible.indexOf(current)
|
|
948
|
-
const focus = (row: HTMLElement | undefined): void => {
|
|
949
|
-
if (row === undefined) return
|
|
950
|
-
event.preventDefault()
|
|
951
|
-
row.focus()
|
|
952
|
-
}
|
|
953
|
-
switch (event.key) {
|
|
954
|
-
case "ArrowDown":
|
|
955
|
-
focus(visible[index + 1])
|
|
956
|
-
break
|
|
957
|
-
case "ArrowUp":
|
|
958
|
-
focus(visible[index - 1])
|
|
959
|
-
break
|
|
960
|
-
case "Home":
|
|
961
|
-
focus(visible[0])
|
|
962
|
-
break
|
|
963
|
-
case "End":
|
|
964
|
-
focus(visible.at(-1))
|
|
965
|
-
break
|
|
966
|
-
case "ArrowRight": {
|
|
967
|
-
if (current.getAttribute("aria-expanded") === "false") {
|
|
968
|
-
event.preventDefault()
|
|
969
|
-
setExpanded(current, true)
|
|
970
|
-
} else {
|
|
971
|
-
const child = current.closest<HTMLElement>(".topology-node")
|
|
972
|
-
?.querySelector<HTMLElement>(":scope > .topology-children > .topology-node > .state-row")
|
|
973
|
-
focus(child ?? undefined)
|
|
974
|
-
}
|
|
975
|
-
break
|
|
976
|
-
}
|
|
977
|
-
case "ArrowLeft": {
|
|
978
|
-
if (current.getAttribute("aria-expanded") === "true") {
|
|
979
|
-
event.preventDefault()
|
|
980
|
-
setExpanded(current, false)
|
|
981
|
-
} else {
|
|
982
|
-
const parent = current.closest<HTMLElement>(".topology-children")
|
|
983
|
-
?.closest<HTMLElement>(".topology-node")
|
|
984
|
-
?.querySelector<HTMLElement>(":scope > .state-row")
|
|
985
|
-
focus(parent ?? undefined)
|
|
986
|
-
}
|
|
987
|
-
break
|
|
988
|
-
}
|
|
989
|
-
case "Enter":
|
|
990
|
-
case " ":
|
|
991
|
-
event.preventDefault()
|
|
992
|
-
current.click()
|
|
993
|
-
break
|
|
994
|
-
case "Escape":
|
|
995
|
-
event.preventDefault()
|
|
996
|
-
clearSelection()
|
|
997
|
-
break
|
|
998
|
-
}
|
|
1033
|
+
updateWalkthroughUi()
|
|
999
1034
|
})
|
|
1000
|
-
|
|
1035
|
+
|
|
1036
|
+
chartPanel.append(toolbar)
|
|
1001
1037
|
if (diagnostics.length > 0) {
|
|
1002
1038
|
const diagnosticList = createElement("div", "diagnostics")
|
|
1003
1039
|
diagnosticList.setAttribute("role", "status")
|
|
@@ -1009,12 +1045,44 @@ export const renderVisualizer = (
|
|
|
1009
1045
|
}
|
|
1010
1046
|
diagnosticList.append(item)
|
|
1011
1047
|
})
|
|
1012
|
-
|
|
1048
|
+
chartPanel.append(diagnosticList)
|
|
1013
1049
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
workspace.append(treePanel, inspector)
|
|
1050
|
+
chartPanel.append(chart, walkthroughDock, zoomControls)
|
|
1051
|
+
hideInspector()
|
|
1052
|
+
workspace.append(chartPanel, inspector, choicePicker)
|
|
1018
1053
|
shell.append(workspace)
|
|
1019
1054
|
root.replaceChildren(shell)
|
|
1055
|
+
|
|
1056
|
+
if (model.roots.length === 0) {
|
|
1057
|
+
const empty = createElement("div", "topology-empty")
|
|
1058
|
+
empty.append(
|
|
1059
|
+
createElement("strong", undefined, "No states yet"),
|
|
1060
|
+
createElement("span", undefined, "The topology will appear as the machine definition becomes available.")
|
|
1061
|
+
)
|
|
1062
|
+
chartHost.replaceChildren(empty)
|
|
1063
|
+
} else {
|
|
1064
|
+
void Effect.runPromise(renderChart(chartHost, visualization, {
|
|
1065
|
+
selectState: handleStateClick,
|
|
1066
|
+
openStateDetails,
|
|
1067
|
+
selectTransition: handleTransitionClick,
|
|
1068
|
+
openTransitionDetails,
|
|
1069
|
+
clearSelection,
|
|
1070
|
+
zoomChanged: updateZoomControls
|
|
1071
|
+
})).then(
|
|
1072
|
+
(view) => {
|
|
1073
|
+
chartView = view
|
|
1074
|
+
updateChartPresentation()
|
|
1075
|
+
updateZoomControls()
|
|
1076
|
+
},
|
|
1077
|
+
(cause) => {
|
|
1078
|
+
const failure = createElement("div", "chart-layout-error")
|
|
1079
|
+
failure.append(
|
|
1080
|
+
createElement("strong", undefined, "The chart could not be laid out"),
|
|
1081
|
+
createElement("span", undefined, cause instanceof Error ? cause.message : String(cause))
|
|
1082
|
+
)
|
|
1083
|
+
chartHost.replaceChildren(failure)
|
|
1084
|
+
}
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
updateWalkthroughUi()
|
|
1020
1088
|
}
|