'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import api from '@/lib/api'; import { toast } from 'react-toastify'; import type { Application, AppLifecycleStatus, ApplicationMigrationEvent, ApplicationMigrationJob, BillingCycle, Cluster } from '@/types'; import { Search, X, Package, Hexagon, User, Database, Box, AlertTriangle, Clock, ArrowRightLeft, RefreshCw } from 'lucide-react'; import { useConfirm } from '@/components/confirm-modal'; import { DeleteButtonLabel } from '@/components/delete-button-label'; import { DeletingCardOverlay, DeletingTableRowOverlay, deletingRowContentClass, } from '@/components/deleting-overlay'; import { TruncatedText } from '@/components/truncated-text'; import { useDebounce } from '@/hooks/useDebounce'; import { useApplicationDelete } from '@/lib/use-application-delete'; const statusColors: Record = { running: 'badge-green', pending: 'badge-yellow', building: 'badge-blue', deploying: 'badge-blue', failed: 'badge-red', build_failed: 'badge-red', cancelled: 'badge-gray', stopped: 'badge-gray', }; const lifecycleColors: Record = { active: 'text-green-600 bg-green-50', suspended: 'text-amber-700 bg-amber-50', pending_deletion: 'text-red-700 bg-red-50', deleted: 'text-gray-500 bg-gray-100', }; const lifecycleLabels: Record = { active: 'Active', suspended: 'Suspended — Unpaid', pending_deletion: 'Pending Deletion', deleted: 'Deleted', }; const cycleLabels: Record = { hourly: 'Hourly', monthly: 'Monthly', yearly: 'Yearly', }; function formatExpiry(expiresAt?: string): { text: string; urgent: boolean } { if (!expiresAt) return { text: '—', urgent: false }; const now = new Date(); const exp = new Date(expiresAt); const diff = exp.getTime() - now.getTime(); if (diff <= 0) return { text: 'Expired', urgent: true }; const hours = Math.floor(diff / 3600000); const days = Math.floor(hours / 24); if (days > 0) return { text: `${days}d ${hours % 24}h`, urgent: days < 3 }; if (hours > 0) return { text: `${hours}h`, urgent: hours < 6 }; const mins = Math.floor(diff / 60000); return { text: `${mins}m`, urgent: true }; } function formatDeletionDate(date?: string): string { if (!date) return ''; return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function migrationStatusBadgeClass(status: ApplicationMigrationJob['status']): string { switch (status) { case 'completed': return 'badge-green'; case 'failed': case 'rolled_back': return 'badge-red'; case 'running': case 'rolling_back': return 'badge-blue'; default: return 'badge-gray'; } } function formatMigrationStatus(status: ApplicationMigrationJob['status']): string { return status.replace(/_/g, ' '); } export default function AdminAppsPage() { const queryClient = useQueryClient(); const confirm = useConfirm(); const [search, setSearch] = useState(''); const debouncedSearch = useDebounce(search, 400); const [migrateApp, setMigrateApp] = useState(null); const [targetClusterId, setTargetClusterId] = useState(''); const { data: apps = [], isLoading } = useQuery({ queryKey: ['admin-applications', debouncedSearch], queryFn: () => api .get('/applications/all', { params: debouncedSearch ? { search: debouncedSearch } : {} }) .then((r) => r.data), }); const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({ invalidateKeys: [['admin-applications']], successMessage: 'Application deleted', }); const { data: clusters = [] } = useQuery({ queryKey: ['admin-clusters'], queryFn: () => api.get('/clusters').then((r) => r.data), }); const { data: migrations = [] } = useQuery({ queryKey: ['application-migrations'], queryFn: () => api.get('/application-migrations').then((r) => r.data), refetchInterval: 5000, }); const selectedMigration = migrateApp ? migrations.find((migration) => migration.applicationId === migrateApp.id) : undefined; const clusterById = new Map(clusters.map((cluster) => [cluster.id, cluster])); const currentCluster = migrateApp?.clusterId ? clusterById.get(migrateApp.clusterId) : undefined; const targetClusters = migrateApp ? clusters.filter((cluster) => cluster.id !== migrateApp.clusterId) : []; const { data: selectedMigrationEvents = [] } = useQuery({ queryKey: ['application-migration-events', selectedMigration?.id], queryFn: () => api.get(`/application-migrations/${selectedMigration!.id}/events`).then((r) => r.data), enabled: !!selectedMigration?.id, refetchInterval: selectedMigration && ['queued', 'running', 'rolling_back'].includes(selectedMigration.status) ? 3000 : false, }); const migrateMutation = useMutation({ mutationFn: ({ appId, clusterId }: { appId: string; clusterId: string }) => api.post(`/application-migrations/applications/${appId}`, { targetClusterId: clusterId, migrateStorage: true, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['application-migrations'] }); toast.success('Migration job queued'); }, onError: (err: any) => toast.error(err?.response?.data?.message || 'Failed to queue migration'), }); const retryMigration = useMutation({ mutationFn: (id: string) => api.post(`/application-migrations/${id}/retry`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['application-migrations'] }); toast.success('Migration retry queued'); }, onError: (err: any) => toast.error(err?.response?.data?.message || 'Failed to retry migration'), }); // Compute status counts from apps const statusCounts = apps.reduce( (acc, app) => { const status = app.deployments?.[0]?.status || 'pending'; if (status === 'running') acc.running++; else if (status === 'stopped') acc.stopped++; else if (status === 'failed' || status === 'build_failed') acc.failed++; else if (status === 'building' || status === 'deploying') acc.deploying++; else acc.pending++; // Lifecycle counts const lc = app.lifecycleStatus || 'active'; if (lc === 'suspended') acc.suspended++; if (lc === 'pending_deletion') acc.pendingDeletion++; return acc; }, { running: 0, stopped: 0, failed: 0, deploying: 0, pending: 0, suspended: 0, pendingDeletion: 0 }, ); if (isLoading) { return (
{[1, 2, 3, 4, 5].map((i) => (
))}
{[1, 2, 3, 4].map((i) => (
))}
); } return (

All Applications

{apps.length} application{apps.length !== 1 ? 's' : ''} {debouncedSearch && ` matching "${debouncedSearch}"`}

{/* Status Summary Cards */}

{statusCounts.running}

Running

{statusCounts.deploying}

Deploying

{statusCounts.pending}

Pending

{statusCounts.stopped}

Stopped

{statusCounts.failed}

Failed

{statusCounts.suspended}

Suspended

{statusCounts.pendingDeletion}

Pending Del.

{/* Search */}
setSearch(e.target.value)} className="input pl-10 w-full" /> {search && ( )}
{apps.length === 0 ? (
{debouncedSearch ? ( <>

No applications found

No results for "{debouncedSearch}". Try a different search.

) : ( <>

No applications yet

No applications have been created by any user.

)}
) : ( <> {/* Desktop Table */}
{apps.map((app) => { const latestStatus = app.deployments?.[0]?.status || 'pending'; const lifecycle = app.lifecycleStatus || 'active'; const expiry = formatExpiry(app.planExpiresAt); const latestMigration = migrations.find((migration) => migration.applicationId === app.id); const assignedCluster = app.clusterId ? clusterById.get(app.clusterId) : undefined; const rowDeleting = isDeleting(app.id); return ( {rowDeleting && ( )} ); })}
Application Owner Status Service Cluster Migration Plan / Expiry Actions
{app.name} {app.runtime}
{app.user ? (
{`${app.user.firstName} ${app.user.lastName}`} {app.user.email} {app.userId}
) : ( {app.userId} )}
{latestStatus} {lifecycle === 'suspended' && } {lifecycleLabels[lifecycle] || lifecycle} {lifecycle === 'pending_deletion' && app.scheduledDeletionAt && ( {`Delete: ${formatDeletionDate(app.scheduledDeletionAt)}`} )} {assignedCluster ? (
{assignedCluster.name} {`${assignedCluster.region || 'N/A'} · ${assignedCluster.status}/${assignedCluster.healthStatus || 'unknown'}`}
) : app.clusterId ? (
Unknown cluster {app.clusterId}
) : ( Not assigned )}
{latestMigration ? ( {formatMigrationStatus(latestMigration.status)} ) : ( )} {app.billingCycle && ( {cycleLabels[app.billingCycle] || app.billingCycle} )} {app.planExpiresAt ? ( {expiry.text} ) : ( No plan )}
View
{/* Mobile / tablet cards */}
{apps.map((app) => { const latestStatus = app.deployments?.[0]?.status || 'pending'; const lifecycle = app.lifecycleStatus || 'active'; const expiry = formatExpiry(app.planExpiresAt); const latestMigration = migrations.find((migration) => migration.applicationId === app.id); const assignedCluster = app.clusterId ? clusterById.get(app.clusterId) : undefined; const cardDeleting = isDeleting(app.id); return (
{cardDeleting && ( )}
{app.name}

{app.runtime}

{latestStatus}
{app.user && (

{app.user.firstName} {app.user.lastName}

{app.user.email}
)}
{lifecycle === 'suspended' && } {lifecycleLabels[lifecycle] || lifecycle} {app.billingCycle && ( {cycleLabels[app.billingCycle] || app.billingCycle} )} {latestMigration && ( {formatMigrationStatus(latestMigration.status)} )}
{lifecycle === 'pending_deletion' && app.scheduledDeletionAt && (

Delete: {formatDeletionDate(app.scheduledDeletionAt)}

)}
Cluster {assignedCluster?.name || (app.clusterId ? 'Unknown' : 'Not assigned')} {assignedCluster && ( {`${assignedCluster.region || 'N/A'} · ${assignedCluster.status}`} )}
Plan {app.planExpiresAt ? ( {expiry.text} ) : ( No plan )}
{app.databaseType} {app.replicas} replica{app.replicas > 1 ? 's' : ''}
View
); })}
)} {migrateApp && (

Migrate Application

Move {migrateApp.name} to a healthy target cluster with logs, retry, and rollback.

Current cluster

{currentCluster?.name || (migrateApp.clusterId ? `Unknown cluster (${migrateApp.clusterId.slice(0, 8)})` : 'Not assigned')}

{currentCluster && (

{currentCluster.region || 'N/A'} · {currentCluster.status}/{currentCluster.healthStatus || 'unknown'}

)}

The current cluster is excluded. Migration is blocked for unhealthy, inactive, or maintenance clusters.

{targetClusters.length === 0 && (

No other cluster is available as a migration target.

)}
{selectedMigration && (

Latest migration

{selectedMigration.currentStep || 'queued'} · attempts {selectedMigration.attempts}/{selectedMigration.maxAttempts}

{selectedMigration.status}
{selectedMigration.errorMessage && (

{selectedMigration.errorMessage}

)}
{selectedMigrationEvents.length === 0 ? (

No events yet.

) : selectedMigrationEvents.map((event) => (
{new Date(event.createdAt).toLocaleTimeString()} {event.step} {event.message}
))}
{['failed', 'rolled_back'].includes(selectedMigration.status) && ( )}
)}
)}
); }