incubator-seata icon indicating copy to clipboard operation
incubator-seata copied to clipboard

Support auto-layout for state machine designer

Open ptyin opened this issue 1 year ago • 14 comments

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 avatar Dec 11 '23 13:12 ptyin

@ptyin

Can you please assign this task to me?

Code-breaker1998 avatar Jan 30 '24 12:01 Code-breaker1998

@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 avatar Jan 30 '24 13:01 ptyin

@ptyin

Can you please assign this task to me?

Hi!The issue has been assigned to you.Looking forward your PR.

xingfudeshi avatar Jan 30 '24 13:01 xingfudeshi

Hi @ptyin ,

Do you have any existing similar PR so that I can review it?

Code-breaker1998 avatar Jan 31 '24 13:01 Code-breaker1998

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.

StateMachineNewDesigner

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.

ptyin avatar Feb 01 '24 05:02 ptyin

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?

Code-breaker1998 avatar Feb 07 '24 17:02 Code-breaker1998

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.

ptyin avatar Feb 07 '24 17:02 ptyin

Below are the some question before I need to start implementation ?

  1. Are we asking user to add details in json if they are not present (i.e using exception)
  2. If by default value is to be added , Can we add any random values to such parameter (style,edge waypoints etc..)?

Code-breaker1998 avatar Feb 08 '24 18:02 Code-breaker1998

Below are the some question before I need to start implementation ?

  1. Are we asking user to add details in json if they are not present (i.e using exception)
  2. If by default value is to be added , Can we add any random values to such parameter (style,edge waypoints etc..)?
  1. No, we should not. What we exactly should do in this PR is to support JSON without style information instead of rasing exceptions.
  2. Randomness is necessary, but just putting random values is not enough. Use auto layout algorithm instead (please refer to bpmn-auto-layout).

ptyin avatar Feb 09 '24 06:02 ptyin

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 ?

Code-breaker1998 avatar Feb 22 '24 07:02 Code-breaker1998

Hi , Any update on this?

Code-breaker1998 avatar Feb 23 '24 14:02 Code-breaker1998

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.

ptyin avatar Feb 23 '24 14:02 ptyin

Yeah, I got that . But consider same example :

StateMachine-qw7ufbv.json

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:

apt

Json format for above diagram :

new.json

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...

Code-breaker1998 avatar Feb 23 '24 15:02 Code-breaker1998

Yeah, I got that . But consider same example :

StateMachine-qw7ufbv.json

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:

apt

Json format for above diagram :

new.json

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:

  1. Next property indicates the next state to forward (only 1)
  2. Compensation property indicates the compensation state if the state machine rollbacks (only 1)
  3. Catch property indicates the next step if error occurs during executing task (multiple)

ptyin avatar Feb 23 '24 16:02 ptyin