preparing for another refactoring...
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ const ROOT_GAP = 48 // vertical gap between independent subtrees (different
|
||||
const MARGIN_X = 60
|
||||
const MARGIN_Y = 60
|
||||
|
||||
const ABSTRACT_W = 160
|
||||
const ABSTRACT_W = 220
|
||||
const ABSTRACT_H = 50
|
||||
const CONCRETE_W = 260
|
||||
const LAMBDA_W = 230
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
|
||||
// Invisible handles — React Flow needs them as edge anchor points,
|
||||
// but they must not appear as interactive dots in the read-only graph.
|
||||
const HANDLE_STYLE = { opacity: 0, width: 6, height: 6, pointerEvents: 'none', border: 'none', background: 'transparent' }
|
||||
const TARGET_HANDLE = <Handle type="target" position={Position.Left} style={HANDLE_STYLE} />
|
||||
const SOURCE_HANDLE = <Handle type="source" position={Position.Right} style={HANDLE_STYLE} />
|
||||
|
||||
const METHOD_COLORS = {
|
||||
GET: { bg: '#0d4429', color: '#4ade80' },
|
||||
POST: { bg: '#172554', color: '#60a5fa' },
|
||||
@@ -10,15 +16,17 @@ const METHOD_COLORS = {
|
||||
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%)' }} />
|
||||
// Shared truncation style — applied to any single-line text that can overflow.
|
||||
const TRUNCATE = {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified handler node — three modes: ABSTRACT, CONCRETE, LAMBDA.
|
||||
* Handles are Left (in) / Right (out) for Left-to-Right DAG layout.
|
||||
* No connection handles: the graph is a read-only visualisation.
|
||||
* Text is clamped to the node width and truncated with an ellipsis.
|
||||
*/
|
||||
export default function HandlerNode({ data, selected }) {
|
||||
|
||||
@@ -30,16 +38,17 @@ export default function HandlerNode({ data, selected }) {
|
||||
border: selected ? '2px solid #60a5fa' : '1px solid #ef4444',
|
||||
borderRadius: 8,
|
||||
padding: '8px 14px',
|
||||
width: 160,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
width: 220,
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{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' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#e2e8f0', fontFamily: 'monospace', ...TRUNCATE }}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,9 +57,9 @@ export default function HandlerNode({ data, selected }) {
|
||||
|
||||
// ── LAMBDA ────────────────────────────────────────────────────────────
|
||||
if (data.isLambda) {
|
||||
const method = data.method || 'GET'
|
||||
const path = data.path || '/'
|
||||
const m = METHOD_COLORS[method] || METHOD_COLORS.OPTIONS
|
||||
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 (
|
||||
@@ -60,15 +69,16 @@ export default function HandlerNode({ data, selected }) {
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
width: 230,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{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 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, overflow: 'hidden' }}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
fontSize: 9, fontWeight: 700, padding: '2px 5px',
|
||||
@@ -77,7 +87,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
{method}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4', ...TRUNCATE }}
|
||||
dangerouslySetInnerHTML={{ __html: pathHtml }}
|
||||
/>
|
||||
</div>
|
||||
@@ -88,6 +98,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
background: '#1f1a0e', color: '#f6ad55',
|
||||
fontSize: 8, fontWeight: 600, padding: '1px 4px',
|
||||
borderRadius: 2, fontFamily: 'monospace',
|
||||
...TRUNCATE, maxWidth: '100%',
|
||||
}}>{mw}</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -104,18 +115,18 @@ export default function HandlerNode({ data, selected }) {
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
width: 260,
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
}}>
|
||||
{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' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: '#e2e8f0', fontFamily: 'monospace', ...TRUNCATE }}>
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,7 +136,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
{/* 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 m = METHOD_COLORS[route.method] || METHOD_COLORS.OPTIONS
|
||||
const pathHtml = (route.path || '/').replace(
|
||||
/\{([^}]+)\}/g,
|
||||
'<span style="color:#a78bfa">{$1}</span>'
|
||||
@@ -134,6 +145,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
marginBottom: i < data.routes.length - 1 ? 5 : 0,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<span style={{
|
||||
background: m.bg, color: m.color,
|
||||
@@ -143,7 +155,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
{route.method}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4' }}
|
||||
style={{ fontSize: 10, fontFamily: 'monospace', color: '#8892a4', ...TRUNCATE }}
|
||||
dangerouslySetInnerHTML={{ __html: pathHtml }}
|
||||
/>
|
||||
</div>
|
||||
@@ -159,6 +171,7 @@ export default function HandlerNode({ data, selected }) {
|
||||
background: '#1f1a0e', color: '#f6ad55',
|
||||
fontSize: 8, fontWeight: 600, padding: '1px 4px',
|
||||
borderRadius: 2, fontFamily: 'monospace',
|
||||
...TRUNCATE, maxWidth: '100%',
|
||||
}}>{mw}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
package dev.relism.ext.routeviewer;
|
||||
|
||||
import dev.relism.ext.routeviewer.model.RouteGraph;
|
||||
import dev.relism.extension.ExtensionContext;
|
||||
import dev.relism.extension.FlashContext;
|
||||
import dev.relism.extension.FlashExtension;
|
||||
import dev.relism.extension.FlashRegistrar;
|
||||
import dev.relism.http.ContentType;
|
||||
@@ -36,7 +36,7 @@ import dev.relism.http.ContentType;
|
||||
* }</pre>
|
||||
*
|
||||
* <p>All route metadata is collected once at boot time via
|
||||
* {@link ExtensionContext#addRouteListener}. Zero overhead on the request hot-path.
|
||||
* {@link FlashContext#addRouteListener}. Zero overhead on the request hot-path.
|
||||
*/
|
||||
public class RouteViewerExtension implements FlashExtension {
|
||||
|
||||
@@ -55,7 +55,7 @@ public class RouteViewerExtension implements FlashExtension {
|
||||
public RouteViewerExtension(String path) { this.path = path; }
|
||||
|
||||
@Override
|
||||
public void install(FlashRegistrar app, ExtensionContext ctx) {
|
||||
public void install(FlashRegistrar app, FlashContext ctx) {
|
||||
RouteViewerHandler shell = new RouteViewerHandler();
|
||||
RouteViewerDataHandler data = new RouteViewerDataHandler(graph);
|
||||
|
||||
|
||||
+25
-1
@@ -47,6 +47,11 @@ public record RouteRecord(
|
||||
/**
|
||||
* Walks the superclass chain, stopping before {@code RequestHandler}.
|
||||
* {@code RequestHandler} is the universal root — showing it adds no information.
|
||||
*
|
||||
* <p>Names are produced by {@link #displayName(Class)} so that static inner classes
|
||||
* appear as {@code OuterClass.InnerClass} (e.g. {@code PostHandlers.List}) rather
|
||||
* than the ambiguous simple name ({@code List}). This guarantees globally unique
|
||||
* display labels in the React Flow graph regardless of inner-class naming collisions.
|
||||
*/
|
||||
private static List<String> buildAbstractionChain(Class<?> cls) {
|
||||
if (cls == null) return List.of();
|
||||
@@ -54,12 +59,31 @@ public record RouteRecord(
|
||||
Class<?> c = cls;
|
||||
while (c != null && !c.equals(Object.class)) {
|
||||
if (ROOT_HANDLER.equals(c.getSimpleName())) break;
|
||||
chain.add(c.getSimpleName());
|
||||
chain.add(displayName(c));
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
return List.copyOf(chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable, globally unique display name for a handler class.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Top-level class {@code HtmlHandler} → {@code "HtmlHandler"}</li>
|
||||
* <li>Static inner class {@code PostHandlers$List} → {@code "PostHandlers.List"}</li>
|
||||
* <li>Deeply nested {@code A$B$C} → {@code "A.B.C"}</li>
|
||||
* </ul>
|
||||
*
|
||||
* Strategy: take {@code c.getName()} (binary name with {@code $} separators),
|
||||
* strip the package prefix, then replace {@code $} with {@code .}.
|
||||
*/
|
||||
private static String displayName(Class<?> c) {
|
||||
String binary = c.getName(); // e.g. dev.relism.bench.handler.api.PostHandlers$List
|
||||
int lastDot = binary.lastIndexOf('.');
|
||||
String local = lastDot >= 0 ? binary.substring(lastDot + 1) : binary; // PostHandlers$List
|
||||
return local.replace('$', '.'); // PostHandlers.List
|
||||
}
|
||||
|
||||
private static List<String> buildPointcuts(Class<?> cls) {
|
||||
if (cls == null) return List.of();
|
||||
List<String> pointcuts = new ArrayList<>();
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user