Spaces:
Sleeping
Sleeping
| import { useState } from 'react'; | |
| import { StatusCard } from './components/StatusCard'; | |
| import { MetricsChart } from './components/MetricsChart'; | |
| import { AddRepoModal } from './components/AddRepoModal'; | |
| import { usePolling } from './hooks/usePolling'; | |
| import type { Repo, RepoStatus, Metrics } from './types'; | |
| const API_BASE = '/api'; | |
| export function App() { | |
| const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null); | |
| const [selectedRange, setSelectedRange] = useState<'hour' | 'day' | 'week' | 'month'>('hour'); | |
| const [autoRefresh, setAutoRefresh] = useState(true); | |
| const [refreshInterval, setRefreshInterval] = useState(5000); | |
| const [showAddModal, setShowAddModal] = useState(false); | |
| const { data: repos, loading: reposLoading, refetch: refetchRepos } = usePolling<Repo[]>( | |
| () => fetch(`${API_BASE}/repos`).then(r => r.json()), | |
| refreshInterval, | |
| autoRefresh | |
| ); | |
| const { data: statusData, loading: statusLoading } = usePolling<RepoStatus | null>( | |
| () => selectedRepoId ? fetch(`${API_BASE}/status?repoId=${selectedRepoId}`).then(r => r.json()) : Promise.resolve(null), | |
| refreshInterval, | |
| autoRefresh && !!selectedRepoId | |
| ); | |
| const { data: metricsData, loading: metricsLoading } = usePolling<Metrics | null>( | |
| () => selectedRepoId ? fetch(`${API_BASE}/metrics?repoId=${selectedRepoId}&range=${selectedRange}`).then(r => r.json()) : Promise.resolve(null), | |
| refreshInterval, | |
| autoRefresh && !!selectedRepoId | |
| ); | |
| const handleAddRepo = async (namespace: string, repo: string) => { | |
| await fetch(`${API_BASE}/repos`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ namespace, repo }), | |
| }); | |
| refetchRepos(); | |
| }; | |
| const handleDeleteRepo = async (repoId: string) => { | |
| await fetch(`${API_BASE}/repos/${repoId}`, { method: 'DELETE' }); | |
| if (selectedRepoId === repoId) setSelectedRepoId(null); | |
| refetchRepos(); | |
| }; | |
| const selectedRepo = repos?.find(r => r.id === selectedRepoId); | |
| return ( | |
| <div className="min-h-screen bg-gray-900 text-white"> | |
| <header className="bg-gray-800 border-b border-gray-700 px-6 py-4"> | |
| <div className="flex items-center justify-between"> | |
| <h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> | |
| SpaceProbe | |
| </h1> | |
| <button | |
| onClick={() => setShowAddModal(true)} | |
| className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition" | |
| > | |
| + Add Repo | |
| </button> | |
| </div> | |
| </header> | |
| <main className="p-6"> | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> | |
| <div className="lg:col-span-1 space-y-4"> | |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> | |
| <h2 className="text-lg font-semibold mb-3">Repositories</h2> | |
| {reposLoading ? ( | |
| <div className="space-y-2"> | |
| {[1, 2, 3].map(i => ( | |
| <div key={i} className="h-12 bg-gray-700 rounded animate-pulse"></div> | |
| ))} | |
| </div> | |
| ) : repos?.length === 0 ? ( | |
| <p className="text-gray-500 text-sm">No repositories added</p> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {repos?.map(repo => ( | |
| <div | |
| key={repo.id} | |
| onClick={() => setSelectedRepoId(repo.id)} | |
| className={`p-3 rounded-lg cursor-pointer transition ${ | |
| selectedRepoId === repo.id | |
| ? 'bg-blue-600/20 border border-blue-500' | |
| : 'bg-gray-700 hover:bg-gray-600' | |
| }`} | |
| > | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm font-medium">{repo.namespace}/{repo.repo}</span> | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDeleteRepo(repo.id); | |
| }} | |
| className="text-gray-500 hover:text-red-400" | |
| > | |
| × | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div className="bg-gray-800 rounded-lg p-4 border border-gray-700"> | |
| <h2 className="text-lg font-semibold mb-3">Settings</h2> | |
| <div className="space-y-3"> | |
| <label className="flex items-center justify-between"> | |
| <span className="text-gray-400">Auto-refresh</span> | |
| <input | |
| type="checkbox" | |
| checked={autoRefresh} | |
| onChange={(e) => setAutoRefresh(e.target.checked)} | |
| className="w-4 h-4 accent-blue-500" | |
| /> | |
| </label> | |
| <div> | |
| <span className="text-gray-400 text-sm">Interval (ms)</span> | |
| <select | |
| value={refreshInterval} | |
| onChange={(e) => setRefreshInterval(Number(e.target.value))} | |
| className="w-full mt-1 bg-gray-700 text-white rounded px-3 py-2" | |
| > | |
| <option value={1000}>1s</option> | |
| <option value={5000}>5s</option> | |
| <option value={10000}>10s</option> | |
| <option value={30000}>30s</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="lg:col-span-3 space-y-6"> | |
| {!selectedRepoId ? ( | |
| <div className="flex items-center justify-center h-64 text-gray-500"> | |
| Select a repository to view metrics | |
| </div> | |
| ) : ( | |
| <> | |
| <StatusCard | |
| status={statusData} | |
| namespace={selectedRepo?.namespace || ''} | |
| repo={selectedRepo?.repo || ''} | |
| loading={statusLoading} | |
| /> | |
| <div className="flex gap-2"> | |
| {(['hour', 'day', 'week', 'month'] as const).map(range => ( | |
| <button | |
| key={range} | |
| onClick={() => setSelectedRange(range)} | |
| className={`px-4 py-2 rounded-lg transition ${ | |
| selectedRange === range | |
| ? 'bg-blue-600 text-white' | |
| : 'bg-gray-700 text-gray-400 hover:bg-gray-600' | |
| }`} | |
| > | |
| {range.charAt(0).toUpperCase() + range.slice(1)} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="grid grid-cols-1 gap-6"> | |
| <MetricsChart data={metricsData} metric="cpu" loading={metricsLoading} /> | |
| <MetricsChart data={metricsData} metric="memory" loading={metricsLoading} /> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| </main> | |
| <AddRepoModal | |
| isOpen={showAddModal} | |
| onClose={() => setShowAddModal(false)} | |
| onSubmit={handleAddRepo} | |
| /> | |
| </div> | |
| ); | |
| } |