@likec4/diagram 1.17.1 → 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.
- package/package.json +17 -15
- package/src/LikeC4Diagram.props.ts +21 -12
- package/src/LikeC4Diagram.tsx +5 -7
- package/src/LikeC4Search.tsx +54 -51
- package/src/hooks/useDiagramState.ts +8 -3
- package/src/hooks/useXYFlow.ts +2 -1
- package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
- package/src/likec4model/useLikeC4Model.ts +21 -11
- package/src/overlays/Overlays.tsx +96 -63
- package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
- package/src/overlays/edge-details/_types.ts +7 -3
- package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
- package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
- package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
- package/src/overlays/element-details/ElementDetailsCard.tsx +91 -78
- package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
- package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
- package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
- package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
- package/src/overlays/relationships-of/SelectElement.tsx +2 -2
- package/src/overlays/relationships-of/_types.ts +2 -2
- package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
- package/src/state/diagramStore.ts +28 -11
- package/src/ui/DiagramTitlePanel.tsx +2 -29
- package/src/ui/Link.tsx +40 -0
- package/src/ui/notation/NotationPanel.tsx +4 -2
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
- package/src/ui/top-left/TopLeftPanel.tsx +5 -5
- package/src/xyflow/edges/EdgeLabel.tsx +4 -4
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
- package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
- package/src/xyflow/nodes/element/ElementNode.tsx +147 -124
- package/src/xyflow/nodes/element/element.css.ts +52 -19
- package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
- package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
- package/src/xyflow/useLayoutConstraints.ts +1 -1
- package/src/xyflow/useXYFlowEvents.ts +18 -5
- package/src/ui/OptionsPanel.css.ts +0 -9
- package/src/ui/OptionsPanel.tsx +0 -70
- package/src/ui/options/EdgeOptions.tsx +0 -142
- package/src/ui/options/NodeOptions.tsx +0 -362
- package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
- 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
|
-
<
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
e
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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().
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|
|
229
|
-
all.add(relation.source)
|
|
230
|
-
all.add(relation.target)
|
|
231
|
-
return
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
54
|
+
{data.technology && (
|
|
55
55
|
<Text component={'div'} className={css.edgeLabelTechnology}>
|
|
56
56
|
{'[ '}
|
|
57
|
-
{data.
|
|
57
|
+
{data.technology}
|
|
58
58
|
{' ]'}
|
|
59
59
|
</Text>
|
|
60
60
|
)}
|