Files
Dashboard/src/components/widgets/DockerUpdatesWidget.tsx
T
Syco 221fa810c9
ci/woodpecker/push/woodpecker Pipeline was successful
Fix Docker widget scrollbar spacing
2026-05-16 17:20:34 +02:00

82 lines
3.1 KiB
TypeScript

import { useEffect, useState } from 'react'
interface ContainerInfo {
name: string
image: string
tag: string
upToDate: boolean | null
registry: string
endpoint: string
}
export function DockerUpdatesWidget() {
const [containers, setContainers] = useState<ContainerInfo[]>([])
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/updates/docker')
.then(r => r.json())
.then(d => {
if (d.error) setError(d.error)
else setContainers(d.containers)
})
.catch(() => setError('Failed to connect'))
.finally(() => setLoading(false))
}, [])
const outdated = containers.filter(c => c.upToDate === false).length
const unknown = containers.filter(c => c.upToDate === null && c.registry !== 'docker.io' && c.registry !== 'ghcr.io').length
return (
<div className="card">
<div className="widget-header">
<div className="widget-title"><span className="dot" />Docker</div>
{!loading && !error && (
<span className="widget-badge" style={outdated > 0 ? { background: 'rgba(248,113,113,0.15)', color: 'var(--red)' } : {}}>
{outdated > 0 ? `${outdated} outdated` : 'all current'}
</span>
)}
</div>
{loading && <div className="widget-loading">Checking images</div>}
{error && <div className="widget-error"> {error}</div>}
{!loading && !error && (
<div className="progress-group" style={{ maxHeight: '260px', overflowY: 'auto', scrollbarWidth: 'thin', paddingRight: '4px' }}>
{[...containers]
.sort((a, b) => {
const rank = (c: ContainerInfo) =>
c.upToDate === false ? 0 : c.upToDate === null ? 1 : 2
return rank(a) - rank(b)
})
.map(c => (
<div key={c.name} className="list-item">
<div className="list-item-left" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
<span>{c.name}</span>
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
{c.image}:{c.tag.startsWith('sha256:') ? c.tag.slice(0, 15) + '…' : c.tag} · {c.endpoint}
</span>
</div>
{c.upToDate === null && c.registry !== 'docker.io' && c.registry !== 'ghcr.io' ? (
<span className="pill pill-blue">ext</span>
) : c.upToDate === true ? (
<span className="pill pill-green"></span>
) : c.upToDate === false ? (
<span className="pill pill-red"> update</span>
) : (
<span className="pill" style={{ background: 'var(--surface2)', color: 'var(--muted)' }}>?</span>
)}
</div>
))}
{unknown > 0 && (
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)', marginTop: 6 }}>
{unknown} non-Docker Hub image{unknown > 1 ? 's' : ''} not checked
</div>
)}
</div>
)}
</div>
)
}