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}