Fill an admin link's URL from a server's own known ports

Both places you add an admin link - a server's own Admin Links section
and the Operations > Admin Links summary page - now offer a "fill from
a known port" dropdown once a server is picked, listing its
agent-reported and manually-noted ports (the same data the Ports
page/card shows). Picking one fills in the URL as
http(s)://<hostname-or-IP>:<port>, guessing https for a handful of
common admin-panel ports, and fills the label too if it's still empty.
The URL field stays fully editable either way.

Shared the address/label logic in a small adminLinkUrl.ts helper used
by both pages. No server-side changes needed - built entirely on the
existing per-server ports and detail endpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-30 00:23:52 +02:00
1 parent 1ff1c6550c
commit 21054aaa8c
3 files changed
+135 -1

No files matched your search

+69 -1
View File
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { Link } from "react-router-dom";
import { api, type AdminLink, type CurrentUser, type ServerRecord } from "../api/client";
import { api, type AdminLink, type CurrentUser, type PortEntry, type ServerRecord } from "../api/client";
import { useSortable } from "../hooks/useSortable";
import SortableTh from "../components/SortableTh";
import { downloadCsv } from "../utils/csv";
import { guessAdminUrl, portOptionLabel } from "../utils/adminLinkUrl";
interface LinkForm {
serverId: number | "";
@@ -26,6 +27,11 @@ export default function AdminLinks({ user }: { user: CurrentUser }) {
const [form, setForm] = useState<LinkForm>(emptyForm);
const [saving, setSaving] = useState(false);
const [portOptions, setPortOptions] = useState<PortEntry[] | null>(null);
const [portAddress, setPortAddress] = useState<string | null>(null);
const [loadingPorts, setLoadingPorts] = useState(false);
const [portPick, setPortPick] = useState("");
function load() {
api.servers.allLinks()
.then((res) => setLinks(res.links))
@@ -40,6 +46,37 @@ export default function AdminLinks({ user }: { user: CurrentUser }) {
.catch(() => {});
}, []);
// Once a server is picked in the add/edit form, offer to fill the URL from one of its own known
// ports (agent-reported or already noted on its Ports card) instead of typing it out by hand.
useEffect(() => {
setPortPick("");
if (form.serverId === "") {
setPortOptions(null);
setPortAddress(null);
return;
}
const serverId = form.serverId;
setLoadingPorts(true);
Promise.all([api.servers.detail(serverId), api.servers.ports.list(serverId)])
.then(([detail, portList]) => {
setPortAddress(detail.server.hostname || detail.ipAddresses[0] || null);
setPortOptions(portList.ports);
})
.catch(() => {
setPortOptions(null);
setPortAddress(null);
})
.finally(() => setLoadingPorts(false));
}, [form.serverId]);
function pickPort(key: string) {
setPortPick(key);
const entry = portOptions?.find((p) => `${p.protocol}:${p.port}` === key);
if (!entry) return;
const url = guessAdminUrl(portAddress, entry);
setForm((f) => ({ ...f, url: url ?? f.url, label: f.label || entry.label || entry.agent?.process || "" }));
}
const filtered = useMemo(() => {
if (!links) return [];
const q = search.trim().toLowerCase();
@@ -166,6 +203,37 @@ export default function AdminLinks({ user }: { user: CurrentUser }) {
onChange={(e) => setForm({ ...form, url: e.target.value })}
/>
</div>
{form.serverId !== "" && (
<div className="col-12">
<label className="form-label">Fill from a known port (optional)</label>
<select
className="form-select"
style={{ maxWidth: 460 }}
value={portPick}
disabled={loadingPorts || !portOptions?.length}
onChange={(e) => pickPort(e.target.value)}
>
<option value="">
{loadingPorts
? "Loading this server's ports…"
: portOptions?.length
? "— pick a port to fill in the URL above —"
: "No known ports for this server yet"}
</option>
{portOptions?.map((p) => (
<option key={`${p.protocol}:${p.port}`} value={`${p.protocol}:${p.port}`}>
{portOptionLabel(p)}
</option>
))}
</select>
{portOptions && portOptions.length > 0 && !portAddress && (
<div className="form-hint">
This server has no known hostname or IP yet, so picking a port won't fill in an address —
see its Ports card under Infrastructure → Servers.
</div>
)}
</div>
)}
</div>
<div className="card-footer d-flex gap-2">
<button type="submit" className="btn btn-primary" disabled={saving}>