feat: add backend image filter with debounced search

Backend:
- Add filter query parameter to /api/v1/registry/image/list
- Filter repositories by name using SQL LIKE query

Frontend:
- Add filter input field above image list table
- Implement 300ms debounce to reduce backend requests
- Use AbortController to cancel pending requests
- Show appropriate message when no images match filter

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
loveuer
2025-11-12 10:57:25 +08:00
parent 01cfb2ede1
commit dfb6fb7624
2 changed files with 46 additions and 10 deletions

View File

@@ -72,6 +72,7 @@ function getDisplayImageName(fullImageName: string): string {
export default function RegistryImageList() {
const [images, setImages] = useState<RegistryImage[]>([])
const [filterText, setFilterText] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [settingsOpen, setSettingsOpen] = useState(false)
@@ -102,25 +103,41 @@ export default function RegistryImageList() {
useEffect(() => {
let abort = false
async function fetchImages() {
const controller = new AbortController()
const fetchImages = async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/v1/registry/image/list')
const url = new URL('/api/v1/registry/image/list', window.location.origin)
if (filterText.trim()) {
url.searchParams.set('filter', filterText.trim())
}
const res = await fetch(url.toString(), { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const result = await res.json()
// Backend returns: {status, msg, data: {images: [...]}}
const list: RegistryImage[] = result.data?.images || []
if (!abort) setImages(list)
if (!abort) {
setImages(list)
}
} catch (e: any) {
if (!abort) setError(e.message)
if (e.name !== 'AbortError' && !abort) {
setError(e.message)
}
} finally {
if (!abort) setLoading(false)
}
}
fetchImages()
return () => { abort = true }
}, [])
const timeoutId = setTimeout(fetchImages, 300)
return () => {
abort = true
controller.abort()
clearTimeout(timeoutId)
}
}, [filterText])
const handleDownload = async (imageName: string) => {
setDownloadingImage(imageName)
@@ -351,6 +368,16 @@ export default function RegistryImageList() {
</Button>
</Stack>
</Box>
<Box sx={{ mb: 2 }}>
<TextField
placeholder="筛选镜像名称..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
size="small"
fullWidth
variant="outlined"
/>
</Box>
{loading && <CircularProgress />}
{error && <Alert severity="error">: {error}</Alert>}
{!loading && !error && (
@@ -388,7 +415,9 @@ export default function RegistryImageList() {
))}
{images.length === 0 && (
<TableRow>
<TableCell colSpan={5} align="center"></TableCell>
<TableCell colSpan={5} align="center">
{filterText ? '没有匹配的镜像' : '暂无镜像'}
</TableCell>
</TableRow>
)}
</TableBody>