feat(rack-planner): implement port-to-port connections (patch cables) with dynamic SVG visualization layer

This commit is contained in:
2026-03-22 14:55:33 -05:00
parent 444d694a06
commit becb55d57c
13 changed files with 449 additions and 28 deletions

View File

@@ -12,6 +12,7 @@ import { vlansRouter } from './routes/vlans';
import { serviceMapRouter } from './routes/serviceMap';
import { nodesRouter } from './routes/nodes';
import { edgesRouter } from './routes/edges';
import connectionsRouter from './routes/connections';
import { authMiddleware } from './middleware/authMiddleware';
import { errorHandler } from './middleware/errorHandler';
@@ -44,6 +45,7 @@ app.use('/api/vlans', vlansRouter);
app.use('/api/maps', serviceMapRouter);
app.use('/api/nodes', nodesRouter);
app.use('/api/edges', edgesRouter);
app.use('/api/connections', connectionsRouter);
// ---- Serve Vite build in production ----
if (process.env.NODE_ENV === 'production') {

View File

@@ -0,0 +1,37 @@
import { Router } from 'express';
import * as connService from '../services/connectionService';
const router = Router();
// POST /api/connections
router.post('/', async (req, res, next) => {
try {
const { fromPortId, toPortId, color, label } = req.body;
const conn = await connService.createConnection({ fromPortId, toPortId, color, label });
res.status(201).json(conn);
} catch (err) {
next(err);
}
});
// DELETE /api/connections/:id
router.delete('/:id', async (req, res, next) => {
try {
await connService.deleteConnection(req.params.id);
res.json({ success: true });
} catch (err) {
next(err);
}
});
// DELETE /api/connections/ports/:p1/:p2 (remove link between two specific ports)
router.delete('/ports/:p1/:p2', async (req, res, next) => {
try {
await connService.deleteByPorts(req.params.p1, req.params.p2);
res.json({ success: true });
} catch (err) {
next(err);
}
});
export default router;

View File

@@ -0,0 +1,34 @@
import { prisma } from '../lib/prisma';
import { AppError } from '../types/index';
export async function createConnection(data: { fromPortId: string; toPortId: string; color?: string; label?: string }) {
// Check if both ports exist
const [from, to] = await Promise.all([
prisma.port.findUnique({ where: { id: data.fromPortId } }),
prisma.port.findUnique({ where: { id: data.toPortId } }),
]);
if (!from || !to) throw new AppError('One or both ports not found', 404, 'NOT_FOUND');
if (from.id === to.id) throw new AppError('Cannot connect a port to itself', 400, 'BAD_REQUEST');
// Check if ports are already occupied?
// (In real life, a port can only have one cable, but we might allow one source and one target per port if we want to be flexible, but better to prevent simple loops)
// Create connection (if it already exists, use upsert or just throw error; @@unique already handles it)
return prisma.connection.create({ data });
}
export async function deleteConnection(id: string) {
return prisma.connection.delete({ where: { id } });
}
export async function deleteByPorts(portId1: string, portId2: string) {
return prisma.connection.deleteMany({
where: {
OR: [
{ fromPortId: portId1, toPortId: portId2 },
{ fromPortId: portId2, toPortId: portId1 },
],
},
});
}

View File

@@ -12,6 +12,8 @@ const rackInclude = {
vlans: {
include: { vlan: true },
},
sourceConnections: true,
targetConnections: true,
},
},
},