127 lines
5.1 KiB
TypeScript
127 lines
5.1 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
|
|
type Container = { id: string; name: string; status: string }
|
|
|
|
function isContainerResponse(value: unknown): value is { containers: Container[] } {
|
|
if (!value || typeof value !== 'object') return false
|
|
const containers = (value as Record<string, unknown>).containers
|
|
return Array.isArray(containers) && containers.every((container: unknown) => {
|
|
if (!container || typeof container !== 'object') return false
|
|
const item = container as Record<string, unknown>
|
|
return typeof item.id === 'string' && typeof item.name === 'string' && typeof item.status === 'string'
|
|
})
|
|
}
|
|
|
|
export default function NodeDockerContainers({ nodeId, clusterId, detailed = false }: {
|
|
nodeId: string; clusterId: string; detailed?: boolean
|
|
}) {
|
|
const [containers, setContainers] = useState<Container[] | null>(null)
|
|
const [status, setStatus] = useState('Connecting…')
|
|
const [attempt, setAttempt] = useState(0)
|
|
|
|
useEffect(() => {
|
|
let active = true
|
|
const controller = new AbortController()
|
|
let socket: WebSocket | undefined
|
|
const disconnected = () => {
|
|
if (!active) return
|
|
setContainers(null)
|
|
setStatus('Docker feed disconnected. Reconnect to try again.')
|
|
}
|
|
const connect = async () => {
|
|
setContainers(null)
|
|
setStatus('Loading node details…')
|
|
try {
|
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
|
|
if (!apiBaseUrl) throw new Error('API URL is not configured.')
|
|
const token = localStorage.getItem('access_token')
|
|
const response = await fetch(
|
|
`${apiBaseUrl.replace(/\/$/, '')}/spock/node-interfaces/${encodeURIComponent(nodeId)}`,
|
|
{
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
signal: controller.signal,
|
|
},
|
|
)
|
|
if (!response.ok) throw new Error('Failed to load node details.')
|
|
const result = await response.json()
|
|
const node = result?.node
|
|
if (result?.status !== 'success' || node?.uuid !== nodeId
|
|
|| typeof node.host !== 'string' || !node.host.trim()) {
|
|
throw new Error('Invalid node host received.')
|
|
}
|
|
if (!active) return
|
|
const host = node.host.trim()
|
|
const hostname = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
|
|
setStatus('Connecting…')
|
|
socket = new WebSocket(`ws://${hostname}:9100/ws/docker/containers`)
|
|
socket.onopen = () => {
|
|
if (active) setStatus('Waiting for containers…')
|
|
}
|
|
socket.onmessage = (event) => {
|
|
if (!active) return
|
|
try {
|
|
const message: unknown = JSON.parse(event.data)
|
|
if (!isContainerResponse(message)) throw new Error('Invalid container data')
|
|
setContainers(message.containers)
|
|
setStatus('Live')
|
|
} catch {
|
|
setContainers(null)
|
|
setStatus('Invalid container data received.')
|
|
}
|
|
}
|
|
socket.onerror = disconnected
|
|
socket.onclose = disconnected
|
|
} catch (error) {
|
|
if (!active) return
|
|
setStatus(error instanceof Error ? error.message : 'Unable to connect to the Docker feed.')
|
|
}
|
|
}
|
|
void connect()
|
|
return () => {
|
|
active = false
|
|
controller.abort()
|
|
socket?.close()
|
|
}
|
|
}, [attempt, nodeId])
|
|
|
|
const nodePath = `/clusters/${encodeURIComponent(clusterId)}/nodes/${encodeURIComponent(nodeId)}`
|
|
const visibleContainers = detailed ? containers : containers?.slice(0, 3)
|
|
|
|
return (
|
|
<section className="node-docker-panel database-connections" aria-labelledby="docker-containers-heading">
|
|
<div className="page-header">
|
|
<h2 id="docker-containers-heading">Docker containers</h2>
|
|
<div className="page-actions">
|
|
{detailed && <Link className="secondary-button" to={nodePath}>Back to node</Link>}
|
|
<p role="status">{status}</p>
|
|
<button type="button" className="secondary-button" onClick={() => {
|
|
setContainers(null)
|
|
setStatus('Connecting…')
|
|
setAttempt((current) => current + 1)
|
|
}}>Reconnect</button>
|
|
</div>
|
|
</div>
|
|
{containers !== null && (containers.length === 0 ? <p>No containers found.</p> : (
|
|
<div className="nodes-table-wrap">
|
|
<table className="nodes-table">
|
|
<thead><tr><th scope="col">Name</th><th scope="col">Container ID</th><th scope="col">Status</th></tr></thead>
|
|
<tbody>
|
|
{visibleContainers?.map((container) => (
|
|
<tr key={container.id}>
|
|
<td>{container.name}</td>
|
|
<td><code>{container.id}</code></td>
|
|
<td><span className={`connection-status${container.status === 'running' ? ' connected' : ''}`}>{container.status}</span></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
))}
|
|
{!detailed && containers && containers.length > 3 && (
|
|
<p className="docker-view-all"><Link to={`${nodePath}/docker-containers`}>View all {containers.length} containers</Link></p>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|