From 7936808193d88ff3be87b060bb293b7cc358aff3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 5 Nov 2025 23:18:03 +0000 Subject: [PATCH] Add custom display name support for Docker hosts This implements the ability for users to assign custom display names to Docker hosts, similar to the existing functionality for Proxmox nodes. This addresses the issue where multiple Docker hosts with identical hostnames but different IPs/domains cannot be easily distinguished in the UI. Backend changes: - Add CustomDisplayName field to DockerHost model (internal/models/models.go:201) - Update UpsertDockerHost to preserve custom display names across updates (internal/models/models.go:1110-1113) - Add SetDockerHostCustomDisplayName method to State for updating names (internal/models/models.go:1221-1235) - Add SetDockerHostCustomDisplayName method to Monitor (internal/monitoring/monitor.go:1070-1088) - Add HandleSetCustomDisplayName API handler (internal/api/docker_agents.go:385-426) - Route /api/agents/docker/hosts/{id}/display-name PUT requests (internal/api/docker_agents.go:117-120) Frontend changes: - Add customDisplayName field to DockerHost TypeScript interface (frontend-modern/src/types/api.ts:136) - Add MonitoringAPI.setDockerHostDisplayName method (frontend-modern/src/api/monitoring.ts:151-187) - Update getDisplayName function to prioritize custom names (frontend-modern/src/components/Settings/DockerAgents.tsx:84-89) - Add inline editing UI with save/cancel buttons in Docker Agents settings (frontend-modern/src/components/Settings/DockerAgents.tsx:1349-1413) - Update sorting to use custom display names (frontend-modern/src/components/Docker/DockerHosts.tsx:58-59) - Update DockerHostSummaryTable to display custom names (frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx:40-42, 87, 120, 254) Users can now click the edit icon next to any Docker host name in Settings > Docker Agents to set a custom display name. The custom name will be preserved across agent reconnections and takes priority over the hostname reported by the agent. Related to #623 --- frontend-modern/src/api/monitoring.ts | 38 +++++++ .../Docker/DockerHostSummaryTable.tsx | 12 ++- .../src/components/Docker/DockerHosts.tsx | 4 +- .../src/components/Settings/DockerAgents.tsx | 102 +++++++++++++++++- frontend-modern/src/types/api.ts | 1 + internal/api/docker_agents.go | 50 +++++++++ internal/models/models.go | 22 ++++ internal/monitoring/monitor.go | 21 ++++ 8 files changed, 242 insertions(+), 8 deletions(-) diff --git a/frontend-modern/src/api/monitoring.ts b/frontend-modern/src/api/monitoring.ts index 4bf5ed3..04a140e 100644 --- a/frontend-modern/src/api/monitoring.ts +++ b/frontend-modern/src/api/monitoring.ts @@ -148,6 +148,44 @@ export class MonitoringAPI { } } + static async setDockerHostDisplayName(hostId: string, displayName: string): Promise { + const url = `${this.baseUrl}/agents/docker/hosts/${encodeURIComponent(hostId)}/display-name`; + + const response = await apiFetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ displayName }), + }); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('Docker host not found'); + } + + let message = `Failed with status ${response.status}`; + try { + const text = await response.text(); + if (text?.trim()) { + message = text.trim(); + try { + const parsed = JSON.parse(text); + if (typeof parsed?.error === 'string' && parsed.error.trim()) { + message = parsed.error.trim(); + } + } catch (_jsonErr) { + // ignore JSON parse errors, fallback to raw text + } + } + } catch (_err) { + // ignore read error, keep default message + } + + throw new Error(message); + } + } + static async allowDockerHostReenroll(hostId: string): Promise { const url = `${this.baseUrl}/agents/docker/hosts/${encodeURIComponent(hostId)}/allow-reenroll`; diff --git a/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx b/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx index 7e3ec65..96da1c7 100644 --- a/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx +++ b/frontend-modern/src/components/Docker/DockerHostSummaryTable.tsx @@ -37,6 +37,10 @@ const isHostOnline = (host: DockerHost) => { return status === 'online' || status === 'running' || status === 'healthy'; }; +const getDisplayName = (host: DockerHost) => { + return host.customDisplayName || host.displayName || host.hostname || host.id; +}; + export const DockerHostSummaryTable: Component = (props) => { const [sortKey, setSortKey] = createSignal('name'); const [sortDirection, setSortDirection] = createSignal('asc'); @@ -80,7 +84,7 @@ export const DockerHostSummaryTable: Component = (p let value = 0; switch (key) { case 'name': - value = hostA.displayName.localeCompare(hostB.displayName); + value = getDisplayName(hostA).localeCompare(getDisplayName(hostB)); break; case 'uptime': value = (a.uptimeSeconds || 0) - (b.uptimeSeconds || 0); @@ -113,7 +117,7 @@ export const DockerHostSummaryTable: Component = (p } if (value === 0) { - value = hostA.displayName.localeCompare(hostB.displayName); + value = getDisplayName(hostA).localeCompare(getDisplayName(hostB)); } return dir === 'asc' ? value : -value; @@ -247,9 +251,9 @@ export const DockerHostSummaryTable: Component = (p
- {summary.host.displayName} + {getDisplayName(summary.host)} - + diff --git a/frontend-modern/src/components/Docker/DockerHosts.tsx b/frontend-modern/src/components/Docker/DockerHosts.tsx index 97a0ea4..62be288 100644 --- a/frontend-modern/src/components/Docker/DockerHosts.tsx +++ b/frontend-modern/src/components/Docker/DockerHosts.tsx @@ -55,8 +55,8 @@ export const DockerHosts: Component = (props) => { const sortedHosts = createMemo(() => { const hosts = props.hosts || []; return [...hosts].sort((a, b) => { - const aName = a.displayName || a.hostname || a.id || ''; - const bName = b.displayName || b.hostname || b.id || ''; + const aName = a.customDisplayName || a.displayName || a.hostname || a.id || ''; + const bName = b.customDisplayName || b.displayName || b.hostname || b.id || ''; return aName.localeCompare(bName); }); }); diff --git a/frontend-modern/src/components/Settings/DockerAgents.tsx b/frontend-modern/src/components/Settings/DockerAgents.tsx index fb1b06e..acd2a0c 100644 --- a/frontend-modern/src/components/Settings/DockerAgents.tsx +++ b/frontend-modern/src/components/Settings/DockerAgents.tsx @@ -70,6 +70,9 @@ export const DockerAgents: Component = () => { const [tokenName, setTokenName] = createSignal(''); const [commandQueuedTime, setCommandQueuedTime] = createSignal(null); const [elapsedSeconds, setElapsedSeconds] = createSignal(0); + const [editingHostId, setEditingHostId] = createSignal(null); + const [editingDisplayName, setEditingDisplayName] = createSignal(''); + const [savingDisplayName, setSavingDisplayName] = createSignal(false); const pulseUrl = () => getPulseBaseUrl(); @@ -81,7 +84,10 @@ export const DockerAgents: Component = () => { return (state.dockerHosts || []).find(host => host.id === id) ?? null; }); - const getDisplayName = (host: DockerHost | { id: string; displayName?: string | null; hostname?: string | null }) => { + const getDisplayName = (host: DockerHost | { id: string; displayName?: string | null; hostname?: string | null; customDisplayName?: string | null }) => { + if ('customDisplayName' in host && host.customDisplayName) { + return host.customDisplayName; + } return host.displayName || host.hostname || host.id; }; @@ -518,6 +524,34 @@ WantedBy=multi-user.target`; } }; + const startEditingDisplayName = (host: DockerHost) => { + setEditingHostId(host.id); + setEditingDisplayName(host.customDisplayName || ''); + }; + + const cancelEditingDisplayName = () => { + setEditingHostId(null); + setEditingDisplayName(''); + }; + + const saveDisplayName = async (hostId: string, originalName: string) => { + const newName = editingDisplayName().trim(); + + setSavingDisplayName(true); + try { + await MonitoringAPI.setDockerHostDisplayName(hostId, newName); + notificationStore.success(`Updated display name for ${originalName}`, 3500); + setEditingHostId(null); + setEditingDisplayName(''); + } catch (error) { + logger.error('Failed to update display name', error); + const message = error instanceof Error ? error.message : 'Failed to update display name'; + notificationStore.error(message, 8000); + } finally { + setSavingDisplayName(false); + } + }; + return (
@@ -1312,7 +1346,71 @@ WantedBy=multi-user.target`; return ( -
{displayName}
+ +
+
{displayName}
+ +
+ Original: {host.displayName || host.hostname} +
+
+
+ +
+ } + > +
+ setEditingDisplayName(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + saveDisplayName(host.id, displayName); + } else if (e.key === 'Escape') { + cancelEditingDisplayName(); + } + }} + placeholder="Custom display name" + disabled={savingDisplayName()} + /> + + +
+
{host.hostname}