@likec4/diagram 1.17.0 → 1.18.0

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 (53) hide show
  1. package/package.json +24 -18
  2. package/src/LikeC4Diagram.props.ts +27 -12
  3. package/src/LikeC4Diagram.tsx +14 -9
  4. package/src/LikeC4Search.tsx +149 -0
  5. package/src/StaticLikeC4Diagram.tsx +1 -0
  6. package/src/hooks/useDiagramState.ts +8 -3
  7. package/src/hooks/useXYFlow.ts +2 -1
  8. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  9. package/src/likec4model/useLikeC4Model.ts +21 -11
  10. package/src/overlays/Overlays.tsx +96 -63
  11. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  12. package/src/overlays/edge-details/_types.ts +7 -3
  13. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  14. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  15. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  16. package/src/overlays/element-details/ElementDetailsCard.tsx +94 -81
  17. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  18. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  19. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  20. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  21. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  22. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  23. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  24. package/src/overlays/relationships-of/_types.ts +2 -2
  25. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  26. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  27. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  28. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  29. package/src/state/aligners.ts +473 -0
  30. package/src/state/diagramStore.layout.ts +67 -0
  31. package/src/state/diagramStore.ts +31 -77
  32. package/src/ui/DiagramTitlePanel.tsx +2 -29
  33. package/src/ui/Link.tsx +40 -0
  34. package/src/ui/notation/NotationPanel.tsx +4 -2
  35. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  36. package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
  37. package/src/ui/top-left/TopLeftPanel.tsx +16 -3
  38. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  39. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  42. package/src/xyflow/nodes/element/ElementNode.tsx +153 -92
  43. package/src/xyflow/nodes/element/element.css.ts +53 -11
  44. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  46. package/src/xyflow/useLayoutConstraints.ts +1 -1
  47. package/src/xyflow/useXYFlowEvents.ts +18 -5
  48. package/src/ui/OptionsPanel.css.ts +0 -9
  49. package/src/ui/OptionsPanel.tsx +0 -70
  50. package/src/ui/options/EdgeOptions.tsx +0 -142
  51. package/src/ui/options/NodeOptions.tsx +0 -362
  52. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  53. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -7,27 +7,27 @@ import * as css from './TabPanelStructure.css'
7
7
  interface ElementTreeNodeData {
8
8
  label: ReactNode
9
9
  value: string
10
- element: LikeC4Model.ElementModel
10
+ element: LikeC4Model.Element
11
11
  type: 'ancestor' | 'current' | 'descedant'
12
12
  children: TreeNodeData[]
13
13
  }
14
14
 
15
- interface MessageNodeData {
15
+ export interface MessageNodeData {
16
16
  label: ReactNode
17
17
  value: string
18
18
  type: 'message'
19
19
  children: TreeNodeData[]
20
20
  }
21
21
 
22
- type TreeNodeData = ElementTreeNodeData | MessageNodeData
22
+ export type TreeNodeData = ElementTreeNodeData | MessageNodeData
23
23
 
24
- type TabElementStructureProps = {
25
- element: LikeC4Model.ElementModel
24
+ type TabPanelStructureProps = {
25
+ element: LikeC4Model.Element
26
26
  }
27
27
 
28
- const ElementLabel = ({
28
+ export const ElementLabel = ({
29
29
  element
30
- }: { element: LikeC4Model.ElementModel; type: 'ancestor' | 'current' | 'descedant' }) => (
30
+ }: { element: LikeC4Model.Element; type: 'ancestor' | 'current' | 'descedant' }) => (
31
31
  <Box className={css.elementLabel}>
32
32
  <Text component="div" fz={'sm'} fw={'500'}>{element.title}</Text>
33
33
  </Box>
@@ -38,9 +38,9 @@ const ElementLabel = ({
38
38
  // }: RenderTreeNodePayload) => {
39
39
  // }
40
40
 
41
- export function TabElementStructure({
41
+ export function TabPanelStructure({
42
42
  element
43
- }: TabElementStructureProps) {
43
+ }: TabPanelStructureProps) {
44
44
  const tree = useTree({
45
45
  multiple: false
46
46
  })
@@ -60,7 +60,7 @@ export function TabElementStructure({
60
60
  value: element.id,
61
61
  element,
62
62
  type: 'current',
63
- children: element.children().map((child): TreeNodeData => ({
63
+ children: [...element.children()].map((child): TreeNodeData => ({
64
64
  label: <ElementLabel type="descedant" element={child} />,
65
65
  value: child.id,
66
66
  element: child,
@@ -93,7 +93,7 @@ export const RelationshipsOverlay = memo<{ subjectId: Fqn }>(function Relationsh
93
93
  <Box flex={'1 0 auto'}>
94
94
  <SelectElement
95
95
  scope={scope}
96
- subject={subject.element}
96
+ subject={subject.$element}
97
97
  onSelect={fqn =>
98
98
  overlay.openOverlay({
99
99
  relationshipsOf: fqn
@@ -1,13 +1,8 @@
1
1
  import { delay, type DiagramView, type Fqn, isAncestor, type Relation } from '@likec4/core'
2
- import { Box, Button, Group, SegmentedControl, Space, Text } from '@mantine/core'
3
- import { useId, useLocalStorage, useStateHistory } from '@mantine/hooks'
2
+ import { useId } from '@mantine/hooks'
4
3
  import { useDebouncedCallback, useDeepCompareEffect, useSyncedRef } from '@react-hookz/web'
5
- import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
6
4
  import {
7
- Background,
8
- BackgroundVariant,
9
5
  getViewportForBounds,
10
- Panel,
11
6
  ReactFlow,
12
7
  type ReactFlowInstance,
13
8
  type ReactFlowProps,
@@ -19,16 +14,13 @@ import {
19
14
  useStoreApi
20
15
  } from '@xyflow/react'
21
16
  import { getNodeDimensions } from '@xyflow/system'
22
- import { memo, type PropsWithChildren, useEffect, useRef, useState } from 'react'
17
+ import { type PropsWithChildren, useRef, useState } from 'react'
23
18
  import { isNullish, map, omit, only, prop, setPath, unique } from 'remeda'
24
19
  import { useUpdateEffect } from '../../hooks'
25
- import { useDiagramStoreApi } from '../../hooks/useDiagramState'
26
20
  import { centerXYInternalNode } from '../../xyflow/utils'
27
- import { useOverlayDialog } from '../OverlayContext'
28
21
  import { cssReactflowMarker } from '../Overlays.css'
29
22
  import type { XYFlowTypes } from './_types'
30
- import { SelectElement } from './SelectElement'
31
- import { useLayoutedRelationships, ZIndexes } from './use-layouted-relationships'
23
+ import { ZIndexes } from './use-layouted-relationships'
32
24
  import { CompoundNode } from './xyflow/CompoundNode'
33
25
  import { ElementNode } from './xyflow/ElementNode'
34
26
  import { EmptyNode } from './xyflow/EmptyNode'
@@ -1,4 +1,4 @@
1
- import { ancestorsFqn, type Element, type Fqn, type ViewID } from '@likec4/core'
1
+ import { ancestorsFqn, type Element, type Fqn, type ViewId } from '@likec4/core'
2
2
  import {
3
3
  ActionIcon,
4
4
  Box,
@@ -25,7 +25,7 @@ export const SelectElement = ({
25
25
  onSelect
26
26
  }: {
27
27
  subject: Element
28
- viewId: ViewID
28
+ viewId: ViewId
29
29
  scope: 'global' | 'view'
30
30
  onSelect: (id: Fqn) => void
31
31
  }) => {
@@ -1,4 +1,4 @@
1
- import type { ComputedNode, Fqn, Relation, ViewID } from '@likec4/core'
1
+ import type { ComputedNode, Fqn, Relation, ViewId } from '@likec4/core'
2
2
  import type {
3
3
  Edge as ReactFlowEdge,
4
4
  InternalNode as ReactFlowInternalNode,
@@ -26,7 +26,7 @@ export namespace XYFlowTypes {
26
26
  left: Port[]
27
27
  right: Port[]
28
28
  }
29
- navigateTo: ViewID | null
29
+ navigateTo: ViewId | null
30
30
  hovered?: boolean
31
31
  layoutId?: string
32
32
  leaving?: boolean
@@ -11,7 +11,7 @@ import {
11
11
  type LikeC4Model,
12
12
  nonNullable,
13
13
  parentFqn,
14
- type RelationID
14
+ type RelationId
15
15
  } from '@likec4/core'
16
16
  import { MarkerType } from '@xyflow/system'
17
17
  import { useMemo } from 'react'
@@ -118,15 +118,12 @@ function createGraph() {
118
118
  return g
119
119
  }
120
120
 
121
- type LikeC4ModelElement = LikeC4Model.ElementModel
122
- // type LikeC4ModelElement = LikeC4ViewModel.Element
123
-
124
121
  type Context = {
125
122
  scope: 'global' | 'view'
126
123
  g: dagre.graphlib.Graph
127
124
  subjectId: Fqn
128
125
  diagramNodes: Map<Fqn, DiagramNode>
129
- subjectElement: LikeC4ModelElement
126
+ subjectElement: LikeC4Model.Element
130
127
  // likec4model: LikeC4Model
131
128
  // subject: XYFlowTypes.ElementNode
132
129
  // Model
@@ -157,7 +154,7 @@ const graphId = (node: XYFlowTypes.Node) => ({
157
154
  })
158
155
 
159
156
  function nodeData(
160
- element: LikeC4Model.ElementModel,
157
+ element: LikeC4Model.Element,
161
158
  ctx: Context
162
159
  ): Omit<XYFlowTypes.NonEmptyNode['data'], 'column'> {
163
160
  // We try to inherit style from existing diagram node
@@ -167,7 +164,7 @@ function nodeData(
167
164
  // color from it if there is no diagram node
168
165
  const ancestor = diagramNode ?? (ctx.scope === 'view'
169
166
  ? pipe(
170
- element.ancestors(),
167
+ element.ancestors().toArray(),
171
168
  map(ancestor => ctx.diagramNodes.get(ancestor.id)),
172
169
  filter(isTruthy),
173
170
  first()
@@ -180,11 +177,11 @@ function nodeData(
180
177
  element: {
181
178
  kind: element.kind,
182
179
  title: diagramNode?.title ?? element.title,
183
- description: diagramNode?.description ?? element.element.description,
180
+ description: diagramNode?.description ?? element.description,
184
181
  color: diagramNode?.color ?? ancestor?.color ?? element.color,
185
182
  shape: diagramNode?.shape ?? element.shape
186
183
  },
187
- navigateTo: diagramNode?.navigateTo ?? first(element.viewsOf())?.id ?? null,
184
+ navigateTo: diagramNode?.navigateTo ?? element.defaultView?.id ?? null,
188
185
  ports: {
189
186
  left: [],
190
187
  right: []
@@ -228,7 +225,7 @@ function createEmptyNode(
228
225
  function createNode(
229
226
  column: ColumnKey,
230
227
  nodeType: Exclude<XYFlowTypes.Node['type'], 'empty'>,
231
- element: LikeC4Model.ElementModel,
228
+ element: LikeC4Model.Element,
232
229
  ctx: Context,
233
230
  depth: number = 0
234
231
  ): XYFlowTypes.ElementNode | XYFlowTypes.CompoundNode {
@@ -245,7 +242,7 @@ function createNode(
245
242
 
246
243
  // Create parent node
247
244
  const parent = pipe(
248
- element.ancestors(),
245
+ element.ancestors().toArray(),
249
246
  takeWhile(ancestor => !isAncestor(ancestor.id, ctx.subjectId)),
250
247
  find(ancestor =>
251
248
  ctx.diagramNodes.has(ancestor.id) || ctx.connected[column].has(ancestor.id)
@@ -363,7 +360,7 @@ function layout(
363
360
  ): {
364
361
  viewIncludesSubject: boolean
365
362
  notIncludedRelations: number
366
- subject: LikeC4ModelElement
363
+ subject: LikeC4Model.Element
367
364
  nodes: XYFlowTypes.Node[]
368
365
  edges: XYFlowTypes.Edge[]
369
366
  bounds: {
@@ -381,7 +378,7 @@ function layout(
381
378
  view.edges.flatMap(e => e.relations)
382
379
  )
383
380
  // Relations that are not included in the view
384
- const notIncludedRelations = new Set<RelationID>()
381
+ const notIncludedRelations = new Set<RelationId>()
385
382
 
386
383
  if (!viewIncludesSubject) {
387
384
  // Reset scope to global if subject is not in the view
@@ -401,16 +398,18 @@ function layout(
401
398
 
402
399
  if (scope === 'global') {
403
400
  relationships = {
404
- incoming: subjectElement.incoming().map(r => r.relationship),
405
- outgoing: subjectElement.outgoing().map(r => r.relationship)
401
+ incoming: subjectElement.incoming().map(r => r.$relationship).toArray(),
402
+ outgoing: subjectElement.outgoing().map(r => r.$relationship).toArray()
406
403
  }
407
404
  } else {
408
- const subjectViewModel = likec4model.view(view.id).element(subjectId)
405
+ const subjectViewModel = likec4model.view(view.id).node(subjectId)
409
406
  relationships = {
410
407
  incoming: subjectViewModel.incoming()
411
- .flatMap(c => c.relationships().map(r => r.relationship)),
408
+ .flatMap(c => c.relationships('model').map(r => r.$relationship))
409
+ .toArray(),
412
410
  outgoing: subjectViewModel.outgoing()
413
- .flatMap(c => c.relationships().map(r => r.relationship))
411
+ .flatMap(c => c.relationships('model').map(r => r.$relationship))
412
+ .toArray()
414
413
  }
415
414
  }
416
415
 
@@ -445,7 +444,7 @@ function layout(
445
444
  })
446
445
 
447
446
  if (viewIncludesSubject) {
448
- const subjectViewModel = likec4model.view(view.id).element(subjectId)
447
+ const subjectViewModel = likec4model.view(view.id).node(subjectId)
449
448
  subjectViewModel.incomers().forEach(incomer => {
450
449
  ctx.connected.incomers.add(incomer.id)
451
450
  })
@@ -86,14 +86,12 @@ export const CompoundNode = memo<CompoundNodeProps>(({
86
86
  {...(selectable && {
87
87
  whileHover: {
88
88
  ...scale(12),
89
- scaleX: (width + 12) / width,
90
- scaleY: (height + 12) / height,
91
89
  transition: {
92
90
  delay: 0.1
93
91
  }
94
92
  },
95
93
  whileTap: {
96
- ...scale(-12)
94
+ ...scale(-8)
97
95
  }
98
96
  })}
99
97
  >
@@ -30,7 +30,7 @@ function selector(s: DiagramState) {
30
30
  return {
31
31
  currentViewId: s.view.id,
32
32
  onNavigateTo: s.onNavigateTo,
33
- onOpenSource: s.onOpenSourceElement
33
+ onOpenSource: s.onOpenSource
34
34
  }
35
35
  }
36
36
 
@@ -56,10 +56,14 @@ export const ElementNode = memo<ElementNodeProps>(({
56
56
  onOpenSource
57
57
  } = useDiagramState(selector)
58
58
 
59
- const scale = (diff: number) => ({
60
- scaleX: (w + diff) / w,
61
- scaleY: (h + diff) / h
62
- })
59
+ const maxWH = Math.max(w, h)
60
+ const scale = (diff: number) => {
61
+ const s = (maxWH + diff) / maxWH
62
+ return ({
63
+ scaleX: s,
64
+ scaleY: s
65
+ })
66
+ }
63
67
 
64
68
  let opacity = 1
65
69
  if (data.dimmed) {
@@ -155,7 +159,9 @@ export const ElementNode = memo<ElementNodeProps>(({
155
159
  <Action
156
160
  onClick={(event) => {
157
161
  event.stopPropagation()
158
- onOpenSource(data.fqn)
162
+ onOpenSource?.({
163
+ element: data.fqn
164
+ })
159
165
  }}>
160
166
  <IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
161
167
  </Action>
@@ -2,7 +2,6 @@ import { ActionIcon, Box, Group, Stack, Text, ThemeIcon, Tooltip as MantineToolt
2
2
  import { IconBoxMultipleFilled, IconZoomScan } from '@tabler/icons-react'
3
3
  import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
- import { useEffect, useRef } from 'react'
6
5
  import { only } from 'remeda'
7
6
  import { useDiagramState } from '../../../hooks/useDiagramState'
8
7
  import { stopPropagation } from '../../../xyflow/utils'