Skip to content

Commit

Permalink
feat: add new mini decl graph to sidebar (#4230)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Jan 29, 2025
1 parent 8ea824d commit 5dc1a72
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 33 deletions.
14 changes: 7 additions & 7 deletions frontend/console/src/features/graph/GraphPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,25 @@ const panelsForNode = (node: FTLNode | null, moduleName: string | null) => {
}

if (node instanceof Config) {
return configPanels(moduleName, node)
return configPanels(moduleName, node, false)
}
if (node instanceof Secret) {
return secretPanels(moduleName, node)
return secretPanels(moduleName, node, false)
}
if (node instanceof Database) {
return databasePanels(moduleName, node)
return databasePanels(moduleName, node, false)
}
if (node instanceof Enum) {
return enumPanels(moduleName, node)
return enumPanels(moduleName, node, false)
}
if (node instanceof Data) {
return dataPanels(moduleName, node)
return dataPanels(moduleName, node, false)
}
if (node instanceof Topic) {
return topicPanels(moduleName, node)
return topicPanels(moduleName, node, false)
}
if (node instanceof Verb) {
return verbPanels(moduleName, node)
return verbPanels(moduleName, node, false)
}
return [] as ExpandablePanelProps[]
}
8 changes: 5 additions & 3 deletions frontend/console/src/features/graph/graph-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ export const nodeColors = {
light: {
verb: colors.indigo[500],
config: colors.sky[400],
data: colors.gray[400],
data: colors.green[500],
database: colors.blue[400],
secret: colors.blue[400],
subscription: colors.violet[400],
topic: colors.violet[400],
enum: colors.green[400],
default: colors.gray[400],
},
dark: {
verb: colors.indigo[600],
config: colors.blue[500],
data: colors.gray[400],
config: colors.sky[500],
data: colors.green[600],
database: colors.blue[600],
secret: colors.blue[500],
subscription: colors.violet[600],
topic: colors.violet[600],
enum: colors.green[600],
default: colors.gray[700],
},
}
Expand Down
160 changes: 160 additions & 0 deletions frontend/console/src/features/modules/decls/DeclGraphPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Controls, type Edge, ReactFlow as Flow, type Node, ReactFlowProvider } from '@xyflow/react'
import dagre from 'dagre'
import { useCallback, useMemo } from 'react'
import type React from 'react'
import type { Edges } from '../../../protos/xyz/block/ftl/console/v1/console_pb'
import { useUserPreferences } from '../../../shared/providers/user-preferences-provider'
import { DeclNode } from '../../graph/DeclNode'
import { getNodeBackgroundColor } from '../../graph/graph-styles'
import { useModules } from '../hooks/use-modules'
import { declSchemaFromModules } from '../schema/schema.utils'
import '@xyflow/react/dist/style.css'
import '../../graph/graph.css'

const NODE_TYPES = {
declNode: DeclNode,
}

interface DeclGraphPaneProps {
edges?: Edges
declName: string
declType: string
moduleName: string
height?: string
}

// Dagre layout function for decl graph
const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'LR') => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))

const nodeWidth = 160
const nodeHeight = 20

dagreGraph.setGraph({
rankdir: direction,
nodesep: 50,
ranksep: 100,
})

// Add nodes to dagre
for (const node of nodes) {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight })
}

// Add edges to dagre
for (const edge of edges) {
dagreGraph.setEdge(edge.source, edge.target)
}

// Apply layout
dagre.layout(dagreGraph)

// Get positions from dagre
for (const node of nodes) {
const nodeWithPosition = dagreGraph.node(node.id)
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
}
}

return { nodes, edges }
}

const createNode = (id: string, label: string, nodeType: string, isDarkMode: boolean, isCenter: boolean): Node => ({
id,
type: 'declNode',
position: { x: 0, y: 0 },
data: {
title: label,
selected: isCenter,
type: 'declNode',
nodeType,
item: { name: label },
style: {
backgroundColor: getNodeBackgroundColor(isDarkMode, nodeType),
},
},
})

const createEdge = (source: string, target: string, isDarkMode: boolean): Edge => ({
id: `edge-${source}->${target}`,
source,
target,
type: 'default',
animated: true,
style: {
stroke: isDarkMode ? '#EC4899' : '#F472B6',
strokeWidth: 2,
},
})

export const DeclGraphPane: React.FC<DeclGraphPaneProps> = ({ edges, declName, declType, moduleName, height }) => {
if (!edges) return null

const { isDarkMode } = useUserPreferences()
const { data: modulesData } = useModules()

const { nodes, graphEdges } = useMemo(() => {
const nodes: Node[] = []
const graphEdges: Edge[] = []

// Create center node (the current decl)
const centerId = `${moduleName}.${declName}`
nodes.push(createNode(centerId, declName, declType, isDarkMode, true))

// Create nodes and edges for inbound references
for (const ref of edges.in) {
const id = `${ref.module}.${ref.name}`
const schema = declSchemaFromModules(ref.module, ref.name, modulesData?.modules || [])
nodes.push(createNode(id, ref.name, schema?.declType || declType, isDarkMode, false))
graphEdges.push(createEdge(id, centerId, isDarkMode))
}

// Create nodes and edges for outbound references
for (const ref of edges.out) {
const id = `${ref.module}.${ref.name}`
const schema = declSchemaFromModules(ref.module, ref.name, modulesData?.modules || [])
nodes.push(createNode(id, ref.name, schema?.declType || declType, isDarkMode, false))
graphEdges.push(createEdge(centerId, id, isDarkMode))
}

const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(nodes, graphEdges)
return { nodes: layoutedNodes, graphEdges: layoutedEdges }
}, [edges, declName, declType, moduleName, isDarkMode, modulesData?.modules])

// Calculate height based on number of nodes
const calculatedHeight = useMemo(() => {
if (height) return height
const nodeCount = Math.max(edges.in.length, edges.out.length, 1)
// 20px node height + 50px node separation + 40px padding
return `${nodeCount * (20 + 50) + 60}px`
}, [edges.in.length, edges.out.length, height])

const onInit = useCallback(() => {
// ReactFlow instance initialization if needed
}, [])

return (
<ReactFlowProvider key={`${moduleName}.${declName}`}>
<div className={isDarkMode ? 'dark' : 'light'} style={{ width: '100%', height: calculatedHeight }}>
<Flow
nodes={nodes}
edges={graphEdges}
nodeTypes={NODE_TYPES}
onInit={onInit}
fitView
minZoom={0.1}
maxZoom={2}
proOptions={{ hideAttribution: true }}
nodesDraggable={false}
nodesConnectable={false}
colorMode={isDarkMode ? 'dark' : 'light'}
>
<Controls showInteractive={false} showZoom={false} />
</Flow>
</div>
</ReactFlowProvider>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Config } from '../../../../protos/xyz/block/ftl/console/v1/console
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const configPanels = (moduleName: string, config: Config) => {
return [
export const configPanels = (moduleName: string, config: Config, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
Expand All @@ -14,5 +15,15 @@ export const configPanels = (moduleName: string, config: Config) => {
],
},
...DeclDefaultPanels(moduleName, config.schema, config.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={config.config?.name || ''} declType='config' moduleName={moduleName} edges={config.edges} />,
})
}

return panels
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ import type { Data } from '../../../../protos/xyz/block/ftl/console/v1/console_p
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const dataPanels = (moduleName: string, data: Data) => {
return [
export const dataPanels = (moduleName: string, data: Data, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
children: [<RightPanelAttribute key='name' name='Name' value={data.data?.name} />],
},
...DeclDefaultPanels(moduleName, data.schema, data.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={data.data?.name || ''} declType='data' moduleName={moduleName} edges={data.edges} />,
})
}

return panels
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Database } from '../../../../protos/xyz/block/ftl/console/v1/conso
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const databasePanels = (moduleName: string, database: Database) => {
return [
export const databasePanels = (moduleName: string, database: Database, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
Expand All @@ -14,5 +15,15 @@ export const databasePanels = (moduleName: string, database: Database) => {
],
},
...DeclDefaultPanels(moduleName, database.schema, database.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={database.database?.name || ''} declType='database' moduleName={moduleName} edges={database.edges} />,
})
}

return panels
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Enum } from '../../../../protos/xyz/block/ftl/console/v1/console_p
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const enumPanels = (moduleName: string, enumDecl: Enum) => {
return [
export const enumPanels = (moduleName: string, enumDecl: Enum, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
Expand All @@ -14,5 +15,15 @@ export const enumPanels = (moduleName: string, enumDecl: Enum) => {
],
},
...DeclDefaultPanels(moduleName, enumDecl.schema, enumDecl.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={enumDecl.enum?.name || ''} declType='enum' moduleName={moduleName} edges={enumDecl.edges} />,
})
}

return panels
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Secret } from '../../../../protos/xyz/block/ftl/console/v1/console
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const secretPanels = (moduleName: string, secret: Secret) => {
return [
export const secretPanels = (moduleName: string, secret: Secret, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
Expand All @@ -14,5 +15,15 @@ export const secretPanels = (moduleName: string, secret: Secret) => {
],
},
...DeclDefaultPanels(moduleName, secret.schema, secret.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={secret.secret?.name || ''} declType='secret' moduleName={moduleName} edges={secret.edges} />,
})
}

return panels
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Topic } from '../../../../protos/xyz/block/ftl/console/v1/console_
import type { ExpandablePanelProps } from '../../../../shared/components/ExpandablePanel'
import { RightPanelAttribute } from '../../../../shared/components/RightPanelAttribute'
import { DeclDefaultPanels } from '../DeclDefaultPanels'
import { DeclGraphPane } from '../DeclGraphPane'

export const topicPanels = (moduleName: string, topic: Topic) => {
return [
export const topicPanels = (moduleName: string, topic: Topic, showGraph = true) => {
const panels: ExpandablePanelProps[] = [
{
title: 'Details',
expanded: true,
Expand All @@ -14,5 +15,15 @@ export const topicPanels = (moduleName: string, topic: Topic) => {
],
},
...DeclDefaultPanels(moduleName, topic.schema, topic.edges),
] as ExpandablePanelProps[]
]

if (showGraph) {
panels.push({
title: 'Graph',
expanded: true,
children: <DeclGraphPane declName={topic.topic?.name || ''} declType='topic' moduleName={moduleName} edges={topic.edges} />,
})
}

return panels
}
Loading

0 comments on commit 5dc1a72

Please sign in to comment.