Make search results open the actual entry, not just its list page
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
009bb3e027
commit
5c1376cf1c
5 files changed
+45
-18
No files matched your search
@@ -19,17 +19,6 @@ const INTEGRATION_TYPE_LABELS: Record<IntegrationType, string> = {
|
|||||||
dockhand: "Dockhand",
|
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<IntegrationType, string> = {
|
|
||||||
tailscale: "/tailscale",
|
|
||||||
proxmox: "/proxmox",
|
|
||||||
synology: "/synology",
|
|
||||||
semaphore: "/semaphore",
|
|
||||||
gitea: "/gitea",
|
|
||||||
dockhand: "/docker",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DNS_PROVIDER_LABELS: Record<DnsProviderType, string> = {
|
const DNS_PROVIDER_LABELS: Record<DnsProviderType, string> = {
|
||||||
cloudflare: "Cloudflare",
|
cloudflare: "Cloudflare",
|
||||||
loopia: "Loopia",
|
loopia: "Loopia",
|
||||||
@@ -58,7 +47,7 @@ function buildItems(res: SearchResponse): PaletteItem[] {
|
|||||||
category: "Secrets",
|
category: "Secrets",
|
||||||
title: s.name,
|
title: s.name,
|
||||||
subtitle: SECRET_TYPE_LABELS[s.type],
|
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) {
|
for (const i of res.ipam) {
|
||||||
@@ -67,7 +56,7 @@ function buildItems(res: SearchResponse): PaletteItem[] {
|
|||||||
category: "IP Addresses",
|
category: "IP Addresses",
|
||||||
title: i.ipAddress,
|
title: i.ipAddress,
|
||||||
subtitle: i.label ?? "",
|
subtitle: i.label ?? "",
|
||||||
path: `/ipam?q=${encodeURIComponent(i.ipAddress)}`,
|
path: `/ipam?q=${encodeURIComponent(i.ipAddress)}&editId=${i.id}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const z of res.dnsZones) {
|
for (const z of res.dnsZones) {
|
||||||
@@ -80,12 +69,19 @@ function buildItems(res: SearchResponse): PaletteItem[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const r of res.dnsRecords) {
|
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({
|
items.push({
|
||||||
key: `record-${r.providerId}-${r.zoneId}-${r.type}-${r.name}-${r.content}`,
|
key: `record-${r.providerId}-${r.zoneId}-${r.type}-${r.name}-${r.content}`,
|
||||||
category: "DNS records",
|
category: "DNS records",
|
||||||
title: `${r.type} ${r.name}`,
|
title: `${r.type} ${r.name}`,
|
||||||
subtitle: `${r.content} · zone ${r.zoneName} · ${r.providerName}`,
|
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) {
|
for (const p of res.dnsProviders) {
|
||||||
@@ -103,7 +99,7 @@ function buildItems(res: SearchResponse): PaletteItem[] {
|
|||||||
category: "Integrations",
|
category: "Integrations",
|
||||||
title: i.name,
|
title: i.name,
|
||||||
subtitle: INTEGRATION_TYPE_LABELS[i.type],
|
subtitle: INTEGRATION_TYPE_LABELS[i.type],
|
||||||
path: INTEGRATION_TYPE_PATH[i.type],
|
path: `/integrations?editId=${i.id}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ export default function Dns({ user }: { user: CurrentUser }) {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
setRecords(res.records);
|
setRecords(res.records);
|
||||||
setSyncedAt(res.syncedAt);
|
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)));
|
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, type CSSProperties } from "react";
|
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 { api, type CurrentUser, type IntegrationSummary, type IntegrationType } from "../api/client";
|
||||||
import IntegrationForm from "../components/IntegrationForm";
|
import IntegrationForm from "../components/IntegrationForm";
|
||||||
import IntegrationEditForm from "../components/IntegrationEditForm";
|
import IntegrationEditForm from "../components/IntegrationEditForm";
|
||||||
@@ -23,6 +24,7 @@ function typeBadgeStyle(colors: Record<string, string>, type: IntegrationType):
|
|||||||
|
|
||||||
export default function Integrations({ user }: { user: CurrentUser }) {
|
export default function Integrations({ user }: { user: CurrentUser }) {
|
||||||
const isAdmin = user.role === "admin";
|
const isAdmin = user.role === "admin";
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [integrations, setIntegrations] = useState<IntegrationSummary[] | null>(null);
|
const [integrations, setIntegrations] = useState<IntegrationSummary[] | null>(null);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
@@ -39,7 +41,13 @@ export default function Integrations({ user }: { user: CurrentUser }) {
|
|||||||
function loadIntegrations() {
|
function loadIntegrations() {
|
||||||
api.integrations
|
api.integrations
|
||||||
.list()
|
.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)));
|
.catch((err) => setError(String(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ export default function Ipam({ user }: { user: CurrentUser }) {
|
|||||||
function load() {
|
function load() {
|
||||||
api.ipam
|
api.ipam
|
||||||
.list()
|
.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)));
|
.catch((err) => setError(String(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ export default function Secrets({ user }: { user: CurrentUser }) {
|
|||||||
function load() {
|
function load() {
|
||||||
api.secrets
|
api.secrets
|
||||||
.list()
|
.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)));
|
.catch((err) => setError(String(err)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user