Skip to content

Commit 55b4fab

Browse files
committed
feat: add route information to api node and routes edge on arch diagram
1 parent 47f1f2f commit 55b4fab

File tree

7 files changed

+171
-44
lines changed

7 files changed

+171
-44
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react'
2+
import { APIMethodBadge } from './APIMethodBadge'
3+
import type { Endpoint } from '@/types'
4+
5+
interface APIRoutesListProps {
6+
endpoints: Endpoint[]
7+
apiAddress: string
8+
}
9+
10+
const APIRoutesList: React.FC<APIRoutesListProps> = ({
11+
endpoints,
12+
apiAddress,
13+
}) => {
14+
return (
15+
<div className="flex flex-col gap-y-2">
16+
{endpoints.map((endpoint) => (
17+
<div key={endpoint.id} className="grid w-full grid-cols-12 gap-4">
18+
<div className="col-span-2 flex">
19+
<APIMethodBadge method={endpoint.method} />
20+
</div>
21+
<div className="col-span-10 flex justify-start">
22+
<a
23+
target="_blank noreferrer noopener"
24+
className="truncate hover:underline"
25+
href={`${apiAddress}${endpoint.path}`}
26+
rel="noreferrer"
27+
>
28+
{endpoint.path}
29+
</a>
30+
</div>
31+
</div>
32+
))}
33+
</div>
34+
)
35+
}
36+
37+
export default APIRoutesList

pkg/dashboard/frontend/src/components/architecture/Architecture.tsx

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ import ReactFlow, {
1111
ReactFlowProvider,
1212
Position,
1313
Panel,
14-
useOnSelectionChange,
15-
getConnectedEdges,
16-
applyEdgeChanges,
17-
type EdgeSelectionChange,
1814
} from 'reactflow'
1915
import Dagre from '@dagrejs/dagre'
2016
import 'reactflow/dist/style.css'
@@ -111,29 +107,6 @@ function ReactFlowLayout() {
111107
[setEdges],
112108
)
113109

114-
useOnSelectionChange({
115-
onChange: ({ nodes: nodesChanged }) => {
116-
const connectedEdges = getConnectedEdges(nodesChanged, edges)
117-
118-
// select all connected edges if node is selected
119-
if (connectedEdges.length) {
120-
setEdges(
121-
applyEdgeChanges(
122-
connectedEdges.map(
123-
(edge) =>
124-
({
125-
id: edge.id,
126-
type: 'select',
127-
selected: true,
128-
}) as EdgeSelectionChange,
129-
),
130-
edges,
131-
),
132-
)
133-
}
134-
},
135-
})
136-
137110
useEffect(() => {
138111
if (!data) return
139112

pkg/dashboard/frontend/src/components/architecture/DetailsDrawer.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import {
88
} from '../ui/drawer'
99
import { Button } from '../ui/button'
1010
import { useCallback, type PropsWithChildren } from 'react'
11-
import { applyNodeChanges, useNodes, useNodeId, useReactFlow } from 'reactflow'
11+
import {
12+
applyNodeChanges,
13+
useNodes,
14+
useNodeId,
15+
useReactFlow,
16+
applyEdgeChanges,
17+
useEdges,
18+
} from 'reactflow'
1219
import type { NodeBaseData } from './nodes/NodeBase'
1320
import type { nodeTypes } from '@/lib/utils/generate-architecture-data'
1421
export interface DetailsDrawerProps extends PropsWithChildren {
@@ -17,27 +24,35 @@ export interface DetailsDrawerProps extends PropsWithChildren {
1724
open: boolean
1825
testHref?: string
1926
footerChildren?: React.ReactNode
27+
// children that are rendered after the services reference
28+
trailingChildren?: React.ReactNode
2029
nodeType: keyof typeof nodeTypes
2130
icon: NodeBaseData['icon']
2231
address?: string
2332
services?: string[]
33+
type?: 'node' | 'edge'
34+
edgeId?: string
2435
}
2536

2637
export const DetailsDrawer = ({
2738
title,
2839
description,
2940
children,
3041
footerChildren,
42+
trailingChildren,
3143
open,
3244
testHref,
3345
icon: Icon,
3446
nodeType,
3547
address,
3648
services,
49+
type = 'node',
50+
edgeId,
3751
}: DetailsDrawerProps) => {
3852
const nodeId = useNodeId()
39-
const { setNodes } = useReactFlow()
53+
const { setNodes, setEdges } = useReactFlow()
4054
const nodes = useNodes()
55+
const edges = useEdges()
4156

4257
const selectServiceNode = useCallback(
4358
(serviceNodeId: string) => {
@@ -63,6 +78,23 @@ export const DetailsDrawer = ({
6378
)
6479

6580
const close = () => {
81+
if (type === 'edge') {
82+
setEdges(
83+
applyEdgeChanges(
84+
[
85+
{
86+
id: edgeId || '',
87+
type: 'select',
88+
selected: false,
89+
},
90+
],
91+
edges,
92+
),
93+
)
94+
95+
return
96+
}
97+
6698
setNodes(
6799
applyNodeChanges(
68100
[
@@ -130,6 +162,7 @@ export const DetailsDrawer = ({
130162
</div>
131163
</div>
132164
) : null}
165+
{trailingChildren}
133166
</div>
134167
<DrawerFooter className="px-0">
135168
{footerChildren}

pkg/dashboard/frontend/src/components/architecture/NitricEdge.tsx

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
useStore,
1010
type ReactFlowState,
1111
} from 'reactflow'
12+
import { DetailsDrawer } from './DetailsDrawer'
13+
import type { Endpoint } from '@/types'
14+
import type { ApiNodeData } from './nodes/APINode'
15+
import type { ServiceNodeData } from './nodes/ServiceNode'
16+
import { Button } from '../ui/button'
17+
import APIRoutesList from '../apis/APIRoutesList'
1218

1319
export default function NitricEdge({
1420
id,
@@ -19,13 +25,17 @@ export default function NitricEdge({
1925
targetX,
2026
targetY,
2127
label,
22-
sourcePosition,
23-
targetPosition,
28+
// sourcePosition,
29+
// targetPosition,
2430
style = {},
2531
markerEnd,
2632
selected,
2733
data,
28-
}: EdgeProps) {
34+
}: EdgeProps<{
35+
type: string
36+
endpoints: Endpoint[]
37+
apiAddress: string
38+
}>) {
2939
const allNodes = useNodes()
3040

3141
const xEqual = sourceX === targetX
@@ -58,9 +68,23 @@ export default function NitricEdge({
5868
curvature: isBiDirectionEdge ? -0.05 : undefined,
5969
})
6070

71+
const isAPIEdge = data?.type === 'api'
72+
73+
const highlightEdge = selected || sourceNode?.selected || targetNode?.selected
74+
75+
const Icon = (targetNode?.data as ServiceNodeData).icon
76+
6177
return (
6278
<>
63-
<BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
79+
<BaseEdge
80+
id={id}
81+
path={edgePath}
82+
style={{
83+
...style,
84+
stroke: highlightEdge ? 'rgb(var(--primary))' : style.stroke,
85+
}}
86+
markerEnd={markerEnd}
87+
/>
6488
{label && (
6589
<EdgeLabelRenderer>
6690
<div
@@ -73,6 +97,45 @@ export default function NitricEdge({
7397
}}
7498
>
7599
{label.toString().toLocaleLowerCase()}
100+
{isAPIEdge && (
101+
<DetailsDrawer
102+
title="Routes"
103+
nodeType="api"
104+
edgeId={id}
105+
type="edge"
106+
icon={(sourceNode?.data as ApiNodeData).icon}
107+
open={Boolean(selected)}
108+
footerChildren={
109+
<Button asChild>
110+
<a
111+
href={`vscode://file/${(targetNode?.data as ServiceNodeData).resource.filePath}`}
112+
>
113+
<Icon className="mr-2 h-4 w-4" />
114+
<span>Open in VSCode</span>
115+
</a>
116+
</Button>
117+
}
118+
>
119+
<div className="mb-4 text-sm">
120+
<span className="font-semibold">
121+
{(sourceNode?.data as ApiNodeData).title}
122+
</span>{' '}
123+
has{' '}
124+
<span className="font-semibold">
125+
{data.endpoints.length}{' '}
126+
{data.endpoints.length === 1 ? 'route' : 'routes'}
127+
</span>{' '}
128+
referenced by{' '}
129+
<span className="font-semibold">
130+
{(targetNode?.data as ServiceNodeData).title}
131+
</span>
132+
</div>
133+
<APIRoutesList
134+
apiAddress={data.apiAddress}
135+
endpoints={data.endpoints}
136+
/>
137+
</DetailsDrawer>
138+
)}
76139
</div>
77140
</EdgeLabelRenderer>
78141
)}

pkg/dashboard/frontend/src/components/architecture/nodes/APINode.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { type ComponentType } from 'react'
22

3-
import type { Api } from '@/types'
3+
import type { Api, Endpoint } from '@/types'
44
import type { NodeProps } from 'reactflow'
55
import NodeBase, { type NodeBaseData } from './NodeBase'
6+
import APIRoutesList from '@/components/apis/APIRoutesList'
67

7-
export type ApiNodeData = NodeBaseData<Api>
8+
export interface ApiNodeData extends NodeBaseData<Api> {
9+
endpoints: Endpoint[]
10+
}
811

912
export const APINode: ComponentType<NodeProps<ApiNodeData>> = (props) => {
1013
const { data } = props
@@ -20,6 +23,15 @@ export const APINode: ComponentType<NodeProps<ApiNodeData>> = (props) => {
2023
testHref: `/`, // TODO add url param to switch to resource
2124
address: data.address,
2225
services: data.resource.requestingServices,
26+
trailingChildren: data.address ? (
27+
<div className="flex flex-col gap-y-1">
28+
<span className="font-bold">Routes:</span>
29+
<APIRoutesList
30+
apiAddress={data.address}
31+
endpoints={data.endpoints}
32+
/>
33+
</div>
34+
) : null,
2335
}}
2436
/>
2537
)

pkg/dashboard/frontend/src/components/architecture/styles.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@
4949
@apply stroke-black/80;
5050
}
5151

52-
.react-flow__edge.selected {
53-
.react-flow__edge-path {
54-
@apply stroke-primary;
55-
}
56-
}
57-
5852
.react-flow__node-api {
5953
--nitric-node-from: #2563eb; /* Blue 600 */
6054
--nitric-node-via: #60a5fa; /* Blue 400 */

pkg/dashboard/frontend/src/lib/utils/generate-architecture-data.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
type BatchNodeData,
6464
} from '@/components/architecture/nodes/BatchNode'
6565
import { PERMISSION_TO_SDK_LABELS } from '../constants'
66+
import { flattenPaths } from './flatten-paths'
6667

6768
export const nodeTypes = {
6869
api: APINode,
@@ -216,6 +217,8 @@ export function generateArchitectureData(data: WebSocketResponse): {
216217
const apiAddress = data.apiAddresses[api.name]
217218
const routes = (api.spec && Object.keys(api.spec.paths)) || []
218219

220+
const allEndpoints = flattenPaths(api.spec)
221+
219222
const node = createNode<ApiNodeData>(api, 'api', {
220223
title: api.name,
221224
resource: api,
@@ -224,6 +227,7 @@ export function generateArchitectureData(data: WebSocketResponse): {
224227
description: `${routes.length} ${
225228
routes.length === 1 ? 'Route' : 'Routes'
226229
}`,
230+
endpoints: allEndpoints,
227231
})
228232

229233
const specEntries = (api.spec && api.spec.paths) || []
@@ -236,10 +240,16 @@ export function generateArchitectureData(data: WebSocketResponse): {
236240
return
237241
}
238242

243+
const target = method['x-nitric-target']['name']
244+
245+
const endpoints = allEndpoints.filter(
246+
(endpoint) => endpoint.requestingService === target,
247+
)
248+
239249
edges.push({
240-
id: `e-${api.name}-${method.operationId}-${method['x-nitric-target']['name']}`,
250+
id: `e-${api.name}-${method.operationId}-${target}`,
241251
source: node.id,
242-
target: method['x-nitric-target']['name'],
252+
target,
243253
animated: true,
244254
markerEnd: {
245255
type: MarkerType.ArrowClosed,
@@ -248,7 +258,12 @@ export function generateArchitectureData(data: WebSocketResponse): {
248258
type: MarkerType.ArrowClosed,
249259
orient: 'auto-start-reverse',
250260
},
251-
label: 'routes',
261+
label: `${endpoints.length} ${endpoints.length === 1 ? 'Route' : 'Routes'}`,
262+
data: {
263+
type: 'api',
264+
endpoints,
265+
apiAddress,
266+
},
252267
})
253268
})
254269
})

0 commit comments

Comments
 (0)