import { useState } from 'react'; import { useLocation } from 'react-router'; import { Card } from './ui/card'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { AppWindow, Database, Globe, Shield, BarChart3, MessageSquare, Search, ExternalLink, CheckCircle, AlertCircle, Download, Trash2, BookOpen, Loader2, Archive, RotateCcw, Settings, Eye, } from 'lucide-react'; import { useInstanceContext } from '../hooks/useInstanceContext'; import { useAvailableApps, useDeployedApps, useAppBackups } from '../hooks/useApps'; import { BackupRestoreModal } from './BackupRestoreModal'; import { AppConfigDialog } from './apps/AppConfigDialog'; import { AppDetailModal } from './apps/AppDetailModal'; import type { App } from '../services/api'; interface MergedApp extends App { deploymentStatus?: 'added' | 'deployed'; url?: string; } type TabView = 'available' | 'installed'; export function AppsComponent() { const location = useLocation(); const { currentInstance } = useInstanceContext(); const { data: availableAppsData, isLoading: loadingAvailable, error: availableError } = useAvailableApps(); const { apps: deployedApps, isLoading: loadingDeployed, error: deployedError, addApp, isAdding, deployApp, isDeploying, deleteApp, isDeleting } = useDeployedApps(currentInstance); // Determine active tab from URL path const activeTab: TabView = location.pathname.endsWith('/installed') ? 'installed' : 'available'; const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const [configDialogOpen, setConfigDialogOpen] = useState(false); const [selectedAppForConfig, setSelectedAppForConfig] = useState(null); const [backupModalOpen, setBackupModalOpen] = useState(false); const [restoreModalOpen, setRestoreModalOpen] = useState(false); const [selectedAppForBackup, setSelectedAppForBackup] = useState(null); const [detailModalOpen, setDetailModalOpen] = useState(false); const [selectedAppForDetail, setSelectedAppForDetail] = useState(null); // Fetch backups for the selected app const { backups, isLoading: backupsLoading, backup: createBackup, isBackingUp, restore: restoreBackup, isRestoring, } = useAppBackups(currentInstance, selectedAppForBackup); // Merge available and deployed apps with URL from deployment const applications: MergedApp[] = (availableAppsData?.apps || []).map(app => { const deployedApp = deployedApps.find(d => d.name === app.name); return { ...app, deploymentStatus: deployedApp?.status as 'added' | 'deployed' | undefined, url: deployedApp?.url, }; }); const isLoading = loadingAvailable || loadingDeployed; // Filter for available apps (not added or deployed) const availableApps = applications.filter(app => !app.deploymentStatus); // Filter for installed apps (added or deployed) const installedApps = applications.filter(app => app.deploymentStatus); // Count running apps - apps that are deployed (not just added) const runningApps = installedApps.filter(app => app.deploymentStatus === 'deployed').length; const getStatusIcon = (status?: string) => { switch (status) { case 'running': return ; case 'error': return ; case 'deploying': return ; case 'stopped': return ; case 'added': return ; case 'deployed': return ; case 'available': return ; default: return null; } }; const getStatusBadge = (app: MergedApp) => { // Determine status: runtime status > deployment status > available const status = app.status?.status || app.deploymentStatus || 'available'; const variants: Record = { available: 'secondary', added: 'outline', deploying: 'default', running: 'success', error: 'destructive', stopped: 'warning', deployed: 'outline', }; const labels: Record = { available: 'Available', added: 'Added', deploying: 'Deploying', running: 'Running', error: 'Error', stopped: 'Stopped', deployed: 'Deployed', }; return ( {labels[status] || status} ); }; const getCategoryIcon = (category?: string) => { switch (category) { case 'database': return ; case 'web': return ; case 'security': return ; case 'monitoring': return ; case 'communication': return ; case 'storage': return ; default: return ; } }; // Separate component for app icon with error handling const AppIcon = ({ app }: { app: MergedApp }) => { const [imageError, setImageError] = useState(false); return (
{app.icon && !imageError ? ( {app.name} setImageError(true)} /> ) : ( getCategoryIcon(app.category) )}
); }; const handleAppAction = (app: MergedApp, action: 'configure' | 'deploy' | 'delete' | 'backup' | 'restore' | 'view') => { if (!currentInstance) return; switch (action) { case 'configure': console.log('[AppsComponent] Configuring app:', { name: app.name, hasDefaultConfig: !!app.defaultConfig, defaultConfigKeys: app.defaultConfig ? Object.keys(app.defaultConfig) : [], fullApp: app, }); setSelectedAppForConfig(app); setConfigDialogOpen(true); break; case 'deploy': deployApp(app.name); break; case 'delete': if (confirm(`Are you sure you want to delete ${app.name}?`)) { deleteApp(app.name); } break; case 'backup': setSelectedAppForBackup(app.name); setBackupModalOpen(true); break; case 'restore': setSelectedAppForBackup(app.name); setRestoreModalOpen(true); break; case 'view': setSelectedAppForDetail(app.name); setDetailModalOpen(true); break; } }; const handleConfigSave = (config: Record) => { if (!selectedAppForConfig) return; addApp({ name: selectedAppForConfig.name, config: config, }); setConfigDialogOpen(false); setSelectedAppForConfig(null); }; const handleBackupConfirm = () => { createBackup(); }; const handleRestoreConfirm = (backupId?: string) => { if (backupId) { restoreBackup(backupId); } }; const categories = ['all', 'database', 'web', 'security', 'monitoring', 'communication', 'storage']; const appsToDisplay = activeTab === 'available' ? availableApps : installedApps; const filteredApps = appsToDisplay.filter(app => { const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) || app.description.toLowerCase().includes(searchTerm.toLowerCase()); const matchesCategory = selectedCategory === 'all' || app.category === selectedCategory; return matchesSearch && matchesCategory; }); // Show message if no instance is selected if (!currentInstance) { return (

No Instance Selected

Please select or create an instance to manage apps.

); } // Show error state if (availableError || deployedError) { return (

Error Loading Apps

{(availableError as Error)?.message || (deployedError as Error)?.message || 'An error occurred'}

); } return (
{/* Educational Intro Section */}

What are Apps in your Personal Cloud?

Apps are the useful programs that make your personal cloud valuable - like having a personal Netflix (media server), Google Drive (file storage), or Gmail (email server) running on your own hardware. Instead of relying on big tech companies, you control your data and services.

Your cluster can run databases, web servers, photo galleries, password managers, backup services, and much more. Each app runs in its own secure container, so they don't interfere with each other and can be easily managed.

App Management

Install and manage applications on your Kubernetes cluster

setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border rounded-lg bg-background" />
{categories.map(category => ( ))}
{isLoading ? ( Loading apps... ) : ( `${runningApps} applications running • ${installedApps.length} installed • ${availableApps.length} available` )}
{isLoading ? (

Loading applications...

) : activeTab === 'available' ? ( // Available Apps Grid
{filteredApps.map((app) => (

{app.name}

{app.version && ( {app.version} )}

{app.description}

))}
) : ( // Installed Apps List
{filteredApps.map((app) => (

{app.name}

{app.version && ( {app.version} )} {getStatusIcon(app.status?.status || app.deploymentStatus)}

{app.description}

{/* Show ingress URL if available */} {app.url && ( {app.url} )} {app.status?.status === 'running' && (
{app.status.namespace && (
Namespace: {app.status.namespace}
)} {app.status.replicas && (
Replicas: {app.status.replicas}
)}
)}
{getStatusBadge(app)}
{/* Available: not added yet - shouldn't show here */} {/* Added: in config but not deployed */} {app.deploymentStatus === 'added' && ( <> )} {/* Deployed: running in Kubernetes */} {app.deploymentStatus === 'deployed' && ( <> {app.status?.status === 'running' && ( <> )} )}
))}
)} {!isLoading && filteredApps.length === 0 && (

No applications found

{searchTerm || selectedCategory !== 'all' ? 'Try adjusting your search or category filter' : activeTab === 'available' ? 'All available apps have been installed' : 'No apps are currently installed' }

)} {/* Backup Modal */} { setBackupModalOpen(false); setSelectedAppForBackup(null); }} mode="backup" appName={selectedAppForBackup || ''} onConfirm={handleBackupConfirm} isPending={isBackingUp} /> {/* Restore Modal */} { setRestoreModalOpen(false); setSelectedAppForBackup(null); }} mode="restore" appName={selectedAppForBackup || ''} backups={backups?.backups || []} isLoading={backupsLoading} onConfirm={handleRestoreConfirm} isPending={isRestoring} /> {/* App Configuration Dialog */} {/* App Detail Modal */} {selectedAppForDetail && currentInstance && ( { setDetailModalOpen(false); setSelectedAppForDetail(null); }} /> )}
); }