|
|
@@ -1,603 +0,0 @@
|
|
|
-import * as React from 'react';
|
|
|
-import * as Icons from '@tabler/icons';
|
|
|
-import {Button, Divider, Group, SimpleGrid, Text, Title, TextInput, Select, Stack, Paper, Alert, Center, MantineProvider, Tabs} from '@mantine/core';
|
|
|
-import { modals, ModalsProvider } from '@mantine/modals';
|
|
|
-
|
|
|
-import {GraphvizComponent} from "../graphviz";
|
|
|
-
|
|
|
-import {newOnion} from '../versioned_model/single_model';
|
|
|
-import {InfoHoverCard, InfoHoverCardOverlay} from "../info_hover_card";
|
|
|
-import {OnionContext} from "../onion_context";
|
|
|
-
|
|
|
-import {mockUuid} from "../../util/test_helpers";
|
|
|
-import {NodeDeletion, EdgeUpdate} from "onion/delta";
|
|
|
-import {DeltaRegistry} from "onion/delta_registry";
|
|
|
-import {VersionRegistry} from "onion/version";
|
|
|
-import {Delta} from "onion/delta";
|
|
|
-import {INodeState, IValueState} from "onion/graph_state";
|
|
|
-import {Version} from "onion/version";
|
|
|
-import {GraphState} from "onion/graph_state";
|
|
|
-
|
|
|
-export const demo_Live_description = <>
|
|
|
- <Title order={4}>
|
|
|
- Live Modeling
|
|
|
- </Title>
|
|
|
- <Text>This demo allows you to edit a Finite State Automaton (FSA) via a projectional editor, and to execute it via an interpreter.
|
|
|
- </Text>
|
|
|
- <Text>The model can be edited during its execution (called <em>live modeling</em> in literature). Live modeling can be done collaboratively as well. This works because the abstract syntax and the execution state are just versioned graphs.
|
|
|
- </Text>
|
|
|
-</>;
|
|
|
-
|
|
|
-const getStateName = (s: INodeState) => (s.outgoing.get("name") as IValueState).value as string;
|
|
|
-
|
|
|
-export function getDemoLive() {
|
|
|
- const deltaRegistry = new DeltaRegistry();
|
|
|
- const generateUUID = mockUuid();
|
|
|
- const versionRegistry = new VersionRegistry();
|
|
|
- const onion = newOnion({readonly: true, deltaRegistry, versionRegistry});
|
|
|
-
|
|
|
- // To efficiently create a delta in the context of the design model only
|
|
|
- const designModelGraphState = new GraphState();
|
|
|
-
|
|
|
- const modelId = generateUUID();
|
|
|
-
|
|
|
- return function DemoSem() {
|
|
|
-
|
|
|
- const {state, reducer, components} = onion.useOnion(reducer => ({
|
|
|
- onUndoClicked: (parentVersion, deltaToUndo) => {
|
|
|
- gotoMatchingDesignVersion(parentVersion);
|
|
|
- reducer.undo(parentVersion, deltaToUndo);
|
|
|
- },
|
|
|
- onRedoClicked: (childVersion, deltaToRedo) => {
|
|
|
- gotoMatchingDesignVersion(childVersion);
|
|
|
- reducer.redo(childVersion, deltaToRedo);
|
|
|
- },
|
|
|
- onVersionClicked: (version: Version) => {
|
|
|
- gotoMatchingDesignVersion(version);
|
|
|
- reducer.gotoVersion(version);
|
|
|
- },
|
|
|
- }));
|
|
|
-
|
|
|
- const gotoMatchingDesignVersion = (rtVersion: Version) => {
|
|
|
- const currentDesignVersion = state.version.getEmbedding("design").version;
|
|
|
- const newDesignVersion = rtVersion.getEmbedding("design").version;
|
|
|
- const path = currentDesignVersion.findPathTo(newDesignVersion)!;
|
|
|
- for (const [linkType, delta] of path) {
|
|
|
- if (linkType === 'p') {
|
|
|
- designModelGraphState.unexec(delta);
|
|
|
- }
|
|
|
- else if (linkType === 'c') {
|
|
|
- designModelGraphState.exec(delta);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // We start out with the 'createList' delta already having occurred:
|
|
|
- React.useEffect(() => {
|
|
|
- // idempotent:
|
|
|
- const modelCreation = deltaRegistry.newNodeCreation(modelId);
|
|
|
- const newVersion = reducer.createAndGotoNewVersion([
|
|
|
- modelCreation,
|
|
|
- deltaRegistry.newEdgeUpdate(modelCreation.createOutgoingEdge("type"), "DesignModel"),
|
|
|
- ], "createDesignModel", versionRegistry.initialVersion,
|
|
|
- newDesignVersion => new Map([
|
|
|
- ["design", {version: newDesignVersion, overridings: new Map()}],
|
|
|
- ]));
|
|
|
- gotoMatchingDesignVersion(newVersion);
|
|
|
- }, []);
|
|
|
-
|
|
|
- type Transition = [string, string, string, INodeState];
|
|
|
-
|
|
|
- const getDesignModelStuff = (graphState: GraphState) => {
|
|
|
- const states: Array<[string, INodeState]> = [];
|
|
|
- const transitions: Array<Transition> = [];
|
|
|
- const events: Array<[string,Array<Transition>]> = [];
|
|
|
- let modelNode: INodeState|null = null;
|
|
|
- let initial: INodeState|undefined;
|
|
|
- let initialStateName : string|null = null;
|
|
|
-
|
|
|
- for (const nodeState of iterNonDeletedNodes(graphState)) {
|
|
|
- const nodeType = getProperty<string>(nodeState, "type")!;
|
|
|
- ({
|
|
|
- "State": () => {
|
|
|
- states.push([getStateName(nodeState), nodeState]);
|
|
|
- },
|
|
|
- "Transition": () => {
|
|
|
- const event = getProperty<string>(nodeState, "event")!;
|
|
|
- const transition: Transition = [
|
|
|
- getProperty<string>(getLink(nodeState, "src")!, "name")!,
|
|
|
- getProperty<string>(getLink(nodeState, "tgt")!, "name")!,
|
|
|
- event,
|
|
|
- nodeState,
|
|
|
- ];
|
|
|
- transitions.push(transition);
|
|
|
- const existingEvent = events.find(([e, transition]) => e === event);
|
|
|
- if (existingEvent === undefined) {
|
|
|
- events.push([event, [transition]]);
|
|
|
- }
|
|
|
- else {
|
|
|
- existingEvent[1].push(transition);
|
|
|
- }
|
|
|
- },
|
|
|
- "DesignModel": () => {
|
|
|
- modelNode = nodeState;
|
|
|
- initial = getLink(nodeState, "initial");
|
|
|
- initialStateName = initial ? getStateName(initial) : null;
|
|
|
- },
|
|
|
- }[nodeType])?.();
|
|
|
- }
|
|
|
-
|
|
|
- const result: {
|
|
|
- states: [string,INodeState][],
|
|
|
- transitions: [string,string,string,INodeState][],
|
|
|
- modelNode: INodeState|null,
|
|
|
- initial: INodeState|undefined,
|
|
|
- initialStateName: string|null,
|
|
|
- events: Array<[string,Array<Transition>]>,
|
|
|
- } = {
|
|
|
- states,
|
|
|
- transitions,
|
|
|
- modelNode,
|
|
|
- initial,
|
|
|
- initialStateName,
|
|
|
- events,
|
|
|
- };
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- const getRuntimeModelStuff = (graphState: GraphState) => {
|
|
|
- const designModelStuff = getDesignModelStuff(graphState);
|
|
|
- let runtimeModelNode: INodeState|null = null;
|
|
|
- let currentStateName : string|null = null;
|
|
|
- let current;
|
|
|
-
|
|
|
- for (const nodeState of iterNonDeletedNodes(graphState)) {
|
|
|
- const nodeType = getProperty<string>(nodeState, "type")!;
|
|
|
- ({
|
|
|
- RuntimeModel: () => {
|
|
|
- runtimeModelNode = nodeState;
|
|
|
- current = getLink(nodeState, "current");
|
|
|
- currentStateName = current ? getStateName(current) : null;
|
|
|
- },
|
|
|
- }[nodeType])?.();
|
|
|
- }
|
|
|
-
|
|
|
- const dotGraph = `digraph {
|
|
|
- bgcolor="transparent";
|
|
|
- rankdir="LR";
|
|
|
- ${designModelStuff.initial !== undefined ? "I [shape=point, width=0.1]; I -> S_"+designModelStuff.initialStateName:""}
|
|
|
- ${designModelStuff.states.map(([name])=>'S_'+name+`[label=${name}, fillcolor=white, style=filled]`
|
|
|
- + (name === currentStateName ? `[penwidth=4.0, shape=circle]`:`[shape=circle]`)
|
|
|
- ).join(' ')}
|
|
|
- ${designModelStuff.transitions.map(([src,tgt,label])=> 'S_'+src + ' -> ' + 'S_'+tgt + ' [label='+label+']').join('\n')}
|
|
|
- }`;
|
|
|
-
|
|
|
- const result: {
|
|
|
- runtimeModelNode: INodeState|null,
|
|
|
- current: INodeState|null,
|
|
|
- currentStateName: string|null,
|
|
|
- dotGraph: string
|
|
|
- } = {
|
|
|
- runtimeModelNode,
|
|
|
- current,
|
|
|
- currentStateName,
|
|
|
- dotGraph};
|
|
|
- return Object.assign(result, designModelStuff);
|
|
|
- };
|
|
|
-
|
|
|
- // Whenever the current version changes, we calculate a bunch of values that are needed in the UI and in its callbacks
|
|
|
- const runtimeStuff = React.useMemo(() => getRuntimeModelStuff(onion.graphState),
|
|
|
- [state.version]); // memoize the values for each version
|
|
|
-
|
|
|
- const designStuff = React.useMemo(() => getDesignModelStuff(designModelGraphState),
|
|
|
- [state.version.getEmbedding("design").version]); // memoize the values for each version
|
|
|
-
|
|
|
- // If the current (runtime) version, or any of its ancestors overrides some delta, we know that there was a conflict between a delta of the runtime version and its embedded design version, meaning that the current execution trace cannot be replayed.
|
|
|
- const notReplayable = (() => {
|
|
|
- let v = state.version;
|
|
|
- while (v !== undefined) {
|
|
|
- const designEmbedding = v.embeddings.get("design");
|
|
|
- if (designEmbedding && designEmbedding.overridings.size > 0) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- v = v.parents[0]?.[0];
|
|
|
- }
|
|
|
- return false;
|
|
|
- })();
|
|
|
-
|
|
|
- const [addStateName, setAddStateName] = React.useState<string>("A");
|
|
|
- const [addTransitionSrc, setAddTransitionSrc] = React.useState<string|null>(null);
|
|
|
- const [addTransitionTgt, setAddTransitionTgt] = React.useState<string|null>(null);
|
|
|
- const [addTransitionEvent, setAddTransitionEvent] = React.useState<string>("e");
|
|
|
-
|
|
|
- const execTransaction = (graphState, modelNode, callback) => {
|
|
|
- graphState.pushState();
|
|
|
- const {compositeLabel} = callback(graphState, modelNode);
|
|
|
- const deltas = graphState.popState();
|
|
|
- return {compositeLabel, deltas};
|
|
|
- }
|
|
|
-
|
|
|
- const editDesignModel = callback => {
|
|
|
- if (runtimeStuff.modelNode !== null) {
|
|
|
- const currentDesignVersion = state.version.getEmbedding("design").version;
|
|
|
-
|
|
|
- // perform edit on run-time model:
|
|
|
- const {compositeLabel, deltas: runtimeDeltas} = execTransaction(onion.graphState, runtimeStuff.modelNode, callback);
|
|
|
-
|
|
|
- // see if the same delta can be applied also on the design model:
|
|
|
- // TODO: this try-catch is waaaayyyy too implicit and error-prone - need to check explicitly whether there are missing dependencies or not.
|
|
|
- let newDesignVersion;
|
|
|
- let designDeltas;
|
|
|
- try {
|
|
|
- newDesignVersion = reducer.createVersion(runtimeDeltas, "design:"+compositeLabel, currentDesignVersion.hash,
|
|
|
- newDesignVersion => new Map([
|
|
|
- ["design", {version: newDesignVersion, overridings: new Map()}],
|
|
|
- ]))!; // will throw if any of runtimeDeltas depends on a delta that is not in currentDesignVersion.
|
|
|
- designDeltas = runtimeDeltas;
|
|
|
- }
|
|
|
- catch (e) {
|
|
|
- // alert("could not apply this exact same delta on the design model - probably there's a missing dependency:\n" + e.toString() + "\nTherefore, I will attempt to create a new delta with the same /intent/ on the design model.");
|
|
|
-
|
|
|
- const {compositeLabel, deltas} = execTransaction(designModelGraphState, designStuff.modelNode, callback);
|
|
|
- designDeltas = deltas;
|
|
|
- newDesignVersion = reducer.createVersion(designDeltas, "design':"+compositeLabel, currentDesignVersion.hash,
|
|
|
- newDesignVersion => new Map([
|
|
|
- ["design", {version: newDesignVersion, overridings: new Map()}],
|
|
|
- ]))!; // won't throw this time
|
|
|
- }
|
|
|
-
|
|
|
- const overridings: Map<Delta,Delta> = new Map(designDeltas
|
|
|
- .map(designDelta => runtimeDeltas.includes(designDelta) ? null // no override - runtimeDeltas includes the exact same delta
|
|
|
- // otherwise, lookup override (which will probably have some additional dependencies on RT deltas)
|
|
|
- : [designDelta, runtimeDeltas.find(runtimeDelta => {
|
|
|
- if (designDelta instanceof NodeDeletion) {
|
|
|
- return runtimeDelta instanceof NodeDeletion && runtimeDelta.node === designDelta.node;
|
|
|
- }
|
|
|
- else if (designDelta instanceof EdgeUpdate) {
|
|
|
- return runtimeDelta instanceof EdgeUpdate && runtimeDelta.overwrites === runtimeDelta.overwrites;
|
|
|
- }
|
|
|
- throw new Error(`I don't know yet how to override this delta: ${runtimeDelta.constructor.name}: ${runtimeDelta.description}`);
|
|
|
- })!])
|
|
|
- .filter(x => x!==null)
|
|
|
- );
|
|
|
- const newRtVersion = reducer.createAndGotoNewVersion(runtimeDeltas, "design:"+compositeLabel,
|
|
|
- state.version, // parent version
|
|
|
- // embeddings:
|
|
|
- newRtVersion => newRtVersion === newDesignVersion ?
|
|
|
- new Map([
|
|
|
- ["design", {version: newDesignVersion, overridings}],
|
|
|
- ])
|
|
|
- : new Map([
|
|
|
- ["design", {version: newDesignVersion, overridings}],
|
|
|
- ["runtime", {version: newRtVersion, overridings: new Map()}],
|
|
|
- ]));
|
|
|
-
|
|
|
- gotoMatchingDesignVersion(newRtVersion);
|
|
|
- }
|
|
|
- }
|
|
|
- const editRuntimeModel = (callback, gotoNewVersion = true) => {
|
|
|
- const currentDesignVersion = state.version.getEmbedding("design").version;
|
|
|
-
|
|
|
- // perform edit on run-time model:
|
|
|
- const {compositeLabel, deltas} = execTransaction(onion.graphState, runtimeStuff.modelNode, callback);
|
|
|
- const newRtVersion = reducer.createVersion(deltas, "runtime:"+compositeLabel,
|
|
|
- state.version.hash, // parent version
|
|
|
- newRtVersion => new Map([
|
|
|
- ["design", {version: currentDesignVersion, overridings: new Map()}],
|
|
|
- ["runtime", {version: newRtVersion, overridings: new Map()}],
|
|
|
- ]));
|
|
|
- if (gotoNewVersion) {
|
|
|
- reducer.gotoVersion(newRtVersion!);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- function* iterNonDeletedNodes(graphState: GraphState) {
|
|
|
- for (const nodeState of graphState.nodes.values()) {
|
|
|
- if (!nodeState.isDeleted) {
|
|
|
- yield nodeState;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- function* iterType(graphState: GraphState, type: string) {
|
|
|
- for (const nodeState of iterNonDeletedNodes(graphState)) {
|
|
|
- const nodeType = (nodeState.outgoing.get("type") as IValueState)?.value as string;
|
|
|
- if (nodeType === type) {
|
|
|
- yield nodeState;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- function getProperty<T>(nodeState: INodeState, property: string): (T|undefined) {
|
|
|
- return (nodeState.outgoing.get(property) as IValueState)?.value as T;
|
|
|
- }
|
|
|
- function getLink(nodeState: INodeState, linkName: string): (INodeState|undefined) {
|
|
|
- return (nodeState.outgoing.get(linkName) as INodeState);
|
|
|
- }
|
|
|
- const findState = (graphState: GraphState, stateName: string) => {
|
|
|
- for (const nodeState of iterType(graphState, "State")) {
|
|
|
- if (getProperty<string>(nodeState, "name") === stateName) {
|
|
|
- return nodeState;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- const findTransition = (graphState: GraphState, srcName: string, tgtName: string, event: string) => {
|
|
|
- for (const nodeState of iterType(graphState, "Transition")) {
|
|
|
- const src = getLink(nodeState, "src");
|
|
|
- const tgt = getLink(nodeState, "tgt");
|
|
|
- const event = getProperty(nodeState, "event");
|
|
|
- if (src && tgt) {
|
|
|
- if (getProperty(src, "name") === srcName && getProperty(tgt, "name") && event === event) {
|
|
|
- return nodeState
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- function onAddState() {
|
|
|
- editDesignModel((graphState, modelNode) => {
|
|
|
- const nodeId = generateUUID();
|
|
|
- const nodeCreation = deltaRegistry.newNodeCreation(nodeId);
|
|
|
- graphState.exec(nodeCreation);
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("type"), "State"));
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("name"), addStateName));
|
|
|
-
|
|
|
- graphState.exec(modelNode.getDeltaForSetEdge(deltaRegistry, "contains-"+JSON.stringify(nodeId), nodeCreation));
|
|
|
-
|
|
|
- // Bonus feature: Auto-increment state names
|
|
|
- if (addStateName.match(/[A-Z]/i)) {
|
|
|
- setAddStateName(String.fromCharCode(addStateName.charCodeAt(0) + 1));
|
|
|
- }
|
|
|
-
|
|
|
- return {compositeLabel: "addState:"+addStateName};
|
|
|
- });
|
|
|
- }
|
|
|
- function onAddTransition() {
|
|
|
- editDesignModel((graphState, modelNode) => {
|
|
|
- const nodeId = generateUUID();
|
|
|
- const nodeCreation = deltaRegistry.newNodeCreation(nodeId);
|
|
|
- graphState.exec(nodeCreation);
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("type"), "Transition"));
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("event"), addTransitionEvent));
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("src"), findState(graphState, addTransitionSrc!)!.creation));
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("tgt"), findState(graphState, addTransitionTgt!)!.creation));
|
|
|
-
|
|
|
- graphState.exec(modelNode.getDeltaForSetEdge(deltaRegistry, "contains-"+JSON.stringify(nodeId), nodeCreation));
|
|
|
-
|
|
|
- return {compositeLabel: "addTransition:"+addTransitionSrc+"--"+addTransitionEvent+"->"+addTransitionTgt};
|
|
|
- });
|
|
|
- }
|
|
|
- function onDeleteState(name: string) {
|
|
|
- editDesignModel((graphState) => {
|
|
|
- const nodeState = findState(graphState, name)!;
|
|
|
- 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(deltaRegistry);
|
|
|
- deltas.forEach(d => graphState.exec(d));
|
|
|
- }
|
|
|
- nodeState.getDeltasForDelete(deltaRegistry).forEach(d => {
|
|
|
- graphState.exec(d);
|
|
|
- });
|
|
|
- return {compositeLabel: "deleteState:"+name};
|
|
|
- });
|
|
|
- }
|
|
|
- function onDeleteTransition(src: string, event: string, tgt: string) {
|
|
|
- editDesignModel((graphState) => {
|
|
|
- const nodeState = findTransition(graphState, src, event, tgt)!;
|
|
|
- nodeState.getDeltasForDelete(deltaRegistry).forEach(d => {
|
|
|
- graphState.exec(d);
|
|
|
- });
|
|
|
- return {compositeLabel: "deleteTransition:"+transitionKey([src, tgt, event])};
|
|
|
- });
|
|
|
- }
|
|
|
- function onInitialStateChange(value) {
|
|
|
- editDesignModel((graphState, modelNode) => {
|
|
|
- graphState.exec(modelNode.getDeltaForSetEdge(deltaRegistry, "initial", findState(graphState, value)?.creation || null));
|
|
|
- return {compositeLabel: "setInitial:"+value};
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- function onInitialize() {
|
|
|
- if (runtimeStuff.modelNode !== null && runtimeStuff.initial !== null) {
|
|
|
- editRuntimeModel((graphState) => {
|
|
|
- const runtimeId = generateUUID();
|
|
|
- const nodeCreation = deltaRegistry.newNodeCreation(runtimeId);
|
|
|
- graphState.exec(nodeCreation);
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("type"), "RuntimeModel"));
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("design"), runtimeStuff.modelNode!.creation));
|
|
|
- // set the 'current' pointer - read dependency on 'initial'.
|
|
|
- graphState.exec(deltaRegistry.newEdgeUpdate(nodeCreation.createOutgoingEdge("current"), runtimeStuff.initial!.creation, [(runtimeStuff.modelNode!.outgoingDeltas.get("initial")!.read())]));
|
|
|
- return {compositeLabel: "initialize"};
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- function onAbort() {
|
|
|
- if (runtimeStuff.runtimeModelNode !== null) {
|
|
|
- editRuntimeModel((graphState) => {
|
|
|
- runtimeStuff.runtimeModelNode!.getDeltasForDelete(deltaRegistry).forEach(d => graphState.exec(d));
|
|
|
- return {compositeLabel: "abort"};
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- function onCurrentStateChange(value) {
|
|
|
- if (runtimeStuff.runtimeModelNode !== null) {
|
|
|
- editRuntimeModel((graphState) => {
|
|
|
- graphState.exec(runtimeStuff.runtimeModelNode!.getDeltaForSetEdge(deltaRegistry, "current", findState(graphState, value)?.creation || null, []));
|
|
|
- return {compositeLabel: "setCurrent:"+value}
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- function onExecuteStep([srcName, tgtName, label, transition]: Transition, gotoNewVersion = false) {
|
|
|
- if (runtimeStuff.runtimeModelNode !== null) {
|
|
|
- editRuntimeModel((graphState) => {
|
|
|
- graphState.exec(
|
|
|
- runtimeStuff.runtimeModelNode!.getDeltaForSetEdge(deltaRegistry,
|
|
|
- "current", // label
|
|
|
- findState(graphState, tgtName)?.creation || null, // target
|
|
|
- // read dependency on src,tgt,label of transition:
|
|
|
- ["src", "tgt", "event"].map(label => transition.outgoingDeltas.get(label)!.read()),
|
|
|
- )
|
|
|
- );
|
|
|
- return {compositeLabel: "executeStep:"+transitionKey([srcName, tgtName, label])};
|
|
|
- }, gotoNewVersion);
|
|
|
- }
|
|
|
- }
|
|
|
- function onExecuteNonDeterministicStep(transitions: Transition[]) {
|
|
|
- if (runtimeStuff.runtimeModelNode !== null) {
|
|
|
- const deterministic = transitions.length <= 1;
|
|
|
- if (!deterministic) {
|
|
|
- modals.open({
|
|
|
- title: (<b>Non-determinism!</b>),
|
|
|
- children: (
|
|
|
- <>
|
|
|
- <Text>Multiple futures exist.</Text>
|
|
|
- <Text>Use <b>Redo</b> or <b>History</b> to select your desired future.</Text>
|
|
|
- <Button fullWidth onClick={modals.closeAll} mt="md">
|
|
|
- OK
|
|
|
- </Button>
|
|
|
- </>
|
|
|
- ),
|
|
|
- });
|
|
|
- }
|
|
|
- for (const transition of transitions) {
|
|
|
- onExecuteStep(transition, deterministic /* only go to next version if only one future exists */);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const transitionKey = ([srcName, tgtName, label]) =>
|
|
|
- srcName+'--('+label+')-->'+tgtName;
|
|
|
-
|
|
|
- return <>
|
|
|
- <MantineProvider>
|
|
|
- <ModalsProvider>
|
|
|
- <OnionContext.Provider value={{generateUUID, deltaRegistry}}>
|
|
|
- <SimpleGrid cols={2}>
|
|
|
- <Stack>
|
|
|
- <Group grow>
|
|
|
- {components.undoRedoButtons}
|
|
|
- </Group>
|
|
|
- <Divider label="(Projectional) Model Editor" labelPosition="centered" />
|
|
|
- {
|
|
|
- runtimeStuff.modelNode === null ?
|
|
|
- <Alert icon={<Icons.IconAlertCircle/>} title="No design model!" color="blue">
|
|
|
- Did you undo its creation?
|
|
|
- </Alert>
|
|
|
- :<></>
|
|
|
- }
|
|
|
- <Group grow>
|
|
|
- <Paper shadow="xs" p="xs" withBorder>
|
|
|
- <Group grow>
|
|
|
- <TextInput value={addStateName} onChange={e => setAddStateName(e.currentTarget.value)} label="State Name" withAsterisk/>
|
|
|
- <Button onClick={onAddState} disabled={addStateName===""||runtimeStuff.modelNode===null||runtimeStuff.states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>} color="green">State</Button>
|
|
|
- </Group>
|
|
|
- </Paper>
|
|
|
- <Select disabled={runtimeStuff.modelNode===null} searchable clearable
|
|
|
- label="Initial State"
|
|
|
- data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))}
|
|
|
- value={runtimeStuff.initialStateName}
|
|
|
- onChange={onInitialStateChange}
|
|
|
- />
|
|
|
- </Group>
|
|
|
- <div style={{minHeight: 26}}>
|
|
|
- <Group>{
|
|
|
- runtimeStuff.states.map(([stateName]) => {
|
|
|
- return <Button compact color="red" key={stateName} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(stateName)}>State {stateName}</Button>
|
|
|
- })
|
|
|
- }</Group>
|
|
|
- </div>
|
|
|
- <Paper shadow="xs" p="xs" withBorder>
|
|
|
- <Group grow>
|
|
|
- <Select searchable withAsterisk clearable label="Source" data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
|
|
|
- <Select searchable withAsterisk clearable label="Target" data={runtimeStuff.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={onAddTransition} leftIcon={<Icons.IconPlus/>} color="green">Transition</Button>
|
|
|
- </Group>
|
|
|
- </Paper>
|
|
|
- <div style={{minHeight: 26}}>
|
|
|
- <Group>{
|
|
|
- runtimeStuff.transitions.map(([srcName, tgtName, label, tNodeState]) => {
|
|
|
- const key = srcName+'--('+label+')-->'+tgtName;
|
|
|
- return <Button compact color="red" key={key} leftIcon={<Icons.IconX/>} onClick={() => onDeleteTransition(srcName, label, tgtName)}>Transition {key}</Button>
|
|
|
- })
|
|
|
- }</Group>
|
|
|
- </div>
|
|
|
-
|
|
|
- <Divider label="Execution" labelPosition="centered" />
|
|
|
-
|
|
|
- {notReplayable ?
|
|
|
- <Alert variant="light" color="orange" title="" icon={<Icons.IconAlertTriangle/>}>
|
|
|
- Current trace cannot be replayed on current design model.
|
|
|
- </Alert>
|
|
|
- : <></>
|
|
|
- }
|
|
|
-
|
|
|
- <Group grow>
|
|
|
- <Button disabled={runtimeStuff.initialStateName === null || runtimeStuff.runtimeModelNode !== null} onClick={onInitialize} leftIcon={<Icons.IconPlayerPlay/>}>Init</Button>
|
|
|
- <Button color="red" disabled={runtimeStuff.runtimeModelNode === null} onClick={onAbort} leftIcon={<Icons.IconPlayerStop/>}>Abort</Button>
|
|
|
- <Select disabled={runtimeStuff.currentStateName === null || runtimeStuff.runtimeModelNode === null} data={runtimeStuff.transitions.filter(([srcName]) => srcName === runtimeStuff.currentStateName).map(t => {
|
|
|
- // @ts-ignore:
|
|
|
- const key = transitionKey(t);
|
|
|
- return {
|
|
|
- value: key,
|
|
|
- label: key,
|
|
|
- }
|
|
|
- })} label="Execute Step" onChange={key => {
|
|
|
- const t = runtimeStuff.transitions.find(t =>
|
|
|
- // @ts-ignore:
|
|
|
- transitionKey(t) == key);
|
|
|
- onExecuteStep(t!);
|
|
|
- }}/>
|
|
|
- <Select disabled={runtimeStuff.runtimeModelNode === null} searchable clearable label={<Group>Current State <InfoHoverCard>
|
|
|
- <Text>Current state updates when executing an execution step, but can also be overridden at any point in time ("god event").</Text>
|
|
|
- </InfoHoverCard></Group>} data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))} value={runtimeStuff.currentStateName} onChange={onCurrentStateChange}/>
|
|
|
- </Group>
|
|
|
-
|
|
|
- <div style={{minHeight: 26}}>
|
|
|
- <Group>{
|
|
|
- designStuff.events.map(([event, transitions]) => {
|
|
|
- const enabledTransitions = transitions.filter(([sourceName]) => runtimeStuff.currentStateName === sourceName);
|
|
|
- return <Button compact disabled={runtimeStuff.runtimeModelNode === null || enabledTransitions.length === 0} color="orange" key={event} leftIcon={<Icons.IconActivity/>} onClick={() => onExecuteNonDeterministicStep(enabledTransitions)}>{event}</Button>
|
|
|
- })
|
|
|
- }</Group>
|
|
|
- </div>
|
|
|
-
|
|
|
- <Tabs defaultValue="cs" keepMounted={false}>
|
|
|
- <Tabs.List grow>
|
|
|
- <Tabs.Tab value="cs">Concrete Syntax (projectional)</Tabs.Tab>
|
|
|
- <Tabs.Tab value="as">Abstract Syntax</Tabs.Tab>
|
|
|
- </Tabs.List>
|
|
|
- <Tabs.Panel value="cs">
|
|
|
- <InfoHoverCardOverlay contents={<>
|
|
|
- <Text>Simple auto-generated concrete syntax, from abstract syntax.</Text>
|
|
|
- <Text>Powered by GraphViz and WebAssembly.</Text>
|
|
|
- </>}>
|
|
|
- <div style={{backgroundColor:"#eee"}}>{
|
|
|
- runtimeStuff.states.length > 0 ?
|
|
|
- <GraphvizComponent dot={runtimeStuff.dotGraph} />
|
|
|
- : <Center position="center" style={{padding:20}}>
|
|
|
- <Text>FSA will appear here.</Text>
|
|
|
- </Center>
|
|
|
- }</div>
|
|
|
- </InfoHoverCardOverlay>
|
|
|
- </Tabs.Panel>
|
|
|
- <Tabs.Panel value="as">
|
|
|
- {components.graphStateComponent}
|
|
|
- </Tabs.Panel>
|
|
|
- </Tabs>
|
|
|
- </Stack>
|
|
|
- <Stack>
|
|
|
- {components.makeTabs("deltaL1", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL0"])}
|
|
|
- {components.makeTabs("merge", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL1Graphviz", "deltaL0", "deltaL0Graphviz"])}
|
|
|
- Run-time model version: {state.version.hash.toString('hex').substring(0,8)}<br/>
|
|
|
- DesignModel version: {state.version.getEmbedding("design").version.hash.toString('hex').substring(0,8)}
|
|
|
- </Stack>
|
|
|
- </SimpleGrid>
|
|
|
- </OnionContext.Provider>
|
|
|
- </ModalsProvider>
|
|
|
- </MantineProvider>
|
|
|
- </>;
|
|
|
- }
|
|
|
-}
|