@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.
- package/package.json +24 -18
- package/src/LikeC4Diagram.props.ts +27 -12
- package/src/LikeC4Diagram.tsx +14 -9
- package/src/LikeC4Search.tsx +149 -0
- package/src/StaticLikeC4Diagram.tsx +1 -0
- 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 +94 -81
- 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/aligners.ts +473 -0
- package/src/state/diagramStore.layout.ts +67 -0
- package/src/state/diagramStore.ts +31 -77
- 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/ManualLayoutToolsButton.tsx +15 -0
- package/src/ui/top-left/TopLeftPanel.tsx +16 -3
- 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 +153 -92
- package/src/xyflow/nodes/element/element.css.ts +53 -11
- 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,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ComputedNode,
|
|
3
|
+
ComputedView,
|
|
4
4
|
type DiagramView,
|
|
5
|
-
type EdgeId,
|
|
6
5
|
type Element,
|
|
7
6
|
type Fqn,
|
|
8
7
|
invariant,
|
|
8
|
+
isDeploymentView,
|
|
9
|
+
isScopedElementView,
|
|
10
|
+
type LikeC4View,
|
|
9
11
|
type Link,
|
|
10
|
-
type
|
|
12
|
+
type ViewId
|
|
11
13
|
} from '@likec4/core'
|
|
12
14
|
import {
|
|
13
15
|
ActionIcon,
|
|
@@ -37,20 +39,20 @@ import {
|
|
|
37
39
|
Tooltip as MantineTooltip,
|
|
38
40
|
UnstyledButton
|
|
39
41
|
} from '@mantine/core'
|
|
40
|
-
import { useViewportSize } from '@mantine/hooks'
|
|
41
|
-
import { IconCheck, IconCopy, IconExternalLink, IconFileSymlink, IconZoomScan } from '@tabler/icons-react'
|
|
42
|
-
import { useInternalNode } from '@xyflow/react'
|
|
42
|
+
import { useSessionStorage, useViewportSize } from '@mantine/hooks'
|
|
43
|
+
import { IconCheck, IconCopy, IconExternalLink, IconFileSymlink, IconStack2, IconZoomScan } from '@tabler/icons-react'
|
|
43
44
|
import clsx from 'clsx'
|
|
44
45
|
import { m, type PanInfo, useDragControls, useMotionValue } from 'framer-motion'
|
|
45
|
-
import {
|
|
46
|
-
import { clamp, find, isNullish, map, only, partition, pick, pipe
|
|
47
|
-
import { useDiagramState, useDiagramStoreApi, useXYFlow } from '../../hooks'
|
|
46
|
+
import { memo, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'
|
|
47
|
+
import { clamp, find, isNullish, map, only, partition, pick, pipe } from 'remeda'
|
|
48
|
+
import { useDiagramState, useDiagramStoreApi, useXYFlow, useXYInternalNode } from '../../hooks'
|
|
48
49
|
import type { ElementIconRenderer, OnNavigateTo } from '../../LikeC4Diagram.props'
|
|
49
|
-
import {
|
|
50
|
+
import { useLikeC4CurrentViewModel } from '../../likec4model'
|
|
50
51
|
import { useOverlayDialog } from '../OverlayContext'
|
|
51
52
|
import * as css from './ElementDetailsCard.css'
|
|
53
|
+
import { TabPanelDeployments } from './TabPanelDeployments'
|
|
52
54
|
import { TabPanelRelationships } from './TabPanelRelationships'
|
|
53
|
-
import {
|
|
55
|
+
import { TabPanelStructure } from './TabPanelStructure'
|
|
54
56
|
|
|
55
57
|
const Divider = MantineDivider.withProps({
|
|
56
58
|
mb: 8,
|
|
@@ -88,62 +90,68 @@ type ElementDetailsCardProps = {
|
|
|
88
90
|
|
|
89
91
|
const MIN_PADDING = 24
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
const TABS = ['Properties', 'Relationships', 'Views', 'Structure', 'Deployments'] as const
|
|
94
|
+
type TabName = typeof TABS[number]
|
|
95
|
+
|
|
96
|
+
export const ElementDetailsCard = memo(({ fqn }: ElementDetailsCardProps) => {
|
|
92
97
|
const windowSize = useViewportSize()
|
|
93
98
|
const windowWidth = windowSize.width || window.innerWidth || 1200,
|
|
94
99
|
windowHeight = windowSize.height || window.innerHeight || 800
|
|
95
100
|
const xyflow = useXYFlow()
|
|
96
|
-
const xynode =
|
|
101
|
+
const xynode = useXYInternalNode(fqn)
|
|
97
102
|
invariant(xynode, `XYNode with id ${fqn} not found`)
|
|
98
103
|
|
|
99
104
|
const overlay = useOverlayDialog()
|
|
100
|
-
const [activeTab, setActiveTab] =
|
|
105
|
+
const [activeTab, setActiveTab] = useSessionStorage<TabName>({
|
|
106
|
+
key: `likec4:element-details:active-tab`,
|
|
107
|
+
defaultValue: 'Properties'
|
|
108
|
+
})
|
|
101
109
|
const diagramApi = useDiagramStoreApi()
|
|
110
|
+
const viewModel = useLikeC4CurrentViewModel()
|
|
102
111
|
const {
|
|
103
112
|
view: currentView,
|
|
104
113
|
renderIcon,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const viewId = currentView.id
|
|
114
|
+
onOpenSource
|
|
115
|
+
} = useDiagramState(pick(['view', 'renderIcon', 'onNavigateTo', 'onOpenSource']))
|
|
116
|
+
const diagramNode = currentView.nodes.find(n => n.id === fqn)
|
|
117
|
+
invariant(diagramNode, `DiagramNode with fqn ${fqn} not found`)
|
|
118
|
+
const nodeModel = viewModel.findNode(fqn)
|
|
119
|
+
invariant(nodeModel && nodeModel.hasElement(), `NodeModel with fqn ${fqn} not found`)
|
|
120
|
+
const elementModel = nodeModel.element
|
|
121
|
+
const viewId = viewModel.id
|
|
114
122
|
|
|
115
123
|
const elementIcon = ElementIcon({
|
|
116
|
-
element,
|
|
124
|
+
element: nodeModel.$node,
|
|
117
125
|
viewId,
|
|
118
126
|
renderIcon
|
|
119
127
|
})
|
|
120
128
|
|
|
121
|
-
const incoming = elementModel.incoming().map(r => r.id)
|
|
122
|
-
const outgoing = elementModel.outgoing().map(r => r.id)
|
|
129
|
+
// const incoming = elementModel.incoming().map(r => r.id).toArray()
|
|
130
|
+
// const outgoing = elementModel.outgoing().map(r => r.id).toArray()
|
|
123
131
|
|
|
124
|
-
const
|
|
132
|
+
// const incomingInView = unique(nodeModel.incoming().flatMap(e => e.$edge.relations).toArray())
|
|
133
|
+
// const outgoingInView = unique(nodeModel.outgoing().flatMap(e => e.$edge.relations).toArray())
|
|
125
134
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
...incoming,
|
|
131
|
-
...outgoing
|
|
132
|
-
].filter(r => !incomingInView.includes(r) && !outgoingInView.includes(r)).length
|
|
135
|
+
// const notIncludedRelations = [
|
|
136
|
+
// ...incoming,
|
|
137
|
+
// ...outgoing
|
|
138
|
+
// ].filter(r => !incomingInView.includes(r) && !outgoingInView.includes(r)).length
|
|
133
139
|
|
|
134
140
|
const [viewsOf, otherViews] = pipe(
|
|
135
|
-
elementModel.views(),
|
|
136
|
-
map(v => v
|
|
137
|
-
partition(
|
|
141
|
+
[...elementModel.views()],
|
|
142
|
+
map(v => v.$view),
|
|
143
|
+
partition(view => {
|
|
144
|
+
const v = view as LikeC4View
|
|
145
|
+
return isScopedElementView(v) && v.viewOf === fqn
|
|
146
|
+
})
|
|
138
147
|
)
|
|
139
148
|
|
|
140
|
-
const defaultView =
|
|
141
|
-
|
|
142
|
-
: (find(viewsOf, v => v.id !== currentView.id) ?? null)
|
|
149
|
+
const defaultView = nodeModel.navigateTo?.$view
|
|
150
|
+
?? find(viewsOf, v => v.id !== viewModel.id) ?? null
|
|
143
151
|
|
|
144
|
-
const defaultLink =
|
|
152
|
+
const defaultLink = only(nodeModel.links)
|
|
145
153
|
|
|
146
|
-
const onNavigateToCb = useCallback((toView:
|
|
154
|
+
const onNavigateToCb = useCallback((toView: ViewId, e?: React.MouseEvent): void => {
|
|
147
155
|
e?.stopPropagation()
|
|
148
156
|
const { onNavigateTo } = diagramApi.getState()
|
|
149
157
|
if (!onNavigateTo) {
|
|
@@ -152,21 +160,21 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
152
160
|
overlay.close(() => {
|
|
153
161
|
diagramApi.setState({
|
|
154
162
|
lastOnNavigate: {
|
|
155
|
-
fromView:
|
|
163
|
+
fromView: viewModel.id,
|
|
156
164
|
toView,
|
|
157
165
|
fromNode: fqn
|
|
158
166
|
}
|
|
159
167
|
})
|
|
160
168
|
onNavigateTo(toView)
|
|
161
169
|
})
|
|
162
|
-
}, [fqn,
|
|
170
|
+
}, [fqn, viewModel.id])
|
|
163
171
|
const controls = useDragControls()
|
|
164
172
|
|
|
165
|
-
const isCompound =
|
|
173
|
+
const isCompound = nodeModel.children.length > 0
|
|
166
174
|
|
|
167
175
|
const fromPositon = xyflow.flowToScreenPosition({
|
|
168
|
-
x: xynode.internals.positionAbsolute.x +
|
|
169
|
-
y: xynode.internals.positionAbsolute.y + (isCompound ? 0 :
|
|
176
|
+
x: xynode.internals.positionAbsolute.x + diagramNode.width / 2,
|
|
177
|
+
y: xynode.internals.positionAbsolute.y + (isCompound ? 0 : diagramNode.height / 2)
|
|
170
178
|
})
|
|
171
179
|
|
|
172
180
|
const _width = Math.min(700, windowWidth - MIN_PADDING * 2)
|
|
@@ -272,14 +280,14 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
272
280
|
overlay.close()
|
|
273
281
|
}
|
|
274
282
|
}}
|
|
275
|
-
data-likec4-color={
|
|
283
|
+
data-likec4-color={nodeModel.color}>
|
|
276
284
|
<FocusTrap>
|
|
277
285
|
<FocusTrapInitialFocus />
|
|
278
286
|
<Box
|
|
279
287
|
className={css.cardHeader}
|
|
280
288
|
onPointerDown={e => controls.start(e)}>
|
|
281
|
-
<Group align="start" justify="space-between" gap={'sm'} mb={'sm'}>
|
|
282
|
-
<Group align="start" gap={'sm'} style={{ cursor: 'default' }}>
|
|
289
|
+
<Group align="start" justify="space-between" gap={'sm'} mb={'sm'} wrap="nowrap">
|
|
290
|
+
<Group align="start" gap={'sm'} style={{ cursor: 'default' }} wrap="nowrap">
|
|
283
291
|
{elementIcon}
|
|
284
292
|
<Box>
|
|
285
293
|
<Text
|
|
@@ -289,9 +297,9 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
289
297
|
className={css.title}>
|
|
290
298
|
{elementModel.title}
|
|
291
299
|
</Text>
|
|
292
|
-
{
|
|
300
|
+
{nodeModel.$node.notation && (
|
|
293
301
|
<Text component="div" c={'dimmed'} fz={'sm'} fw={500} lh={1.3} lineClamp={1}>
|
|
294
|
-
{
|
|
302
|
+
{nodeModel.$node.notation}
|
|
295
303
|
</Text>
|
|
296
304
|
)}
|
|
297
305
|
</Box>
|
|
@@ -304,18 +312,18 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
304
312
|
}}
|
|
305
313
|
/>
|
|
306
314
|
</Group>
|
|
307
|
-
<Group align="baseline" gap={'sm'}>
|
|
315
|
+
<Group align="baseline" gap={'sm'} wrap="nowrap">
|
|
308
316
|
<Box>
|
|
309
317
|
<SmallLabel>kind</SmallLabel>
|
|
310
|
-
<Badge radius={'sm'} size="sm" fw={600} color="gray">{
|
|
318
|
+
<Badge radius={'sm'} size="sm" fw={600} color="gray">{elementModel.kind}</Badge>
|
|
311
319
|
</Box>
|
|
312
320
|
<Box flex={1}>
|
|
313
321
|
<SmallLabel>tags</SmallLabel>
|
|
314
322
|
<Flex gap={4} flex={1} mt={6}>
|
|
315
|
-
{
|
|
323
|
+
{elementModel.tags.map((tag) => (
|
|
316
324
|
<Badge key={tag} radius={'sm'} size="sm" fw={600} variant="gradient">#{tag}</Badge>
|
|
317
325
|
))}
|
|
318
|
-
{
|
|
326
|
+
{elementModel.tags.length === 0 && <Badge radius={'sm'} size="sm" fw={600} color="gray">—</Badge>}
|
|
319
327
|
</Flex>
|
|
320
328
|
</Box>
|
|
321
329
|
<ActionIconGroup
|
|
@@ -342,7 +350,9 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
342
350
|
radius="sm"
|
|
343
351
|
onClick={e => {
|
|
344
352
|
e.stopPropagation()
|
|
345
|
-
diagramApi.getState().
|
|
353
|
+
diagramApi.getState().onOpenSource?.({
|
|
354
|
+
element: elementModel.id
|
|
355
|
+
})
|
|
346
356
|
}}
|
|
347
357
|
>
|
|
348
358
|
<IconFileSymlink stroke={1.8} style={{ width: '62%' }} />
|
|
@@ -377,40 +387,33 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
377
387
|
panel: css.tabsPanel
|
|
378
388
|
}}>
|
|
379
389
|
<TabsList>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
</TabsTab>
|
|
386
|
-
<TabsTab value="Views">
|
|
387
|
-
Views
|
|
388
|
-
</TabsTab>
|
|
389
|
-
<TabsTab value="Structure">
|
|
390
|
-
Structure
|
|
391
|
-
</TabsTab>
|
|
390
|
+
{TABS.map(tab => (
|
|
391
|
+
<TabsTab key={tab} value={tab}>
|
|
392
|
+
{tab}
|
|
393
|
+
</TabsTab>
|
|
394
|
+
))}
|
|
392
395
|
</TabsList>
|
|
393
396
|
|
|
394
397
|
<TabsPanel value="Properties">
|
|
395
398
|
<ScrollArea scrollbars="y" type="auto">
|
|
396
399
|
<Box className={css.propertiesGrid} pt={'xs'}>
|
|
397
400
|
<ElementProperty title="description" emptyValue="no description">
|
|
398
|
-
{
|
|
401
|
+
{elementModel.description}
|
|
399
402
|
</ElementProperty>
|
|
400
|
-
{
|
|
403
|
+
{elementModel.technology && (
|
|
401
404
|
<ElementProperty title="technology">
|
|
402
|
-
{
|
|
405
|
+
{elementModel.technology}
|
|
403
406
|
</ElementProperty>
|
|
404
407
|
)}
|
|
405
|
-
{
|
|
408
|
+
{elementModel.links.length > 0 && (
|
|
406
409
|
<>
|
|
407
410
|
<PropertyLabel>links</PropertyLabel>
|
|
408
411
|
<Stack gap={'xs'} align="flex-start">
|
|
409
|
-
{
|
|
412
|
+
{elementModel.links.map((link, i) => <ElementLink key={i} value={link} />)}
|
|
410
413
|
</Stack>
|
|
411
414
|
</>
|
|
412
415
|
)}
|
|
413
|
-
{elementModel
|
|
416
|
+
{elementModel.$element.metadata && <ElementMetata value={elementModel.$element.metadata} />}
|
|
414
417
|
</Box>
|
|
415
418
|
</ScrollArea>
|
|
416
419
|
</TabsPanel>
|
|
@@ -419,6 +422,7 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
419
422
|
{activeTab === 'Relationships' && (
|
|
420
423
|
<TabPanelRelationships
|
|
421
424
|
element={elementModel}
|
|
425
|
+
node={nodeModel}
|
|
422
426
|
currentView={currentView} />
|
|
423
427
|
)}
|
|
424
428
|
</TabsPanel>
|
|
@@ -460,7 +464,13 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
460
464
|
|
|
461
465
|
<TabsPanel value="Structure">
|
|
462
466
|
<ScrollArea scrollbars="y" type="auto">
|
|
463
|
-
<
|
|
467
|
+
<TabPanelStructure element={elementModel} />
|
|
468
|
+
</ScrollArea>
|
|
469
|
+
</TabsPanel>
|
|
470
|
+
|
|
471
|
+
<TabsPanel value="Deployments">
|
|
472
|
+
<ScrollArea scrollbars="y" type="auto">
|
|
473
|
+
<TabPanelDeployments elementFqn={elementModel.id} />
|
|
464
474
|
</ScrollArea>
|
|
465
475
|
</TabsPanel>
|
|
466
476
|
</Tabs>
|
|
@@ -477,16 +487,17 @@ export function ElementDetailsCard({ fqn }: ElementDetailsCardProps) {
|
|
|
477
487
|
</RemoveScroll>
|
|
478
488
|
</m.dialog>
|
|
479
489
|
)
|
|
480
|
-
}
|
|
490
|
+
})
|
|
491
|
+
ElementDetailsCard.displayName = 'ElementDetailsCard'
|
|
481
492
|
|
|
482
493
|
const ElementIcon = (
|
|
483
494
|
{ element, viewId, renderIcon: RenderIcon }: {
|
|
484
|
-
element:
|
|
495
|
+
element: ComputedNode
|
|
485
496
|
viewId: string
|
|
486
497
|
renderIcon: ElementIconRenderer | null
|
|
487
498
|
}
|
|
488
499
|
) => {
|
|
489
|
-
if (!element.icon) {
|
|
500
|
+
if (!element.icon || element.icon === 'none') {
|
|
490
501
|
return null
|
|
491
502
|
}
|
|
492
503
|
let icon = null as React.ReactNode
|
|
@@ -525,7 +536,9 @@ const ViewButton = ({
|
|
|
525
536
|
<UnstyledButton className={css.viewButton} onClick={e => onNavigateTo(view.id, e)}>
|
|
526
537
|
<Group gap={6} align="start" wrap="nowrap">
|
|
527
538
|
<ThemeIcon size={'sm'} variant="transparent">
|
|
528
|
-
|
|
539
|
+
{isDeploymentView(view)
|
|
540
|
+
? <IconStack2 stroke={1.8} />
|
|
541
|
+
: <IconZoomScan stroke={1.8} />}
|
|
529
542
|
</ThemeIcon>
|
|
530
543
|
<Box>
|
|
531
544
|
<Text component="div" className={css.viewButtonTitle} lineClamp={1}>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { rem, Text, ThemeIcon } from '@mantine/core'
|
|
2
|
+
import { globalStyle, style } from '@vanilla-extract/css'
|
|
3
|
+
import { easings, mantine, transitions, whereDark } from '../../theme-vars'
|
|
4
|
+
|
|
5
|
+
export const treeNode = style({})
|
|
6
|
+
globalStyle(`${treeNode}[data-level='1']`, {
|
|
7
|
+
marginBottom: rem(16)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const treeNodeLabel = style({
|
|
11
|
+
cursor: 'default',
|
|
12
|
+
marginTop: 0,
|
|
13
|
+
marginBottom: 0
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const label = style({
|
|
17
|
+
transition: transitions.fast,
|
|
18
|
+
color: mantine.colors.gray[7],
|
|
19
|
+
':hover': {
|
|
20
|
+
transitionTimingFunction: easings.out
|
|
21
|
+
},
|
|
22
|
+
selectors: {
|
|
23
|
+
[`${whereDark} &`]: {
|
|
24
|
+
color: mantine.colors.dark[1]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
globalStyle(`${label} > *`, {
|
|
29
|
+
transition: transitions.fast
|
|
30
|
+
})
|
|
31
|
+
globalStyle(`${label}:hover > :not([data-no-transform])`, {
|
|
32
|
+
transitionTimingFunction: easings.out,
|
|
33
|
+
transform: 'translateX(1px)'
|
|
34
|
+
})
|
|
35
|
+
globalStyle(`.mantine-Button-root:hover ${label} > :not([data-no-transform])`, {
|
|
36
|
+
transitionTimingFunction: easings.out,
|
|
37
|
+
transform: 'translateX(1px)'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const nodeLabel = style([label, {}])
|
|
41
|
+
|
|
42
|
+
export const instanceLabel = style([label, {
|
|
43
|
+
cursor: 'pointer',
|
|
44
|
+
width: '100%',
|
|
45
|
+
justifyContent: 'stretch',
|
|
46
|
+
flexWrap: 'nowrap',
|
|
47
|
+
height: rem(36),
|
|
48
|
+
paddingInlineStart: rem(16),
|
|
49
|
+
paddingInlineEnd: rem(10),
|
|
50
|
+
borderRadius: mantine.radius.sm,
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
color: mantine.colors.gray[7],
|
|
53
|
+
selectors: {
|
|
54
|
+
[`${whereDark} &`]: {
|
|
55
|
+
color: mantine.colors.gray.lightColor
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
':hover': {
|
|
59
|
+
background: mantine.colors.gray.lightHover
|
|
60
|
+
}
|
|
61
|
+
}])
|
|
62
|
+
globalStyle(`${instanceLabel} .tabler-icon`, {
|
|
63
|
+
transition: transitions.fast,
|
|
64
|
+
width: '90%',
|
|
65
|
+
opacity: 0.65
|
|
66
|
+
})
|
|
67
|
+
// globalStyle(`.mantine-Button-root:hover ${instanceLabel} .tabler-icon`, {
|
|
68
|
+
// opacity: 0.8
|
|
69
|
+
// })
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { Fqn, LikeC4Model } from '@likec4/core'
|
|
2
|
+
import { Alert, Box, Button, Group, Menu, rem, Text, ThemeIcon, Tree, useTree } from '@mantine/core'
|
|
3
|
+
import { IconInfoCircle, IconTarget, IconZoomScan } from '@tabler/icons-react'
|
|
4
|
+
import { memo, type ReactNode, useEffect, useMemo } from 'react'
|
|
5
|
+
import { useCurrentViewId, useDiagramStoreApi } from '../../hooks'
|
|
6
|
+
import { useLikeC4ElementModel } from '../../likec4model/useLikeC4Model'
|
|
7
|
+
import { stopPropagation } from '../../xyflow/utils'
|
|
8
|
+
import * as css from './TabPanelDeployments.css'
|
|
9
|
+
|
|
10
|
+
interface TreeNodeData {
|
|
11
|
+
label: ReactNode
|
|
12
|
+
value: string
|
|
13
|
+
type: 'node' | 'instance'
|
|
14
|
+
children: TreeNodeData[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type TabPanelDeploymentsProps = {
|
|
18
|
+
elementFqn: Fqn
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DeploymentNodeRenderer = ({
|
|
22
|
+
node
|
|
23
|
+
}: {
|
|
24
|
+
node: LikeC4Model.DeploymentNode
|
|
25
|
+
}) => (
|
|
26
|
+
<Group className={css.nodeLabel} gap={6} align="baseline" wrap="nowrap">
|
|
27
|
+
<Text component="div" fz={11} c="dimmed">{node.kind}:</Text>
|
|
28
|
+
<Text component="div" fz={'sm'} fw={'500'}>{node.title}</Text>
|
|
29
|
+
</Group>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const DeployedInstanceRenderer = (
|
|
33
|
+
{
|
|
34
|
+
instance
|
|
35
|
+
}: {
|
|
36
|
+
instance: LikeC4Model.DeployedInstance
|
|
37
|
+
}
|
|
38
|
+
) => {
|
|
39
|
+
const currentViewId = useCurrentViewId()
|
|
40
|
+
const diagramApi = useDiagramStoreApi()
|
|
41
|
+
const views = [...instance.views()]
|
|
42
|
+
return (
|
|
43
|
+
(
|
|
44
|
+
<Group className={css.instanceLabel} gap={4}>
|
|
45
|
+
<ThemeIcon color="gray" variant="transparent" size={'xs'} flex={0}>
|
|
46
|
+
<IconTarget stroke={1.2} />
|
|
47
|
+
</ThemeIcon>
|
|
48
|
+
<Text component="div" fz={'sm'} fw={'500'} flex={'1 1 100%'}>{instance.title}</Text>
|
|
49
|
+
<Box onClick={stopPropagation} pos={'relative'} data-no-transform flex={0}>
|
|
50
|
+
{views.length === 0 && (
|
|
51
|
+
<Button size="compact-xs" variant="transparent" color="gray" disabled>
|
|
52
|
+
no views
|
|
53
|
+
</Button>
|
|
54
|
+
)}
|
|
55
|
+
{views.length > 0 && (
|
|
56
|
+
<Menu
|
|
57
|
+
shadow="md"
|
|
58
|
+
withinPortal={false}
|
|
59
|
+
position="bottom-start"
|
|
60
|
+
offset={0}
|
|
61
|
+
// trigger={'click-hover'}
|
|
62
|
+
// openDelay={100}
|
|
63
|
+
// closeDelay={200}
|
|
64
|
+
closeOnClickOutside
|
|
65
|
+
clickOutsideEvents={['pointerdown', 'mousedown', 'click']}
|
|
66
|
+
closeOnEscape
|
|
67
|
+
trapFocus
|
|
68
|
+
>
|
|
69
|
+
<Menu.Target>
|
|
70
|
+
<Button size="compact-xs" variant="subtle" color="gray">
|
|
71
|
+
{views.length} view{views.length > 1 ? 's' : ''}
|
|
72
|
+
</Button>
|
|
73
|
+
</Menu.Target>
|
|
74
|
+
|
|
75
|
+
<Menu.Dropdown>
|
|
76
|
+
{views.map(view => (
|
|
77
|
+
<Menu.Item
|
|
78
|
+
key={view.id}
|
|
79
|
+
px={'xs'}
|
|
80
|
+
py={4}
|
|
81
|
+
disabled={view.id === currentViewId}
|
|
82
|
+
leftSection={
|
|
83
|
+
<ThemeIcon size={'sm'} variant="transparent" color="gray">
|
|
84
|
+
<IconZoomScan stroke={1.8} opacity={0.65} />
|
|
85
|
+
</ThemeIcon>
|
|
86
|
+
}
|
|
87
|
+
styles={{
|
|
88
|
+
itemSection: {
|
|
89
|
+
marginInlineEnd: rem(8)
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
onClick={e => {
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
diagramApi.getState().onNavigateTo?.(view.id)
|
|
95
|
+
}}>
|
|
96
|
+
{view.title}
|
|
97
|
+
</Menu.Item>
|
|
98
|
+
))}
|
|
99
|
+
</Menu.Dropdown>
|
|
100
|
+
</Menu>
|
|
101
|
+
)}
|
|
102
|
+
</Box>
|
|
103
|
+
</Group>
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const TabPanelDeployments = memo<TabPanelDeploymentsProps>(({ elementFqn }) => {
|
|
109
|
+
const element = useLikeC4ElementModel(elementFqn)
|
|
110
|
+
const deployments = [...element.deployments()]
|
|
111
|
+
|
|
112
|
+
const tree = useTree({
|
|
113
|
+
multiple: false
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const data = useMemo(() => {
|
|
117
|
+
let roots = [] as TreeNodeData[]
|
|
118
|
+
let treeItems = new Map<string, TreeNodeData>()
|
|
119
|
+
|
|
120
|
+
for (const instance of element.deployments()) {
|
|
121
|
+
let instanceNode: TreeNodeData = {
|
|
122
|
+
label: <DeployedInstanceRenderer instance={instance} />,
|
|
123
|
+
value: instance.id,
|
|
124
|
+
type: 'instance' as const,
|
|
125
|
+
children: []
|
|
126
|
+
}
|
|
127
|
+
treeItems.set(instance.id, instanceNode)
|
|
128
|
+
|
|
129
|
+
let ancestor = instance.parent as LikeC4Model.DeploymentNode | null
|
|
130
|
+
while (ancestor) {
|
|
131
|
+
let ancestorNode = treeItems.get(ancestor.id)
|
|
132
|
+
if (ancestorNode) {
|
|
133
|
+
ancestorNode.children.push(instanceNode)
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
ancestorNode = {
|
|
137
|
+
label: <DeploymentNodeRenderer node={ancestor} />,
|
|
138
|
+
value: ancestor.id,
|
|
139
|
+
type: 'node' as const,
|
|
140
|
+
children: [instanceNode]
|
|
141
|
+
}
|
|
142
|
+
treeItems.set(ancestor.id, ancestorNode)
|
|
143
|
+
instanceNode = ancestorNode
|
|
144
|
+
ancestor = ancestor.parent
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!ancestor && !roots.includes(instanceNode)) {
|
|
148
|
+
roots.push(instanceNode)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return roots
|
|
152
|
+
}, [element])
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
tree.expandAllNodes()
|
|
156
|
+
}, [data])
|
|
157
|
+
|
|
158
|
+
if (deployments.length === 0) {
|
|
159
|
+
return (
|
|
160
|
+
<Alert variant="light" color="gray" icon={<IconInfoCircle />}>
|
|
161
|
+
This element does not have any deployments
|
|
162
|
+
</Alert>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
// <Box></Box>
|
|
168
|
+
<Tree
|
|
169
|
+
levelOffset={'sm'}
|
|
170
|
+
allowRangeSelection={false}
|
|
171
|
+
classNames={{
|
|
172
|
+
node: css.treeNode,
|
|
173
|
+
label: css.treeNodeLabel
|
|
174
|
+
}}
|
|
175
|
+
styles={{
|
|
176
|
+
root: {
|
|
177
|
+
position: 'relative',
|
|
178
|
+
|
|
179
|
+
width: 'min-content',
|
|
180
|
+
minWidth: 300
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
data={data}
|
|
184
|
+
tree={tree}
|
|
185
|
+
renderNode={({ node, selected, elementProps, hasChildren }) => (
|
|
186
|
+
<Box
|
|
187
|
+
{...elementProps}
|
|
188
|
+
style={{
|
|
189
|
+
...(!hasChildren && {
|
|
190
|
+
marginBottom: rem(4)
|
|
191
|
+
})
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{hasChildren
|
|
195
|
+
? (
|
|
196
|
+
<Button
|
|
197
|
+
fullWidth
|
|
198
|
+
color={'gray'}
|
|
199
|
+
variant={selected ? 'transparent' : 'subtle'}
|
|
200
|
+
size="xs"
|
|
201
|
+
justify="flex-start"
|
|
202
|
+
styles={{
|
|
203
|
+
root: {
|
|
204
|
+
position: 'unset',
|
|
205
|
+
paddingInlineStart: rem(16)
|
|
206
|
+
}
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{node.label}
|
|
210
|
+
</Button>
|
|
211
|
+
)
|
|
212
|
+
: node.label}
|
|
213
|
+
</Box>
|
|
214
|
+
)}
|
|
215
|
+
/>
|
|
216
|
+
)
|
|
217
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DiagramView, type
|
|
1
|
+
import { type DiagramView, type LikeC4Model, nameFromFqn } from '@likec4/core'
|
|
2
2
|
import { Box, Button, Group, Paper, SegmentedControl, Stack, Text, ThemeIcon } from '@mantine/core'
|
|
3
3
|
import { useId } from '@mantine/hooks'
|
|
4
4
|
import { IconArrowRight, IconExternalLink, IconInfoCircle } from '@tabler/icons-react'
|
|
@@ -14,26 +14,25 @@ import * as css from './TabPanelRelationships.css'
|
|
|
14
14
|
|
|
15
15
|
type RelationshipsTabPanelProps = {
|
|
16
16
|
currentView: DiagramView
|
|
17
|
-
|
|
17
|
+
node: LikeC4Model.Node
|
|
18
|
+
element: LikeC4Model.Element
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function TabPanelRelationships({
|
|
21
22
|
currentView,
|
|
23
|
+
node,
|
|
22
24
|
element
|
|
23
25
|
}: RelationshipsTabPanelProps) {
|
|
24
26
|
const layoutId = useId()
|
|
25
27
|
const enableRelationshipBrowser = useDiagramState(s => s.enableRelationshipBrowser)
|
|
26
28
|
const overlay = useOverlayDialog()
|
|
27
29
|
const [scope, setScope] = useState<'global' | 'view'>('view')
|
|
28
|
-
const node = nonNullable(currentView.nodes.find((n) => n.id === element.id))
|
|
29
30
|
|
|
30
|
-
const incoming = element.incoming().map(r => r.id)
|
|
31
|
-
const outgoing = element.outgoing().map(r => r.id)
|
|
31
|
+
const incoming = element.incoming().map(r => r.id).toArray()
|
|
32
|
+
const outgoing = element.outgoing().map(r => r.id).toArray()
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const incomingInView = unique(node.inEdges.flatMap(findRelationIds))
|
|
36
|
-
const outgoingInView = unique(node.outEdges.flatMap(findRelationIds))
|
|
34
|
+
const incomingInView = unique(node.incoming().flatMap(e => e.$edge.relations).toArray())
|
|
35
|
+
const outgoingInView = unique(node.outgoing().flatMap(e => e.$edge.relations).toArray())
|
|
37
36
|
|
|
38
37
|
const notIncludedRelations = [
|
|
39
38
|
...incoming,
|