Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dash): add route to service relationship views in dashboard #833

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions pkg/dashboard/frontend/cypress/e2e/api-explorer.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
const expectedEndpoints = [
'first-api-/all-methods-DELETE',
'first-api-/all-methods-GET',
'first-api-/all-methods-OPTIONS',
'first-api-/all-methods-PATCH',
'first-api-/all-methods-POST',
'first-api-/all-methods-PUT',
'first-api-/header-test-GET',
'first-api-/json-test-POST',
'first-api-/path-test/{name}-GET',
'first-api-/query-test-GET',
'first-api-/schedule-count-GET',
'first-api-/topic-count-GET',
'second-api-/content-type-binary-GET',
'second-api-/content-type-css-GET',
'second-api-/content-type-html-GET',
'second-api-/content-type-image-GET',
'second-api-/content-type-xml-GET',
'second-api-/image-from-bucket-DELETE',
'second-api-/image-from-bucket-GET',
'second-api-/image-from-bucket-PUT',
'second-api-/very-nested-files-PUT',
'my-db-api-/get-GET',
'my-secret-api-/get-GET',
'my-secret-api-/set-POST',
'my-secret-api-/set-binary-POST',
]

describe('APIs spec', () => {
beforeEach(() => {
cy.viewport('macbook-16')
Expand All @@ -6,38 +34,40 @@ describe('APIs spec', () => {
})

it('should retrieve correct apis and endpoints', () => {
// open api routes for testing
cy.get('[data-rct-item-id="second-api"]').click()

const expectedEndpoints = [
'first-api',
'first-api-/all-methods-DELETE',
'first-api-/all-methods-GET',
'first-api-/all-methods-OPTIONS',
'first-api-/all-methods-PATCH',
'first-api-/all-methods-POST',
'first-api-/all-methods-PUT',
'first-api-/header-test-GET',
'first-api-/json-test-POST',
'first-api-/path-test/{name}-GET',
'first-api-/query-test-GET',
'first-api-/schedule-count-GET',
'first-api-/topic-count-GET',
'second-api-/content-type-binary-GET',
'second-api-/content-type-css-GET',
'second-api-/content-type-html-GET',
'second-api-/content-type-image-GET',
'second-api-/content-type-xml-GET',
'second-api-/image-from-bucket-DELETE',
'second-api-/image-from-bucket-GET',
'second-api-/image-from-bucket-PUT',
'second-api-/very-nested-files-PUT',
]
cy.get('[data-rct-item-id="my-db-api"]').click()
cy.get('[data-rct-item-id="my-secret-api"]').click()

expectedEndpoints.forEach((id) => {
cy.get(`[data-rct-item-id="${id}"]`).should('exist')
})
})

it('should have correct service reference', () => {
// open api routes for testing
cy.get('[data-rct-item-id="second-api"]').click()
cy.get('[data-rct-item-id="my-db-api"]').click()
cy.get('[data-rct-item-id="my-secret-api"]').click()

expectedEndpoints.forEach((id) => {
cy.get(`[data-rct-item-id="${id}"]`).click()

let expectedServiceFile = 'my-test-service.ts'

if (id.includes('my-db-api')) {
expectedServiceFile = 'my-test-db.ts'
} else if (id.includes('my-secret-api')) {
expectedServiceFile = 'my-test-secret.ts'
}

cy.getTestEl('requesting-service').should(
'contain.text',
expectedServiceFile,
)
})
})

it('should allow query params', () => {
cy.intercept('/api/call/**').as('apiCall')

Expand Down
26 changes: 26 additions & 0 deletions pkg/dashboard/frontend/cypress/e2e/architecture.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,30 @@ describe('Architecture Spec', () => {
expect(cy.contains('.react-flow__node', content)).to.exist
})
})

it('should have correct routes drawer content', () => {
const expected = [
[
'edge-label-e-first-api-services/my-test-service.ts',
'DELETE/all-methodsGET/all-methodsOPTIONS/all-methodsPATCH/all-methodsPOST/all-methodsPUT/all-methodsGET/header-testPOST/json-testGET/path-test/{name}GET/query-testGET/schedule-countGET/topic-count',
],
[
'edge-label-e-second-api-services/my-test-service.ts',
'GET/content-type-binaryGET/content-type-cssGET/content-type-htmlGET/content-type-imageGET/content-type-xmlDELETE/image-from-bucketGET/image-from-bucketPUT/image-from-bucketPUT/very-nested-files',
],
[
'edge-label-e-my-secret-api-services/my-test-secret.ts',
'GET/getPOST/setPOST/set-binary',
],
['edge-label-e-my-db-api-services/my-test-db.ts', 'GET/get'],
]

expected.forEach(([edge, routes]) => {
cy.getTestEl(edge).click({
force: true,
})

cy.getTestEl('api-routes-list').should('have.text', routes)
})
})
})
21 changes: 21 additions & 0 deletions pkg/dashboard/frontend/src/components/apis/APIExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,27 @@ const APIExplorer = () => {
</Button>
</div>
</div>
{selectedApiEndpoint.requestingService ? (
<div className={'flex items-center gap-1'}>
<span className="font-semibold">Referenced by:</span>
<div className={'flex items-start gap-1'}>
<Tooltip>
<TooltipTrigger asChild>
<a
data-testid="requesting-service"
className="text-md h-auto p-0 hover:underline"
href={`vscode://file/${data?.services.find((svc) => svc.name === selectedApiEndpoint.requestingService)?.filePath}`}
>
{selectedApiEndpoint.requestingService}
</a>
</TooltipTrigger>
<TooltipContent>
<p>Open in VSCode</p>
</TooltipContent>
</Tooltip>
</div>
</div>
) : null}
{selectedDoesNotExist && (
<NotFoundAlert>
Endpoint not found. It might have been updated or removed.
Expand Down
37 changes: 37 additions & 0 deletions pkg/dashboard/frontend/src/components/apis/APIRoutesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { APIMethodBadge } from './APIMethodBadge'
import type { Endpoint } from '@/types'

interface APIRoutesListProps {
endpoints: Endpoint[]
apiAddress: string
}

const APIRoutesList: React.FC<APIRoutesListProps> = ({
endpoints,
apiAddress,
}) => {
return (
<div className="flex flex-col gap-y-2" data-testid="api-routes-list">
{endpoints.map((endpoint) => (
<div key={endpoint.id} className="grid w-full grid-cols-12 gap-4">
<div className="col-span-3 flex">
<APIMethodBadge method={endpoint.method} />
</div>
<div className="col-span-9 flex justify-start">
<a
target="_blank noreferrer noopener"
className="truncate hover:underline"
href={`${apiAddress}${endpoint.path}`}
rel="noreferrer"
>
{endpoint.path}
</a>
</div>
</div>
))}
</div>
)
}

export default APIRoutesList
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ import ReactFlow, {
ReactFlowProvider,
Position,
Panel,
useOnSelectionChange,
getConnectedEdges,
applyEdgeChanges,
type EdgeSelectionChange,
} from 'reactflow'
import Dagre from '@dagrejs/dagre'
import 'reactflow/dist/style.css'
Expand Down Expand Up @@ -111,29 +107,6 @@ function ReactFlowLayout() {
[setEdges],
)

useOnSelectionChange({
onChange: ({ nodes: nodesChanged }) => {
const connectedEdges = getConnectedEdges(nodesChanged, edges)

// select all connected edges if node is selected
if (connectedEdges.length) {
setEdges(
applyEdgeChanges(
connectedEdges.map(
(edge) =>
({
id: edge.id,
type: 'select',
selected: true,
}) as EdgeSelectionChange,
),
edges,
),
)
}
},
})

useEffect(() => {
if (!data) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {
} from '../ui/drawer'
import { Button } from '../ui/button'
import { useCallback, type PropsWithChildren } from 'react'
import { applyNodeChanges, useNodes, useNodeId, useReactFlow } from 'reactflow'
import {
applyNodeChanges,
useNodes,
useNodeId,
useReactFlow,
applyEdgeChanges,
useEdges,
} from 'reactflow'
import type { NodeBaseData } from './nodes/NodeBase'
import type { nodeTypes } from '@/lib/utils/generate-architecture-data'
export interface DetailsDrawerProps extends PropsWithChildren {
Expand All @@ -17,27 +24,35 @@ export interface DetailsDrawerProps extends PropsWithChildren {
open: boolean
testHref?: string
footerChildren?: React.ReactNode
// children that are rendered after the services reference
trailingChildren?: React.ReactNode
nodeType: keyof typeof nodeTypes
icon: NodeBaseData['icon']
address?: string
services?: string[]
type?: 'node' | 'edge'
edgeId?: string
}

export const DetailsDrawer = ({
title,
description,
children,
footerChildren,
trailingChildren,
open,
testHref,
icon: Icon,
nodeType,
address,
services,
type = 'node',
edgeId,
}: DetailsDrawerProps) => {
const nodeId = useNodeId()
const { setNodes } = useReactFlow()
const { setNodes, setEdges } = useReactFlow()
const nodes = useNodes()
const edges = useEdges()

const selectServiceNode = useCallback(
(serviceNodeId: string) => {
Expand All @@ -63,6 +78,23 @@ export const DetailsDrawer = ({
)

const close = () => {
if (type === 'edge') {
setEdges(
applyEdgeChanges(
[
{
id: edgeId || '',
type: 'select',
selected: false,
},
],
edges,
),
)

return
}

setNodes(
applyNodeChanges(
[
Expand Down Expand Up @@ -130,6 +162,7 @@ export const DetailsDrawer = ({
</div>
</div>
) : null}
{trailingChildren}
</div>
<DrawerFooter className="px-0">
{footerChildren}
Expand Down
Loading
Loading