@vyr/service-graph 0.0.1

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/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@vyr/service-graph",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "./src/index.ts",
6
+ "author": "",
7
+ "license": "MIT",
8
+ "dependencies": {
9
+ "vue": "3.5.22",
10
+ "@vyr/locale": "0.0.1",
11
+ "@vyr/engine": "0.0.1",
12
+ "@vyr/service": "0.0.1",
13
+ "jsondiffpatch": "^0.6.0",
14
+ "@antv/x6": "^2.18.1",
15
+ "@antv/x6-plugin-selection": "^2.2.2",
16
+ "@antv/x6-plugin-stencil": "^2.1.5",
17
+ "@antv/x6-plugin-history": "2.2.4"
18
+ },
19
+ "files": [
20
+ "package.json",
21
+ "src/"
22
+ ]
23
+ }
@@ -0,0 +1,309 @@
1
+ import { Generate, Listener } from '@vyr/engine'
2
+ import { Cell, CellView, Edge, Graph, Shape } from '@antv/x6'
3
+ import { Selection } from '@antv/x6-plugin-selection'
4
+ import { Stencil } from '@antv/x6-plugin-stencil'
5
+ import { History } from '@antv/x6-plugin-history'
6
+ import { Options } from '@antv/x6/lib/graph/options'
7
+ import { Unit } from './common/Unit'
8
+ import { language } from './locale'
9
+
10
+ interface GraphObserver {
11
+ resize: (e: any) => void
12
+ select: (cell: Cell[]) => void
13
+ change: (e: {}) => void
14
+ remove: (cell: Cell) => void
15
+ link: (e: { source: string, target: string }) => void
16
+ unlink: (e: { source: string, target: string }) => void
17
+ dispose: (e: any) => void
18
+ }
19
+ type Connecting = Options.Connecting
20
+
21
+ class GraphDrawer extends Listener<GraphObserver> {
22
+ private isReadonly = false
23
+ readonly connecting
24
+ readonly dom = document.createElement('div')
25
+ readonly selection: Selection
26
+ readonly stencil: Stencil
27
+ readonly graph: Graph
28
+ readonly history: History
29
+ readonly tool
30
+ selectEdge = ''
31
+ container: Element | null = null
32
+ needTriggerChange = false
33
+
34
+ constructor() {
35
+ super()
36
+ this.dom = document.createElement('div')
37
+ this.connecting = {
38
+ snap: true,
39
+ allowMulti: false,
40
+ allowBlank: false,
41
+ allowLoop: false,
42
+ allowNode: false,
43
+ highlight: true,
44
+ connector: 'rounded',
45
+ connectionPoint: 'boundary',
46
+ router: 'manhattan',
47
+ validateMagnet: this._validateMagnet,
48
+ validateConnection: this._validateConnection,
49
+ }
50
+
51
+ this.graph = new Graph({
52
+ container: this.dom,
53
+ autoResize: true,
54
+ mousewheel: true,
55
+ panning: { enabled: true },
56
+ interacting: this._interacting,
57
+ connecting: {
58
+ ...this.connecting,
59
+ createEdge: this._createEdge,
60
+ },
61
+ })
62
+ this.selection = new Selection({
63
+ enabled: true,
64
+ multiple: true,
65
+ rubberband: true,
66
+ movable: true,
67
+ modifiers: 'ctrl',
68
+ showNodeSelectionBox: true,
69
+ filter(cell) {
70
+ return cell instanceof Unit ? true : false
71
+ }
72
+ })
73
+ this.stencil = new Stencil({
74
+ target: this.graph,
75
+ title: language.get('graphDrawer.stencil.title'),
76
+ layoutOptions: { columns: 1, columnWidth: 200, dx: 20, },
77
+ stencilGraphHeight: 0,
78
+ stencilGraphPadding: 0,
79
+ groups: [{ name: 'inspector' }],
80
+ })
81
+ this.history = new History({ enabled: false, })
82
+ this.graph.use(this.selection)
83
+ this.graph.use(this.history)
84
+
85
+ this.tool = document.createElement('div')
86
+ this.tool.style.width = '240px';
87
+ this.tool.style.height = '100%';
88
+ this.tool.style.position = 'absolute';
89
+ this.tool.style.top = '0px'
90
+ this.tool.style.left = '0px'
91
+ this.tool.style.zIndex = '999'
92
+ this.tool.appendChild(this.stencil.container)
93
+ this.stencil.dnd.options.dndContainer = this.tool
94
+
95
+ this.graph.on('node:added', this._changeListener)
96
+ this.graph.on('node:removed', this._changeListener)
97
+ this.graph.on('edge:removed', this._changeListener)
98
+ this.graph.on('node:moved', this._changeListener)
99
+
100
+ this.graph.on('node:selected', this._delaySelect)
101
+ this.graph.on('node:unselected', this._delaySelect)
102
+
103
+ this.graph.on('edge:connected', (e) => {
104
+ //@ts-ignore
105
+ const source = e.edge.source.cell
106
+ //@ts-ignore
107
+ const target = e.edge.target.cell
108
+
109
+ const sourceCell = this.graph.getCellById(source)
110
+ const targetCell = this.graph.getCellById(target)
111
+ if (sourceCell instanceof Unit && targetCell instanceof Unit) {
112
+ this.trigger('link', { source, target })
113
+ this._changeListener()
114
+ }
115
+ })
116
+
117
+ //边
118
+ this.graph.on('edge:mouseenter', ({ edge }) => {
119
+ })
120
+ this.graph.on('edge:mouseleave', ({ edge }) => {
121
+ })
122
+ this.graph.on('edge:click', ({ edge }) => {
123
+ this._unselectEdge()
124
+ edge.setAttrs({ line: Unit.edge.highlight })
125
+ this.selectEdge = edge.id
126
+ })
127
+ this.graph.on('edge:removed', ({ edge }: any) => {
128
+ if (edge.id === this.selectEdge) this.selectEdge = ''
129
+ const source = this.graph.getCellById(edge.source.cell)
130
+ const target = this.graph.getCellById(edge.target.cell)
131
+ if (source instanceof Unit && target instanceof Unit) {
132
+ this.trigger('unlink', { source: edge.source.cell, target: edge.target.cell })
133
+ }
134
+ })
135
+
136
+ this.graph.on('blank:mouseup', () => {
137
+ this._unselectEdge()
138
+ })
139
+ this.graph.on('blank:contextmenu', (e) => {
140
+ })
141
+ this.graph.on('cell:contextmenu', ({ cell, x, y }) => {
142
+ if (cell instanceof Unit) cell.rightClick({ cell, x, y })
143
+ })
144
+ //点击节点
145
+ this.graph.on('cell:click', ({ cell, x, y }) => {
146
+ this._unselectEdge()
147
+ if (cell instanceof Unit) cell.click({ cell, x, y })
148
+ })
149
+
150
+ window.addEventListener('resize', this._resize)
151
+ window.addEventListener('keydown', this._delete)
152
+
153
+ this.listen('dispose', () => {
154
+ window.removeEventListener('resize', this._resize)
155
+ window.removeEventListener('keydown', this._delete)
156
+ })
157
+ }
158
+
159
+ private _interacting: CellView.Interacting = ({ cell }) => {
160
+ const result = {
161
+ nodeMovable: true
162
+ }
163
+
164
+ if (cell instanceof Unit) result.nodeMovable = cell.isMove()
165
+
166
+ return result
167
+ }
168
+ private _validateMagnet: Connecting['validateMagnet'] = ({ cell, magnet }) => {
169
+ return cell instanceof Unit ? cell.isCreateEdge(magnet) : false
170
+ }
171
+ private _createEdge: Connecting['createEdge'] = () => {
172
+ return new Shape.Edge({ attrs: Unit.edge })
173
+ }
174
+ private _validateConnection: Connecting['validateConnection'] = ({ sourceCell, targetCell, targetMagnet }) => {
175
+ if (!targetMagnet || !sourceCell) return false
176
+ return targetCell instanceof Unit ? targetCell.isConnection(targetMagnet, sourceCell) : false
177
+ }
178
+ private _undo = (e: KeyboardEvent) => {
179
+ if (e.ctrlKey === false || ['KeyZ'].includes(e.code) === false) return
180
+ if (this.history.canUndo()) this.history.undo()
181
+ }
182
+ private _redo = (e: KeyboardEvent) => {
183
+ if (e.ctrlKey === false || ['KeyY'].includes(e.code) === false) return
184
+ if (this.history.canRedo()) this.history.redo()
185
+ }
186
+ private _unselectEdge = () => {
187
+ if (this.selectEdge) {
188
+ const oldEdge = this.graph.getCellById(this.selectEdge)
189
+ oldEdge.setAttrs({ line: Unit.edge.line })
190
+ }
191
+ this.selectEdge = ''
192
+ }
193
+
194
+ private _delaySelect = Generate.delayExecute(() => {
195
+ const cells = this.selection.getSelectedCells()
196
+ this.trigger('select', cells)
197
+ })
198
+ private _changeListener = () => {
199
+ if (this.needTriggerChange === false) return
200
+ this._delayChange()
201
+ }
202
+ private _delayChange = Generate.delayExecute(() => {
203
+ this.trigger('change', {})
204
+ })
205
+ private _resize = Generate.delayExecute(() => {
206
+ if (!this.container) return
207
+ const rect = this.container.getBoundingClientRect()
208
+ this.graph.resize(rect.width, rect.height)
209
+ this.trigger('resize', {})
210
+ })
211
+
212
+ private _delete = (e: KeyboardEvent) => {
213
+ if (this.isReadonly === true) return
214
+ if (['Delete'].includes(e.code) === false) return
215
+ const cells = this.selection.getSelectedCells()
216
+ for (const cell of cells) this.remove(cell)
217
+ if (this.selectEdge) this.remove(this.graph.getCellById(this.selectEdge))
218
+ }
219
+
220
+ private _isEdgePort(data: { [k: string]: any }): data is { cell: string } {
221
+ return data?.cell ? true : false
222
+ }
223
+
224
+ getMapper(edge: Edge) {
225
+ if (this._isEdgePort(edge.source) === false) return null
226
+ if (this._isEdgePort(edge.target) === false) return null
227
+ return { source: edge.source.cell, target: edge.target.cell }
228
+ }
229
+
230
+ getEdgeOfMapper(source: string, target: string) {
231
+ const edge = {
232
+ shape: 'edge',
233
+ source: { cell: source, port: 'out' },
234
+ target: { cell: target, port: 'in' },
235
+ attrs: Unit.edge,
236
+ }
237
+
238
+ return edge
239
+ }
240
+
241
+ setReadonly(readonly: boolean) {
242
+ this.isReadonly = readonly
243
+ let display = 'none'
244
+ if (readonly) {
245
+ this.graph.options.interacting = false
246
+ } else {
247
+ this.graph.options.interacting = this._interacting
248
+ display = 'block'
249
+ }
250
+ this.tool.style.display = display
251
+ }
252
+
253
+ mount(container: Element | string) {
254
+ this.container = container instanceof Element ? container : document.getElementById(container) ?? null
255
+ if (this.container === null) return
256
+ this.container.appendChild(this.dom)
257
+ this.container.appendChild(this.tool)
258
+ this._resize()
259
+ window.addEventListener('keydown', this._redo)
260
+ window.addEventListener('keydown', this._undo)
261
+ }
262
+
263
+ unmount() {
264
+ if (!this.container) return
265
+ window.removeEventListener('keydown', this._redo)
266
+ window.removeEventListener('keydown', this._undo)
267
+ this.container.removeChild(this.dom)
268
+ this.container = null
269
+ }
270
+
271
+ find(id: string) {
272
+ const cells = this.graph.getCells()
273
+ for (const cell of cells) {
274
+ if (cell.id === id) return cell as Unit
275
+ }
276
+
277
+ return null
278
+ }
279
+
280
+ remove(cell: Cell) {
281
+ const parentCell = cell.parent
282
+ this.graph.removeCell(cell)
283
+
284
+ if (parentCell) {
285
+ const subs = parentCell.getChildren() as Cell[]
286
+ for (let i = 0; i < subs.length; i++) {
287
+ const sub = subs[i]
288
+ sub.setProp('position', { x: 0, y: i * Unit.height })
289
+ }
290
+ }
291
+
292
+ if (cell instanceof Unit) this.trigger('remove', cell)
293
+ cell.dispose()
294
+ }
295
+
296
+ clear() {
297
+ this._unselectEdge()
298
+ this.graph.resetCells([])
299
+ // this.history.clean()
300
+ this.selection.clean()
301
+ }
302
+
303
+ dispose() {
304
+ this.clear()
305
+ this.unmount()
306
+ }
307
+ }
308
+
309
+ export { GraphDrawer }
@@ -0,0 +1,181 @@
1
+ import { reactive, AsyncComponentLoader, defineAsyncComponent } from 'vue'
2
+ import { Service } from "@vyr/service"
3
+ import { DeserializationObject, InteractionInputCollection, ObjectUtils, RoutineDescriptor, RoutineMapper, RoutineNode } from '@vyr/engine'
4
+ import { Unit } from './common/Unit'
5
+ import { BranchUnit, ConditionUnit, ExecuteUnit, RoutineUnit as GraphRoutineUnit } from './common/RoutineUnit'
6
+ import { GraphDrawer } from './GraphDrawer'
7
+
8
+ class GraphState {
9
+ width = '60%'
10
+ height = '60%'
11
+ visible = false
12
+ descriptor = ''
13
+ event = ''
14
+ inputs: InteractionInputCollection = {}
15
+ routine: DeserializationObject<RoutineDescriptor> | null = null
16
+
17
+ reset() {
18
+ this.descriptor = ''
19
+ this.event = ''
20
+ this.inputs = {}
21
+ this.routine = null
22
+ }
23
+ }
24
+
25
+ class GraphService extends Service {
26
+ private _map = new Map()
27
+ private _drawer: GraphDrawer | null = null
28
+ get drawer() {
29
+ if (this._drawer === null) {
30
+ this._drawer = new GraphDrawer()
31
+ this._drawer.stencil.load([
32
+ new ExecuteUnit({ drawer: this._drawer, label: 'Execute' },),
33
+ new BranchUnit({ drawer: this._drawer, label: 'Branch' }),
34
+ new ConditionUnit({ drawer: this._drawer, label: 'Condition' }),
35
+ ], 'inspector')
36
+ }
37
+ return this._drawer
38
+ }
39
+ readonly state = reactive(new GraphState())
40
+ routine: DeserializationObject<RoutineDescriptor> | null = null
41
+
42
+ private _doNodes(nodes: RoutineNode[]) {
43
+ for (const node of nodes) {
44
+ if (node.routine === 'Execute') {
45
+ const cell = new ExecuteUnit({ ...node, drawer: this.drawer })
46
+ this.drawer.graph.addCell(cell)
47
+ } else if (node.routine === 'Condition') {
48
+ this.drawer.graph.addCell(new ConditionUnit({ ...node, drawer: this.drawer }))
49
+ } else if (node.routine === 'Branch') {
50
+ this.drawer.graph.addCell(new BranchUnit({ ...node, drawer: this.drawer }))
51
+ }
52
+ }
53
+
54
+ }
55
+ private _doMappers(mappers: RoutineMapper[]) {
56
+ for (const mapper of mappers) {
57
+ const edge = this.drawer.getEdgeOfMapper(mapper.source, mapper.target)
58
+ this.drawer.graph.addEdge(edge)
59
+ }
60
+ }
61
+
62
+ private _diff(cur: RoutineNode[], old: RoutineNode[]) {
63
+ const addQueue: RoutineNode[] = []
64
+ const removeQueue: RoutineNode[] = []
65
+ const updateQueue: RoutineNode[] = []
66
+
67
+ const temp = new Map<string, RoutineNode>()
68
+ for (const oldItem of old) {
69
+ temp.set(oldItem.id, oldItem)
70
+ }
71
+
72
+ for (const curItem of cur) {
73
+ const oldItem = temp.get(curItem.id)
74
+ if (oldItem === undefined) {
75
+ addQueue.push(curItem)
76
+ } else {
77
+ if (ObjectUtils.equals(curItem, oldItem) === false) updateQueue.push(curItem)
78
+ temp.delete(curItem.id)
79
+ }
80
+ }
81
+
82
+ removeQueue.push(...temp.values())
83
+
84
+ return {
85
+ addQueue,
86
+ removeQueue,
87
+ updateQueue
88
+ }
89
+ }
90
+
91
+ render(routine: DeserializationObject<RoutineDescriptor>) {
92
+ this.routine = routine
93
+
94
+ this.drawer.needTriggerChange = false
95
+ this._doNodes(routine.nodes)
96
+ this._doMappers(routine.mappers)
97
+ this.drawer.needTriggerChange = true
98
+ }
99
+
100
+ update(curRoutine: DeserializationObject<RoutineDescriptor>) {
101
+ if (this.routine === null) return
102
+
103
+ this.drawer.needTriggerChange = false
104
+ this.drawer.selectEdge = ''
105
+ this.drawer.history.clean()
106
+ const selects = this.drawer.selection.getSelectedCells()
107
+
108
+ const edges = this.drawer.graph.getEdges()
109
+ for (const edge of edges) this.drawer.remove(edge)
110
+
111
+ const patch = this._diff(curRoutine.nodes, this.routine.nodes)
112
+
113
+ let needCleanSelection = false
114
+ for (const remove of patch.removeQueue) {
115
+ const cell = this.drawer.graph.getCellById(remove.id)
116
+ if (!cell) continue
117
+ if (selects.includes(cell)) needCleanSelection = true
118
+ this.drawer.remove(cell)
119
+ }
120
+
121
+ if (needCleanSelection) this.drawer.selection.clean()
122
+
123
+ this._doNodes(patch.addQueue)
124
+
125
+ for (const update of patch.updateQueue) {
126
+ const cell = this.drawer.graph.getCellById(update.id) as Unit
127
+ if (cell) cell.setVMeta(update)
128
+ }
129
+
130
+ this._doMappers(curRoutine.mappers)
131
+ this.drawer.needTriggerChange = true
132
+
133
+ this.routine = curRoutine
134
+ }
135
+
136
+ build() {
137
+ const routine = new RoutineDescriptor()
138
+ const cells = this.drawer.graph.getCells()
139
+
140
+ for (const cell of cells) {
141
+ if (cell instanceof GraphRoutineUnit) {
142
+ const node = {
143
+ id: cell.id,
144
+ label: cell.vMeta.label ?? '',
145
+ url: cell.vMeta.url,
146
+ routine: cell.vMeta.routine,
147
+ input: cell.vMeta.input,
148
+ position: cell.getPosition(),
149
+ }
150
+ routine.nodes.push(node)
151
+ } else if (cell.isEdge()) {
152
+ const mapper = this.drawer.getMapper(cell)
153
+ if (mapper) routine.mappers.push(mapper)
154
+ }
155
+ }
156
+
157
+ const roots = this.drawer.graph.getRootNodes()
158
+ for (const root of roots) routine.roots.push(root.id)
159
+
160
+ return { ...routine, uuid: this.routine?.uuid }
161
+ }
162
+
163
+ get(id: string) {
164
+ return this._map.get(id) ?? null
165
+ }
166
+
167
+ keys() {
168
+ return [...this._map.keys()]
169
+ }
170
+
171
+ set(id: string, executor: AsyncComponentLoader): void {
172
+ const component = defineAsyncComponent({
173
+ loader: executor,
174
+ timeout: 5000
175
+ })
176
+ this._map.set(id, component)
177
+ }
178
+
179
+ }
180
+
181
+ export { GraphService }