@likec4/diagram 1.27.1 → 1.27.3

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.
Files changed (123) hide show
  1. package/bundle/index.js +2152 -6757
  2. package/dist/LikeC4Diagram.js +1 -1
  3. package/dist/LikeC4Diagram.props.d.ts +5 -3
  4. package/dist/StaticLikeC4Diagram.js +1 -1
  5. package/dist/_virtual/set.js +5 -0
  6. package/dist/_virtual/set2.js +3 -0
  7. package/dist/base/BaseXYFlow.d.ts +2 -1
  8. package/dist/base/primitives/compound/CompoundNodeContainer.d.ts +1 -1
  9. package/dist/base/primitives/compound/CompoundNodeContainer.js +0 -1
  10. package/dist/base/primitives/edge/EdgeLabelContainer.js +3 -2
  11. package/dist/base/primitives/element/ElementActionButtons.js +8 -11
  12. package/dist/base/primitives/element/ElementNodeContainer.css.js +4 -4
  13. package/dist/base/updateEdges.js +1 -9
  14. package/dist/base/updateNodes.js +1 -12
  15. package/dist/bundle/LikeC4View.js +2 -2
  16. package/dist/bundle/LikeC4View.props.d.ts +7 -7
  17. package/dist/hooks/useCurrentViewId.js +8 -2
  18. package/dist/hooks/useDiagram.d.ts +1522 -685
  19. package/dist/hooks/useDiagram.js +21 -24
  20. package/dist/likec4diagram/DiagramXYFlow.js +6 -6
  21. package/dist/likec4diagram/ui/notation/NotationPanel.css.js +3 -3
  22. package/dist/likec4diagram/ui/notation/NotationPanel.js +10 -4
  23. package/dist/node_modules/.pnpm/mnemonist@0.40.3_patch_hash_5c12abb43fcff6fd81e53021b89075d53249640d165a2556cc1e58d71201a13a/node_modules/mnemonist/set.js +369 -0
  24. package/dist/overlays/Overlays.js +3 -1
  25. package/dist/overlays/element-details/actor.d.ts +2769 -173
  26. package/dist/overlays/overlay/Overlay.css.d.ts +26 -2
  27. package/dist/overlays/overlay/Overlay.css.js +66 -18
  28. package/dist/overlays/overlay/Overlay.d.ts +7 -6
  29. package/dist/overlays/overlay/Overlay.js +12 -10
  30. package/dist/overlays/overlaysActor.js +2 -2
  31. package/dist/overlays/relationship-details/RelationshipDetails.js +113 -70
  32. package/dist/overlays/relationship-details/SelectEdge.d.ts +1 -1
  33. package/dist/overlays/relationship-details/SelectEdge.js +15 -9
  34. package/dist/overlays/relationship-details/_types.d.ts +4 -1
  35. package/dist/overlays/relationship-details/actor.d.ts +425 -128
  36. package/dist/overlays/relationship-details/actor.js +157 -53
  37. package/dist/overlays/relationship-details/compute.d.ts +12 -0
  38. package/dist/overlays/relationship-details/compute.js +139 -0
  39. package/dist/overlays/relationship-details/custom/ElementActions.js +27 -5
  40. package/dist/overlays/relationship-details/custom/edgeTypes.js +1 -1
  41. package/dist/overlays/relationship-details/custom/nodeTypes.d.ts +2 -1
  42. package/dist/overlays/relationship-details/hooks.d.ts +336 -6
  43. package/dist/overlays/relationship-details/hooks.js +11 -12
  44. package/dist/overlays/relationship-details/layout-to-xyflow.d.ts +12 -0
  45. package/dist/overlays/relationship-details/layout-to-xyflow.js +145 -0
  46. package/dist/overlays/relationship-details/layout.d.ts +22 -0
  47. package/dist/overlays/relationship-details/layout.js +314 -0
  48. package/dist/overlays/relationships-browser/RelationshipsBrowser.d.ts +3 -1
  49. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +2 -2
  50. package/dist/overlays/relationships-browser/_types.d.ts +2 -0
  51. package/dist/overlays/relationships-browser/actor.d.ts +130 -14
  52. package/dist/overlays/relationships-browser/actor.js +30 -16
  53. package/dist/overlays/relationships-browser/custom/ElementActions.js +5 -3
  54. package/dist/overlays/relationships-browser/custom/edgeTypes.d.ts +3 -1
  55. package/dist/overlays/relationships-browser/custom/edgeTypes.js +1 -1
  56. package/dist/overlays/relationships-browser/hooks.d.ts +3 -1
  57. package/dist/overlays/relationships-browser/hooks.js +6 -9
  58. package/dist/overlays/relationships-browser/{-useRelationshipsView.d.ts → layout.d.ts} +3 -1
  59. package/dist/overlays/relationships-browser/{-useRelationshipsView.js → layout.js} +21 -8
  60. package/dist/overlays/relationships-browser/useViewToNodesEdges.d.ts +1 -1
  61. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +3 -1
  62. package/dist/packages/core/src/compute-view/relationships-view/compute.js +1 -1
  63. package/dist/packages/core/src/model/connection/model/ConnectionModel.js +121 -0
  64. package/dist/packages/core/src/model/connection/model/find.js +39 -0
  65. package/dist/packages/core/src/model/connection/ops.js +7 -0
  66. package/dist/packages/core/src/utils/const.js +3 -0
  67. package/dist/packages/core/src/utils/fqn.js +9 -1
  68. package/dist/packages/core/src/utils/iterable/find.js +21 -0
  69. package/dist/packages/core/src/utils/iterable/some.js +21 -0
  70. package/dist/packages/core/src/utils/set.js +48 -0
  71. package/dist/packages/core/src/utils/string-hash.js +13 -0
  72. package/dist/state/diagram-machine.d.ts +1552 -703
  73. package/dist/state/diagram-machine.js +39 -3
  74. package/dist/state/syncManualLayoutActor.d.ts +1 -1
  75. package/dist/styles.css.js +1 -1
  76. package/dist/utils/index.d.ts +0 -1
  77. package/package.json +11 -11
  78. package/src/LikeC4Diagram.props.ts +7 -3
  79. package/src/LikeC4Diagram.tsx +1 -1
  80. package/src/StaticLikeC4Diagram.tsx +1 -1
  81. package/src/base/BaseXYFlow.tsx +2 -1
  82. package/src/base/primitives/compound/CompoundNodeContainer.tsx +0 -2
  83. package/src/base/primitives/edge/EdgeLabelContainer.tsx +7 -5
  84. package/src/base/primitives/element/ElementActionButtons.tsx +10 -11
  85. package/src/base/primitives/element/ElementNodeContainer.css.ts +4 -4
  86. package/src/base/updateEdges.ts +8 -8
  87. package/src/base/updateNodes.ts +11 -11
  88. package/src/bundle/LikeC4View.props.ts +7 -7
  89. package/src/bundle/LikeC4View.tsx +2 -2
  90. package/src/hooks/useCurrentViewId.ts +4 -2
  91. package/src/hooks/useDiagram.ts +22 -24
  92. package/src/likec4diagram/DiagramXYFlow.tsx +6 -6
  93. package/src/likec4diagram/ui/notation/NotationPanel.css.ts +2 -2
  94. package/src/likec4diagram/ui/notation/NotationPanel.tsx +8 -6
  95. package/src/overlays/Overlays.tsx +4 -2
  96. package/src/overlays/overlay/Overlay.css.ts +67 -17
  97. package/src/overlays/overlay/Overlay.tsx +27 -24
  98. package/src/overlays/overlaysActor.ts +1 -1
  99. package/src/overlays/relationship-details/RelationshipDetails.tsx +131 -68
  100. package/src/overlays/relationship-details/SelectEdge.tsx +19 -12
  101. package/src/overlays/relationship-details/_types.ts +3 -1
  102. package/src/overlays/relationship-details/actor.ts +212 -68
  103. package/src/overlays/relationship-details/compute.ts +186 -0
  104. package/src/overlays/relationship-details/custom/ElementActions.tsx +27 -18
  105. package/src/overlays/relationship-details/custom/edgeTypes.tsx +1 -1
  106. package/src/overlays/relationship-details/hooks.ts +12 -13
  107. package/src/overlays/relationship-details/layout-to-xyflow.ts +172 -0
  108. package/src/overlays/relationship-details/layout.ts +470 -0
  109. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +2 -2
  110. package/src/overlays/relationships-browser/_types.ts +2 -0
  111. package/src/overlays/relationships-browser/actor.ts +39 -14
  112. package/src/overlays/relationships-browser/custom/ElementActions.tsx +5 -3
  113. package/src/overlays/relationships-browser/custom/edgeTypes.tsx +1 -1
  114. package/src/overlays/relationships-browser/hooks.ts +7 -10
  115. package/src/overlays/relationships-browser/{-useRelationshipsView.ts → layout.ts} +26 -33
  116. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +3 -1
  117. package/src/state/diagram-machine.ts +51 -6
  118. package/src/utils/index.ts +0 -1
  119. package/dist/overlays/relationship-details/useLayoutedDetails.d.ts +0 -19
  120. package/dist/overlays/relationship-details/useLayoutedDetails.js +0 -267
  121. package/dist/utils/types.d.ts +0 -8
  122. package/src/overlays/relationship-details/useLayoutedDetails.ts +0 -376
  123. package/src/utils/types.ts +0 -6
@@ -1,8 +1,8 @@
1
- import { type EdgeId, nonNullable } from '@likec4/core'
1
+ import { type EdgeId, type Fqn, nonNullable } from '@likec4/core'
2
2
  import { useCallbackRef } from '@mantine/hooks'
3
3
  import { useSelector } from '@xstate/react'
4
4
  import { shallowEqual } from 'fast-equals'
5
- import { createContext, useContext, useMemo, useTransition } from 'react'
5
+ import { createContext, useContext, useMemo } from 'react'
6
6
  import type { OverlaysActorRef } from '../overlaysActor'
7
7
  import type { RelationshipDetailsActorRef, RelationshipDetailsSnapshot } from './actor'
8
8
 
@@ -23,23 +23,22 @@ export function useRelationshipDetailsState<T = unknown>(
23
23
 
24
24
  export function useRelationshipDetails() {
25
25
  const actor = useRelationshipDetailsActor()
26
- const [, startTransition] = useTransition()
27
26
  return useMemo(() => ({
28
27
  actor,
28
+ get rootElementId(): string {
29
+ return `relationship-details-${actor.sessionId.replaceAll(':', '_')}`
30
+ },
29
31
  getState: () => actor.getSnapshot().context,
30
32
  send: actor.send,
31
- navigateTo: (edgeId: EdgeId) => {
32
- startTransition(() => {
33
- actor.send({
34
- type: 'navigate.to',
35
- edgeId,
36
- })
37
- })
33
+ navigateTo: (...params: [edgeId: EdgeId] | [source: Fqn, target: Fqn]) => {
34
+ if (params.length === 1) {
35
+ actor.send({ type: 'navigate.to', params: { edgeId: params[0] } })
36
+ } else {
37
+ actor.send({ type: 'navigate.to', params: { source: params[0], target: params[1] } })
38
+ }
38
39
  },
39
40
  fitDiagram: () => {
40
- startTransition(() => {
41
- actor.send({ type: 'fitDiagram' })
42
- })
41
+ actor.send({ type: 'fitDiagram' })
43
42
  },
44
43
  close: () => {
45
44
  if (actor._parent) {
@@ -0,0 +1,172 @@
1
+ import {
2
+ type Fqn,
3
+ nonNullable,
4
+ Queue,
5
+ } from '@likec4/core'
6
+ import { hasAtLeast } from 'remeda'
7
+ import { ZIndexes } from '../../base/const'
8
+ import type { RelationshipDetailsTypes } from './_types'
9
+ import { type LayoutResult } from './layout'
10
+
11
+ // const nodeZIndex = (node: DiagramNode) => node.level - (node.children.length > 0 ? 1 : 0)
12
+ export function layoutResultToXYFlow(
13
+ layout: LayoutResult,
14
+ ): {
15
+ xynodes: RelationshipDetailsTypes.Node[]
16
+ xyedges: RelationshipDetailsTypes.Edge[]
17
+ bounds: { x: number; y: number; width: number; height: number }
18
+ } {
19
+ const xynodes = [] as RelationshipDetailsTypes.Node[],
20
+ xyedges = [] as RelationshipDetailsTypes.Edge[],
21
+ nodeLookup = new Map<Fqn, LayoutResult.Node>()
22
+
23
+ type TraverseItem = {
24
+ node: LayoutResult.Node
25
+ parent: LayoutResult.Node | null
26
+ }
27
+ const queue = Queue.from(layout.nodes.reduce(
28
+ (acc, node) => {
29
+ nodeLookup.set(node.id, node)
30
+ if (!node.parent) {
31
+ acc.push({ node, parent: null })
32
+ }
33
+ return acc
34
+ },
35
+ [] as TraverseItem[],
36
+ ))
37
+ const ns = ''
38
+ const nodeById = (id: Fqn) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`)
39
+
40
+ let next: TraverseItem | undefined
41
+ while ((next = queue.dequeue())) {
42
+ const { node, parent } = next
43
+ const isCompound = hasAtLeast(node.children, 1)
44
+ if (isCompound) {
45
+ for (const child of node.children) {
46
+ queue.enqueue({ node: nodeById(child), parent: node })
47
+ }
48
+ }
49
+
50
+ const position = {
51
+ x: node.position[0],
52
+ y: node.position[1],
53
+ }
54
+ if (parent) {
55
+ position.x -= parent.position[0]
56
+ position.y -= parent.position[1]
57
+ }
58
+
59
+ const id = ns + node.id
60
+
61
+ const base = {
62
+ id,
63
+ draggable: false,
64
+ selectable: true,
65
+ focusable: true,
66
+ deletable: false,
67
+ position,
68
+ zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
69
+ style: {
70
+ width: node.width,
71
+ height: node.height,
72
+ },
73
+ initialWidth: node.width,
74
+ initialHeight: node.height,
75
+ ...(parent && {
76
+ parentId: ns + parent.id,
77
+ }),
78
+ } satisfies Omit<RelationshipDetailsTypes.Node, 'data' | 'type'>
79
+
80
+ const fqn = node.modelRef
81
+ const navigateTo = { navigateTo: node.navigateTo ?? null }
82
+
83
+ switch (true) {
84
+ case isCompound: {
85
+ xynodes.push(
86
+ {
87
+ ...base,
88
+ type: 'compound',
89
+ data: {
90
+ column: node.column,
91
+ title: node.title,
92
+ color: node.color,
93
+ style: node.style,
94
+ depth: node.depth ?? 0,
95
+ icon: node.icon ?? 'none',
96
+ ports: node.ports,
97
+ fqn,
98
+ ...navigateTo,
99
+ },
100
+ },
101
+ )
102
+ break
103
+ }
104
+ default: {
105
+ xynodes.push(
106
+ {
107
+ ...base,
108
+ type: 'element' as const,
109
+ data: {
110
+ column: node.column,
111
+ fqn,
112
+ title: node.title,
113
+ technology: node.technology,
114
+ description: node.description,
115
+ height: node.height,
116
+ width: node.width,
117
+ color: node.color,
118
+ shape: node.shape,
119
+ icon: node.icon ?? 'none',
120
+ ports: node.ports,
121
+ style: node.style,
122
+ ...navigateTo,
123
+ },
124
+ },
125
+ )
126
+ }
127
+ }
128
+ }
129
+ for (
130
+ const {
131
+ source,
132
+ target,
133
+ relationId,
134
+ label,
135
+ technology,
136
+ description,
137
+ navigateTo = null,
138
+ color = 'gray',
139
+ line = 'dashed',
140
+ ...edge
141
+ } of layout.edges
142
+ ) {
143
+ const id = ns + edge.id
144
+ xyedges.push({
145
+ id,
146
+ type: 'relationship',
147
+ source: ns + source,
148
+ target: ns + target,
149
+ sourceHandle: edge.sourceHandle,
150
+ targetHandle: edge.targetHandle,
151
+ zIndex: ZIndexes.Edge,
152
+ // selectable: selectable,
153
+ // hidden: !visiblePredicate(edge),
154
+ deletable: false,
155
+ data: {
156
+ relationId,
157
+ label,
158
+ color,
159
+ navigateTo,
160
+ line,
161
+ ...(technology && { technology }),
162
+ ...(description && { description }),
163
+ },
164
+ })
165
+ }
166
+
167
+ return {
168
+ xynodes,
169
+ xyedges,
170
+ bounds: layout.bounds,
171
+ }
172
+ }
@@ -0,0 +1,470 @@
1
+ import {
2
+ treeFromElements,
3
+ } from '@likec4/core/compute-view/relationships'
4
+ import type {
5
+ DiagramEdge,
6
+ DiagramNode,
7
+ DiagramView,
8
+ EdgeId,
9
+ Fqn,
10
+ IconUrl,
11
+ NodeId,
12
+ NonEmptyArray,
13
+ Point,
14
+ RelationId,
15
+ } from '@likec4/core/types'
16
+
17
+ import dagre, { type EdgeConfig, type GraphLabel } from '@dagrejs/dagre'
18
+ import { invariant } from '@likec4/core'
19
+ import type { AnyAux, ElementModel, LikeC4ViewModel, RelationshipModel } from '@likec4/core/model'
20
+ import {
21
+ DefaultMap,
22
+ ifind,
23
+ } from '@likec4/core/utils'
24
+ import {
25
+ filter,
26
+ find,
27
+ map,
28
+ mapToObj,
29
+ pipe,
30
+ prop,
31
+ reduce,
32
+ sortBy,
33
+ tap,
34
+ } from 'remeda'
35
+ import type { RelationshipDetailsTypes } from './_types'
36
+ import type { RelationshipDetailsViewData } from './compute'
37
+
38
+ /**
39
+ * All constants related to the layout
40
+ */
41
+ const Sizes = {
42
+ dagre: {
43
+ ranksep: 60,
44
+ nodesep: 35,
45
+ edgesep: 25,
46
+ } satisfies GraphLabel,
47
+ edgeLabel: {
48
+ width: 220,
49
+ height: 14,
50
+ // minlen: 1,
51
+ } satisfies EdgeConfig,
52
+
53
+ nodeWidth: 330,
54
+ nodeHeight: 180,
55
+
56
+ // Spacer between elements in a compound node
57
+ // 0 means no spacer
58
+ spacerHeight: 0,
59
+
60
+ compound: {
61
+ labelHeight: 2,
62
+ paddingTop: 50,
63
+ paddingBottom: 32,
64
+ },
65
+ }
66
+
67
+ // /**
68
+ // * All constants related to the layout
69
+ // */
70
+ // const Sizes = {
71
+ // dagre: {
72
+ // ranksep: 60,
73
+ // nodesep: 35,
74
+ // edgesep: 25,
75
+ // } satisfies GraphLabel,
76
+ // edgeLabel: {
77
+ // width: 120,
78
+ // height: 10,
79
+ // minlen: 1,
80
+ // weight: 1,
81
+ // } satisfies EdgeConfig,
82
+
83
+ // emptyNodeOffset: 120,
84
+
85
+ // nodeWidth: 330,
86
+ // nodeHeight: 180,
87
+
88
+ // // Spacer between elements in a compound node
89
+ // // 0 means no spacer
90
+ // spacerHeight: 0,
91
+
92
+ // compound: {
93
+ // labelHeight: 2,
94
+ // paddingTop: 50,
95
+ // paddingBottom: 32,
96
+ // },
97
+ // }
98
+ type NodeData = {
99
+ column: RelationshipDetailsTypes.Column
100
+ portId: string
101
+ element: ElementModel
102
+ isCompound: boolean
103
+ inPorts: string[]
104
+ outPorts: string[]
105
+ }
106
+ function createGraph() {
107
+ const g = new dagre.graphlib.Graph<NodeData>({
108
+ directed: true,
109
+ compound: true,
110
+ multigraph: true,
111
+ })
112
+ g.setGraph({
113
+ ...Sizes.dagre,
114
+ rankdir: 'LR',
115
+ })
116
+ g.setDefaultEdgeLabel(() => ({ ...Sizes.edgeLabel }))
117
+ g.setDefaultNodeLabel(() => ({}))
118
+
119
+ return g
120
+ }
121
+ type G = ReturnType<typeof createGraph>
122
+
123
+ const PortSuffix = '-port'
124
+
125
+ function createNodes(
126
+ column: RelationshipDetailsTypes.Column,
127
+ elements: ReadonlySet<ElementModel>,
128
+ g: G,
129
+ ) {
130
+ const graphNodes = new DefaultMap<Fqn, { id: string; portId: string }>(key => ({
131
+ id: `${column}-${key}`,
132
+ portId: `${column}-${key}`,
133
+ }))
134
+ const tree = treeFromElements(elements)
135
+ for (const element of tree.sorted) {
136
+ const isCompound = tree.children(element).length > 0
137
+ const fqn = element.id
138
+ const id = `${column}-${fqn}`
139
+ const portId = isCompound ? `${id}${PortSuffix}` : id
140
+ graphNodes.set(fqn, {
141
+ id,
142
+ portId,
143
+ })
144
+ g.setNode(id, {
145
+ column,
146
+ element,
147
+ isCompound,
148
+ portId,
149
+ inPorts: [],
150
+ outPorts: [],
151
+ width: Sizes.nodeWidth,
152
+ height: Sizes.nodeHeight,
153
+ })
154
+
155
+ if (isCompound) {
156
+ g.setNode(portId, {
157
+ element,
158
+ portId,
159
+ isCompound,
160
+ inPorts: [],
161
+ outPorts: [],
162
+ width: Sizes.nodeWidth - Sizes.dagre.ranksep,
163
+ height: Sizes.compound.labelHeight,
164
+ })
165
+ g.setParent(portId, id)
166
+ // g.node(id).padding = 60
167
+ }
168
+
169
+ const parent = tree.parent(element)
170
+ if (parent) {
171
+ g.setParent(id, `${column}-${parent.id}`)
172
+ }
173
+ }
174
+ return {
175
+ ...tree,
176
+ byId: (id: string) => {
177
+ const element = tree.byId(id)
178
+ const graph = graphNodes.get(element.id)
179
+ return {
180
+ element,
181
+ graph,
182
+ }
183
+ },
184
+ graphNodes,
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Apply dagre layout to the graph
190
+ * And return a function to get node bounds for xyflow
191
+ */
192
+ function applyDagreLayout(g: G) {
193
+ dagre.layout(g, {
194
+ // disableOptimalOrderHeuristic: true,
195
+ })
196
+ return (nodeId: string) => {
197
+ const node = g.node(nodeId)
198
+ const { x, y, width, height } = node
199
+ return {
200
+ position: {
201
+ x: x - Math.round(width / 2),
202
+ y: y - Math.round(height / 2),
203
+ },
204
+ width,
205
+ height,
206
+ }
207
+ }
208
+ }
209
+
210
+ export type LayoutResult = {
211
+ // subject: Fqn
212
+ // subjectExistsInScope: boolean
213
+ nodes: LayoutResult.Node[]
214
+ edges: LayoutResult.Edge[]
215
+ bounds: DiagramView['bounds']
216
+ }
217
+
218
+ export namespace LayoutResult {
219
+ export type Node = Omit<DiagramNode, 'modelRef' | 'deploymentRef' | 'inEdges' | 'outEdges'> & {
220
+ modelRef: Fqn
221
+ column: RelationshipDetailsTypes.Column
222
+ ports: RelationshipDetailsTypes.Ports
223
+ // existsInCurrentView: boolean
224
+ }
225
+ export type Edge = Omit<DiagramEdge, 'relations'> & {
226
+ relationId: RelationId
227
+ sourceHandle: string
228
+ targetHandle: string
229
+ // existsInCurrentView: boolean
230
+ // column: RelationshipsBrowserTypes.Column
231
+ }
232
+ }
233
+
234
+ export function layoutRelationshipDetails(
235
+ data: RelationshipDetailsViewData,
236
+ scope: LikeC4ViewModel<AnyAux> | null,
237
+ ): LayoutResult {
238
+ const g = createGraph()
239
+
240
+ const sources = createNodes('sources', data.sources, g),
241
+ targets = createNodes('targets', data.targets, g)
242
+
243
+ const edges: Array<{
244
+ name: string
245
+ source: string // node id
246
+ target: string // node id
247
+ sourceHandle: string
248
+ targetHandle: string
249
+ relationship: RelationshipModel
250
+ }> = Array.from(data.relationships).map(r => {
251
+ const source = sources.byId(r.source.id).graph
252
+ const target = targets.byId(r.target.id).graph
253
+ const name = r.id
254
+
255
+ g.node(source.id).outPorts.push(target.id)
256
+ g.node(target.id).inPorts.push(source.id)
257
+
258
+ g.setEdge(source.portId, target.portId, {
259
+ ...Sizes.edgeLabel,
260
+ }, name)
261
+ return {
262
+ name,
263
+ source: source.id,
264
+ sourceHandle: source.id + '_out' + (g.node(source.id).outPorts.length - 1),
265
+ target: target.id,
266
+ targetHandle: target.id + '_in' + (g.node(target.id).inPorts.length - 1),
267
+ relationship: r,
268
+ }
269
+ })
270
+
271
+ // Grow nodes with more than 2 ports
272
+ const nodeIds = [
273
+ ...sources.graphNodes.values(),
274
+ ...targets.graphNodes.values(),
275
+ ]
276
+ for (const { id: nodeId } of nodeIds) {
277
+ const node = g.node(nodeId)
278
+ if (node.isCompound) {
279
+ continue
280
+ }
281
+ const edgeCount = Math.max(g.inEdges(nodeId)?.length ?? 0, g.outEdges(nodeId)?.length ?? 0)
282
+ if (edgeCount > 3) {
283
+ node.height = node.height + (edgeCount - 4) * 14
284
+ }
285
+ }
286
+
287
+ const edgeCount = g.edgeCount()
288
+ if (edgeCount > 5) {
289
+ for (const edge of g.edges()) {
290
+ g.setEdge(edge, {
291
+ ...Sizes.edgeLabel,
292
+ width: edgeCount > 10 ? 800 : 400,
293
+ })
294
+ }
295
+ }
296
+
297
+ const dagreBounds = applyDagreLayout(g)
298
+
299
+ // Calculate bounds for all nodes except compounds
300
+ // We shrink compounds to fit their children
301
+ const _calculatedNodeBounds = pipe(
302
+ nodeIds,
303
+ // Compound nodes have different portId
304
+ filter(n => n.id === n.portId),
305
+ mapToObj(n => [n.id, dagreBounds(n.id)]),
306
+ )
307
+
308
+ function nodeBounds(nodeId: string): ReturnType<typeof dagreBounds> {
309
+ return _calculatedNodeBounds[nodeId] ??= pipe(
310
+ g.children(nodeId) as string[] | undefined ?? [],
311
+ filter(id => !id.endsWith(PortSuffix)),
312
+ map(id => nodeBounds(id)),
313
+ tap(bounds => {
314
+ invariant(bounds.length > 0, `Node ${nodeId} has no nested nodes`)
315
+ }),
316
+ reduce((acc, bounds) => {
317
+ return {
318
+ minY: Math.min(acc.minY, bounds.position.y),
319
+ maxY: Math.max(acc.maxY, bounds.position.y + bounds.height),
320
+ }
321
+ }, { minY: Infinity, maxY: -Infinity }),
322
+ ({ minY, maxY }) => {
323
+ const {
324
+ position: { x },
325
+ width,
326
+ } = dagreBounds(nodeId)
327
+ minY = minY - Sizes.compound.paddingTop
328
+ maxY = maxY + Sizes.compound.paddingBottom
329
+
330
+ return {
331
+ position: {
332
+ x,
333
+ y: minY,
334
+ },
335
+ width,
336
+ height: maxY - minY,
337
+ }
338
+ },
339
+ )
340
+ }
341
+
342
+ function nodeLevel(nodeId: string): number {
343
+ const parent = g.parent(nodeId)
344
+ if (parent) {
345
+ return nodeLevel(parent) + 1
346
+ }
347
+ return 0
348
+ }
349
+ function nodeDepth(nodeId: string): number {
350
+ const children = g.children(nodeId) as string[] | undefined ?? []
351
+ if (children.length === 0) {
352
+ return 0
353
+ }
354
+ return 1 + Math.max(...children.map(nodeDepth))
355
+ }
356
+
357
+ // Sort ports in subject vertically
358
+ const sortedPorts = (nodeId: string, type: 'in' | 'out', ports: string[]) => {
359
+ return pipe(
360
+ ports,
361
+ map((port, index) => {
362
+ return {
363
+ port: nodeId + '_' + type + index,
364
+ topY: nodeBounds(port).position.y,
365
+ }
366
+ }),
367
+ sortBy(prop('topY')),
368
+ map(prop('port')),
369
+ )
370
+ }
371
+ // In case we got negative positions
372
+ let minX = 0
373
+ let minY = 0
374
+
375
+ const nodes = nodeIds.map(({ id }): LayoutResult.Node => {
376
+ const { element, inPorts, outPorts, column } = g.node(id)
377
+ let { position, width, height } = nodeBounds(id)
378
+
379
+ const parentId = g.parent(id)
380
+
381
+ const children = (g.children(id) as NodeId[] | undefined ?? []).filter(c => !c.endsWith(PortSuffix))
382
+
383
+ minX = Math.min(minX, position.x)
384
+ minY = Math.min(minY, position.y)
385
+
386
+ const navigateTo = scope ? ifind(element.scopedViews(), v => v.id !== scope.id)?.id ?? null : null
387
+
388
+ const inheritFromNode = scope?.findNodeWithElement(element.id)
389
+ const scopedAncestor = scope && !inheritFromNode
390
+ ? ifind(element.ancestors(), a => !!scope.findNodeWithElement(a.id))?.id
391
+ : null
392
+ const inheritFromNodeOrAncestor = inheritFromNode ?? (scopedAncestor && scope?.findNodeWithElement(scopedAncestor))
393
+
394
+ return {
395
+ id: id as NodeId,
396
+ parent: parentId as NodeId ?? null,
397
+ position: [position.x, position.y],
398
+ title: element.title,
399
+ description: element.description,
400
+ technology: element.technology,
401
+ tags: null,
402
+ links: null,
403
+ color: inheritFromNodeOrAncestor?.color ?? element.color,
404
+ shape: inheritFromNode?.shape ?? element.shape,
405
+ icon: inheritFromNode?.icon ?? element.icon ?? 'none' as IconUrl,
406
+ modelRef: element.id,
407
+ kind: element.kind,
408
+ level: nodeLevel(id),
409
+ labelBBox: {
410
+ x: position.x,
411
+ y: position.y,
412
+ width: width,
413
+ height: height,
414
+ },
415
+ style: {
416
+ ...(inheritFromNode ?? inheritFromNodeOrAncestor)?.style,
417
+ ...element.$element.style,
418
+ },
419
+ navigateTo,
420
+ ...(children.length > 0 && { depth: nodeDepth(id) }),
421
+ children,
422
+ width,
423
+ height,
424
+ column,
425
+ ports: {
426
+ in: sortedPorts(id, 'in', inPorts),
427
+ out: sortedPorts(id, 'out', outPorts),
428
+ },
429
+ }
430
+ })
431
+
432
+ return {
433
+ bounds: {
434
+ x: Math.min(minX, 0),
435
+ y: Math.min(minY, 0),
436
+ width: g.graph().width ?? 100,
437
+ height: g.graph().height ?? 100,
438
+ },
439
+ nodes,
440
+ edges: g.edges().reduce((acc, e) => {
441
+ const edge = g.edge(e)
442
+ const ename = e.name
443
+ if (!ename) {
444
+ return acc
445
+ }
446
+ const { name, source, target, relationship, sourceHandle, targetHandle } = find(edges, e => e.name === ename)!
447
+ const label = relationship.title ?? 'untitled'
448
+ const navigateTo = relationship.navigateTo?.id ?? null
449
+ const description = relationship.description ?? null
450
+ const technology = relationship.technology ?? null
451
+ acc.push({
452
+ id: name as EdgeId,
453
+ source: source as NodeId,
454
+ sourceHandle,
455
+ target: target as NodeId,
456
+ targetHandle,
457
+ label,
458
+ color: relationship.color,
459
+ ...(navigateTo && { navigateTo }),
460
+ ...(description && { description }),
461
+ ...(technology && { technology }),
462
+ points: edge.points.map(p => [p.x, p.y] as Point) as unknown as NonEmptyArray<Point>,
463
+ line: relationship.line,
464
+ relationId: relationship.id,
465
+ parent: null,
466
+ })
467
+ return acc
468
+ }, [] as LayoutResult.Edge[]),
469
+ }
470
+ }
@@ -8,7 +8,6 @@ import { AnimatePresence, LayoutGroup, m } from 'framer-motion'
8
8
  import { memo, useEffect, useRef } from 'react'
9
9
  import type { SnapshotFrom } from 'xstate'
10
10
  import { BaseXYFlow } from '../../base/BaseXYFlow'
11
- import { useRelationshipsView } from './-useRelationshipsView'
12
11
  import type { RelationshipsBrowserTypes, RelationshipsBrowserTypes as Types } from './_types'
13
12
  import type { RelationshipsBrowserActorRef } from './actor'
14
13
  import { ViewPadding } from './const'
@@ -18,6 +17,7 @@ import {
18
17
  useRelationshipsBrowser,
19
18
  useRelationshipsBrowserState,
20
19
  } from './hooks'
20
+ import { useRelationshipsView } from './layout'
21
21
  import { SelectElement } from './SelectElement'
22
22
 
23
23
  const nodeTypes = {
@@ -168,7 +168,7 @@ const RelationshipsBrowserInner = memo(() => {
168
168
  if (instance.viewportInitialized) {
169
169
  browser.send({ type: 'xyflow.init', instance, store })
170
170
  }
171
- }, [store, instance, browser])
171
+ }, [store, instance.viewportInitialized, browser])
172
172
 
173
173
  const layouted = useRelationshipsView(subjectId, viewId, scope)
174
174
  const [historySubjectId, historyOps, { history, current }] = useStateHistory(subjectId)
@@ -83,6 +83,8 @@ export namespace RelationshipsBrowserTypes {
83
83
  // Extend the edge types provided by SharedFlowTypes with RelationshipsOfEdgeData
84
84
 
85
85
  export type EdgeData = Base.EdgeData<{
86
+ sourceFqn: Fqn
87
+ targetFqn: Fqn
86
88
  relations: NonEmptyArray<RelationId>
87
89
  color: Color | undefined
88
90
  label: string | null