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:
@@ -1,14 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Application } from '@/types';
|
||||
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
|
||||
import { Database, Plus, AlertTriangle, Clock } from 'lucide-react';
|
||||
import { useConfirm } from '@/components/confirm-modal';
|
||||
import { DeleteButtonLabel } from '@/components/delete-button-label';
|
||||
import { DeletingCardOverlay, deletingResourceMessage } from '@/components/deleting-overlay';
|
||||
import { filterManagedServices } from '@/lib/product-type';
|
||||
import { useApplicationDelete } from '@/lib/use-application-delete';
|
||||
|
||||
const lifecycleColors: Record<string, string> = {
|
||||
active: 'text-green-600 bg-green-50',
|
||||
@@ -50,7 +52,6 @@ function formatExpiry(expiresAt?: string): { text: string; urgent: boolean } {
|
||||
}
|
||||
|
||||
export default function ServicesPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const { data: servicesRaw = [], isLoading } = useQuery<Application[]>({
|
||||
@@ -59,18 +60,10 @@ export default function ServicesPage() {
|
||||
});
|
||||
const services = filterManagedServices(servicesRaw);
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/applications/${id}`),
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications', 'managed'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resource-credits'] });
|
||||
if (res.data?.resourceCredit) {
|
||||
toast.success('Service deleted. Prepaid resources are on your dashboard.');
|
||||
} else {
|
||||
toast.success('Service deleted');
|
||||
}
|
||||
},
|
||||
onError: () => toast.error('Failed to delete service'),
|
||||
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
|
||||
invalidateKeys: [['applications', 'managed']],
|
||||
successMessage: 'Service deleted',
|
||||
successWithCreditMessage: 'Service deleted. Prepaid resources are on your dashboard.',
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
@@ -126,13 +119,17 @@ export default function ServicesPage() {
|
||||
const lifecycle = svc.lifecycleStatus || 'active';
|
||||
const expiry = formatExpiry(svc.planExpiresAt);
|
||||
const latestStatus = svc.deployments?.[0]?.status || 'pending';
|
||||
const cardDeleting = isDeleting(svc.id);
|
||||
return (
|
||||
<div
|
||||
key={svc.id}
|
||||
className={`card-hover flex flex-col sm:flex-row sm:items-center gap-4 ${
|
||||
className={`relative card-hover flex flex-col sm:flex-row sm:items-center gap-4 ${
|
||||
lifecycle === 'suspended' ? 'border-l-4 border-l-amber-400' : ''
|
||||
} ${lifecycle === 'pending_deletion' ? 'border-l-4 border-l-red-400' : ''}`}
|
||||
} ${lifecycle === 'pending_deletion' ? 'border-l-4 border-l-red-400' : ''} ${cardDeleting ? 'bg-gray-50' : ''}`}
|
||||
>
|
||||
{cardDeleting && (
|
||||
<DeletingCardOverlay message={deletingResourceMessage('service', svc.name)} />
|
||||
)}
|
||||
<Link href={`/dashboard/services/${svc.id}`} className="flex items-center gap-3 flex-1 min-w-0 group">
|
||||
<div className="w-10 h-10 rounded-lg bg-indigo-50 flex items-center justify-center shrink-0">
|
||||
<Database className="w-5 h-5 text-indigo-600" />
|
||||
@@ -161,7 +158,8 @@ export default function ServicesPage() {
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-ghost text-sm text-red-600"
|
||||
className="btn-ghost text-sm text-red-600 disabled:opacity-50 disabled:pointer-events-none"
|
||||
disabled={isAnyDeleting}
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Delete service?',
|
||||
@@ -169,10 +167,10 @@ export default function ServicesPage() {
|
||||
confirmText: 'Delete',
|
||||
variant: 'danger',
|
||||
});
|
||||
if (ok) deleteMutation.mutate(svc.id);
|
||||
if (ok) deleteApplication(svc.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
<DeleteButtonLabel loading={isDeleting(svc.id)} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user