Improve delete UX and prepaid credit time display.
Show minutes and local expiry for resource credits; add shared delete hook with row/card loading overlays, detail-page deleting modal, and disabled controls to prevent double-delete. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -109,8 +109,15 @@ export function DatabaseSnapshotsPanel({
|
||||
},
|
||||
});
|
||||
|
||||
const [deletingSnapshotId, setDeletingSnapshotId] = useState<string | null>(null);
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/snapshots/${id}`),
|
||||
onMutate: (id) => {
|
||||
setDeletingSnapshotId(id);
|
||||
},
|
||||
onSettled: () => {
|
||||
setDeletingSnapshotId(null);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['snapshots', serviceId] });
|
||||
toast.success('Backup deleted');
|
||||
@@ -281,11 +288,15 @@ export function DatabaseSnapshotsPanel({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(snap)}
|
||||
disabled={deleteMutation.isPending}
|
||||
disabled={deletingSnapshotId !== null}
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg disabled:opacity-50"
|
||||
title="Delete backup"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{deletingSnapshotId === snap.id ? (
|
||||
<Clock className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -293,11 +304,15 @@ export function DatabaseSnapshotsPanel({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(snap)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg shrink-0"
|
||||
disabled={deletingSnapshotId !== null}
|
||||
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg shrink-0 disabled:opacity-50"
|
||||
title="Remove failed backup"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{deletingSnapshotId === snap.id ? (
|
||||
<Clock className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Clock } from 'lucide-react';
|
||||
|
||||
export function DeleteButtonLabel({ loading, label = 'Delete' }: { loading?: boolean; label?: string }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<Clock className="w-3 h-3 inline animate-spin" /> Deleting…
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <>{label}</>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { Clock } from 'lucide-react';
|
||||
|
||||
export function DeletingModal({
|
||||
open,
|
||||
resourceName,
|
||||
resourceKind = 'application',
|
||||
}: {
|
||||
open: boolean;
|
||||
resourceName: string;
|
||||
resourceKind?: 'application' | 'service';
|
||||
}) {
|
||||
if (!open) return null;
|
||||
|
||||
const detail =
|
||||
resourceKind === 'service'
|
||||
? 'Removing this service and its data from the cluster. This may take a minute.'
|
||||
: 'Removing this application, deployments, and data from the cluster. This may take a minute.';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/45 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="deleting-modal-title"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className="bg-white rounded-2xl shadow-xl max-w-md w-full p-8 text-center space-y-3">
|
||||
<div className="mx-auto w-14 h-14 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<Clock className="w-7 h-7 text-red-600 animate-spin" />
|
||||
</div>
|
||||
<h2 id="deleting-modal-title" className="text-lg font-semibold text-gray-900">
|
||||
Deleting…
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
<span className="font-semibold text-gray-900">{resourceName}</span> is being permanently removed.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{detail}</p>
|
||||
<p className="text-xs text-amber-700 font-medium pt-1">Please wait — do not close this page.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { Clock } from 'lucide-react';
|
||||
|
||||
export function deletingResourceMessage(
|
||||
kind: 'application' | 'service',
|
||||
name?: string,
|
||||
): string {
|
||||
if (kind === 'service') {
|
||||
return name ? `Deleting “${name}”…` : 'Deleting service…';
|
||||
}
|
||||
return name ? `Deleting “${name}”…` : 'Deleting application…';
|
||||
}
|
||||
|
||||
/** Single table cell spanning the full row — content centered in the row. */
|
||||
export function DeletingTableRowCell({
|
||||
colSpan,
|
||||
message = 'Deleting…',
|
||||
}: {
|
||||
colSpan: number;
|
||||
message?: string;
|
||||
}) {
|
||||
return (
|
||||
<td colSpan={colSpan} className="px-6 py-4 bg-white/95" aria-live="polite" aria-busy="true">
|
||||
<div className="flex min-h-[52px] w-full items-center justify-center gap-2 text-sm font-semibold text-gray-800">
|
||||
<Clock className="w-4 h-4 shrink-0 animate-spin text-red-600" />
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
/** Full-card overlay while delete is in progress. Parent must be `relative`. */
|
||||
export function DeletingCardOverlay({ message = 'Deleting…' }: { message?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/92 backdrop-blur-[2px] text-sm font-semibold text-gray-800"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
<Clock className="w-4 h-4 shrink-0 animate-spin text-red-600" />
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user