SpaceProbe1 / frontend /src /App.tsx
a9's picture
Upload 27 files
9b2dc95 verified
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>
);
}