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))); }