diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 55d525de4..368e19576 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -294,6 +294,7 @@ function App() { containers: [], dockerHosts: [], removedDockerHosts: [], + removedHosts: [], hosts: [], storage: [], cephClusters: [], diff --git a/frontend-modern/src/api/monitoring.ts b/frontend-modern/src/api/monitoring.ts index 59ea448f6..859d0b108 100644 --- a/frontend-modern/src/api/monitoring.ts +++ b/frontend-modern/src/api/monitoring.ts @@ -186,6 +186,36 @@ export class MonitoringAPI { } } + static async allowHostReenroll(hostId: string): Promise { + const url = `${this.baseUrl}/agents/host/${encodeURIComponent(hostId)}/allow-reenroll`; + + const response = await apiFetch(url, { + method: 'POST', + }); + + if (!response.ok) { + 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 (_err) { + // ignore parse error, use raw text + } + } + } catch (_err) { + // ignore read error + } + + 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/Settings/UnifiedAgents.tsx b/frontend-modern/src/components/Settings/UnifiedAgents.tsx index a9eb7a8d2..5558c6a93 100644 --- a/frontend-modern/src/components/Settings/UnifiedAgents.tsx +++ b/frontend-modern/src/components/Settings/UnifiedAgents.tsx @@ -606,6 +606,11 @@ export const UnifiedAgents: Component = () => { return removed.sort((a, b) => b.removedAt - a.removedAt); }); + const removedHosts = createMemo(() => { + const removed = state.removedHosts || []; + return removed.sort((a, b) => b.removedAt - a.removedAt); + }); + const kubernetesClusters = createMemo(() => { const clusters = state.kubernetesClusters || []; return clusters.slice().sort((a, b) => (a.displayName || a.name || a.id).localeCompare(b.displayName || b.name || b.id)); @@ -704,6 +709,22 @@ export const UnifiedAgents: Component = () => { }); }); + removedHosts().forEach(host => { + const name = host.displayName || host.hostname || host.id; + rows.push({ + rowKey: `removed-host-${host.id}`, + id: host.id, + name, + hostname: host.hostname, + displayName: host.displayName, + types: ['host'], + status: 'removed', + removedAt: host.removedAt, + scope: getScopeInfo(undefined), + searchText: [name, host.hostname, host.id].filter(Boolean).join(' ').toLowerCase(), + }); + }); + removedKubernetesClusters().forEach(cluster => { const name = cluster.displayName || cluster.name || cluster.id; rows.push({ @@ -867,6 +888,16 @@ export const UnifiedAgents: Component = () => { } }; + const handleAllowHostReenroll = async (hostId: string, hostname?: string) => { + try { + await MonitoringAPI.allowHostReenroll(hostId); + notificationStore.success(`Re-enrollment allowed for ${hostname || hostId}. Restart the agent to reconnect.`); + } catch (err) { + logger.error('Failed to allow host re-enrollment', err); + notificationStore.error('Failed to allow re-enrollment'); + } + }; + const handleRemoveKubernetesCluster = async (clusterId: string) => { if (!confirm('Are you sure you want to remove this Kubernetes cluster? This will stop monitoring but will not uninstall the agent from the cluster.')) return; @@ -1718,12 +1749,21 @@ export const UnifiedAgents: Component = () => { }> handleAllowKubernetesReenroll(row.id, row.name)} - class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" - > - Allow re-enroll - + handleAllowKubernetesReenroll(row.id, row.name)} + class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" + > + Allow re-enroll + + }> + + }>