Show linked node addresses as text with an edit button

- Each linked node shows its Monitor address as plain text, marked Automatic when it was detected
- A pencil button opens the address field with Save and Cancel only when it needs changing
- The node cards keep the dialog background so the editable fields stand out
- The Add button keeps white text when disabled
- New texts translated into all languages
This commit is contained in:
MacRimi
2026-10-18 19:45:54 +02:00
parent b720b4a8de
commit c6f919b085
9 changed files with 57 additions and 23 deletions
+33 -15
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"
import useSWR, { mutate as refreshCache } from "swr"
import { ArrowLeft, ChevronDown, DatabaseBackup, ExternalLink, Globe, Plus, Server, Trash2 } from "lucide-react"
import { ArrowLeft, ChevronDown, DatabaseBackup, ExternalLink, Globe, Pencil, Plus, Server, Trash2 } from "lucide-react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
@@ -280,6 +280,7 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
const { data, mutate } = useSWR<MonitorLinks>("/api/monitor-links?all=1", fetcher, { revalidateOnFocus: false })
const [form, setForm] = useState({ name: "", url: "", product: "pve" })
const [addresses, setAddresses] = useState<Record<string, string>>({})
const [editing, setEditing] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState("")
@@ -304,7 +305,7 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
<DialogDescription>{t("monitorLinks.dialogDescription")}</DialogDescription>
</DialogHeader>
<div className="space-y-3 rounded-lg border border-border bg-card p-4">
<div className="space-y-3 rounded-lg border border-border p-4">
<p className="text-sm font-medium">{t("monitorLinks.addTitle")}</p>
<div className="grid gap-3 sm:grid-cols-[1fr_1.5fr_auto]">
<div className="space-y-1.5">
@@ -327,7 +328,7 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
</Select>
</div>
</div>
<Button size="sm" className="bg-blue-600 text-white hover:bg-blue-700" disabled={busy || !form.url.trim()} onClick={() => run(async () => {
<Button size="sm" className="bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-600/50 disabled:text-white disabled:opacity-100" disabled={busy || !form.url.trim()} onClick={() => run(async () => {
await send("/api/monitor-links", "POST", form)
setForm({ name: "", url: "", product: "pve" })
})}><Plus className="mr-2 h-4 w-4" />{t("monitorLinks.add")}</Button>
@@ -338,8 +339,12 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
<div className="space-y-3">
{data?.links.length === 0 && <p className="text-sm text-muted-foreground">{t("monitorLinks.empty")}</p>}
{data?.links.map(link => {
const address = addresses[link.key] ?? (link.manual || link.custom_url ? link.url || "" : "")
return <div key={link.key} className={`space-y-2 rounded-lg border border-border bg-card p-3 ${link.hidden ? "opacity-60" : ""}`}>
const own = link.manual || link.custom_url
const shown = link.url || ""
const draft = addresses[link.key] ?? (own ? shown : "")
const isEditing = editing === link.key
// A read-only card is a panel; while its address is edited it lets the field stand out.
return <div key={link.key} className={`space-y-2 rounded-lg border border-border p-3 ${isEditing ? "" : "bg-card"} ${link.hidden ? "opacity-60" : ""}`}>
<div className="flex items-center gap-2">
<ProductIcon product={link.product} className="h-4 w-4 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate font-medium">{link.name}</span>
@@ -354,16 +359,29 @@ function ManageLinksDialog({ open, onOpenChange }: { open: boolean; onOpenChange
<Trash2 className="h-4 w-4 text-red-500" />
</Button>}
</div>
<div className="flex gap-2">
<Input value={address} disabled={busy} aria-label={t("monitorLinks.address")}
placeholder={link.manual ? "" : `${t("monitorLinks.autoAddress")}${link.url ? `: ${link.url}` : ""}`}
onChange={event => setAddresses({ ...addresses, [link.key]: event.target.value })} />
<Button variant="outline" size="sm" className="h-10" disabled={busy || addresses[link.key] === undefined}
onClick={() => run(async () => {
await send(`/api/monitor-links/${encodeURIComponent(link.key)}`, "PATCH", { url: addresses[link.key] })
setAddresses(({ [link.key]: _, ...rest }) => rest)
})}>{t("monitorLinks.save")}</Button>
</div>
{isEditing
? <div className="flex gap-2">
<Input value={draft} disabled={busy} autoFocus aria-label={t("monitorLinks.address")}
placeholder={link.manual ? "" : `${t("monitorLinks.autoAddress")}${shown ? `: ${shown}` : ""}`}
onChange={event => setAddresses({ ...addresses, [link.key]: event.target.value })} />
<Button size="sm" className="h-10 bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-600/50 disabled:text-white disabled:opacity-100"
disabled={busy || addresses[link.key] === undefined || (link.manual && !draft.trim())}
onClick={() => run(async () => {
await send(`/api/monitor-links/${encodeURIComponent(link.key)}`, "PATCH", { url: addresses[link.key] })
setAddresses(({ [link.key]: _, ...rest }) => rest)
setEditing(null)
})}>{t("monitorLinks.save")}</Button>
<Button variant="outline" size="sm" className="h-10" disabled={busy}
onClick={() => { setAddresses(({ [link.key]: _, ...rest }) => rest); setEditing(null) }}>{t("monitorLinks.cancel")}</Button>
</div>
: <div className="flex items-center gap-2">
<span className="min-w-0 flex-1 truncate font-mono text-sm text-foreground">{shown || "—"}</span>
{!link.manual && !link.custom_url && <span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground">{t("monitorLinks.autoAddress")}</span>}
<Button variant="ghost" size="sm" disabled={busy} aria-label={t("monitorLinks.changeAddress")} title={t("monitorLinks.changeAddress")}
onClick={() => setEditing(link.key)}>
<Pencil className="h-4 w-4 text-muted-foreground" />
</Button>
</div>}
</div>
})}
</div>
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "Verknüpfte Knoten hinzufügen?",
"knownBody": "{name} verknüpft diese Monitore. Wählen Sie aus, welche Sie diesem Monitor hinzufügen möchten.",
"addSelected": "Auswahl hinzufügen",
"pbsWebInterface": "PBS-Weboberfläche"
"pbsWebInterface": "PBS-Weboberfläche",
"changeAddress": "Adresse ändern",
"cancel": "Abbrechen"
}
}
+3 -1
View File
@@ -6587,6 +6587,8 @@
"knownTitle": "Add linked nodes?",
"knownBody": "{name} links these Monitors. Choose the ones to add to this Monitor.",
"addSelected": "Add selected",
"pbsWebInterface": "PBS web interface"
"pbsWebInterface": "PBS web interface",
"changeAddress": "Change address",
"cancel": "Cancel"
}
}
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "¿Añadir nodos enlazados?",
"knownBody": "{name} enlaza estos Monitores. Elige los que quieres añadir a este Monitor.",
"addSelected": "Añadir seleccionados",
"pbsWebInterface": "Interfaz web de PBS"
"pbsWebInterface": "Interfaz web de PBS",
"changeAddress": "Cambiar dirección",
"cancel": "Cancelar"
}
}
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "Ajouter des nœuds liés ?",
"knownBody": "{name} relie ces moniteurs. Choisissez ceux à ajouter à ce moniteur.",
"addSelected": "Ajouter la sélection",
"pbsWebInterface": "Interface web de PBS"
"pbsWebInterface": "Interface web de PBS",
"changeAddress": "Modifier l'adresse",
"cancel": "Annuler"
}
}
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "Aggiungere nodi collegati?",
"knownBody": "{name} collega questi monitor. Scegli quelli da aggiungere a questo monitor.",
"addSelected": "Aggiungi selezionati",
"pbsWebInterface": "Interfaccia web di PBS"
"pbsWebInterface": "Interfaccia web di PBS",
"changeAddress": "Cambia indirizzo",
"cancel": "Annulla"
}
}
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "Adicionar nós vinculados?",
"knownBody": "{name} liga estes Monitores. Escolha os que quer adicionar a este Monitor.",
"addSelected": "Adicionar selecionados",
"pbsWebInterface": "Interface web do PBS"
"pbsWebInterface": "Interface web do PBS",
"changeAddress": "Alterar endereço",
"cancel": "Cancelar"
}
}
+3 -1
View File
@@ -7378,6 +7378,8 @@
"knownTitle": "Pridať prepojené uzly?",
"knownBody": "{name} prepája tieto monitory. Vyberte tie, ktoré chcete pridať do tohto monitora.",
"addSelected": "Pridať vybrané",
"pbsWebInterface": "Webové rozhranie PBS"
"pbsWebInterface": "Webové rozhranie PBS",
"changeAddress": "Zmeniť adresu",
"cancel": "Zrušiť"
}
}
+3 -1
View File
@@ -7379,6 +7379,8 @@
"knownTitle": "Lägga till länkade noder?",
"knownBody": "{name} länkar dessa monitorer. Välj vilka du vill lägga till på denna monitor.",
"addSelected": "Lägg till valda",
"pbsWebInterface": "PBS webbgränssnitt"
"pbsWebInterface": "PBS webbgränssnitt",
"changeAddress": "Ändra adress",
"cancel": "Avbryt"
}
}