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:
1 parent
1ff1c6550c
commit
21054aaa8c
3 files changed
+135
-1
No files matched your search
@@ -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}>
|
||||
|
||||
Reference in new issue
Block a user