preparing for a conceptual refactoring...
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
/**
|
||||
* Abstract handler node — represents a handler in the inheritance chain
|
||||
* that is not directly bound to a route. Minimal styling, emphasizes the hierarchy.
|
||||
*/
|
||||
export default function AbstractHandlerNode({ data }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: '#13161f',
|
||||
border: '1px solid #2a2f42',
|
||||
borderRadius: 8,
|
||||
padding: '8px 12px',
|
||||
minWidth: 160,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
opacity: 0.8,
|
||||
}}>
|
||||
{/* Extends from parent handler */}
|
||||
<Handle type="target" position={Position.Top}
|
||||
style={{ background: '#2a2f42' }} />
|
||||
{/* Extends to child handler */}
|
||||
<Handle type="source" position={Position.Bottom}
|
||||
style={{ background: '#2a2f42' }} />
|
||||
|
||||
<div style={{
|
||||
fontSize: 10, color: '#4a5568', fontWeight: 700, letterSpacing: 0.5,
|
||||
marginBottom: 3,
|
||||
}}>
|
||||
ABSTRACT
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12, fontWeight: 500, color: '#6b7694',
|
||||
fontFamily: 'monospace',
|
||||
}}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
const METHOD_COLORS = {
|
||||
GET: { bg: '#0d4429', color: '#4ade80' },
|
||||
POST: { bg: '#172554', color: '#60a5fa' },
|
||||
PUT: { bg: '#451a03', color: '#fb923c' },
|
||||
PATCH: { bg: '#2e1065', color: '#c084fc' },
|
||||
DELETE: { bg: '#450a0a', color: '#f87171' },
|
||||
OPTIONS: { bg: '#1c1917', color: '#a8a29e' },
|
||||
HEAD: { bg: '#1c1917', color: '#a8a29e' },
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete handler node — represents a handler that is bound to one or more routes.
|
||||
* Shows handler name, HTTP method + path, and middleware stack as inline badges.
|
||||
*/
|
||||
export default function ConcreteHandlerNode({ data }) {
|
||||
const method = data.methods?.[0] || ''
|
||||
const path = data.paths?.[0] || ''
|
||||
const m = METHOD_COLORS[method] || METHOD_COLORS.OPTIONS
|
||||
const pathHtml = path.replace(/\{([^}]+)\}/g, '<span style="color:#a78bfa">{$1}</span>')
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1a1d27',
|
||||
border: '1px solid #3b82f6',
|
||||
borderRadius: 8,
|
||||
padding: '10px 14px',
|
||||
minWidth: 240,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{/* Extends from parent handler */}
|
||||
<Handle type="target" position={Position.Top}
|
||||
style={{ background: '#2e3347' }} />
|
||||
{/* Extends to child handler (if any) */}
|
||||
<Handle type="source" position={Position.Bottom}
|
||||
style={{ background: '#2e3347' }} />
|
||||
|
||||
{/* Handler class name */}
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<span style={{
|
||||
fontSize: 10, color: '#60a5fa', fontWeight: 700, letterSpacing: 0.5,
|
||||
}}>
|
||||
HANDLER
|
||||
</span>
|
||||
<div style={{
|
||||
fontSize: 13, fontWeight: 700, color: '#e2e8f0',
|
||||
fontFamily: 'monospace', marginTop: 2,
|
||||
}}>
|
||||
{data.handlerName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ height: 1, background: '#2e3347', marginBottom: 6 }} />
|
||||
|
||||
{/* Method badge + path */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
fontSize: 10, fontWeight: 700, padding: '2px 7px',
|
||||
borderRadius: 4, fontFamily: 'monospace', flexShrink: 0,
|
||||
}}>
|
||||
{method}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 11, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
dangerouslySetInnerHTML={{ __html: pathHtml }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Middleware badges */}
|
||||
{data.middleware && data.middleware.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{data.middleware.map(mw => (
|
||||
<span key={mw} style={{
|
||||
background: '#1f1a0e',
|
||||
color: '#f6ad55',
|
||||
fontSize: 9, fontWeight: 600, padding: '2px 6px',
|
||||
borderRadius: 3, fontFamily: 'monospace', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{mw}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
const METHOD_COLORS = {
|
||||
GET: { bg: '#0d4429', color: '#4ade80' },
|
||||
POST: { bg: '#172554', color: '#60a5fa' },
|
||||
PUT: { bg: '#451a03', color: '#fb923c' },
|
||||
PATCH: { bg: '#2e1065', color: '#c084fc' },
|
||||
DELETE: { bg: '#450a0a', color: '#f87171' },
|
||||
OPTIONS: { bg: '#1c1917', color: '#a8a29e' },
|
||||
HEAD: { bg: '#1c1917', color: '#a8a29e' },
|
||||
}
|
||||
|
||||
// Handles for LR layout: parent flows in from the LEFT, children exit to the RIGHT
|
||||
const TARGET_HANDLE = <Handle type="target" position={Position.Left}
|
||||
style={{ left: 0, top: '50%', transform: 'translateY(-50%)' }} />
|
||||
const SOURCE_HANDLE = <Handle type="source" position={Position.Right}
|
||||
style={{ right: 0, top: '50%', transform: 'translateY(-50%)' }} />
|
||||
|
||||
/**
|
||||
* Unified handler node — three modes: ABSTRACT, CONCRETE, LAMBDA.
|
||||
* Handles are Left (in) / Right (out) for Left-to-Right DAG layout.
|
||||
*/
|
||||
export default function HandlerNode({ data, selected }) {
|
||||
|
||||
// ── ABSTRACT ──────────────────────────────────────────────────────────
|
||||
if (data.isAbstract) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'rgba(239,68,68,0.05)',
|
||||
border: selected ? '2px solid #60a5fa' : '1px solid #ef4444',
|
||||
borderRadius: 8,
|
||||
padding: '8px 14px',
|
||||
width: 160,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
{TARGET_HANDLE}
|
||||
{SOURCE_HANDLE}
|
||||
<div style={{ fontSize: 9, color: '#ef4444', fontWeight: 700, letterSpacing: 0.5, marginBottom: 3 }}>
|
||||
ABSTRACT
|
||||
</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#e2e8f0', fontFamily: 'monospace' }}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── LAMBDA ────────────────────────────────────────────────────────────
|
||||
if (data.isLambda) {
|
||||
const method = data.method || 'GET'
|
||||
const path = data.path || '/'
|
||||
const m = METHOD_COLORS[method] || METHOD_COLORS.OPTIONS
|
||||
const pathHtml = path.replace(/\{([^}]+)\}/g, '<span style="color:#a78bfa">{$1}</span>')
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1a1d27',
|
||||
border: selected ? '2px solid #60a5fa' : '1px solid #3b82f6',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
width: 230,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
{TARGET_HANDLE}
|
||||
{SOURCE_HANDLE}
|
||||
<div style={{ fontSize: 9, color: '#60a5fa', fontWeight: 700, letterSpacing: 0.5, marginBottom: 7 }}>
|
||||
LAMBDA
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
fontSize: 9, fontWeight: 700, padding: '2px 5px',
|
||||
borderRadius: 3, fontFamily: 'monospace', flexShrink: 0,
|
||||
}}>
|
||||
{method}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
dangerouslySetInnerHTML={{ __html: pathHtml }}
|
||||
/>
|
||||
</div>
|
||||
{data.middleware?.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 3, flexWrap: 'wrap', marginTop: 7 }}>
|
||||
{data.middleware.map(mw => (
|
||||
<span key={mw} style={{
|
||||
background: '#1f1a0e', color: '#f6ad55',
|
||||
fontSize: 8, fontWeight: 600, padding: '1px 4px',
|
||||
borderRadius: 2, fontFamily: 'monospace',
|
||||
}}>{mw}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── CONCRETE ──────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1a1d27',
|
||||
border: selected ? '2px solid #60a5fa' : '1px solid #3b82f6',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
width: 260,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
{TARGET_HANDLE}
|
||||
{SOURCE_HANDLE}
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 9, color: '#60a5fa', fontWeight: 700, letterSpacing: 0.5, marginBottom: 2 }}>
|
||||
HANDLER
|
||||
</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: '#e2e8f0', fontFamily: 'monospace' }}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: '#2e3347', marginBottom: 8 }} />
|
||||
|
||||
{/* Routes */}
|
||||
<div style={{ marginBottom: data.middleware?.length > 0 ? 8 : 0 }}>
|
||||
{data.routes?.map((route, i) => {
|
||||
const m = METHOD_COLORS[route.method] || METHOD_COLORS.OPTIONS
|
||||
const pathHtml = (route.path || '/').replace(
|
||||
/\{([^}]+)\}/g,
|
||||
'<span style="color:#a78bfa">{$1}</span>'
|
||||
)
|
||||
return (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
marginBottom: i < data.routes.length - 1 ? 5 : 0,
|
||||
}}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
fontSize: 8, fontWeight: 700, padding: '2px 5px',
|
||||
borderRadius: 3, fontFamily: 'monospace', flexShrink: 0,
|
||||
}}>
|
||||
{route.method}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
dangerouslySetInnerHTML={{ __html: pathHtml }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Middleware */}
|
||||
{data.middleware?.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{data.middleware.map(mw => (
|
||||
<span key={mw} style={{
|
||||
background: '#1f1a0e', color: '#f6ad55',
|
||||
fontSize: 8, fontWeight: 600, padding: '1px 4px',
|
||||
borderRadius: 2, fontFamily: 'monospace',
|
||||
}}>{mw}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
export default function MiddlewareNode({ data }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1f1a0e',
|
||||
border: '1px solid #92400e',
|
||||
borderRadius: 8,
|
||||
padding: '7px 14px',
|
||||
minWidth: 150,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{/* shared node: sends applies edges to all routes that use this MW */}
|
||||
<Handle id="out-right" type="source" position={Position.Right}
|
||||
style={{ background: '#92400e' }} />
|
||||
|
||||
<div style={{ fontSize: 10, color: '#f6ad55', fontWeight: 700, letterSpacing: 0.5, marginBottom: 2 }}>
|
||||
MIDDLEWARE
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#fde68a', fontFamily: 'monospace' }}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
const METHOD_COLORS = {
|
||||
GET: { bg: '#0d4429', color: '#4ade80' },
|
||||
POST: { bg: '#172554', color: '#60a5fa' },
|
||||
PUT: { bg: '#451a03', color: '#fb923c' },
|
||||
PATCH: { bg: '#2e1065', color: '#c084fc' },
|
||||
DELETE: { bg: '#450a0a', color: '#f87171' },
|
||||
OPTIONS: { bg: '#1c1917', color: '#a8a29e' },
|
||||
HEAD: { bg: '#1c1917', color: '#a8a29e' },
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined entry node — shows the concrete handler name above
|
||||
* and the HTTP method + path below. Replaces the old split
|
||||
* Route → Handler[0] pair.
|
||||
*/
|
||||
export default function RouteNode({ data }) {
|
||||
const m = METHOD_COLORS[data.method] ?? METHOD_COLORS.OPTIONS
|
||||
const path = data.path.replace(/\{([^}]+)\}/g, '<span style="color:#a78bfa">{$1}</span>')
|
||||
const isSimple = !data.handlerName || data.handlerName === 'Simple Handler'
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: '#1a1d27',
|
||||
border: '1px solid #3b82f6',
|
||||
borderRadius: 8,
|
||||
padding: '8px 14px',
|
||||
minWidth: 200,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{/* receives MW → entry "applies" edges */}
|
||||
<Handle id="in-left" type="target" position={Position.Left}
|
||||
style={{ background: '#2e3347' }} />
|
||||
{/* sends extends edge to abstract parent chain */}
|
||||
<Handle id="out-bottom" type="source" position={Position.Bottom}
|
||||
style={{ background: '#2e3347' }} />
|
||||
|
||||
{/* Handler class name */}
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 10, color: '#60a5fa', fontWeight: 700, letterSpacing: 0.5 }}>
|
||||
{isSimple ? 'HANDLER' : 'HANDLER'}
|
||||
</span>
|
||||
<div style={{
|
||||
fontSize: 13, fontWeight: 700, color: '#e2e8f0',
|
||||
fontFamily: 'monospace', marginTop: 1,
|
||||
}}>
|
||||
{isSimple ? 'Simple Handler' : data.handlerName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ height: 1, background: '#2e3347', marginBottom: 6 }} />
|
||||
|
||||
{/* Method badge + path */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
fontSize: 10, fontWeight: 700, padding: '2px 7px',
|
||||
borderRadius: 4, fontFamily: 'monospace', flexShrink: 0,
|
||||
}}>{data.method}</span>
|
||||
<span
|
||||
style={{ fontSize: 12, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
dangerouslySetInnerHTML={{ __html: path }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Section header — no edges, purely positional grouping.
|
||||
* Styled as a slim label bar above the router's route group.
|
||||
*/
|
||||
export default function RouterNode({ data }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(90deg, #16122a 0%, #1a1d27 100%)',
|
||||
border: '1px solid #3d2f7a',
|
||||
borderLeft: '3px solid #7c6af7',
|
||||
borderRadius: 6,
|
||||
padding: '6px 14px',
|
||||
minWidth: 240,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}>
|
||||
<span style={{ fontSize: 10, color: '#7c6af7', fontWeight: 700, letterSpacing: 1, flexShrink: 0 }}>
|
||||
ROUTER
|
||||
</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#e2e8f0', fontFamily: 'monospace' }}>
|
||||
{data.namespace}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#4a5568', marginLeft: 'auto' }}>
|
||||
{data.routerType} · {data.routeCount}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user