From 8437f5ef490adc672ea8fce233b43e9502b0ec37 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sat, 7 Mar 2026 14:42:34 +0100 Subject: [PATCH] feat: customizable link color per edge - Add custom_color field to EdgeData type, Edge DB model and all schemas - HomelableEdge applies custom_color as stroke override (before selected highlight) - EDGE_DEFAULT_COLORS extracted to utils/edgeColors.ts (react-refresh compliant) - EdgeModal: color picker row showing effective color (custom or type default) with hex value, Reset button when custom color is active - Auto-migration adds custom_color column to edges table --- backend/app/db/database.py | 2 + backend/app/db/models.py | 1 + backend/app/schemas/canvas.py | 1 + backend/app/schemas/edges.py | 2 + frontend/src/App.tsx | 1 + .../src/components/canvas/edges/index.tsx | 2 + frontend/src/components/modals/EdgeModal.tsx | 37 +++++++++++++++++++ frontend/src/types/index.ts | 1 + frontend/src/utils/edgeColors.ts | 9 +++++ 9 files changed, 56 insertions(+) create mode 100644 frontend/src/utils/edgeColors.ts diff --git a/backend/app/db/database.py b/backend/app/db/database.py index ef5bad8..95c7ff6 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -25,6 +25,8 @@ async def init_db() -> None: await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN container_mode BOOLEAN NOT NULL DEFAULT 0") with suppress(Exception): await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN custom_colors JSON") + with suppress(Exception): + await conn.exec_driver_sql("ALTER TABLE edges ADD COLUMN custom_color TEXT") async def get_db(): diff --git a/backend/app/db/models.py b/backend/app/db/models.py index f402eee..d71c180 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -54,6 +54,7 @@ class Edge(Base): label: Mapped[str | None] = mapped_column(String) vlan_id: Mapped[int | None] = mapped_column(Integer) speed: Mapped[str | None] = mapped_column(String) + custom_color: 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 86291e8..e105475 100644 --- a/backend/app/schemas/canvas.py +++ b/backend/app/schemas/canvas.py @@ -34,6 +34,7 @@ class EdgeSave(BaseModel): label: str | None = None vlan_id: int | None = None speed: str | None = None + custom_color: str | None = None class CanvasSaveRequest(BaseModel): diff --git a/backend/app/schemas/edges.py b/backend/app/schemas/edges.py index a8380f1..15b3a7c 100644 --- a/backend/app/schemas/edges.py +++ b/backend/app/schemas/edges.py @@ -10,6 +10,7 @@ class EdgeBase(BaseModel): label: str | None = None vlan_id: int | None = None speed: str | None = None + custom_color: str | None = None class EdgeCreate(EdgeBase): @@ -21,6 +22,7 @@ class EdgeUpdate(BaseModel): label: str | None = None vlan_id: int | None = None speed: str | None = None + custom_color: str | None = None class EdgeResponse(EdgeBase): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 508c454..99c6e16 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -64,6 +64,7 @@ export default function App() { label: e.data?.label ?? null, vlan_id: e.data?.vlan_id ?? null, speed: e.data?.speed ?? null, + custom_color: e.data?.custom_color ?? null, })) await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: {} }) markSaved() diff --git a/frontend/src/components/canvas/edges/index.tsx b/frontend/src/components/canvas/edges/index.tsx index 8407f1a..e733532 100644 --- a/frontend/src/components/canvas/edges/index.tsx +++ b/frontend/src/components/canvas/edges/index.tsx @@ -26,9 +26,11 @@ export function HomelableEdge({ id, sourceX, sourceY, targetX, targetY, sourcePo const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }) const edgeType: EdgeType = data?.type ?? 'ethernet' + const customColor = data?.custom_color as string | undefined const style: React.CSSProperties = { ...EDGE_STYLES[edgeType], ...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id as number | undefined) } : {}), + ...(customColor ? { stroke: customColor } : {}), ...(selected ? { stroke: '#00d4ff', filter: 'drop-shadow(0 0 4px #00d4ff88)' } : {}), } diff --git a/frontend/src/components/modals/EdgeModal.tsx b/frontend/src/components/modals/EdgeModal.tsx index cfb6a71..6db9b12 100644 --- a/frontend/src/components/modals/EdgeModal.tsx +++ b/frontend/src/components/modals/EdgeModal.tsx @@ -1,10 +1,12 @@ import { useState } from 'react' +import { RotateCcw } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { EDGE_TYPE_LABELS, type EdgeData, type EdgeType } from '@/types' +import { EDGE_DEFAULT_COLORS } from '@/utils/edgeColors' const EDGE_TYPES = Object.entries(EDGE_TYPE_LABELS) as [EdgeType, string][] @@ -21,6 +23,9 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title = const [type, setType] = useState(initial?.type ?? 'ethernet') const [label, setLabel] = useState(initial?.label ?? '') const [vlanId, setVlanId] = useState(initial?.vlan_id?.toString() ?? '') + const [customColor, setCustomColor] = useState(initial?.custom_color) + + const effectiveColor = customColor ?? EDGE_DEFAULT_COLORS[type] const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -28,6 +33,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title = type, label: label || undefined, vlan_id: type === 'vlan' && vlanId ? parseInt(vlanId) : undefined, + custom_color: customColor, }) onClose() } @@ -84,6 +90,37 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title = /> +
+
+ + {customColor && ( + + )} +
+