From 5c1376cf1c6c8f44c55a9e4107922985de496293 Mon Sep 17 00:00:00 2001 From: Bobban Rydh Date: Sun, 20 Sep 2026 19:16:09 +0200 Subject: [PATCH] Make search results open the actual entry, not just its list page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a search result for a secret/IP/integration/DNS record only filtered or scrolled to the right page — you still had to find and click it yourself. Now: - Secrets/IPAM results pass ?editId= and the page auto-opens that row's existing edit form on load (viewers still just get the ?q= filter, since they can't edit). - Integration results now link to /integrations?editId= (which auto-opens IntegrationEditForm for that row) instead of the type's live dashboard page (/proxmox, /tailscale, etc.) — the dashboard can't distinguish between two integrations of the same type anyway, so it wasn't landing on "the" entry when more than one existed. - DNS record results now also pass recordType/recordName/recordContent so Dns.tsx, after auto-selecting the right zone, finds that exact record by (type, name, content) and opens its edit form too. That triple was used instead of an id because the records-list endpoint returns both the cache table's internal integer id and the provider's own string record id under different field names, and matching by content sidesteps that ambiguity entirely rather than risking picking the wrong one. Servers and DNS zones/providers already landed on their real entry (server detail page; zone/provider auto-selected) so those are unchanged. Co-Authored-By: Claude Sonnet 5 --- web/src/components/CommandPalette.tsx | 26 +++++++++++--------------- web/src/pages/Dns.tsx | 11 +++++++++++ web/src/pages/Integrations.tsx | 10 +++++++++- web/src/pages/Ipam.tsx | 8 +++++++- web/src/pages/Secrets.tsx | 8 +++++++- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/web/src/components/CommandPalette.tsx b/web/src/components/CommandPalette.tsx index f39abb6..47641a6 100644 --- a/web/src/components/CommandPalette.tsx +++ b/web/src/components/CommandPalette.tsx @@ -19,17 +19,6 @@ const INTEGRATION_TYPE_LABELS: Record = { dockhand: "Dockhand", }; -// No per-integration-instance route exists yet — every integration of a -// given type links to that type's single dashboard page. -const INTEGRATION_TYPE_PATH: Record = { - tailscale: "/tailscale", - proxmox: "/proxmox", - synology: "/synology", - semaphore: "/semaphore", - gitea: "/gitea", - dockhand: "/docker", -}; - const DNS_PROVIDER_LABELS: Record = { cloudflare: "Cloudflare", loopia: "Loopia", @@ -58,7 +47,7 @@ function buildItems(res: SearchResponse): PaletteItem[] { category: "Secrets", title: s.name, subtitle: SECRET_TYPE_LABELS[s.type], - path: `/secrets?q=${encodeURIComponent(s.name)}`, + path: `/secrets?q=${encodeURIComponent(s.name)}&editId=${s.id}`, }); } for (const i of res.ipam) { @@ -67,7 +56,7 @@ function buildItems(res: SearchResponse): PaletteItem[] { category: "IP Addresses", title: i.ipAddress, subtitle: i.label ?? "", - path: `/ipam?q=${encodeURIComponent(i.ipAddress)}`, + path: `/ipam?q=${encodeURIComponent(i.ipAddress)}&editId=${i.id}`, }); } for (const z of res.dnsZones) { @@ -80,12 +69,19 @@ function buildItems(res: SearchResponse): PaletteItem[] { }); } for (const r of res.dnsRecords) { + const params = new URLSearchParams({ + providerId: String(r.providerId), + zoneId: r.zoneId, + recordType: r.type, + recordName: r.name, + recordContent: r.content, + }); items.push({ key: `record-${r.providerId}-${r.zoneId}-${r.type}-${r.name}-${r.content}`, category: "DNS records", title: `${r.type} ${r.name}`, subtitle: `${r.content} · zone ${r.zoneName} · ${r.providerName}`, - path: `/dns?providerId=${r.providerId}&zoneId=${encodeURIComponent(r.zoneId)}`, + path: `/dns?${params.toString()}`, }); } for (const p of res.dnsProviders) { @@ -103,7 +99,7 @@ function buildItems(res: SearchResponse): PaletteItem[] { category: "Integrations", title: i.name, subtitle: INTEGRATION_TYPE_LABELS[i.type], - path: INTEGRATION_TYPE_PATH[i.type], + path: `/integrations?editId=${i.id}`, }); } return items; diff --git a/web/src/pages/Dns.tsx b/web/src/pages/Dns.tsx index 57ab815..226e834 100644 --- a/web/src/pages/Dns.tsx +++ b/web/src/pages/Dns.tsx @@ -104,6 +104,17 @@ export default function Dns({ user }: { user: CurrentUser }) { .then((res) => { setRecords(res.records); setSyncedAt(res.syncedAt); + // Deep-link from global search (?recordType=&recordName=&recordContent=) — jump + // straight to that record's edit form. Matched by (type, name, content) rather + // than an id, since a record's identifier isn't reliably exposed to the frontend + // for this purpose, but that triple is unambiguous within one zone. + const linkedType = searchParams.get("recordType"); + const linkedName = searchParams.get("recordName"); + const linkedContent = searchParams.get("recordContent"); + if (linkedType && linkedName && linkedContent && canEdit) { + const match = res.records.find((r) => r.type === linkedType && r.name === linkedName && r.content === linkedContent); + if (match) startEditRecord(match); + } }) .catch((err) => setError(err instanceof Error ? err.message : String(err))); } diff --git a/web/src/pages/Integrations.tsx b/web/src/pages/Integrations.tsx index 8f6366d..1ced03f 100644 --- a/web/src/pages/Integrations.tsx +++ b/web/src/pages/Integrations.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, type CSSProperties } from "react"; +import { useSearchParams } from "react-router-dom"; import { api, type CurrentUser, type IntegrationSummary, type IntegrationType } from "../api/client"; import IntegrationForm from "../components/IntegrationForm"; import IntegrationEditForm from "../components/IntegrationEditForm"; @@ -23,6 +24,7 @@ function typeBadgeStyle(colors: Record, type: IntegrationType): export default function Integrations({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; + const [searchParams] = useSearchParams(); const [integrations, setIntegrations] = useState(null); const [adding, setAdding] = useState(false); @@ -39,7 +41,13 @@ export default function Integrations({ user }: { user: CurrentUser }) { function loadIntegrations() { api.integrations .list() - .then((res) => setIntegrations(res.integrations)) + .then((res) => { + setIntegrations(res.integrations); + // Deep-link from global search (?editId=) — jump straight to that integration's edit form. + const editId = Number(searchParams.get("editId")); + const target = res.integrations.find((i) => i.id === editId); + if (target && isAdmin) setEditingIntegration(target); + }) .catch((err) => setError(String(err))); } diff --git a/web/src/pages/Ipam.tsx b/web/src/pages/Ipam.tsx index f3d144d..0f9e8d3 100644 --- a/web/src/pages/Ipam.tsx +++ b/web/src/pages/Ipam.tsx @@ -29,7 +29,13 @@ export default function Ipam({ user }: { user: CurrentUser }) { function load() { api.ipam .list() - .then((res) => setEntries(res.entries)) + .then((res) => { + setEntries(res.entries); + // Deep-link from global search (?editId=) — jump straight to that entry's edit form. + const editId = Number(searchParams.get("editId")); + const target = res.entries.find((e) => e.id === editId); + if (target && canEdit) startEdit(target); + }) .catch((err) => setError(String(err))); } diff --git a/web/src/pages/Secrets.tsx b/web/src/pages/Secrets.tsx index caa59bf..642e358 100644 --- a/web/src/pages/Secrets.tsx +++ b/web/src/pages/Secrets.tsx @@ -43,7 +43,13 @@ export default function Secrets({ user }: { user: CurrentUser }) { function load() { api.secrets .list() - .then((res) => setSecrets(res.secrets)) + .then((res) => { + setSecrets(res.secrets); + // Deep-link from global search (?editId=) — jump straight to that secret's edit form. + const editId = Number(searchParams.get("editId")); + const target = res.secrets.find((s) => s.id === editId); + if (target && canEdit) startEdit(target); + }) .catch((err) => setError(String(err))); }