|
@@ -0,0 +1,284 @@
|
|
|
+import * as React from 'react';
|
|
|
+import * as Icons from '@tabler/icons';
|
|
|
+import {Button, Divider, Group, Image, SimpleGrid, Space, Text, Title, TextInput, Select, Stack} from '@mantine/core';
|
|
|
+
|
|
|
+import { Graphviz } from 'graphviz-react';
|
|
|
+
|
|
|
+import {newVersionedModel, undoButtonHelpText, VersionedModelState,} from '../versioned_model/single_model';
|
|
|
+import {OnionContext} from "../onion_context";
|
|
|
+import {mockUuid} from "onion/test_helpers";
|
|
|
+import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
|
|
|
+import {INodeState, IValueState} from "onion/graph_state";
|
|
|
+
|
|
|
+export const demo_Sem_description = <>
|
|
|
+ <Title order={4}>
|
|
|
+ Semantics
|
|
|
+ </Title>
|
|
|
+</>;
|
|
|
+
|
|
|
+export function getDemoSem() {
|
|
|
+ const primitiveRegistry = new PrimitiveRegistry();
|
|
|
+ const generateUUID = mockUuid();
|
|
|
+
|
|
|
+ const designModelId = generateUUID();
|
|
|
+
|
|
|
+ const designModel = newVersionedModel({readonly: true});
|
|
|
+
|
|
|
+ return function DemoSem() {
|
|
|
+
|
|
|
+ const [designModelState, setDesignModelState] = React.useState<VersionedModelState>(designModel.initialState);
|
|
|
+
|
|
|
+ const designModelReducer = designModel.getReducer(setDesignModelState);
|
|
|
+
|
|
|
+ // We start out with the 'createList' delta already having occurred:
|
|
|
+ React.useEffect(() => {
|
|
|
+ // idempotent:
|
|
|
+ const designModelCreation = primitiveRegistry.newNodeCreation(designModelId);
|
|
|
+ designModelReducer.createAndGotoNewVersion([
|
|
|
+ designModelCreation,
|
|
|
+ primitiveRegistry.newEdgeCreation(designModelCreation, "type", "DesignModel"),
|
|
|
+ ], "createDesignModel", designModel.initialState.version);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+
|
|
|
+ const [dotGraph, setDotGraph] = React.useState("digraph {}");
|
|
|
+
|
|
|
+ const [initialState, setInitialState] = React.useState<string|null>(null);
|
|
|
+ const [currentState, setCurrentState] = React.useState<string|null>(null);
|
|
|
+
|
|
|
+ // Whenever the current version changes, we calculate a bunch of values that are needed in the UI and in its callbacks
|
|
|
+ const [states, transitions, designModelNode, initial,
|
|
|
+ runtimeModelNode, current] = React.useMemo(() => {
|
|
|
+ const states: Array<[string, INodeState]> = [];
|
|
|
+ const transitions: Array<[string, string, string, INodeState]> = [];
|
|
|
+ let designModelNode: INodeState|null = null;
|
|
|
+ let runtimeModelNode: INodeState|null = null;
|
|
|
+ let initialStateName : string|null = null;
|
|
|
+ let currentStateName : string|null = null;
|
|
|
+ let initial;
|
|
|
+ let current;
|
|
|
+ for (const nodeState of designModel.graphState.nodes.values()) {
|
|
|
+ if (nodeState.isDeleted) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const getStateName = s => (s.getOutgoingEdges().get("name") as IValueState).value as string;
|
|
|
+
|
|
|
+ const nodeType = (nodeState.getOutgoingEdges().get("type") as IValueState)?.value as string;
|
|
|
+
|
|
|
+ if (nodeType === "State") {
|
|
|
+ states.push([getStateName(nodeState), nodeState]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nodeType === "Transition") {
|
|
|
+ transitions.push([
|
|
|
+ getStateName(nodeState.getOutgoingEdges().get("src") as INodeState),
|
|
|
+ getStateName(nodeState.getOutgoingEdges().get("tgt") as INodeState),
|
|
|
+ (nodeState.getOutgoingEdges().get("event") as IValueState).value as string,
|
|
|
+ nodeState,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nodeType === "DesignModel") {
|
|
|
+ designModelNode = nodeState;
|
|
|
+ initial = nodeState.getOutgoingEdges().get("initial") as INodeState;
|
|
|
+ initialStateName = initial ? getStateName(initial) : null;
|
|
|
+ setInitialState(initialStateName);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nodeType === "RuntimeModel") {
|
|
|
+ runtimeModelNode = nodeState;
|
|
|
+ current = nodeState.getOutgoingEdges().get("current") as INodeState;
|
|
|
+ currentStateName = current ? getStateName(current) : null;
|
|
|
+ setCurrentState(currentStateName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setDotGraph(`
|
|
|
+ digraph {
|
|
|
+ ${states.map(([name])=>name
|
|
|
+ + (name === initialStateName ? `[color=blue, style=filled, fontcolor=white]`:``)
|
|
|
+ + (name === currentStateName ? `[shape=doublecircle]`:`[shape=circle]`)
|
|
|
+ ).join(' ')}
|
|
|
+ ${transitions.map(([src,tgt,label])=> src + ' -> ' + tgt + ' [label='+label+']').join('\n')}
|
|
|
+ }`);
|
|
|
+
|
|
|
+ return [states, transitions, designModelNode, initial,
|
|
|
+ runtimeModelNode, current];
|
|
|
+ }, [designModelState.version]);
|
|
|
+
|
|
|
+ const designModelComponents = designModel.getReactComponents(designModelState, setDesignModelState, {
|
|
|
+ onUserEdit: (deltas, description) => {
|
|
|
+ const newVersion = designModelReducer.createAndGotoNewVersion(deltas, description);
|
|
|
+ },
|
|
|
+ onUndoClicked: designModelReducer.undo,
|
|
|
+ onRedoClicked: designModelReducer.redo,
|
|
|
+ onVersionClicked: designModelReducer.gotoVersion,
|
|
|
+ onMerge: designModelReducer.appendVersions,
|
|
|
+ });
|
|
|
+
|
|
|
+ const [addStateName, setAddStateName] = React.useState<string>("A");
|
|
|
+ const [addTransitionSrc, setAddTransitionSrc] = React.useState<string>("");
|
|
|
+ const [addTransitionTgt, setAddTransitionTgt] = React.useState<string>("");
|
|
|
+ const [addTransitionEvent, setAddTransitionEvent] = React.useState<string>("e");
|
|
|
+
|
|
|
+ function addState() {
|
|
|
+ if (designModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+
|
|
|
+ const nodeId = generateUUID();
|
|
|
+ const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
|
|
|
+ designModel.graphState.exec(nodeCreation);
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "State"));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "name", addStateName));
|
|
|
+
|
|
|
+ designModelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => designModel.graphState.exec(d));
|
|
|
+
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "addState:"+addStateName);
|
|
|
+
|
|
|
+ // Auto-increment state names
|
|
|
+ if (addStateName.match(/[A-Z]/i)) {
|
|
|
+ setAddStateName(String.fromCharCode(addStateName.charCodeAt(0) + 1));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const getState = name => {
|
|
|
+ const s = states.find(([n]) => n === name);
|
|
|
+ if (s !== undefined) return s[1].creation;
|
|
|
+ }
|
|
|
+
|
|
|
+ function addTransition() {
|
|
|
+ if (designModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+
|
|
|
+ const nodeId = generateUUID();
|
|
|
+ const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
|
|
|
+ designModel.graphState.exec(nodeCreation);
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "Transition"));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "event", addTransitionEvent));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "src", getState(addTransitionSrc)!));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "tgt", getState(addTransitionTgt)!));
|
|
|
+
|
|
|
+ designModelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => designModel.graphState.exec(d));
|
|
|
+
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "addTransition:"+addTransitionSrc+"--"+addTransitionEvent+"->"+addTransitionTgt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onDeleteState(name: string, nodeState: INodeState) {
|
|
|
+ if (designModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+ while (true) {
|
|
|
+ // Delete all incoming and outgoing transitions:
|
|
|
+ const found = nodeState.getIncomingEdges().find(([label]) => label === "src" || label === "tgt");
|
|
|
+ if (found === undefined) break;
|
|
|
+ const [_, from] = found;
|
|
|
+ const deltas = from.getDeltasForDelete(primitiveRegistry);
|
|
|
+ console.log({deltas});
|
|
|
+ deltas.forEach(d => designModel.graphState.exec(d));
|
|
|
+ }
|
|
|
+ nodeState.getDeltasForDelete(primitiveRegistry).forEach(d => designModel.graphState.exec(d));
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ console.log({deltas});
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "deleteState:"+name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onInitialStateChange(value) {
|
|
|
+ if (designModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+ designModelNode.getDeltasForSetEdge(primitiveRegistry, "initial", getState(value) || null).forEach(d => designModel.graphState.exec(d));
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "setInitial:"+value);
|
|
|
+ setInitialState(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onInitialize() {
|
|
|
+ if (designModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+ const runtimeId = generateUUID();
|
|
|
+ const nodeCreation = primitiveRegistry.newNodeCreation(runtimeId);
|
|
|
+ designModel.graphState.exec(nodeCreation);
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "RuntimeModel"));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "design", designModelNode.creation));
|
|
|
+ designModel.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "current", initial.creation));
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "initialize");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onAbort() {
|
|
|
+ if (runtimeModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+ runtimeModelNode.getDeltasForDelete(primitiveRegistry).forEach(d => designModel.graphState.exec(d));
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "abort");
|
|
|
+ setCurrentState(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function onCurrentStateChange(value) {
|
|
|
+ if (runtimeModelNode !== null) {
|
|
|
+ designModel.graphState.pushState();
|
|
|
+ runtimeModelNode.getDeltasForSetEdge(primitiveRegistry, "current", getState(value) || null).forEach(d => designModel.graphState.exec(d));
|
|
|
+ const deltas = designModel.graphState.popState();
|
|
|
+ designModelReducer.createAndGotoNewVersion(deltas, "setCurrent:"+value);
|
|
|
+ setCurrentState(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return <>
|
|
|
+ <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
|
|
|
+ <SimpleGrid cols={2}>
|
|
|
+ <Stack>
|
|
|
+ <Group grow>
|
|
|
+ {designModelComponents.undoRedoButtons}
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ <TextInput value={addStateName} onChange={e => setAddStateName(e.currentTarget.value)} label="State Name" withAsterisk/>
|
|
|
+ <Button onClick={addState} disabled={addStateName===""||states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>}>Add State</Button>
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ <Select searchable clearable label="Source" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
|
|
|
+ <Select searchable clearable label="Target" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionTgt} onChange={setAddTransitionTgt}/>
|
|
|
+ <TextInput value={addTransitionEvent} onChange={e => setAddTransitionEvent(e.currentTarget.value)} label="Event" />
|
|
|
+ <Button disabled={addTransitionSrc === null || addTransitionTgt === null} onClick={addTransition} leftIcon={<Icons.IconPlus/>}>Add Transition</Button>
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ {
|
|
|
+ states.map(([stateName, stateNodeState]) => {
|
|
|
+ return <Button key={stateName} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(stateName, stateNodeState)}>Delete State {stateName}</Button>
|
|
|
+ })
|
|
|
+ }
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ {
|
|
|
+ transitions.map(([srcName, tgtName, label, tNodeState]) => {
|
|
|
+ const key = srcName+'--('+label+')-->'+tgtName;
|
|
|
+ return <Button key={key} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(key, tNodeState)}>Delete Transition {key}</Button>
|
|
|
+ })
|
|
|
+ }
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ <Select searchable clearable label="Initial State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={initialState} onChange={onInitialStateChange}/>
|
|
|
+ <Button disabled={initialState === null || runtimeModelNode !== null} onClick={onInitialize} leftIcon={<Icons.IconPlayerPlay/>}>Initialize Execution</Button>
|
|
|
+ </Group>
|
|
|
+ <Group>
|
|
|
+ <Select disabled={runtimeModelNode === null} searchable clearable label="Current State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={currentState} onChange={onCurrentStateChange}/>
|
|
|
+ <Button disabled={runtimeModelNode === null} onClick={onAbort} leftIcon={<Icons.IconPlayerStop/>}>Abort Execution</Button>
|
|
|
+ </Group>
|
|
|
+ <Graphviz dot={dotGraph} options={{fit:false}} className="canvas"/>
|
|
|
+ <Text>Powered by GraphViz and WebAssembly.</Text>
|
|
|
+ </Stack>
|
|
|
+ <Stack>
|
|
|
+ {designModelComponents.makeTabs("merge", ["state", "merge", "deltaL1", "deltaL0"])}
|
|
|
+ {designModelComponents.makeTabs("deltaL1", ["state", "merge", "deltaL1", "deltaL0"])}
|
|
|
+ </Stack>
|
|
|
+ </SimpleGrid>
|
|
|
+ </OnionContext.Provider>
|
|
|
+ </>;
|
|
|
+ }
|
|
|
+}
|