feat: add storage cleanup tab and implement blob purging functionality in StorageDashboard
This commit is contained in:
parent
e2bc2254a6
commit
5455e9b94c
@ -12,6 +12,8 @@ import {
|
||||
CheckCircle2,
|
||||
Save,
|
||||
Loader2,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -135,13 +137,44 @@ const QuotaEditModal = ({
|
||||
const StorageDashboard = (): ReactElement => {
|
||||
const { primaryColor } = useAppTheme();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"stats" | "quota">("stats");
|
||||
const [activeTab, setActiveTab] = useState<"stats" | "quota" | "cleanup">("stats");
|
||||
const [stats, setStats] = useState<StorageStats | null>(null);
|
||||
const [quota, setQuota] = useState<StorageQuota | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
|
||||
// Storage Cleanup (purgeSoftDeletedBlobs) states
|
||||
const [olderThanHours, setOlderThanHours] = useState(24);
|
||||
const [isPurging, setIsPurging] = useState(false);
|
||||
const [purgeResult, setPurgeResult] = useState<number | null>(null);
|
||||
const [purgeError, setPurgeError] = useState<string | null>(null);
|
||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
||||
|
||||
const handlePurge = async () => {
|
||||
setIsConfirmModalOpen(false);
|
||||
setIsPurging(true);
|
||||
setPurgeResult(null);
|
||||
setPurgeError(null);
|
||||
try {
|
||||
const res = await fileAttachmentService.purgeSoftDeletedBlobs(olderThanHours);
|
||||
if (res.success) {
|
||||
setPurgeResult(res.data.purged);
|
||||
// Refresh usage stats and quota following successful purge
|
||||
await loadData();
|
||||
} else {
|
||||
setPurgeError("Failed to execute purge on the storage provider.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setPurgeError(
|
||||
err?.response?.data?.error?.message || "Failed to purge unreferenced files."
|
||||
);
|
||||
console.error("Purge error:", err);
|
||||
} finally {
|
||||
setIsPurging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@ -243,6 +276,22 @@ const StorageDashboard = (): ReactElement => {
|
||||
>
|
||||
Quota Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("cleanup")}
|
||||
className={cn(
|
||||
"px-6 py-3 text-sm font-bold transition-all border-b-2",
|
||||
activeTab === "cleanup"
|
||||
? "text-[#0e1b2a]"
|
||||
: "border-transparent text-[#9aa6b2] hover:text-[#475569]",
|
||||
)}
|
||||
style={
|
||||
activeTab === "cleanup"
|
||||
? { borderBottomColor: primaryColor, color: primaryColor }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
Storage Cleanup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === "stats" && (
|
||||
@ -462,8 +511,130 @@ const StorageDashboard = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "cleanup" && (
|
||||
<div className="space-y-6 animate-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-2xl overflow-hidden shadow-sm">
|
||||
<div className="px-6 py-5 border-b border-[rgba(0,0,0,0.08)] flex items-center gap-2">
|
||||
<Trash2
|
||||
className="w-5 h-5 text-red-500"
|
||||
/>
|
||||
<h3 className="text-base font-black text-[#0e1b2a]">
|
||||
Storage Maintenance & Cleanup
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-sm text-[#475569] leading-relaxed">
|
||||
When files are deleted, the system decrements their reference counts. Files marked as{" "}
|
||||
<strong>soft-deleted with 0 active references</strong> are preserved temporarily on the storage provider
|
||||
to allow background services (like AI embeddings, audits, or indexes) to finish processing them.
|
||||
</p>
|
||||
<p className="text-sm text-[#475569] mt-3 leading-relaxed">
|
||||
Use this administrative tool to permanently purge these unreferenced physical files from the storage provider and reclaim space.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-red-100 rounded-xl p-5 bg-red-50/30 max-w-2xl flex gap-4">
|
||||
<AlertTriangle className="w-5 h-5 text-red-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-red-800">Warning: Permanent Action</h4>
|
||||
<p className="text-xs text-red-700 mt-1 leading-relaxed">
|
||||
Purging files is irreversible. Once deleted from the physical storage provider (local disk, Azure, or AWS),
|
||||
the file binaries cannot be recovered.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md pt-2 space-y-4">
|
||||
<FormField
|
||||
label="Purge Files Older Than (Hours)"
|
||||
type="number"
|
||||
value={olderThanHours}
|
||||
onChange={(e) => setOlderThanHours(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
placeholder="e.g. 24 (0 = purge all eligible immediately)"
|
||||
/>
|
||||
<span className="text-[11px] text-[#9aa6b2] block -mt-2">
|
||||
Set to 0 to instantly purge all unreferenced soft-deleted blobs, or specify hours (e.g., 24) to keep files deleted within that timeframe.
|
||||
</span>
|
||||
|
||||
{purgeResult !== null && (
|
||||
<div className="p-4 rounded-xl bg-green-50 border border-green-200 text-green-800 text-sm flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4.5 h-4.5 text-green-600 shrink-0" />
|
||||
<span>
|
||||
Successfully purged <strong>{purgeResult}</strong> orphaned file binary/binaries, freeing up storage space!
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{purgeError && (
|
||||
<div className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-800 text-sm">
|
||||
{purgeError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<PrimaryButton
|
||||
onClick={() => setIsConfirmModalOpen(true)}
|
||||
disabled={isPurging}
|
||||
className="bg-red-600 hover:bg-red-700 text-white font-bold flex items-center gap-2 border-red-600 shadow-sm"
|
||||
>
|
||||
{isPurging ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
Purge Unreferenced Files
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isConfirmModalOpen && (
|
||||
<Modal
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={() => setIsConfirmModalOpen(false)}
|
||||
title="Confirm Storage Purge"
|
||||
maxWidth="sm"
|
||||
footer={
|
||||
<div className="flex gap-2 justify-end w-full">
|
||||
<button
|
||||
onClick={() => setIsConfirmModalOpen(false)}
|
||||
disabled={isPurging}
|
||||
className="px-4 py-2 border border-gray-300 rounded-xl text-sm font-medium hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePurge}
|
||||
disabled={isPurging}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold flex items-center gap-2 disabled:opacity-50 transition-colors shadow-sm"
|
||||
>
|
||||
{isPurging && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
Yes, Purge Permanently
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-base font-bold text-gray-900">Are you absolutely sure?</h3>
|
||||
<p className="text-sm text-gray-500 leading-relaxed">
|
||||
This will permanently delete all unreferenced physical file binaries older than{" "}
|
||||
<strong>{olderThanHours} hours</strong> from your storage provider. This action is irreversible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{isEditModalOpen && (
|
||||
<QuotaEditModal
|
||||
isOpen={isEditModalOpen}
|
||||
|
||||
@ -138,8 +138,6 @@ export interface UploadFilesParams {
|
||||
files: File[];
|
||||
entity_type: string; // required
|
||||
entity_id: string; // required — must be valid UUID
|
||||
/** UUID from the suppliers table. When provided, file deduplication is scoped to this supplier. */
|
||||
supplier_id?: string | null;
|
||||
category?: string; // optional string label
|
||||
category_id?: string; // optional
|
||||
description?: string; // max 500 chars
|
||||
@ -230,7 +228,6 @@ export const fileAttachmentService = {
|
||||
formData.append('file', params.files[0]);
|
||||
formData.append('entity_type', params.entity_type);
|
||||
formData.append('entity_id', params.entity_id);
|
||||
if (params.supplier_id) formData.append('supplier_id', params.supplier_id);
|
||||
if (params.category) formData.append('category', params.category);
|
||||
if (params.category_id) formData.append('category_id', params.category_id);
|
||||
if (params.description) formData.append('description', params.description);
|
||||
@ -250,7 +247,6 @@ export const fileAttachmentService = {
|
||||
params.files.forEach((file) => formData.append('files', file));
|
||||
formData.append('entity_type', params.entity_type);
|
||||
formData.append('entity_id', params.entity_id);
|
||||
if (params.supplier_id) formData.append('supplier_id', params.supplier_id);
|
||||
if (params.category) formData.append('category', params.category);
|
||||
if (params.category_id) formData.append('category_id', params.category_id);
|
||||
if (params.description) formData.append('description', params.description);
|
||||
@ -409,7 +405,7 @@ export const fileAttachmentService = {
|
||||
* @param olderThanHours - 0 means purge all eligible blobs regardless of age.
|
||||
*/
|
||||
purgeSoftDeletedBlobs: async (olderThanHours = 0): Promise<{ success: boolean; data: { purged: number } }> => {
|
||||
const response = await apiClient.post('/files/blobs/purge-soft-deleted', null, {
|
||||
const response = await apiClient.post('/files/blobs/purge-soft-deleted', {}, {
|
||||
params: olderThanHours > 0 ? { older_than_hours: olderThanHours } : {},
|
||||
});
|
||||
return response.data;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user