incubator-seata
incubator-seata copied to clipboard
Support auto-layout for state machine designer
Why you need it?
Currently, there are two ways to define a StateMachine
. One is to use the designer, and the other is to directly write JSON. If users adopt the latter (i.e., do not write JSON through the designer), then it is not supported to import it into the designer because there is no style information (shape, edge positioning). If auto-layout is supported, this scenario will be indirectly supported.
How it could be?
You can extend SagaImporter
to add style information.
@ptyin
Can you please assign this task to me?
@ptyin
Can you please assign this task to me?
Sure, you are more than welcomed. I do not have repo permissions right now, but you can just work on it.
I recommend you can refer to bpmn-auto-layout implementation, or any other ways you like. Looking forward to your further feedback and PR.
@ptyin
Can you please assign this task to me?
Hi!The issue has been assigned to you.Looking forward your PR.
Hi @ptyin ,
Do you have any existing similar PR so that I can review it?
Hi @ptyin ,
Do you have any existing similar PR so that I can review it?
Some designer related PRs include:
- #6169
- #6118
However, I don not have any ongoing PRs with similar functionality. You can check README to get acquainted with the designer project structure and some diagram-js basics first.
Remember the key for auto-layout is to add missing style
, edge
(if any outgoing edge) and catch
(if any attachers) properties for each element.
Example
Suppose we have a Saga state machine definition as followed,
{
"Name": "StateMachineAutoLayout",
"Comment": "This state machine is modeled by designer tools.",
"Version": "0.0.1",
"States": {
"ServiceTask-a9h2o51": {
"Name": "ServiceTask-a9h2o51",
"CompensateState": "CompensateFirstState",
"Type": "ServiceTask",
"Next": "ServiceTask-vdij28l"
},
"CompensateFirstState": {
"Name": "CompensateFirstState",
"IsForCompensation": true,
"Type": "ServiceTask"
},
"ServiceTask-vdij28l": {
"Name": "ServiceTask-vdij28l",
"IsForCompensation": false,
"Catch": [
{
"Exceptions": [],
"Next": "CompensationTrigger-uldp2ou"
}
],
"Type": "ServiceTask",
"Next": "Succeed-d4d41uw"
},
"CompensationTrigger-uldp2ou": {
"Name": "CompensationTrigger-uldp2ou",
"Type": "CompensationTrigger",
"Next": "Fail-9roxcv5"
},
"Fail-9roxcv5": {
"Name": "Fail-9roxcv5",
"Type": "Fail"
},
"Succeed-d4d41uw": {
"Name": "Succeed-d4d41uw",
"Type": "Succeed"
}
},
"StartState": "ServiceTask-a9h2o51"
}
It can be directly parsed by Java StateMachineEngine
, however it cannot be imported to the designer due to lack of style information (like shape boundaries, edge waypoints, attached position, etc.). What we expect is to add these information, make the definition something like
{
"Name": "StateMachineAutoLayout",
"Comment": "This state machine is modeled by designer tools.",
"Version": "0.0.1",
"style": {
"bounds": {
"x": 200,
"y": 200,
"width": 36,
"height": 36
}
},
"States": {
"ServiceTask-a9h2o51": {
"style": {
"bounds": {
"x": 300,
"y": 178,
"width": 100,
"height": 80
}
},
"Name": "ServiceTask-a9h2o51",
"IsForCompensation": false,
"CompensateState": "CompensateFirstState",
"Type": "ServiceTask",
"edge": {
"CompensateFirstState": {
"style": {
"waypoints": [
{
"original": {
"x": 350,
"y": 258
},
"x": 350,
"y": 258
},
{
"x": 350,
"y": 290
},
{
"original": {
"x": 350,
"y": 310
},
"x": 350,
"y": 310
}
],
"source": "ServiceTask-a9h2o51",
"target": "CompensateFirstState"
},
"Type": "Compensation"
},
"ServiceTask-vdij28l": {
"style": {
"waypoints": [
{
"original": {
"x": 400,
"y": 218
},
"x": 400,
"y": 218
},
{
"x": 530,
"y": 218
},
{
"original": {
"x": 550,
"y": 218
},
"x": 550,
"y": 218
}
],
"source": "ServiceTask-a9h2o51",
"target": "ServiceTask-vdij28l"
},
"Type": "Transition"
}
},
"Next": "ServiceTask-vdij28l"
},
"CompensateFirstState": {
"style": {
"bounds": {
"x": 300,
"y": 310,
"width": 100,
"height": 80
}
},
"Name": "CompensateFirstState",
"IsForCompensation": true,
"Type": "ServiceTask"
},
"ServiceTask-vdij28l": {
"style": {
"bounds": {
"x": 550,
"y": 178,
"width": 100,
"height": 80
}
},
"Name": "ServiceTask-vdij28l",
"IsForCompensation": false,
"Catch": [
{
"Exceptions": [],
"Next": "CompensationTrigger-uldp2ou"
}
],
"Type": "ServiceTask",
"catch": {
"style": {
"bounds": {
"x": 632,
"y": 240,
"width": 36,
"height": 36
}
},
"edge": {
"CompensationTrigger-uldp2ou": {
"style": {
"waypoints": [
{
"original": {
"x": 650,
"y": 276
},
"x": 650,
"y": 276
},
{
"x": 650,
"y": 282
},
{
"original": {
"x": 650,
"y": 302
},
"x": 650,
"y": 302
}
],
"source": "ServiceTask-vdij28l",
"target": "CompensationTrigger-uldp2ou"
},
"Type": "ExceptionMatch"
}
}
},
"Next": "Succeed-d4d41uw",
"edge": {
"Succeed-d4d41uw": {
"style": {
"waypoints": [
{
"original": {
"x": 650,
"y": 218
},
"x": 650,
"y": 218
},
{
"x": 792,
"y": 218
},
{
"original": {
"x": 812,
"y": 218
},
"x": 812,
"y": 218
}
],
"source": "ServiceTask-vdij28l",
"target": "Succeed-d4d41uw"
},
"Type": "Transition"
}
}
},
"CompensationTrigger-uldp2ou": {
"style": {
"bounds": {
"x": 632,
"y": 302,
"width": 36,
"height": 36
}
},
"Name": "CompensationTrigger-uldp2ou",
"Type": "CompensationTrigger",
"Next": "Fail-9roxcv5",
"edge": {
"Fail-9roxcv5": {
"style": {
"waypoints": [
{
"original": {
"x": 668,
"y": 320
},
"x": 668,
"y": 320
},
{
"x": 792,
"y": 320
},
{
"original": {
"x": 812,
"y": 320
},
"x": 812,
"y": 320
}
],
"source": "CompensationTrigger-uldp2ou",
"target": "Fail-9roxcv5"
},
"Type": "Transition"
}
}
},
"Fail-9roxcv5": {
"style": {
"bounds": {
"x": 812,
"y": 302,
"width": 36,
"height": 36
}
},
"Name": "Fail-9roxcv5",
"Type": "Fail"
},
"Succeed-d4d41uw": {
"style": {
"bounds": {
"x": 812,
"y": 200,
"width": 36,
"height": 36
}
},
"Name": "Succeed-d4d41uw",
"Type": "Succeed"
}
},
"StartState": "ServiceTask-a9h2o51",
"edge": {
"style": {
"waypoints": [
{
"original": {
"x": 236,
"y": 218
},
"x": 236,
"y": 218
},
{
"x": 280,
"y": 218
},
{
"original": {
"x": 300,
"y": 218
},
"x": 300,
"y": 218
}
],
"target": "ServiceTask-a9h2o51"
},
"Type": "Transition"
}
}
And then it can be imported to the designer.
The layout algorithm is up to you, just make sure the final layout is human-readable.
If you have any questions or problems, please let me know. You can just leave comments here.
Just I want confirmation whether I am on right way or not
Issue is we are not able to import the designer directly through json due to absense of property such as style,edge waypoint,attached position.. etc.
Am I right?
Just I want confirmation whether I am on right way or not
Issue is we are not able to import the designer directly through json due to absense of property such as style,edge waypoint,attached position.. etc.
Am I right?
Exactly, you are on the right track.
Below are the some question before I need to start implementation ?
- Are we asking user to add details in json if they are not present (i.e using exception)
- If by default value is to be added , Can we add any random values to such parameter (style,edge waypoints etc..)?
Below are the some question before I need to start implementation ?
- Are we asking user to add details in json if they are not present (i.e using exception)
- If by default value is to be added , Can we add any random values to such parameter (style,edge waypoints etc..)?
- No, we should not. What we exactly should do in this PR is to support JSON without style information instead of rasing exceptions.
- Randomness is necessary, but just putting random values is not enough. Use auto layout algorithm instead (please refer to bpmn-auto-layout).
Consider following example :
{ "Name": "StateMachine-qw7ufbv", "Comment": "This state machine is modeled by designer tools.", "Version": "0.0.1", "style": { "bounds": { "x": 200, "y": 200, "width": 36, "height": 36 } }, "States": { "ServiceTask-6cc74ej": { "Name": "ServiceTask-6cc74ej", "IsForCompensation": false, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "CompensateState": "ServiceTask-vhxci8q", "Type": "ServiceTask", "edge": { "ServiceTask-vezv7z5": { "Type": "Compensation", "style": { "waypoints": [ { "x": 400, "y": 258 }, { "x": 400, "y": 350 }, { "x": 400, "y": 370 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-vezv7z5" } }, "ServiceTask-vhxci8q": { "Type": "Compensation", "style": { "waypoints": [ { "x": 400, "y": 258 }, { "x": 590, "y": 350 }, { "x": 590, "y": 370 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-vhxci8q" } }, "ServiceTask-pz9bz4t": { "Type": "Transition", "style": { "waypoints": [ { "x": 450, "y": 218 }, { "x": 760, "y": 218 }, { "x": 780, "y": 218 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-pz9bz4t" } } }, "Next": "ServiceTask-pz9bz4t" }, "ServiceTask-vezv7z5": { "Name": "ServiceTask-vezv7z5", "IsForCompensation": true, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 350, "y": 370, "width": 100, "height": 80 } } }, "ServiceTask-vhxci8q": { "Name": "ServiceTask-vhxci8q", "IsForCompensation": true, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 540, "y": 370, "width": 100, "height": 80 } } }, "ServiceTask-pz9bz4t": { "Name": "ServiceTask-pz9bz4t", "IsForCompensation": false, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 780, "y": 178, "width": 100, "height": 80 } } } }, "StartState": "ServiceTask-6cc74ej", "edge": { "Type": "Transition", "style": { "waypoints": [ { "x": 236, "y": 218 }, { "x": 330, "y": 218 }, { "x": 350, "y": 218 } ], "target": "ServiceTask-6cc74ej" } } }
Suppose in state "ServiceTask-6cc74ej" if edge property is not present then on what basis we should draw the state diagram ?. Ans also want to know which are some property compulsary to be present if in above example edge property is not present ?
Hi , Any update on this?
Consider following example :
{ "Name": "StateMachine-qw7ufbv", "Comment": "This state machine is modeled by designer tools.", "Version": "0.0.1", "style": { "bounds": { "x": 200, "y": 200, "width": 36, "height": 36 } }, "States": { "ServiceTask-6cc74ej": { "Name": "ServiceTask-6cc74ej", "IsForCompensation": false, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "CompensateState": "ServiceTask-vhxci8q", "Type": "ServiceTask", "edge": { "ServiceTask-vezv7z5": { "Type": "Compensation", "style": { "waypoints": [ { "x": 400, "y": 258 }, { "x": 400, "y": 350 }, { "x": 400, "y": 370 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-vezv7z5" } }, "ServiceTask-vhxci8q": { "Type": "Compensation", "style": { "waypoints": [ { "x": 400, "y": 258 }, { "x": 590, "y": 350 }, { "x": 590, "y": 370 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-vhxci8q" } }, "ServiceTask-pz9bz4t": { "Type": "Transition", "style": { "waypoints": [ { "x": 450, "y": 218 }, { "x": 760, "y": 218 }, { "x": 780, "y": 218 } ], "source": "ServiceTask-6cc74ej", "target": "ServiceTask-pz9bz4t" } } }, "Next": "ServiceTask-pz9bz4t" }, "ServiceTask-vezv7z5": { "Name": "ServiceTask-vezv7z5", "IsForCompensation": true, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 350, "y": 370, "width": 100, "height": 80 } } }, "ServiceTask-vhxci8q": { "Name": "ServiceTask-vhxci8q", "IsForCompensation": true, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 540, "y": 370, "width": 100, "height": 80 } } }, "ServiceTask-pz9bz4t": { "Name": "ServiceTask-pz9bz4t", "IsForCompensation": false, "Input": [{}], "Output": {}, "Status": {}, "Retry": [], "ServiceName": "", "ServiceMethod": "", "Type": "ServiceTask", "style": { "bounds": { "x": 780, "y": 178, "width": 100, "height": 80 } } } }, "StartState": "ServiceTask-6cc74ej", "edge": { "Type": "Transition", "style": { "waypoints": [ { "x": 236, "y": 218 }, { "x": 330, "y": 218 }, { "x": 350, "y": 218 } ], "target": "ServiceTask-6cc74ej" } } }
Suppose in state "ServiceTask-6cc74ej" if edge property is not present then on what basis we should draw the state diagram ?. Ans also want to know which are some property compulsary to be present if in above example edge property is not present ?
if the edge
prop of a state (let say a state called A) is not presented but the Next
of A is configured (let say B). We should add an edge from A to B, right? An example property is something like following, all the properties listed below are compulsary. You can start the designer locally, and test if your algorithm works.
"edge": {
"B": {
"style": {
"waypoints": [
{
"original": {
"x": 668,
"y": 320
},
"x": 668,
"y": 320
},
{
"x": 792,
"y": 320
},
{
"original": {
"x": 812,
"y": 320
},
"x": 812,
"y": 320
}
],
"source": "A",
"target": "B"
},
"Type": "Transition"
}
}
Considering there are some domain specific knowledge like the type of edge can be a Transition
, Compensation
, ExceptionMatch
, ChoiceEntry
, etc. I suggest you study on Seata Saga first and read the source code of designer.
Yeah, I got that . But consider same example :
In state "ServiceTask-6cc74ej" there are two CompensateState which are "ServiceTask-vezv7z5" and "ServiceTask-vhxci8q" , in above json format on the basis of edge property we are able to draw the compensation state , but what if there is no edge state , i will add edge state on basis of CompensateState which is given as "ServiceTask-vhxci8q" in above json so diagram created from myside will be as below:
Json format for above diagram :
I want to draw an edge from "ServiceTask-6cc74ej" to "ServiceTask-vezv7z5" how to draw ? as it does contain one compensateState as "ServiceTask-vhxci8q and there is no Next Property?
You can consider these an edge case...
Yeah, I got that . But consider same example :
In state "ServiceTask-6cc74ej" there are two CompensateState which are "ServiceTask-vezv7z5" and "ServiceTask-vhxci8q" , in above json format on the basis of edge property we are able to draw the compensation state , but what if there is no edge state , i will add edge state on basis of CompensateState which is given as "ServiceTask-vhxci8q" in above json so diagram created from myside will be as below:
Json format for above diagram :
I want to draw an edge from "ServiceTask-6cc74ej" to "ServiceTask-vezv7z5" how to draw ? as it does contain one compensateState as "ServiceTask-vhxci8q and there is no Next Property?
You can consider these an edge case...
I see. Actually a task state (ServiceTask, ScriptTask or SubStateMachine) can only have 1 CompensateState. Sure, you can draw two compensation state in designer. However, that is another unimplemented feature (validation rules for forbidding multiple compensation and somet other stuff). For a task state, you should just care about the following situations:
-
Next
property indicates the next state to forward (only 1) -
Compensation
property indicates the compensation state if the state machine rollbacks (only 1) -
Catch
property indicates the next step if error occurs during executing task (multiple)