fix(rack-planner): revamped port layout with ml-auto and border-l to ensure SFPs are visible on the right

This commit is contained in:
2026-03-22 15:24:04 -05:00
parent f6b6f49379
commit 96adb1e130
2 changed files with 32 additions and 30 deletions

View File

@@ -54,11 +54,9 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
portRows.push(mainPorts.slice(i, i + PORTS_PER_ROW)); portRows.push(mainPorts.slice(i, i + PORTS_PER_ROW));
} }
} else if (sidePorts.length > 0) { } else if (sidePorts.length > 0) {
// If only special ports exist, create an empty first row to hold them
portRows.push([]); portRows.push([]);
} }
// Calculate vertical space
const availableForPorts = height - 16; const availableForPorts = height - 16;
const maxRows = Math.max(1, Math.floor(availableForPorts / 14)); const maxRows = Math.max(1, Math.floor(availableForPorts / 14));
const visibleRows = portRows.length > 0 ? portRows.slice(0, maxRows) : []; const visibleRows = portRows.length > 0 ? portRows.slice(0, maxRows) : [];
@@ -174,17 +172,17 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
> >
{/* Port grid — primary face content */} {/* Port grid — primary face content */}
{hasPorts && previewUSize === null ? ( {hasPorts && previewUSize === null ? (
<div className="flex flex-col gap-[3px] px-2 pt-[5px]"> <div className="flex flex-col gap-1.5 px-2 pt-1.5">
{visibleRows.map((row, rowIdx) => ( {visibleRows.map((row, rowIdx) => (
<div key={rowIdx} className="flex justify-between items-center w-full"> <div key={rowIdx} className="flex items-center w-full min-h-[12px]">
{/* Standard ports */} {/* Standard ports Group */}
<div className="flex gap-[3px]"> <div className="flex gap-[3px] flex-wrap">
{row.map((port) => { {row.map((port) => {
const hasVlan = port.vlans.length > 0; const hasVlan = port.vlans.length > 0;
const vlanColor = hasVlan const vlanColor = hasVlan
? port.mode === 'ACCESS' ? port.mode === 'ACCESS'
? port.vlans[0]?.vlan?.color || '#10b981' ? port.vlans[0]?.vlan?.color || '#10b981'
: '#8b5cf6' : '#a78bfa'
: '#475569'; : '#475569';
const isCablingSource = cablingFromPortId === port.id; const isCablingSource = cablingFromPortId === port.id;
@@ -195,32 +193,28 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => handlePortClick(e, port.id)} onClick={(e) => handlePortClick(e, port.id)}
aria-label={`Port ${port.portNumber}`} aria-label={`Port ${port.portNumber}`}
title={`Port ${port.portNumber}${port.label ? ` · ${port.label}` : ''}${ title={`Port ${port.portNumber}\n${port.portType}${port.label ? ` · ${port.label}` : ''}`}
hasVlan ? ` (VLAN ${port.vlans.map((v) => v.vlan?.vlanId).filter(Boolean).join(',')})` : '' style={{ backgroundColor: vlanColor }}
}\nShift+Click for settings`}
style={{ backgroundColor: vlanColor, borderColor: 'rgba(0,0,0,0.2)' }}
className={cn( className={cn(
'w-2.5 h-2.5 rounded-sm border transition-all shrink-0 hover:scale-110 hover:brightness-125', 'w-2 h-2 rounded-full border border-black/20 hover:scale-125 transition-all outline-none',
isCablingSource && 'ring-2 ring-blue-400 ring-offset-1 ring-offset-slate-900 animate-pulse' isCablingSource && 'ring-2 ring-white ring-offset-1 ring-offset-slate-900 animate-pulse'
)} )}
/> />
); );
})} })}
</div> </div>
{/* SFP/WAN group (on first row) */} {/* SFP/WAN Group (push to right) */}
{rowIdx === 0 && sidePorts.length > 0 && ( {rowIdx === 0 && sidePorts.length > 0 && (
<div className="flex gap-[3px] ml-auto"> <div className="flex gap-1.5 ml-auto border-l border-slate-700/50 pl-1.5 h-3 items-center">
{sidePorts.map((port) => { {sidePorts.map((port) => {
const hasVlan = port.vlans.length > 0; const hasVlan = port.vlans.length > 0;
const isSfp = ['SFP', 'SFP_PLUS', 'QSFP'].includes(port.portType); const isSfp = port.portType?.includes('SFP');
const isWan = port.portType === 'WAN'; const isWan = port.portType === 'WAN';
const vlanColor = hasVlan const vlanColor = hasVlan
? port.mode === 'ACCESS' ? port.vlans[0]?.vlan?.color || '#3b82f6'
? port.vlans[0]?.vlan?.color || '#10b981' : isWan ? '#2563eb' : '#94a3b8';
: '#8b5cf6'
: isWan ? '#3b82f6' : '#64748b';
const isCablingSource = cablingFromPortId === port.id; const isCablingSource = cablingFromPortId === port.id;
@@ -230,16 +224,12 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
data-port-id={port.id} data-port-id={port.id}
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => handlePortClick(e, port.id)} onClick={(e) => handlePortClick(e, port.id)}
aria-label={`${port.portType} Port ${port.portNumber}`} title={`${port.portType} ${port.portNumber}`}
title={`${port.portType} Port ${port.portNumber}${port.label ? ` · ${port.label}` : ''}${ style={{ backgroundColor: vlanColor }}
hasVlan ? ` (VLAN ${port.vlans.map((v) => v.vlan?.vlanId).filter(Boolean).join(',')})` : ''
}\nShift+Click for settings`}
style={{ backgroundColor: vlanColor, borderColor: isSfp ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' }}
className={cn( className={cn(
'w-2.5 h-2.5 border transition-all shrink-0 hover:scale-110 hover:brightness-125', 'w-2.5 h-2.5 transition-transform hover:scale-125 border border-black/40',
isSfp ? 'rounded-none rotate-45 scale-[0.85]' : 'rounded-full', isSfp ? 'rounded-none rotate-45 scale-75' : 'rounded-full ring-1 ring-white/10',
isCablingSource && 'ring-2 ring-blue-400 ring-offset-1 ring-offset-slate-900 animate-pulse', isCablingSource && 'ring-2 ring-white ring-offset-1 ring-offset-slate-900 animate-pulse'
isWan && 'ring-1 ring-blue-400/50'
)} )}
/> />
); );
@@ -249,7 +239,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
</div> </div>
))} ))}
{hiddenPortCount > 0 && ( {hiddenPortCount > 0 && (
<span className="text-[9px] text-white/40 leading-none">+{hiddenPortCount} more</span> <span className="text-[10px] text-slate-500">+{hiddenPortCount} ports</span>
)} )}
</div> </div>
) : ( ) : (

12
scripts/check_ports.ts Normal file
View File

@@ -0,0 +1,12 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function check() {
const modules = await prisma.module.findMany({
include: { ports: true }
});
console.log(JSON.stringify(modules, null, 2));
}
check().catch(console.error);