1.0.18
This commit is contained in:
1 parent
d83ca684d7
commit
8ee4fe4d68
49 files changed
+2126
-261
No files matched your search
@@ -13,6 +13,8 @@
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Trash2, Upload, RefreshCw, Play, Search, Layers, Server, ShieldCheck, CheckSquare, Square, Tag, Check, XCircle, Icon, AlertTriangle, X, Images, Copy, Download, ChevronRight, ChevronDown, Loader2, ArrowUp, ArrowDown, ArrowUpDown, CircleDashed, CircleDot, Circle, Filter } from 'lucide-svelte';
|
||||
import { broom, whale } from '@lucide/lab';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
|
||||
import BatchOperationModal from '$lib/components/BatchOperationModal.svelte';
|
||||
import ImageHistoryModal from './ImageHistoryModal.svelte';
|
||||
@@ -144,17 +146,20 @@
|
||||
let batchOpTitle = $state('');
|
||||
let batchOpOperation = $state('');
|
||||
let batchOpItems = $state<Array<{ id: string; name: string }>>([]);
|
||||
let batchOpTotalSize = $state<number | undefined>(undefined);
|
||||
|
||||
// Copy ID state
|
||||
let copiedId = $state<string | null>(null);
|
||||
let copyIdFailed = $state(false);
|
||||
|
||||
async function copyImageId(imageId: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(imageId);
|
||||
const ok = await copyToClipboard(imageId);
|
||||
if (ok) {
|
||||
copiedId = imageId;
|
||||
pendingTimeouts.push(setTimeout(() => copiedId = null, 2000));
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
} else {
|
||||
copyIdFailed = true;
|
||||
pendingTimeouts.push(setTimeout(() => copyIdFailed = false, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,6 +471,7 @@
|
||||
: img.id.slice(7, 19);
|
||||
return { id: img.id, name: displayName };
|
||||
});
|
||||
batchOpTotalSize = selectedInFilter.reduce((sum, img) => sum + img.size, 0);
|
||||
showBatchOpModal = true;
|
||||
}
|
||||
|
||||
@@ -481,7 +487,15 @@
|
||||
const response = await fetch(appendEnvParam('/api/prune/images', envId), { method: 'POST' });
|
||||
if (response.ok) {
|
||||
pruneStatus = 'success';
|
||||
toast.success('Dangling images pruned');
|
||||
const data = await response.json();
|
||||
const deleted = data.result?.ImagesDeleted;
|
||||
const spaceReclaimed = data.result?.SpaceReclaimed ?? 0;
|
||||
const count = deleted?.length ?? 0;
|
||||
if (count > 0) {
|
||||
toast.success(`Pruned ${count} image${count !== 1 ? 's' : ''}, freed ${formatBytes(spaceReclaimed)}`);
|
||||
} else {
|
||||
toast.success('No dangling images to prune');
|
||||
}
|
||||
await fetchImages();
|
||||
} else {
|
||||
pruneStatus = 'error';
|
||||
@@ -501,7 +515,15 @@
|
||||
const response = await fetch(appendEnvParam('/api/prune/images?dangling=false', envId), { method: 'POST' });
|
||||
if (response.ok) {
|
||||
pruneUnusedStatus = 'success';
|
||||
toast.success('Unused images pruned');
|
||||
const data = await response.json();
|
||||
const deleted = data.result?.ImagesDeleted;
|
||||
const spaceReclaimed = data.result?.SpaceReclaimed ?? 0;
|
||||
const count = deleted?.length ?? 0;
|
||||
if (count > 0) {
|
||||
toast.success(`Pruned ${count} image${count !== 1 ? 's' : ''}, freed ${formatBytes(spaceReclaimed)}`);
|
||||
} else {
|
||||
toast.success('No unused images to prune');
|
||||
}
|
||||
await fetchImages();
|
||||
} else {
|
||||
pruneUnusedStatus = 'error';
|
||||
@@ -516,6 +538,7 @@
|
||||
|
||||
async function removeImage(id: string, tagName: string) {
|
||||
deleteError = null;
|
||||
const imageSize = images.find(img => img.id === id)?.size;
|
||||
try {
|
||||
const response = await fetch(appendEnvParam(`/api/images/${encodeURIComponent(id)}?force=true`, envId), { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
@@ -527,7 +550,8 @@
|
||||
}, 5000));
|
||||
return;
|
||||
}
|
||||
toast.success(`Deleted ${tagName}`);
|
||||
const sizeStr = imageSize ? ` (${formatBytes(imageSize)})` : '';
|
||||
toast.success(`Deleted ${tagName}${sizeStr}`);
|
||||
await fetchImages();
|
||||
} catch (error) {
|
||||
console.error('Failed to remove image:', error);
|
||||
@@ -608,6 +632,14 @@
|
||||
return `${(mb / 1024).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatImageDate(timestamp: number): string {
|
||||
return formatDate(new Date(timestamp * 1000));
|
||||
}
|
||||
@@ -1186,6 +1218,7 @@
|
||||
items={batchOpItems}
|
||||
envId={envId ?? undefined}
|
||||
options={{ force: true }}
|
||||
totalSize={batchOpTotalSize}
|
||||
onClose={() => showBatchOpModal = false}
|
||||
onComplete={handleBatchComplete}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user