71 lines
3.9 KiB
TypeScript
71 lines
3.9 KiB
TypeScript
import {RefreshCw, Trash2} from 'lucide-react'
|
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|
import {deleteFlatItem, listFlatItems} from '@/api/cloudProxyClient'
|
|
import type {CloudResource} from '@/types/resource'
|
|
|
|
interface EcrImageBrowserProps {
|
|
resource?: CloudResource
|
|
runtimeReachable: boolean
|
|
}
|
|
|
|
export function EcrImageBrowser({resource, runtimeReachable}: EcrImageBrowserProps) {
|
|
const queryClient = useQueryClient()
|
|
const imagesQuery = useQuery({
|
|
queryKey: ['ecr-images', resource?.id],
|
|
queryFn: ({signal}) => listFlatItems('aws', 'ecr', resource?.id ?? '', undefined, signal),
|
|
enabled: Boolean(resource) && runtimeReachable,
|
|
})
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (imageDigest: string) => deleteFlatItem('aws', 'ecr', resource?.id ?? '', imageDigest),
|
|
onSuccess: () => queryClient.invalidateQueries({queryKey: ['ecr-images', resource?.id]}),
|
|
})
|
|
|
|
if (!resource) {
|
|
return <section className="table-panel"><div className="empty compact"><h3>Select a repository</h3><p>Choose an ECR repository to browse its images.</p></div></section>
|
|
}
|
|
|
|
const images = imagesQuery.data?.items ?? []
|
|
return (
|
|
<section className="table-panel" style={{marginTop: 16}}>
|
|
<div className="input-row resource-table-bar">
|
|
<div>
|
|
<p className="eyebrow">Repository images</p>
|
|
<span className="muted">{images.length} images in {resource.name}</span>
|
|
</div>
|
|
<button className="button" type="button" disabled={!runtimeReachable || imagesQuery.isFetching} onClick={() => imagesQuery.refetch()}>
|
|
<RefreshCw size={14}/>{imagesQuery.isFetching ? 'Loading' : 'Refresh'}
|
|
</button>
|
|
</div>
|
|
{imagesQuery.error instanceof Error && <p className="error-text compact-text">{imagesQuery.error.message}</p>}
|
|
{imagesQuery.isLoading && <div className="muted padded">Loading images</div>}
|
|
{imagesQuery.isSuccess && images.length === 0 && <div className="empty compact"><h3>No images</h3><p>Push an image to this repository to see it here.</p></div>}
|
|
{images.length > 0 && <div className="cosmos-items-table">
|
|
<table className="table ecr-images-table">
|
|
<thead><tr><th>Tags</th><th>Digest</th><th>Pushed</th><th>Size</th><th/></tr></thead>
|
|
<tbody>{images.map((image) => {
|
|
const tags = Array.isArray(image.body.imageTags) ? image.body.imageTags.filter((tag): tag is string => typeof tag === 'string') : []
|
|
const size = typeof image.body.imageSizeInBytes === 'number' ? formatBytes(image.body.imageSizeInBytes) : '-'
|
|
return <tr key={image.id}>
|
|
<td>{tags.length ? tags.join(', ') : <span className="muted">Untagged</span>}</td>
|
|
<td><code>{shortDigest(image.id)}</code></td>
|
|
<td>{image.timestamp ? new Date(image.timestamp).toLocaleString() : '-'}</td>
|
|
<td>{size}</td>
|
|
<td><button className="icon-btn danger" type="button" title="Delete image" aria-label={`Delete ${image.id}`} disabled={!runtimeReachable || deleteMutation.isPending} onClick={() => deleteMutation.mutate(image.id)}><Trash2 size={14}/></button></td>
|
|
</tr>
|
|
})}</tbody>
|
|
</table>
|
|
</div>}
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function shortDigest(digest: string): string {
|
|
return digest.length > 20 ? `${digest.slice(0, 19)}...` : digest
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
|
|
}
|