import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { listUpstreamSources, createUpstreamSource, updateUpstreamSource, deleteUpstreamSource, testUpstreamSource, } from '../api'; import { UpstreamSource, SourceType, AuthType } from '../types'; import './AdminCachePage.css'; const SOURCE_TYPES: SourceType[] = ['npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic']; const AUTH_TYPES: AuthType[] = ['none', 'basic', 'bearer', 'api_key']; function AdminCachePage() { const { user, loading: authLoading } = useAuth(); const navigate = useNavigate(); // Upstream sources state const [sources, setSources] = useState([]); const [loadingSources, setLoadingSources] = useState(true); const [sourcesError, setSourcesError] = useState(null); // Create/Edit form state const [showForm, setShowForm] = useState(false); const [editingSource, setEditingSource] = useState(null); const [formData, setFormData] = useState({ name: '', source_type: 'generic' as SourceType, url: '', enabled: true, auth_type: 'none' as AuthType, username: '', password: '', priority: 100, }); const [formError, setFormError] = useState(null); const [isSaving, setIsSaving] = useState(false); // Test result state const [testingId, setTestingId] = useState(null); const [testResults, setTestResults] = useState>({}); // Delete confirmation state const [deletingId, setDeletingId] = useState(null); // Success message const [successMessage, setSuccessMessage] = useState(null); // Error modal state const [showErrorModal, setShowErrorModal] = useState(false); const [selectedError, setSelectedError] = useState<{ sourceName: string; error: string } | null>(null); useEffect(() => { if (!authLoading && !user) { navigate('/login', { state: { from: '/admin/cache' } }); } }, [user, authLoading, navigate]); useEffect(() => { if (user && user.is_admin) { loadSources(); } }, [user]); useEffect(() => { if (successMessage) { const timer = setTimeout(() => setSuccessMessage(null), 3000); return () => clearTimeout(timer); } }, [successMessage]); async function loadSources() { setLoadingSources(true); setSourcesError(null); try { const data = await listUpstreamSources(); setSources(data); } catch (err) { setSourcesError(err instanceof Error ? err.message : 'Failed to load sources'); } finally { setLoadingSources(false); } } function openCreateForm() { setEditingSource(null); setFormData({ name: '', source_type: 'generic', url: '', enabled: true, auth_type: 'none', username: '', password: '', priority: 100, }); setFormError(null); setShowForm(true); } function openEditForm(source: UpstreamSource) { setEditingSource(source); setFormData({ name: source.name, source_type: source.source_type, url: source.url, enabled: source.enabled, auth_type: source.auth_type, username: source.username || '', password: '', priority: source.priority, }); setFormError(null); setShowForm(true); } async function handleFormSubmit(e: React.FormEvent) { e.preventDefault(); if (!formData.name.trim()) { setFormError('Name is required'); return; } if (!formData.url.trim()) { setFormError('URL is required'); return; } setIsSaving(true); setFormError(null); try { let savedSourceId: string | null = null; if (editingSource) { // Update existing source await updateUpstreamSource(editingSource.id, { name: formData.name.trim(), source_type: formData.source_type, url: formData.url.trim(), enabled: formData.enabled, auth_type: formData.auth_type, username: formData.username.trim() || undefined, password: formData.password || undefined, priority: formData.priority, }); savedSourceId = editingSource.id; setSuccessMessage('Source updated successfully'); } else { // Create new source const newSource = await createUpstreamSource({ name: formData.name.trim(), source_type: formData.source_type, url: formData.url.trim(), enabled: formData.enabled, auth_type: formData.auth_type, username: formData.username.trim() || undefined, password: formData.password || undefined, priority: formData.priority, }); savedSourceId = newSource.id; setSuccessMessage('Source created successfully'); } setShowForm(false); await loadSources(); // Auto-test the source after save if (savedSourceId) { testSourceById(savedSourceId); } } catch (err) { setFormError(err instanceof Error ? err.message : 'Failed to save source'); } finally { setIsSaving(false); } } async function handleDelete(source: UpstreamSource) { if (!window.confirm(`Delete upstream source "${source.name}"? This cannot be undone.`)) { return; } setDeletingId(source.id); try { await deleteUpstreamSource(source.id); setSuccessMessage(`Source "${source.name}" deleted`); await loadSources(); } catch (err) { setSourcesError(err instanceof Error ? err.message : 'Failed to delete source'); } finally { setDeletingId(null); } } async function handleTest(source: UpstreamSource) { testSourceById(source.id); } async function testSourceById(sourceId: string) { setTestingId(sourceId); setTestResults((prev) => ({ ...prev, [sourceId]: { success: true, message: 'Testing...' } })); try { const result = await testUpstreamSource(sourceId); setTestResults((prev) => ({ ...prev, [sourceId]: { success: result.success, message: result.success ? `OK (${result.elapsed_ms}ms)` : result.error || `HTTP ${result.status_code}`, }, })); } catch (err) { setTestResults((prev) => ({ ...prev, [sourceId]: { success: false, message: err instanceof Error ? err.message : 'Test failed', }, })); } finally { setTestingId(null); } } function showError(sourceName: string, error: string) { setSelectedError({ sourceName, error }); setShowErrorModal(true); } if (authLoading) { return
Loading...
; } if (!user?.is_admin) { return (
Access denied. Admin privileges required.
); } return (

Upstream Sources

{successMessage &&
{successMessage}
} {/* Upstream Sources Section */}
{loadingSources ? (

Loading sources...

) : sourcesError ? (
{sourcesError}
) : sources.length === 0 ? (

No upstream sources configured.

) : ( {sources.map((source) => ( ))}
Name Type URL Priority Status Test Actions
{source.name} {source.source === 'env' && ( ENV )} {source.source_type} {source.url} {source.priority} {source.enabled ? 'Enabled' : 'Disabled'} {testingId === source.id ? ( ) : testResults[source.id] ? ( testResults[source.id].success ? ( ) : ( showError(source.name, testResults[source.id].message)} >● ) ) : null} {source.source !== 'env' && ( )}
)}
{/* Create/Edit Modal */} {showForm && (
setShowForm(false)}>
e.stopPropagation()}>

{editingSource ? 'Edit Upstream Source' : 'Add Upstream Source'}

{formError &&
{formError}
}
setFormData({ ...formData, name: e.target.value })} placeholder="e.g., npm-private" required />
setFormData({ ...formData, priority: parseInt(e.target.value) || 100 })} min="1" /> Lower = higher priority
setFormData({ ...formData, url: e.target.value })} placeholder="https://registry.example.com" required />
{formData.auth_type !== 'none' && (
{(formData.auth_type === 'basic' || formData.auth_type === 'api_key') && (
setFormData({ ...formData, username: e.target.value })} placeholder={formData.auth_type === 'api_key' ? 'X-API-Key' : 'username'} />
)}
setFormData({ ...formData, password: e.target.value })} placeholder={editingSource ? '(unchanged)' : ''} /> {editingSource && ( Leave empty to keep existing {formData.auth_type === 'bearer' ? 'token' : 'credentials'} )}
)}
{editingSource && ( )}
)} {/* Error Details Modal */} {showErrorModal && selectedError && (
setShowErrorModal(false)}>
e.stopPropagation()}>

Connection Error: {selectedError.sourceName}

{selectedError.error}
)}
); } export default AdminCachePage;