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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user