diff --git a/apps/frontend/src/features/architecture/components/ArchWorkspace.tsx b/apps/frontend/src/features/architecture/components/ArchWorkspace.tsx index a126f77..8ec68e5 100644 --- a/apps/frontend/src/features/architecture/components/ArchWorkspace.tsx +++ b/apps/frontend/src/features/architecture/components/ArchWorkspace.tsx @@ -57,36 +57,39 @@ function ArchWorkspaceInner({ model, source }: ArchWorkspaceInnerProps) { heatmapMode, bookmarkedNodes, hiddenNodes, + collapsedLayers, showAllNodes, + showAllLayers, isolatedSubtree, setIsolatedSubtree, openContextMenu, closeContextMenu, } = useArchitectureStore(); - const { nodes: initialNodes, edges: initialEdges } = useMemo( - () => getLayoutedElements(model.nodes, model.edges, { + const layoutOptions = useMemo(() => ({ + layers: model.detectedLayers, heatmapMode, bookmarks: bookmarkedNodes, hiddenNodes, isolatedSubtree, - }), - [model, heatmapMode, bookmarkedNodes, hiddenNodes, isolatedSubtree] + collapsedLayers, + }), [model.detectedLayers, heatmapMode, bookmarkedNodes, hiddenNodes, isolatedSubtree, collapsedLayers]); + + const { nodes: initialNodes, edges: initialEdges } = useMemo( + () => getLayoutedElements(model.nodes, model.edges, layoutOptions), + [model.nodes, model.edges, layoutOptions] ); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); useEffect(() => { - const { nodes: newNodes, edges: newEdges } = getLayoutedElements(model.nodes, model.edges, { - heatmapMode, - bookmarks: bookmarkedNodes, - hiddenNodes, - isolatedSubtree, - }); + const { nodes: newNodes, edges: newEdges } = getLayoutedElements(model.nodes, model.edges, layoutOptions); setNodes(newNodes); setEdges(newEdges); - }, [heatmapMode, bookmarkedNodes, hiddenNodes, isolatedSubtree, model, setNodes, setEdges]); + const frame = requestAnimationFrame(() => reactFlowInstance.fitView({ padding: 0.12, duration: 300 })); + return () => cancelAnimationFrame(frame); + }, [layoutOptions, model.nodes, model.edges, reactFlowInstance, setNodes, setEdges]); useEffect(() => { setNodes((nds) => @@ -192,13 +195,15 @@ function ArchWorkspaceInner({ model, source }: ArchWorkspaceInnerProps) { setIsolatedSubtree(null); showAllNodes(); const { nodes: newNodes, edges: newEdges } = getLayoutedElements(model.nodes, model.edges, { - heatmapMode, - bookmarks: bookmarkedNodes, + ...layoutOptions, + hiddenNodes: new Set(), + collapsedLayers: new Set(), }); + showAllLayers(); setNodes(newNodes); setEdges(newEdges); setTimeout(() => reactFlowInstance.fitView({ padding: 0.2, duration: 300 }), 50); - }, [model, setNodes, setEdges, reactFlowInstance, heatmapMode, bookmarkedNodes, setIsolatedSubtree, showAllNodes]); + }, [model, setNodes, setEdges, reactFlowInstance, layoutOptions, setIsolatedSubtree, showAllNodes, showAllLayers]); const handleExportPng = useCallback(() => { const el = document.querySelector('.react-flow') as HTMLElement; @@ -301,7 +306,7 @@ function ArchWorkspaceInner({ model, source }: ArchWorkspaceInnerProps) { onPaneClick={handlePaneClick} nodeTypes={nodeTypes} fitView - fitViewOptions={{ padding: 0.2 }} + fitViewOptions={{ padding: 0.12 }} minZoom={0.1} maxZoom={3} proOptions={{ hideAttribution: true }} diff --git a/apps/frontend/src/features/architecture/components/ArchitectureNode.test.tsx b/apps/frontend/src/features/architecture/components/ArchitectureNode.test.tsx new file mode 100644 index 0000000..5d95d98 --- /dev/null +++ b/apps/frontend/src/features/architecture/components/ArchitectureNode.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { ReactFlowProvider, type NodeProps } from '@xyflow/react'; +import type { ArchFlowNode } from './ArchitectureNode'; +import { ArchitectureNode } from './ArchitectureNode'; + +describe('ArchitectureNode', () => { + it('keeps the name, type, layer, and relationship state visible', () => { + const props = { + id: 'module:orders', + type: 'architectureNode', + data: { + label: 'Orders', + nodeType: 'service', + layer: 'business-logic', + relationshipState: 'connected', + description: 'Coordinates order processing.', + filesCount: 3, + complexity: 'medium', + }, + } as NodeProps; + + render( + + + , + ); + + expect(screen.getByText('Orders')).toBeInTheDocument(); + expect(screen.getByText('Service · Business Logic')).toBeInTheDocument(); + expect(screen.getByText('Connected')).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/src/features/architecture/components/ArchitectureNode.tsx b/apps/frontend/src/features/architecture/components/ArchitectureNode.tsx index d125ed4..c7da009 100644 --- a/apps/frontend/src/features/architecture/components/ArchitectureNode.tsx +++ b/apps/frontend/src/features/architecture/components/ArchitectureNode.tsx @@ -6,7 +6,7 @@ import { ListTodo, Zap, } from 'lucide-react'; import { cn } from '@/shared/utils/cn'; -import type { ArchNodeType } from '@/shared/types/architecture'; +import type { ArchNodeType, RelationshipState } from '@/shared/types/architecture'; const nodeConfig: Record = { frontend: { icon: Monitor, color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/30' }, @@ -31,6 +31,8 @@ const nodeConfig: Record; +const relationshipStateConfig: Record = { + connected: { label: 'Connected', className: 'bg-success/10 text-success' }, + 'no-observed-relationships': { label: 'No observed', className: 'bg-warning/10 text-warning' }, + unresolved: { label: 'Unresolved', className: 'bg-destructive/10 text-destructive' }, + 'not-extracted': { label: 'Not extracted', className: 'bg-muted text-muted-foreground' }, +}; + +function formatLabel(value: string): string { + return value.replace(/[-_]/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + export const ArchitectureNode = memo(({ data }: NodeProps) => { const config = nodeConfig[data.nodeType] || nodeConfig.utilities; const Icon = config.icon; @@ -59,7 +72,7 @@ export const ArchitectureNode = memo(({ data }: NodeProps) => { return (
) => { >
@@ -77,13 +90,16 @@ export const ArchitectureNode = memo(({ data }: NodeProps) => {
-

{data.label}

+

{data.label}

{data.isBookmarked && ( * )}
+

+ {formatLabel(data.nodeType)} · {formatLabel(data.layer)} +

{data.description}

-
+
{data.filesCount > 0 && ( {data.filesCount} files )} @@ -97,12 +113,15 @@ export const ArchitectureNode = memo(({ data }: NodeProps) => { > {data.complexity} + + {relationshipStateConfig[data.relationshipState].label} +
diff --git a/apps/frontend/src/features/architecture/components/ModuleExplorer.tsx b/apps/frontend/src/features/architecture/components/ModuleExplorer.tsx index 1998f66..4a22aa2 100644 --- a/apps/frontend/src/features/architecture/components/ModuleExplorer.tsx +++ b/apps/frontend/src/features/architecture/components/ModuleExplorer.tsx @@ -4,7 +4,16 @@ import { cn } from '@/shared/utils/cn'; import { useArchitectureStore } from '../store'; export function ModuleExplorer() { - const { model, expandedModules, toggleModule, selectedNodeId, setSelectedNodeId, setHighlightedNodeIds } = useArchitectureStore(); + const { + model, + expandedModules, + toggleModule, + collapsedLayers, + toggleLayer, + selectedNodeId, + setSelectedNodeId, + setHighlightedNodeIds, + } = useArchitectureStore(); if (!model) return null; @@ -32,10 +41,18 @@ export function ModuleExplorer() { return (