@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
@@ -1,9 +1,10 @@
1
- import { ActionIcon, Box, FocusTrap, RemoveScroll } from '@mantine/core'
1
+ import { ActionIcon, Box, Button, FocusTrap, Group, Notification, RemoveScroll, Text } from '@mantine/core'
2
2
  import { useHotkeys } from '@mantine/hooks'
3
3
  import { IconX } from '@tabler/icons-react'
4
4
  import { ReactFlowProvider as XYFlowProvider } from '@xyflow/react'
5
5
  import { AnimatePresence, m } from 'framer-motion'
6
6
  import { memo, type PropsWithChildren, useEffect, useMemo, useRef } from 'react'
7
+ import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'
7
8
  import { isNullish } from 'remeda'
8
9
  import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../hooks/useDiagramState'
9
10
  import { EdgeDetailsXYFlow } from './edge-details/EdgeDetailsXYFlow'
@@ -12,6 +13,36 @@ import { OverlayContext, useOverlayDialog } from './OverlayContext'
12
13
  import * as css from './Overlays.css'
13
14
  import { RelationshipsOverlay } from './relationships-of/RelationshipsOverlay'
14
15
 
16
+ function Fallback({ error, resetErrorBoundary }: FallbackProps) {
17
+ // Call resetErrorBoundary() to reset the error boundary and retry the render.
18
+ const errorString = error instanceof Error ? error.message : 'Unknown error'
19
+ return (
20
+ <Box className={css.container} p={'lg'}>
21
+ <Notification
22
+ icon={<IconX style={{ width: 16, height: 16 }} />}
23
+ styles={{
24
+ icon: {
25
+ alignSelf: 'flex-start'
26
+ }
27
+ }}
28
+ color={'red'}
29
+ title={'Oops, something went wrong'}
30
+ py={'lg'}
31
+ withCloseButton={false}>
32
+ <Text
33
+ style={{
34
+ whiteSpace: 'preserve-breaks'
35
+ }}>
36
+ {errorString}
37
+ </Text>
38
+ <Group gap={'xs'} mt="sm">
39
+ <Button color="gray" size="xs" variant="light" onClick={() => resetErrorBoundary()}>Reset</Button>
40
+ </Group>
41
+ </Notification>
42
+ </Box>
43
+ )
44
+ }
45
+
15
46
  export const Overlays = memo(() => {
16
47
  const diagramStore = useDiagramStoreApi()
17
48
  const {
@@ -53,68 +84,70 @@ export const Overlays = memo(() => {
53
84
 
54
85
  return (
55
86
  <OverlayContext.Provider value={ctxValue}>
56
- <AnimatePresence initial={false} key={viewId} onExitComplete={onExitComplete}>
57
- {activeOverlay?.elementDetails && (
58
- <ElementDetailsCard key={'details card'} fqn={activeOverlay.elementDetails} />
59
- )}
60
- </AnimatePresence>
61
- <AnimatePresence initial={false} onExitComplete={onExitComplete}>
62
- {activeOverlay && isNullish(activeOverlay.elementDetails) && (
63
- <RemoveScroll forwardProps>
64
- <Box
65
- component={m.div}
66
- className={css.container}
67
- data-likec4-color="gray"
68
- initial={{
69
- '--backdrop-blur': '0px',
70
- '--backdrop-opacity': '0%',
71
- opacity: 0,
72
- translateY: -15
73
- }}
74
- animate={{
75
- '--backdrop-blur': '10px',
76
- '--backdrop-opacity': '70%',
77
- opacity: 1,
78
- translateY: 0
79
- }}
80
- exit={{
81
- '--backdrop-blur': '1px',
82
- '--backdrop-opacity': '0%',
83
- translateY: -5,
84
- opacity: 0,
85
- transition: {
86
- duration: .2
87
- }
88
- }}
89
- >
90
- <FocusTrap>
91
- {activeOverlay.relationshipsOf && <RelationshipsOverlay subjectId={activeOverlay.relationshipsOf} />}
92
- {activeOverlay.edgeDetails && (
93
- <XYFlowProvider
94
- defaultNodes={[]}
95
- defaultEdges={[]}>
96
- <EdgeDetailsXYFlow edgeId={activeOverlay.edgeDetails} />
97
- </XYFlowProvider>
98
- )}
99
- <Box pos={'absolute'} top={'1rem'} right={'1rem'}>
100
- <ActionIcon
101
- variant="default"
102
- // color="gray"
103
- size={'lg'}
104
- // data-autofocus
105
- // autoFocus
106
- onClick={(e) => {
107
- e.stopPropagation()
108
- ctxValue.close()
109
- }}>
110
- <IconX />
111
- </ActionIcon>
112
- </Box>
113
- </FocusTrap>
114
- </Box>
115
- </RemoveScroll>
116
- )}
117
- </AnimatePresence>
87
+ <ErrorBoundary FallbackComponent={Fallback} onReset={() => ctxValue.close()}>
88
+ <AnimatePresence initial={false} key={viewId} onExitComplete={onExitComplete}>
89
+ {activeOverlay?.elementDetails && (
90
+ <ElementDetailsCard key={'details card'} fqn={activeOverlay.elementDetails} />
91
+ )}
92
+ </AnimatePresence>
93
+ <AnimatePresence initial={false} onExitComplete={onExitComplete}>
94
+ {activeOverlay && isNullish(activeOverlay.elementDetails) && (
95
+ <RemoveScroll forwardProps>
96
+ <Box
97
+ component={m.div}
98
+ className={css.container}
99
+ data-likec4-color="gray"
100
+ initial={{
101
+ '--backdrop-blur': '0px',
102
+ '--backdrop-opacity': '0%',
103
+ opacity: 0,
104
+ translateY: -15
105
+ }}
106
+ animate={{
107
+ '--backdrop-blur': '10px',
108
+ '--backdrop-opacity': '70%',
109
+ opacity: 1,
110
+ translateY: 0
111
+ }}
112
+ exit={{
113
+ '--backdrop-blur': '1px',
114
+ '--backdrop-opacity': '0%',
115
+ translateY: -5,
116
+ opacity: 0,
117
+ transition: {
118
+ duration: .2
119
+ }
120
+ }}
121
+ >
122
+ <FocusTrap>
123
+ {activeOverlay.relationshipsOf && <RelationshipsOverlay subjectId={activeOverlay.relationshipsOf} />}
124
+ {activeOverlay.edgeDetails && (
125
+ <XYFlowProvider
126
+ defaultNodes={[]}
127
+ defaultEdges={[]}>
128
+ <EdgeDetailsXYFlow edgeId={activeOverlay.edgeDetails} />
129
+ </XYFlowProvider>
130
+ )}
131
+ <Box pos={'absolute'} top={'1rem'} right={'1rem'}>
132
+ <ActionIcon
133
+ variant="default"
134
+ // color="gray"
135
+ size={'lg'}
136
+ // data-autofocus
137
+ // autoFocus
138
+ onClick={(e) => {
139
+ e.stopPropagation()
140
+ ctxValue.close()
141
+ }}>
142
+ <IconX />
143
+ </ActionIcon>
144
+ </Box>
145
+ </FocusTrap>
146
+ </Box>
147
+ </RemoveScroll>
148
+ )}
149
+ </AnimatePresence>
150
+ </ErrorBoundary>
118
151
  </OverlayContext.Provider>
119
152
  )
120
153
  })
@@ -194,7 +194,9 @@ export const EdgeDetailsXYFlow = memo<{ edgeId: EdgeId }>(function EdgeDetailsXY
194
194
  // }}
195
195
  onEdgeClick={(e, edge) => {
196
196
  e.stopPropagation()
197
- diagramStore.getState().onOpenSourceRelation?.(edge.data.relation.id)
197
+ diagramStore.getState().onOpenSource?.({
198
+ relation: edge.data.relationId
199
+ })
198
200
  }}
199
201
  // onEdgeClick={(e, edge) => {
200
202
  // e.stopPropagation()
@@ -1,4 +1,4 @@
1
- import type { ComputedNode, Fqn, Relation, ViewID } from '@likec4/core'
1
+ import type { ComputedNode, Fqn, RelationId, ViewId } from '@likec4/core'
2
2
  import type { Edge as ReactFlowEdge, Node as ReactFlowNode, ReactFlowInstance } from '@xyflow/react'
3
3
  import type { SetRequired } from 'type-fest'
4
4
 
@@ -10,7 +10,7 @@ export namespace XYFlowTypes {
10
10
  in: string[]
11
11
  out: string[]
12
12
  }
13
- navigateTo: ViewID | null
13
+ navigateTo: ViewId | null
14
14
  hovered?: boolean
15
15
  /**
16
16
  * Whether the node is dimmed
@@ -27,7 +27,11 @@ export namespace XYFlowTypes {
27
27
 
28
28
  export type Edge = Omit<ReactFlowEdge, 'data' | 'type'> & {
29
29
  data: {
30
- relation: Relation
30
+ technology: string | null
31
+ description: string | null
32
+ relationId: RelationId
33
+ // relation: Relation
34
+ navigateTo: ViewId | null
31
35
  hovered?: boolean
32
36
  dimmed?: boolean
33
37
  }
@@ -84,7 +84,7 @@ const graphId = (node: XYFlowTypes.Node) => ({
84
84
  })
85
85
 
86
86
  function nodeData(
87
- element: LikeC4Model.ElementModel,
87
+ element: LikeC4Model.Element,
88
88
  ctx: Context
89
89
  ): Omit<XYFlowTypes.Node['data'], 'column'> {
90
90
  // We try to inherit style from existing diagram node
@@ -93,7 +93,7 @@ function nodeData(
93
93
  // Ansector separetely, because we want to inherit
94
94
  // color from it if there is no diagram node
95
95
  const ancestor = diagramNode ?? pipe(
96
- element.ancestors(),
96
+ element.ancestors().toArray(),
97
97
  map(ancestor => ctx.diagramNodes.get(ancestor.id)),
98
98
  filter(isTruthy),
99
99
  first()
@@ -104,11 +104,11 @@ function nodeData(
104
104
  element: {
105
105
  kind: element.kind,
106
106
  title: diagramNode?.title ?? element.title,
107
- description: diagramNode?.description ?? element.element.description,
107
+ description: diagramNode?.description ?? element.description,
108
108
  color: diagramNode?.color ?? ancestor?.color ?? element.color,
109
109
  shape: diagramNode?.shape ?? element.shape
110
110
  },
111
- navigateTo: diagramNode?.navigateTo ?? first(element.viewsOf())?.id ?? null,
111
+ navigateTo: diagramNode?.navigateTo ?? first(element.scopedViews().take(1).toArray())?.id ?? null,
112
112
  ports: {
113
113
  in: [],
114
114
  out: []
@@ -118,7 +118,7 @@ function nodeData(
118
118
 
119
119
  function createNode(
120
120
  nodeType: XYFlowTypes.Node['type'],
121
- element: LikeC4Model.ElementModel,
121
+ element: LikeC4Model.Element,
122
122
  ctx: Context
123
123
  ): XYFlowTypes.Node {
124
124
  let node = ctx.xynodes.get(element.id)
@@ -129,7 +129,7 @@ function createNode(
129
129
 
130
130
  // Create parent node
131
131
  const parent = pipe(
132
- element.ancestors(),
132
+ element.ancestors().toArray(),
133
133
  takeWhile(ancestor => !isAncestor(ancestor.id, ctx.edge.source) && !isAncestor(ancestor.id, ctx.edge.target)),
134
134
  first(),
135
135
  found => found ? createNode('compound', found, ctx) : null
@@ -225,10 +225,14 @@ function layout(
225
225
 
226
226
  const relations = edge.relations
227
227
  .map(r => {
228
- const relation = likec4model.relationship(r).relationship
229
- all.add(relation.source)
230
- all.add(relation.target)
231
- return relation
228
+ const relation = likec4model.relationship(r)
229
+ all.add(relation.source.id)
230
+ all.add(relation.target.id)
231
+ return {
232
+ source: relation.source.id,
233
+ target: relation.target.id,
234
+ relation: relation.$relationship
235
+ }
232
236
  })
233
237
  .sort(compareRelations)
234
238
  .reverse()
@@ -258,10 +262,10 @@ function layout(
258
262
  g.setDefaultEdgeLabel(() => ({ width: 250 }))
259
263
  }
260
264
 
261
- for (const relation of relations) {
262
- const source = ctx.xynodes.get(relation.source)
265
+ for (const { relation, ...points } of relations) {
266
+ const source = ctx.xynodes.get(points.source)
263
267
  invariant(source, 'source node not found')
264
- const target = ctx.xynodes.get(relation.target)
268
+ const target = ctx.xynodes.get(points.target)
265
269
  invariant(target, 'target node not found')
266
270
 
267
271
  source.data.ports.out.push(target.id)
@@ -276,7 +280,10 @@ function layout(
276
280
  sourceHandle: target.id,
277
281
  targetHandle: source.id,
278
282
  data: {
279
- relation
283
+ relationId: relation.id,
284
+ navigateTo: relation.navigateTo ?? null,
285
+ technology: relation.technology ?? null,
286
+ description: relation.description ?? null
280
287
  },
281
288
  label: relation.title,
282
289
  zIndex: ZIndexes.edge,
@@ -29,7 +29,7 @@ function selector(s: DiagramState) {
29
29
  currentViewId: s.view.id,
30
30
  enableRelationshipBrowser: s.enableRelationshipBrowser,
31
31
  onNavigateTo: s.onNavigateTo,
32
- onOpenSource: s.onOpenSourceElement
32
+ onOpenSource: s.onOpenSource
33
33
  }
34
34
  }
35
35
 
@@ -119,7 +119,9 @@ export function ElementNode({
119
119
  <Action
120
120
  onClick={(event) => {
121
121
  event.stopPropagation()
122
- onOpenSource(data.fqn)
122
+ onOpenSource({
123
+ element: data.fqn
124
+ })
123
125
  }}>
124
126
  <IconFileSymlink stroke={1.8} style={{ width: '72%' }} />
125
127
  </Action>
@@ -2,7 +2,7 @@ import { ActionIcon, Box, Stack, Text } from '@mantine/core'
2
2
  import { IconZoomScan } from '@tabler/icons-react'
3
3
  import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getBezierPath } from '@xyflow/react'
4
4
  import clsx from 'clsx'
5
- import { useDiagramState, useDiagramStoreApi } from '../../../hooks/useDiagramState'
5
+ import { useDiagramState } from '../../../hooks/useDiagramState'
6
6
  import { stopPropagation } from '../../../xyflow/utils'
7
7
  import { useOverlayDialog } from '../../OverlayContext'
8
8
  import type { XYFlowTypes } from '../_types'
@@ -17,7 +17,7 @@ export function RelationshipEdge({
17
17
  const overlay = useOverlayDialog()
18
18
  const onNavigateTo = useDiagramState(s => s.onNavigateTo)
19
19
  const [edgePath, labelX, labelY] = getBezierPath(props)
20
- const navigateTo = data.relation.navigateTo
20
+ const navigateTo = data.navigateTo
21
21
  return (
22
22
  <>
23
23
  <g
@@ -51,10 +51,10 @@ export function RelationshipEdge({
51
51
  {label}
52
52
  </Text>
53
53
  )}
54
- {data.relation.technology && (
54
+ {data.technology && (
55
55
  <Text component={'div'} className={css.edgeLabelTechnology}>
56
56
  {'[ '}
57
- {data.relation.technology}
57
+ {data.technology}
58
58
  {' ]'}
59
59
  </Text>
60
60
  )}