@likec4/diagram 1.27.0 → 1.27.2

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 (151) hide show
  1. package/bundle/index.js +2343 -6880
  2. package/dist/LikeC4Diagram.js +3 -9
  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 +1 -2
  10. package/dist/base/primitives/edge/EdgeLabel.css.js +1 -4
  11. package/dist/base/primitives/edge/EdgeLabelContainer.js +1 -1
  12. package/dist/base/primitives/edge/EdgePath.js +0 -3
  13. package/dist/base/primitives/edge/edge.css.js +2 -0
  14. package/dist/base/primitives/element/ElementActionButtons.js +5 -10
  15. package/dist/base/primitives/element/ElementNodeContainer.css.js +5 -4
  16. package/dist/base/primitives/element/ElementShape.css.js +1 -12
  17. package/dist/base/primitives/element/ElementTitle.css.js +3 -2
  18. package/dist/base/types.js +2 -2
  19. package/dist/base/updateEdges.js +1 -9
  20. package/dist/base/updateNodes.js +1 -12
  21. package/dist/bundle/LikeC4View.js +2 -2
  22. package/dist/bundle/LikeC4View.props.d.ts +7 -7
  23. package/dist/components/Link.js +1 -1
  24. package/dist/hooks/useCurrentViewId.js +8 -2
  25. package/dist/hooks/useDiagram.d.ts +2659 -1996
  26. package/dist/hooks/useDiagram.js +16 -8
  27. package/dist/likec4diagram/DiagramXYFlow.js +10 -9
  28. package/dist/likec4diagram/custom/edges/RelationshipEdge.js +30 -41
  29. package/dist/likec4diagram/custom/nodes/toolbar/Toolbar.js +1 -1
  30. package/dist/likec4diagram/custom/nodes/toolbar/styles.css.js +6 -1
  31. package/dist/likec4diagram/ui/controls/LayoutDriftNotification.js +13 -5
  32. package/dist/likec4diagram/ui/notation/NotationPanel.css.js +3 -3
  33. package/dist/likec4diagram/ui/notation/NotationPanel.js +10 -4
  34. package/dist/likec4diagram/useLayoutConstraints.d.ts +11 -10
  35. package/dist/likec4diagram/useLayoutConstraints.js +64 -61
  36. package/dist/node_modules/.pnpm/mnemonist@0.40.3_patch_hash_5c12abb43fcff6fd81e53021b89075d53249640d165a2556cc1e58d71201a13a/node_modules/mnemonist/set.js +369 -0
  37. package/dist/overlays/Overlays.js +5 -3
  38. package/dist/overlays/element-details/actor.d.ts +2769 -173
  39. package/dist/overlays/overlay/Overlay.css.d.ts +26 -2
  40. package/dist/overlays/overlay/Overlay.css.js +83 -16
  41. package/dist/overlays/overlay/Overlay.d.ts +7 -6
  42. package/dist/overlays/overlay/Overlay.js +39 -32
  43. package/dist/overlays/overlaysActor.js +2 -2
  44. package/dist/overlays/relationship-details/RelationshipDetails.js +113 -70
  45. package/dist/overlays/relationship-details/SelectEdge.d.ts +1 -1
  46. package/dist/overlays/relationship-details/SelectEdge.js +15 -9
  47. package/dist/overlays/relationship-details/_types.d.ts +4 -1
  48. package/dist/overlays/relationship-details/actor.d.ts +415 -128
  49. package/dist/overlays/relationship-details/actor.js +162 -53
  50. package/dist/overlays/relationship-details/compute.d.ts +12 -0
  51. package/dist/overlays/relationship-details/compute.js +139 -0
  52. package/dist/overlays/relationship-details/custom/ElementActions.js +27 -5
  53. package/dist/overlays/relationship-details/custom/edgeTypes.js +1 -1
  54. package/dist/overlays/relationship-details/custom/nodeTypes.d.ts +2 -1
  55. package/dist/overlays/relationship-details/hooks.d.ts +336 -6
  56. package/dist/overlays/relationship-details/hooks.js +9 -7
  57. package/dist/overlays/relationship-details/layout-to-xyflow.d.ts +12 -0
  58. package/dist/overlays/relationship-details/layout-to-xyflow.js +149 -0
  59. package/dist/overlays/relationship-details/layout.d.ts +22 -0
  60. package/dist/overlays/relationship-details/layout.js +314 -0
  61. package/dist/overlays/relationships-browser/RelationshipsBrowser.d.ts +3 -1
  62. package/dist/overlays/relationships-browser/RelationshipsBrowser.js +2 -2
  63. package/dist/overlays/relationships-browser/SelectElement.js +4 -4
  64. package/dist/overlays/relationships-browser/_types.d.ts +2 -0
  65. package/dist/overlays/relationships-browser/actor.d.ts +130 -14
  66. package/dist/overlays/relationships-browser/actor.js +30 -16
  67. package/dist/overlays/relationships-browser/custom/ElementActions.js +5 -3
  68. package/dist/overlays/relationships-browser/custom/edgeTypes.d.ts +3 -1
  69. package/dist/overlays/relationships-browser/custom/edgeTypes.js +1 -1
  70. package/dist/overlays/relationships-browser/hooks.d.ts +3 -1
  71. package/dist/overlays/relationships-browser/hooks.js +1 -1
  72. package/dist/overlays/relationships-browser/{-useRelationshipsView.d.ts → layout.d.ts} +2 -0
  73. package/dist/overlays/relationships-browser/{-useRelationshipsView.js → layout.js} +19 -2
  74. package/dist/overlays/relationships-browser/useViewToNodesEdges.d.ts +1 -1
  75. package/dist/overlays/relationships-browser/useViewToNodesEdges.js +3 -1
  76. package/dist/packages/core/src/compute-view/relationships-view/compute.js +1 -1
  77. package/dist/packages/core/src/model/connection/model/ConnectionModel.js +121 -0
  78. package/dist/packages/core/src/model/connection/model/find.js +39 -0
  79. package/dist/packages/core/src/model/connection/ops.js +7 -0
  80. package/dist/packages/core/src/utils/const.js +3 -0
  81. package/dist/packages/core/src/utils/fqn.js +9 -1
  82. package/dist/packages/core/src/utils/iterable/find.js +21 -0
  83. package/dist/packages/core/src/utils/iterable/some.js +21 -0
  84. package/dist/packages/core/src/utils/set.js +48 -0
  85. package/dist/packages/core/src/utils/string-hash.js +13 -0
  86. package/dist/state/DiagramActorProvider.d.ts +4 -2
  87. package/dist/state/DiagramActorProvider.js +30 -37
  88. package/dist/state/assign.js +2 -2
  89. package/dist/state/diagram-machine.d.ts +1731 -1040
  90. package/dist/state/diagram-machine.js +126 -60
  91. package/dist/state/syncManualLayoutActor.d.ts +1 -1
  92. package/dist/styles.css.js +1 -1
  93. package/package.json +11 -11
  94. package/src/LikeC4Diagram.props.ts +7 -3
  95. package/src/LikeC4Diagram.tsx +2 -8
  96. package/src/StaticLikeC4Diagram.tsx +1 -1
  97. package/src/base/BaseXYFlow.tsx +2 -1
  98. package/src/base/primitives/compound/CompoundNodeContainer.tsx +1 -3
  99. package/src/base/primitives/edge/EdgeLabel.css.ts +1 -4
  100. package/src/base/primitives/edge/EdgeLabelContainer.tsx +1 -1
  101. package/src/base/primitives/edge/EdgePath.tsx +0 -3
  102. package/src/base/primitives/edge/edge.css.ts +2 -0
  103. package/src/base/primitives/element/ElementActionButtons.tsx +6 -7
  104. package/src/base/primitives/element/ElementNodeContainer.css.ts +5 -4
  105. package/src/base/primitives/element/ElementShape.css.ts +1 -12
  106. package/src/base/primitives/element/ElementTitle.css.ts +3 -2
  107. package/src/base/types.ts +2 -7
  108. package/src/base/updateEdges.ts +8 -8
  109. package/src/base/updateNodes.ts +11 -11
  110. package/src/bundle/LikeC4View.props.ts +7 -7
  111. package/src/bundle/LikeC4View.tsx +2 -2
  112. package/src/components/Link.tsx +3 -1
  113. package/src/hooks/useCurrentViewId.ts +4 -2
  114. package/src/hooks/useDiagram.ts +18 -9
  115. package/src/likec4diagram/DiagramXYFlow.tsx +11 -9
  116. package/src/likec4diagram/custom/edges/RelationshipEdge.tsx +39 -48
  117. package/src/likec4diagram/custom/nodes/toolbar/Toolbar.tsx +2 -2
  118. package/src/likec4diagram/custom/nodes/toolbar/styles.css.ts +5 -0
  119. package/src/likec4diagram/ui/controls/LayoutDriftNotification.tsx +8 -4
  120. package/src/likec4diagram/ui/notation/NotationPanel.css.ts +2 -2
  121. package/src/likec4diagram/ui/notation/NotationPanel.tsx +8 -6
  122. package/src/likec4diagram/useLayoutConstraints.ts +84 -89
  123. package/src/overlays/Overlays.tsx +5 -3
  124. package/src/overlays/overlay/Overlay.css.ts +83 -15
  125. package/src/overlays/overlay/Overlay.tsx +52 -41
  126. package/src/overlays/overlaysActor.ts +1 -1
  127. package/src/overlays/relationship-details/RelationshipDetails.tsx +131 -68
  128. package/src/overlays/relationship-details/SelectEdge.tsx +19 -12
  129. package/src/overlays/relationship-details/_types.ts +3 -1
  130. package/src/overlays/relationship-details/actor.ts +217 -68
  131. package/src/overlays/relationship-details/compute.ts +186 -0
  132. package/src/overlays/relationship-details/custom/ElementActions.tsx +27 -18
  133. package/src/overlays/relationship-details/custom/edgeTypes.tsx +1 -1
  134. package/src/overlays/relationship-details/hooks.ts +10 -8
  135. package/src/overlays/relationship-details/layout-to-xyflow.ts +195 -0
  136. package/src/overlays/relationship-details/layout.ts +470 -0
  137. package/src/overlays/relationships-browser/RelationshipsBrowser.tsx +2 -2
  138. package/src/overlays/relationships-browser/SelectElement.tsx +5 -5
  139. package/src/overlays/relationships-browser/_types.ts +2 -0
  140. package/src/overlays/relationships-browser/actor.ts +39 -14
  141. package/src/overlays/relationships-browser/custom/ElementActions.tsx +5 -3
  142. package/src/overlays/relationships-browser/custom/edgeTypes.tsx +1 -1
  143. package/src/overlays/relationships-browser/hooks.ts +2 -2
  144. package/src/overlays/relationships-browser/{-useRelationshipsView.ts → layout.ts} +23 -13
  145. package/src/overlays/relationships-browser/useViewToNodesEdges.ts +3 -1
  146. package/src/state/DiagramActorProvider.tsx +40 -50
  147. package/src/state/assign.ts +2 -2
  148. package/src/state/diagram-machine.ts +151 -75
  149. package/dist/overlays/relationship-details/useLayoutedDetails.d.ts +0 -19
  150. package/dist/overlays/relationship-details/useLayoutedDetails.js +0 -267
  151. package/src/overlays/relationship-details/useLayoutedDetails.ts +0 -376
@@ -0,0 +1,195 @@
1
+ import dagre, { type GraphLabel, type Label } from '@dagrejs/dagre'
2
+ import {
3
+ type AbstractRelation,
4
+ type DiagramEdge,
5
+ type DiagramView,
6
+ type EdgeId,
7
+ type Fqn,
8
+ type LikeC4Model,
9
+ type XYPoint,
10
+ compareFqnHierarchically,
11
+ compareRelations,
12
+ DiagramNode,
13
+ invariant,
14
+ isAncestor,
15
+ nonNullable,
16
+ Queue,
17
+ } from '@likec4/core'
18
+ import { useSelector } from '@xstate/react'
19
+ import { deepEqual } from 'fast-equals'
20
+ import { useMemo } from 'react'
21
+ import { filter, first, forEach, hasAtLeast, isTruthy, map, pipe, prop, reverse, sort, sortBy, takeWhile } from 'remeda'
22
+ import { ZIndexes } from '../../base/const'
23
+ import { useLikeC4Model } from '../../likec4model'
24
+ import type { RelationshipDetailsTypes } from './_types'
25
+ import type { RelationshipDetailsSnapshot } from './actor'
26
+ import {
27
+ type RelationshipDetailsViewData,
28
+ computeEdgeDetailsViewData,
29
+ computeRelationshipDetailsViewData,
30
+ } from './compute'
31
+ import { useRelationshipDetailsActor, useRelationshipDetailsState } from './hooks'
32
+ import { type LayoutResult, layoutRelationshipDetails } from './layout'
33
+
34
+ // const nodeZIndex = (node: DiagramNode) => node.level - (node.children.length > 0 ? 1 : 0)
35
+ export function layoutResultToXYFlow(
36
+ layout: LayoutResult,
37
+ ): {
38
+ xynodes: RelationshipDetailsTypes.Node[]
39
+ xyedges: RelationshipDetailsTypes.Edge[]
40
+ bounds: { x: number; y: number; width: number; height: number }
41
+ } {
42
+ const xynodes = [] as RelationshipDetailsTypes.Node[],
43
+ xyedges = [] as RelationshipDetailsTypes.Edge[],
44
+ nodeLookup = new Map<Fqn, LayoutResult.Node>()
45
+
46
+ type TraverseItem = {
47
+ node: LayoutResult.Node
48
+ parent: LayoutResult.Node | null
49
+ }
50
+ const queue = Queue.from(layout.nodes.reduce(
51
+ (acc, node) => {
52
+ nodeLookup.set(node.id, node)
53
+ if (!node.parent) {
54
+ acc.push({ node, parent: null })
55
+ }
56
+ return acc
57
+ },
58
+ [] as TraverseItem[],
59
+ ))
60
+ const ns = ''
61
+ const nodeById = (id: Fqn) => nonNullable(nodeLookup.get(id), `Node not found: ${id}`)
62
+
63
+ let next: TraverseItem | undefined
64
+ while ((next = queue.dequeue())) {
65
+ const { node, parent } = next
66
+ const isCompound = hasAtLeast(node.children, 1)
67
+ if (isCompound) {
68
+ for (const child of node.children) {
69
+ queue.enqueue({ node: nodeById(child), parent: node })
70
+ }
71
+ }
72
+
73
+ const position = {
74
+ x: node.position[0],
75
+ y: node.position[1],
76
+ }
77
+ if (parent) {
78
+ position.x -= parent.position[0]
79
+ position.y -= parent.position[1]
80
+ }
81
+
82
+ const id = ns + node.id
83
+
84
+ const base = {
85
+ id,
86
+ draggable: false,
87
+ selectable: true,
88
+ focusable: true,
89
+ deletable: false,
90
+ position,
91
+ zIndex: isCompound ? ZIndexes.Compound : ZIndexes.Element,
92
+ style: {
93
+ width: node.width,
94
+ height: node.height,
95
+ },
96
+ initialWidth: node.width,
97
+ initialHeight: node.height,
98
+ ...(parent && {
99
+ parentId: ns + parent.id,
100
+ }),
101
+ } satisfies Omit<RelationshipDetailsTypes.Node, 'data' | 'type'>
102
+
103
+ const fqn = node.modelRef
104
+ const navigateTo = { navigateTo: node.navigateTo ?? null }
105
+
106
+ switch (true) {
107
+ case isCompound: {
108
+ xynodes.push(
109
+ {
110
+ ...base,
111
+ type: 'compound',
112
+ data: {
113
+ column: node.column,
114
+ title: node.title,
115
+ color: node.color,
116
+ style: node.style,
117
+ depth: node.depth ?? 0,
118
+ icon: node.icon ?? 'none',
119
+ ports: node.ports,
120
+ fqn,
121
+ ...navigateTo,
122
+ },
123
+ },
124
+ )
125
+ break
126
+ }
127
+ default: {
128
+ xynodes.push(
129
+ {
130
+ ...base,
131
+ type: 'element' as const,
132
+ data: {
133
+ column: node.column,
134
+ fqn,
135
+ title: node.title,
136
+ technology: node.technology,
137
+ description: node.description,
138
+ height: node.height,
139
+ width: node.width,
140
+ color: node.color,
141
+ shape: node.shape,
142
+ icon: node.icon ?? 'none',
143
+ ports: node.ports,
144
+ style: node.style,
145
+ ...navigateTo,
146
+ },
147
+ },
148
+ )
149
+ }
150
+ }
151
+ }
152
+ for (
153
+ const {
154
+ source,
155
+ target,
156
+ relationId,
157
+ label,
158
+ technology,
159
+ description,
160
+ navigateTo = null,
161
+ color = 'gray',
162
+ line = 'dashed',
163
+ ...edge
164
+ } of layout.edges
165
+ ) {
166
+ const id = ns + edge.id
167
+ xyedges.push({
168
+ id,
169
+ type: 'relationship',
170
+ source: ns + source,
171
+ target: ns + target,
172
+ sourceHandle: edge.sourceHandle,
173
+ targetHandle: edge.targetHandle,
174
+ zIndex: ZIndexes.Edge,
175
+ // selectable: selectable,
176
+ // hidden: !visiblePredicate(edge),
177
+ deletable: false,
178
+ data: {
179
+ relationId,
180
+ label,
181
+ color,
182
+ navigateTo,
183
+ line,
184
+ ...(technology && { technology }),
185
+ ...(description && { description }),
186
+ },
187
+ })
188
+ }
189
+
190
+ return {
191
+ xynodes,
192
+ xyedges,
193
+ bounds: layout.bounds,
194
+ }
195
+ }
@@ -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)
@@ -61,9 +61,9 @@ export const SelectElement = memo(() => {
61
61
  }, [subjectId])
62
62
 
63
63
  return (
64
- <Group ref={root} gap={'xs'} wrap={'nowrap'} pos={'relative'}>
64
+ <Group ref={root} gap={'xs'} pos={'relative'}>
65
65
  {enableSelectSubject && (
66
- <>
66
+ <Group gap={4} wrap="nowrap">
67
67
  <Box
68
68
  fz={'xs'}
69
69
  fw={'500'}
@@ -159,10 +159,10 @@ export const SelectElement = memo(() => {
159
159
  </PopoverDropdown>
160
160
  </Popover>
161
161
  </Box>
162
- </>
162
+ </Group>
163
163
  )}
164
164
  {enableChangeScope && (
165
- <>
165
+ <Group gap={4} wrap="nowrap">
166
166
  {/* Show if only "select" is enabled */}
167
167
  {enableSelectSubject && (
168
168
  <Box
@@ -220,7 +220,7 @@ export const SelectElement = memo(() => {
220
220
  />
221
221
  </Tooltip>
222
222
  </div>
223
- </>
223
+ </Group>
224
224
  )}
225
225
  </Group>
226
226
  )