2026-07-29 13:47:45 +00:00
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
|
|
import { Sidebar } from '@/components/layout/Sidebar'
|
|
|
|
|
|
import { Dashboard } from '@/components/layout/Dashboard'
|
|
|
|
|
|
import { ConnectionDialog } from '@/components/connections/ConnectionDialog'
|
|
|
|
|
|
import { VMList } from '@/components/vms/VMList'
|
|
|
|
|
|
import { VMDetail } from '@/components/vms/VMDetail'
|
|
|
|
|
|
import { TaskList } from '@/components/tasks/TaskList'
|
|
|
|
|
|
import { BackupList } from '@/components/backups/BackupList'
|
|
|
|
|
|
import { CommandPalette } from '@/components/command/CommandPalette'
|
|
|
|
|
|
import { StorageOverview } from '@/components/storage/StorageOverview'
|
|
|
|
|
|
import { StorageDetail } from '@/components/storage/StorageDetail'
|
|
|
|
|
|
import { SettingsPage } from '@/components/settings/SettingsPage'
|
|
|
|
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
|
|
|
|
|
import { ToastProvider } from '@/components/ui/toast'
|
|
|
|
|
|
import { useConnectionStore } from '@/stores/connectionStore'
|
|
|
|
|
|
import { useUIStore } from '@/stores/uiStore'
|
|
|
|
|
|
import { useWebSocket } from '@/hooks/useWebSocket'
|
|
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
|
|
import type { ProxmoxVM } from '@/types/proxmox'
|
|
|
|
|
|
|
|
|
|
|
|
const queryClient = new QueryClient({
|
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
|
queries: {
|
|
|
|
|
|
retry: 1,
|
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
type View =
|
|
|
|
|
|
| { type: 'dashboard' }
|
|
|
|
|
|
| { type: 'vms' }
|
|
|
|
|
|
| { type: 'vm-detail'; vm: ProxmoxVM }
|
|
|
|
|
|
| { type: 'tasks' }
|
|
|
|
|
|
| { type: 'backups' }
|
|
|
|
|
|
| { type: 'storage' }
|
2026-07-29 19:35:37 +00:00
|
|
|
|
| { type: 'storage-detail'; storage: string; node: string }
|
2026-07-29 13:47:45 +00:00
|
|
|
|
| { type: 'settings' }
|
|
|
|
|
|
|
|
|
|
|
|
function AppContent() {
|
|
|
|
|
|
const activeConnectionId = useConnectionStore((s) => s.activeConnectionId)
|
|
|
|
|
|
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false)
|
|
|
|
|
|
const [view, setView] = useState<View>({ type: 'dashboard' })
|
|
|
|
|
|
const commandPaletteOpen = useUIStore((s) => s.commandPaletteOpen)
|
|
|
|
|
|
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen)
|
|
|
|
|
|
|
2026-07-29 18:27:27 +00:00
|
|
|
|
// Auto-open login dialog when no active connection
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!activeConnectionId && !connectionDialogOpen) {
|
|
|
|
|
|
setConnectionDialogOpen(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [activeConnectionId, connectionDialogOpen])
|
|
|
|
|
|
|
2026-07-29 13:47:45 +00:00
|
|
|
|
// WebSocket integration – connects when a connection is active
|
|
|
|
|
|
useWebSocket(activeConnectionId)
|
|
|
|
|
|
|
|
|
|
|
|
// Global keyboard shortcut: Cmd/Ctrl+K to open command palette
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
setCommandPaletteOpen(!commandPaletteOpen)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
window.addEventListener('keydown', handleGlobalKeyDown)
|
|
|
|
|
|
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
|
|
|
|
|
|
}, [commandPaletteOpen, setCommandPaletteOpen])
|
|
|
|
|
|
|
|
|
|
|
|
const handleNavigate = (newView: View) => {
|
|
|
|
|
|
setView(newView)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderMainContent = () => {
|
|
|
|
|
|
if (view.type === 'settings') {
|
|
|
|
|
|
return <SettingsPage />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!activeConnectionId) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
|
<div className="text-center space-y-4">
|
2026-07-29 18:27:27 +00:00
|
|
|
|
<h2 className="text-2xl font-semibold">ProxmoxDesktop</h2>
|
2026-07-29 13:47:45 +00:00
|
|
|
|
<p className="text-muted-foreground">
|
2026-07-29 18:27:27 +00:00
|
|
|
|
Connect to a Proxmox server to get started
|
2026-07-29 13:47:45 +00:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (view.type) {
|
|
|
|
|
|
case 'dashboard':
|
2026-07-29 19:35:37 +00:00
|
|
|
|
return <Dashboard connectionId={activeConnectionId} onNavigate={(viewName) => {
|
|
|
|
|
|
switch (viewName) {
|
|
|
|
|
|
case 'dashboard': handleNavigate({ type: 'dashboard' }); break
|
|
|
|
|
|
case 'vms': handleNavigate({ type: 'vms' }); break
|
|
|
|
|
|
case 'tasks': handleNavigate({ type: 'tasks' }); break
|
|
|
|
|
|
case 'backups': handleNavigate({ type: 'backups' }); break
|
|
|
|
|
|
case 'storage': handleNavigate({ type: 'storage' }); break
|
|
|
|
|
|
default: break
|
|
|
|
|
|
}
|
|
|
|
|
|
}} />
|
2026-07-29 13:47:45 +00:00
|
|
|
|
case 'vms':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<VMList
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onVMClick={(vm) => handleNavigate({ type: 'vm-detail', vm })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'vm-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<VMDetail
|
|
|
|
|
|
vm={view.vm}
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
onBack={() => handleNavigate({ type: 'vms' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'tasks':
|
|
|
|
|
|
return <TaskList connectionId={activeConnectionId} />
|
|
|
|
|
|
case 'backups':
|
|
|
|
|
|
return <BackupList connectionId={activeConnectionId} />
|
|
|
|
|
|
case 'storage':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<StorageOverview
|
|
|
|
|
|
connectionId={activeConnectionId}
|
2026-07-29 19:35:37 +00:00
|
|
|
|
onStorageClick={(storage, node) =>
|
|
|
|
|
|
handleNavigate({ type: 'storage-detail', storage, node })
|
2026-07-29 13:47:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'storage-detail':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<StorageDetail
|
|
|
|
|
|
connectionId={activeConnectionId}
|
|
|
|
|
|
storage={view.storage}
|
2026-07-29 19:35:37 +00:00
|
|
|
|
node={view.node}
|
2026-07-29 13:47:45 +00:00
|
|
|
|
onBack={() => handleNavigate({ type: 'storage' })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
2026-07-29 19:35:37 +00:00
|
|
|
|
default:
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
|
<p className="text-muted-foreground">Unknown view: {(view as { type: string }).type}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
2026-07-29 13:47:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-screen bg-background">
|
|
|
|
|
|
<Sidebar
|
|
|
|
|
|
onAddConnection={() => setConnectionDialogOpen(true)}
|
|
|
|
|
|
activeView={view.type}
|
2026-07-29 19:39:17 +00:00
|
|
|
|
onNavigate={(v) => handleNavigate(v as View)}
|
2026-07-29 13:47:45 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<main className="flex-1 overflow-hidden">
|
|
|
|
|
|
{renderMainContent()}
|
|
|
|
|
|
</main>
|
|
|
|
|
|
<ConnectionDialog
|
|
|
|
|
|
open={connectionDialogOpen}
|
|
|
|
|
|
onOpenChange={setConnectionDialogOpen}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<CommandPalette
|
|
|
|
|
|
open={commandPaletteOpen}
|
|
|
|
|
|
onOpenChange={setCommandPaletteOpen}
|
|
|
|
|
|
onNavigate={handleNavigate}
|
|
|
|
|
|
onAddConnection={() => setConnectionDialogOpen(true)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<QueryClientProvider client={queryClient}>
|
|
|
|
|
|
<ToastProvider>
|
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
|
<AppContent />
|
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
|
</ToastProvider>
|
|
|
|
|
|
</QueryClientProvider>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default App
|