Remove public internet features and fix upstream source UI (#107)
This commit is contained in:
@@ -156,6 +156,7 @@
|
||||
.source-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.url-cell {
|
||||
@@ -168,7 +169,6 @@
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.public-badge,
|
||||
.env-badge,
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
@@ -179,11 +179,6 @@
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.public-badge {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.env-badge {
|
||||
background-color: #fff3e0;
|
||||
color: #e65100;
|
||||
@@ -213,17 +208,64 @@
|
||||
}
|
||||
|
||||
.test-result {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.test-result.success {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.test-result.failure {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-result.failure:hover {
|
||||
background-color: #ffcdd2;
|
||||
}
|
||||
|
||||
.test-result.testing {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
/* Error Modal */
|
||||
.error-modal-content {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-modal-content h3 {
|
||||
margin-top: 0;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.error-modal-content .error-details {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.error-modal-content .modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
@@ -38,7 +38,6 @@ function AdminCachePage() {
|
||||
source_type: 'generic' as SourceType,
|
||||
url: '',
|
||||
enabled: true,
|
||||
is_public: true,
|
||||
auth_type: 'none' as AuthType,
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -60,6 +59,10 @@ function AdminCachePage() {
|
||||
// Success message
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(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' } });
|
||||
@@ -113,7 +116,6 @@ function AdminCachePage() {
|
||||
source_type: 'generic',
|
||||
url: '',
|
||||
enabled: true,
|
||||
is_public: true,
|
||||
auth_type: 'none',
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -130,7 +132,6 @@ function AdminCachePage() {
|
||||
source_type: source.source_type,
|
||||
url: source.url,
|
||||
enabled: source.enabled,
|
||||
is_public: source.is_public,
|
||||
auth_type: source.auth_type,
|
||||
username: source.username || '',
|
||||
password: '',
|
||||
@@ -155,6 +156,8 @@ function AdminCachePage() {
|
||||
setFormError(null);
|
||||
|
||||
try {
|
||||
let savedSourceId: string | null = null;
|
||||
|
||||
if (editingSource) {
|
||||
// Update existing source
|
||||
await updateUpstreamSource(editingSource.id, {
|
||||
@@ -162,30 +165,35 @@ function AdminCachePage() {
|
||||
source_type: formData.source_type,
|
||||
url: formData.url.trim(),
|
||||
enabled: formData.enabled,
|
||||
is_public: formData.is_public,
|
||||
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
|
||||
await createUpstreamSource({
|
||||
const newSource = await createUpstreamSource({
|
||||
name: formData.name.trim(),
|
||||
source_type: formData.source_type,
|
||||
url: formData.url.trim(),
|
||||
enabled: formData.enabled,
|
||||
is_public: formData.is_public,
|
||||
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 {
|
||||
@@ -211,24 +219,28 @@ function AdminCachePage() {
|
||||
}
|
||||
|
||||
async function handleTest(source: UpstreamSource) {
|
||||
setTestingId(source.id);
|
||||
setTestResults((prev) => ({ ...prev, [source.id]: { success: true, message: 'Testing...' } }));
|
||||
testSourceById(source.id);
|
||||
}
|
||||
|
||||
async function testSourceById(sourceId: string) {
|
||||
setTestingId(sourceId);
|
||||
setTestResults((prev) => ({ ...prev, [sourceId]: { success: true, message: 'Testing...' } }));
|
||||
|
||||
try {
|
||||
const result = await testUpstreamSource(source.id);
|
||||
const result = await testUpstreamSource(sourceId);
|
||||
setTestResults((prev) => ({
|
||||
...prev,
|
||||
[source.id]: {
|
||||
[sourceId]: {
|
||||
success: result.success,
|
||||
message: result.success
|
||||
? `Connected (${result.elapsed_ms}ms)`
|
||||
? `OK (${result.elapsed_ms}ms)`
|
||||
: result.error || `HTTP ${result.status_code}`,
|
||||
},
|
||||
}));
|
||||
} catch (err) {
|
||||
setTestResults((prev) => ({
|
||||
...prev,
|
||||
[source.id]: {
|
||||
[sourceId]: {
|
||||
success: false,
|
||||
message: err instanceof Error ? err.message : 'Test failed',
|
||||
},
|
||||
@@ -238,13 +250,16 @@ function AdminCachePage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSettingsToggle(field: 'allow_public_internet' | 'auto_create_system_projects') {
|
||||
function showError(sourceName: string, error: string) {
|
||||
setSelectedError({ sourceName, error });
|
||||
setShowErrorModal(true);
|
||||
}
|
||||
|
||||
async function handleSettingsToggle(field: 'auto_create_system_projects') {
|
||||
if (!settings) return;
|
||||
|
||||
// Check if env override is active
|
||||
const isOverridden =
|
||||
(field === 'allow_public_internet' && settings.allow_public_internet_env_override !== null) ||
|
||||
(field === 'auto_create_system_projects' && settings.auto_create_system_projects_env_override !== null);
|
||||
const isOverridden = field === 'auto_create_system_projects' && settings.auto_create_system_projects_env_override !== null;
|
||||
|
||||
if (isOverridden) {
|
||||
alert('This setting is overridden by an environment variable and cannot be changed via UI.');
|
||||
@@ -291,28 +306,6 @@ function AdminCachePage() {
|
||||
<div className="error-message">{settingsError}</div>
|
||||
) : settings ? (
|
||||
<div className="settings-grid">
|
||||
<div className="setting-item">
|
||||
<label className="toggle-label">
|
||||
<span className="setting-name">
|
||||
Allow Public Internet
|
||||
{settings.allow_public_internet_env_override !== null && (
|
||||
<span className="env-badge" title="Overridden by environment variable">
|
||||
ENV
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="setting-description">
|
||||
When disabled (air-gap mode), requests to public sources are blocked.
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
className={`toggle-button ${settings.allow_public_internet ? 'on' : 'off'}`}
|
||||
onClick={() => handleSettingsToggle('allow_public_internet')}
|
||||
disabled={updatingSettings || settings.allow_public_internet_env_override !== null}
|
||||
>
|
||||
{settings.allow_public_internet ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="setting-item">
|
||||
<label className="toggle-label">
|
||||
<span className="setting-name">
|
||||
@@ -364,6 +357,7 @@ function AdminCachePage() {
|
||||
<th>Priority</th>
|
||||
<th>Status</th>
|
||||
<th>Source</th>
|
||||
<th>Test</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -372,7 +366,6 @@ function AdminCachePage() {
|
||||
<tr key={source.id} className={source.enabled ? '' : 'disabled-row'}>
|
||||
<td>
|
||||
<span className="source-name">{source.name}</span>
|
||||
{source.is_public && <span className="public-badge">Public</span>}
|
||||
</td>
|
||||
<td>{source.source_type}</td>
|
||||
<td className="url-cell">{source.url}</td>
|
||||
@@ -391,13 +384,34 @@ function AdminCachePage() {
|
||||
'Database'
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{testingId === source.id ? (
|
||||
<span className="test-result testing">Testing...</span>
|
||||
) : testResults[source.id] ? (
|
||||
testResults[source.id].success ? (
|
||||
<span className="test-result success" title={testResults[source.id].message}>
|
||||
OK
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="test-result failure"
|
||||
title="Click to see details"
|
||||
onClick={() => showError(source.name, testResults[source.id].message)}
|
||||
>
|
||||
Error
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span className="test-result" style={{ opacity: 0.5 }}>—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="actions-cell">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => handleTest(source)}
|
||||
disabled={testingId === source.id}
|
||||
>
|
||||
{testingId === source.id ? 'Testing...' : 'Test'}
|
||||
Test
|
||||
</button>
|
||||
{source.source !== 'env' && (
|
||||
<>
|
||||
@@ -413,11 +427,6 @@ function AdminCachePage() {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{testResults[source.id] && (
|
||||
<span className={`test-result ${testResults[source.id].success ? 'success' : 'failure'}`}>
|
||||
{testResults[source.id].message}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -498,16 +507,6 @@ function AdminCachePage() {
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_public}
|
||||
onChange={(e) => setFormData({ ...formData, is_public: e.target.checked })}
|
||||
/>
|
||||
Public Internet Source
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -573,6 +572,21 @@ function AdminCachePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Details Modal */}
|
||||
{showErrorModal && selectedError && (
|
||||
<div className="modal-overlay" onClick={() => setShowErrorModal(false)}>
|
||||
<div className="error-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>Connection Error: {selectedError.sourceName}</h3>
|
||||
<div className="error-details">{selectedError.error}</div>
|
||||
<div className="modal-actions">
|
||||
<button className="btn" onClick={() => setShowErrorModal(false)}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user