diff --git a/backend/app/db/database.py b/backend/app/db/database.py index dae56ac..644c439 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -40,6 +40,8 @@ async def init_db() -> None: await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN source_handle TEXT") with suppress(Exception): await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN target_handle TEXT") + with suppress(Exception): + await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN animated BOOLEAN NOT NULL DEFAULT 0") async def get_db() -> AsyncGenerator[AsyncSession, None]: diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 8c749ac..4f84454 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -58,6 +58,7 @@ class Edge(Base): speed: Mapped[str | None] = mapped_column(String) custom_color: Mapped[str | None] = mapped_column(String) path_style: Mapped[str | None] = mapped_column(String) + animated: Mapped[bool] = mapped_column(Boolean, default=False) source_handle: Mapped[str | None] = mapped_column(String) target_handle: Mapped[str | None] = mapped_column(String) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) diff --git a/backend/app/schemas/canvas.py b/backend/app/schemas/canvas.py index 3eee05c..e3b4948 100644 --- a/backend/app/schemas/canvas.py +++ b/backend/app/schemas/canvas.py @@ -37,6 +37,7 @@ class EdgeSave(BaseModel): speed: str | None = None custom_color: str | None = None path_style: str | None = None + animated: bool = False source_handle: str | None = None target_handle: str | None = None diff --git a/backend/app/schemas/edges.py b/backend/app/schemas/edges.py index d68f806..0e77c9a 100644 --- a/backend/app/schemas/edges.py +++ b/backend/app/schemas/edges.py @@ -12,6 +12,7 @@ class EdgeBase(BaseModel): speed: str | None = None custom_color: str | None = None path_style: str | None = None + animated: bool = False source_handle: str | None = None target_handle: str | None = None @@ -27,6 +28,7 @@ class EdgeUpdate(BaseModel): speed: str | None = None custom_color: str | None = None path_style: str | None = None + animated: bool | None = None source_handle: str | None = None target_handle: str | None = None diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c38a71d..5d3e3ce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -111,6 +111,7 @@ export default function App() { speed: e.data?.speed ?? null, custom_color: e.data?.custom_color ?? null, path_style: e.data?.path_style ?? null, + animated: e.data?.animated ?? false, // Normalize stub handle IDs: "top-t" / "bottom-t" are invisible target stubs; // map them back to their canonical source handle ID so reload works correctly. source_handle: e.sourceHandle === 'top-t' ? 'top' : e.sourceHandle === 'bottom-t' ? 'bottom' : (e.sourceHandle ?? null), diff --git a/frontend/src/components/canvas/edges/index.tsx b/frontend/src/components/canvas/edges/index.tsx index 3d7ddb6..8d3fab7 100644 --- a/frontend/src/components/canvas/edges/index.tsx +++ b/frontend/src/components/canvas/edges/index.tsx @@ -46,9 +46,32 @@ export function HomelableEdge({ id, sourceX, sourceY, targetX, targetY, sourcePo ...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}), } + // Animated dot: slightly brighter + thicker than the base edge, travels source→target + const dotColor = customColor ?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : edgeColors[edgeType as keyof typeof edgeColors] as string) + const dotWidth = (style.strokeWidth as number ?? 2) + 1.5 + return ( <> + {data?.animated && ( + + + + )} {data?.label && (
(initial?.custom_color) const [pathStyle, setPathStyle] = useState(initial?.path_style ?? 'bezier') + const [animated, setAnimated] = useState(initial?.animated ?? false) const effectiveColor = customColor ?? EDGE_DEFAULT_COLORS[type] @@ -36,6 +37,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title = vlan_id: type === 'vlan' && vlanId ? parseInt(vlanId) : undefined, custom_color: customColor, path_style: pathStyle, + animated: animated || undefined, }) onClose() } @@ -113,6 +115,22 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title =
+
+ + +
+
diff --git a/frontend/src/index.css b/frontend/src/index.css index c41b4e1..ac5baf1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -115,3 +115,13 @@ .font-mono { font-family: 'JetBrains Mono', monospace; } + +/* Edge flow animation — dot traveling from source to target */ +@keyframes flow-dot { + from { stroke-dashoffset: 0; } + to { stroke-dashoffset: -10000; } +} +.edge-flow-dot { + animation: flow-dot 2.5s linear infinite; + pointer-events: none; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bdf6001..5a44a7e 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -81,6 +81,7 @@ export interface EdgeData extends Record { speed?: string custom_color?: string path_style?: EdgePathStyle + animated?: boolean } export const NODE_TYPE_LABELS: Record = {