diff --git a/internal/handlers/connections.go b/internal/handlers/connections.go index f1e55a4..02e1b22 100644 --- a/internal/handlers/connections.go +++ b/internal/handlers/connections.go @@ -14,6 +14,59 @@ func (h *Handlers) ConnectionModal(w http.ResponseWriter, r *http.Request) { h.renderConnectionModal(w, portIDStr) } +func (h *Handlers) ConnectionConnectForm(w http.ResponseWriter, r *http.Request) { + p1Str := r.URL.Query().Get("p1") + p2Str := r.URL.Query().Get("p2") + + p1ID, _ := strconv.ParseInt(p1Str, 10, 64) + p2ID, _ := strconv.ParseInt(p2Str, 10, 64) + + port1, _ := h.Store.PortGetByID(p1ID) + port2, _ := h.Store.PortGetByID(p2ID) + var dev1, dev2 *models.Device + if port1 != nil { + dev1, _ = h.Store.DeviceGetByID(port1.DeviceID) + } + if port2 != nil { + dev2, _ = h.Store.DeviceGetByID(port2.DeviceID) + } + + connTypes, _ := h.Store.ConnectionTypeGetAll() + if connTypes == nil { + connTypes = []models.ConnectionType{} + } + + type ConnectFormData struct { + PortID1 int64 + PortID2 int64 + Port1Name string + Port2Name string + Device1Name string + Device2Name string + ConnectionTypes []models.ConnectionType + } + + data := ConnectFormData{ + PortID1: p1ID, + PortID2: p2ID, + ConnectionTypes: connTypes, + } + if port1 != nil { + data.Port1Name = port1.Name + } + if port2 != nil { + data.Port2Name = port2.Name + } + if dev1 != nil { + data.Device1Name = dev1.Name + } + if dev2 != nil { + data.Device2Name = dev2.Name + } + + h.render(w, "connect_form.html", data) +} + type FlatPort struct { ID int64 Name string diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 1afe3ab..5ad9623 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -46,6 +46,7 @@ func New(store *db.Store) *Handlers { fragments := []string{ "connection_modal.html", + "connect_form.html", } tmpl := make(map[string]*template.Template) diff --git a/main.go b/main.go index 39dc99c..7883a71 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ func main() { mux.HandleFunc("POST /models/{id}/update", h.ModelUpdate) mux.HandleFunc("POST /models/{id}/delete", h.ModelDelete) mux.HandleFunc("GET /connections/{portId}", h.ConnectionModal) + mux.HandleFunc("GET /connections/connect-form", h.ConnectionConnectForm) mux.HandleFunc("POST /connections/create", h.ConnectionCreate) mux.HandleFunc("POST /connections/{id}/edit", h.ConnectionEdit) mux.HandleFunc("POST /connections/{id}/delete", h.ConnectionDelete) diff --git a/static/js/app.js b/static/js/app.js index 36c915f..99f80a8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -8,7 +8,13 @@ function closeModal() { } document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') closeModal(); + if (e.key === 'Escape') { + if (document.getElementById('modal-overlay').classList.contains('open')) { + closeModal(); + } else { + exitConnectMode(); + } + } }); document.body.addEventListener('htmx:afterSwap', function(evt) { @@ -16,3 +22,59 @@ document.body.addEventListener('htmx:afterSwap', function(evt) { openModal(); } }); + +var connectMode = false; +var selectedPort = null; +var selectedEl = null; + +function toggleConnectMode() { + connectMode = !connectMode; + if (connectMode) { + document.body.classList.add('connect-mode'); + } else { + exitConnectMode(); + } + updateConnectButtons(); +} + +function exitConnectMode() { + connectMode = false; + document.body.classList.remove('connect-mode'); + if (selectedEl) selectedEl.classList.remove('port-selected'); + selectedPort = null; + selectedEl = null; + updateConnectButtons(); + document.getElementById('connect-status').textContent = ''; +} + +function updateConnectButtons() { + document.querySelectorAll('.btn-connect-mode').forEach(function(btn) { + btn.textContent = connectMode ? 'Cancel Connect' : 'Connect Mode'; + btn.classList.toggle('active', connectMode); + }); +} + +function portClick(el, portId, evt) { + if (!connectMode) return true; + evt.preventDefault(); + evt.stopPropagation(); + + if (selectedPort === portId) { + exitConnectMode(); + return false; + } + + if (selectedPort === null) { + selectedPort = portId; + selectedEl = el; + el.classList.add('port-selected'); + document.getElementById('connect-status').textContent = 'Selected ' + el.textContent.trim() + '. Click another port.'; + } else { + var p1 = selectedPort; + var p2 = portId; + exitConnectMode(); + openModal(); + htmx.ajax('GET', '/connections/connect-form?p1=' + p1 + '&p2=' + p2, {target: '#modal-content', swap: 'innerHTML'}); + } + return false; +} diff --git a/static/style.css b/static/style.css index 254ce7a..255d764 100644 --- a/static/style.css +++ b/static/style.css @@ -180,6 +180,13 @@ th { background: var(--surface); font-weight: 600; } .outlet-icon { font-size: 1.2em; } .outlet-label { font-family: monospace; font-size: 0.8em; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } +/* CONNECT MODE */ +body.connect-mode .port, body.connect-mode .outlet { cursor: crosshair; } +body.connect-mode .port:hover, body.connect-mode .outlet:hover { border-color: var(--accent); box-shadow: 0 0 4px var(--accent); } +.port-selected, body.connect-mode .port-selected { border-color: var(--accent) !important; background: var(--surface2) !important; box-shadow: 0 0 8px var(--accent); } +.btn-connect-mode.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.connect-status { margin-left: 0.5em; color: var(--accent); font-size: 0.85em; font-style: italic; } + /* PORT EDITOR */ .port-entry { display: flex; gap: 0.5em; align-items: center; margin-bottom: 0.3em; } .port-entry input { width: 18em; margin-bottom: 0; } diff --git a/templates/_port_list.html b/templates/_port_list.html index cbb4ce3..06f7909 100644 --- a/templates/_port_list.html +++ b/templates/_port_list.html @@ -8,10 +8,8 @@ {{range $.Connections}}{{if and .Port1 .Port2}}{{if eq .Port1.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{else if eq .Port2.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{end}}{{end}}{{end}} {{.Name}} @@ -25,10 +23,8 @@ {{range $.Connections}}{{if and .Port1 .Port2}}{{if eq .Port1.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{else if eq .Port2.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{end}}{{end}}{{end}} {{.Name}} diff --git a/templates/_power_strip.html b/templates/_power_strip.html index 3dde06f..1071e68 100644 --- a/templates/_power_strip.html +++ b/templates/_power_strip.html @@ -6,10 +6,8 @@ {{range $.Connections}}{{if and .Port1 .Port2}}{{if eq .Port1.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{else if eq .Port2.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{end}}{{end}}{{end}} 🔌 {{.Name}} diff --git a/templates/connect_form.html b/templates/connect_form.html new file mode 100644 index 0000000..ee22b56 --- /dev/null +++ b/templates/connect_form.html @@ -0,0 +1,23 @@ + + × + Create Connection + + {{.Device1Name}} [{{.Port1Name}}] + — + {{.Device2Name}} [{{.Port2Name}}] + + + + + + Type + + {{range .ConnectionTypes}}{{.Name}}{{end}} + + + Label end 1 + Label end 2 + Color + Create Connection + + diff --git a/templates/device.html b/templates/device.html index 565d579..92dfe9d 100644 --- a/templates/device.html +++ b/templates/device.html @@ -39,6 +39,10 @@ Ports + + Connect Mode + + {{if .Device.Model}}{{if or .Device.Model.FrontImage .Device.Model.BackImage}} {{if .Device.Model.FrontImage}}Front{{end}} diff --git a/templates/rack.html b/templates/rack.html index 24c5b59..0cf32fe 100644 --- a/templates/rack.html +++ b/templates/rack.html @@ -50,6 +50,10 @@ Devices & Ports + + Connect Mode + + {{range .RackedDevices}} diff --git a/templates/wall_sockets.html b/templates/wall_sockets.html index 1185833..d59fe70 100644 --- a/templates/wall_sockets.html +++ b/templates/wall_sockets.html @@ -17,10 +17,9 @@ {{range $.Connections}}{{if and .Port1 .Port2}}{{if eq .Port1.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{else if eq .Port2.ID $pid}}{{$color = .Color}}{{$bg = .Color}}{{end}}{{end}}{{end}} {{.Name}}
+ {{.Device1Name}} [{{.Port1Name}}] + — + {{.Device2Name}} [{{.Port2Name}}] +