4 Commits

Author SHA1 Message Date
Mondo Diaz
a93bf84b83 Move delete button from table to edit modal
- Delete button now appears in the edit modal footer (left side)
- Cleaner table layout with just Test and Edit buttons
- Delete button only shows when editing existing sources
2026-01-29 14:16:10 -06:00
Mondo Diaz
2c123d8d0f Simplify cache management UI and improve test status display (#107)
- Remove Global Settings section (auto-create system projects is always enabled)
- Rename page from "Cache Management" to "Upstream Sources"
- Change test status from text badges to colored dots (green/red)
- Add pulse animation for testing state
- Click red dot to see error details in modal
- Remove unused CacheSettings types and API functions
2026-01-29 14:12:55 -06:00
Mondo Diaz
858b45d434 Merge branch 'fix/purge-seed-data-user-id' into 'main'
Fix purge_seed_data type mismatch for access_permissions.user_id (#107)

See merge request esv/bsf/bsf-integration/orchard/orchard-mvp!54
2026-01-29 13:48:21 -06:00
Mondo Diaz
95470b2bf6 Fix purge_seed_data type mismatch for access_permissions.user_id (#107) 2026-01-29 13:48:21 -06:00
6 changed files with 71 additions and 240 deletions

View File

@@ -6,12 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Fixed
- Fixed purge_seed_data crash when deleting access permissions - was comparing UUID to VARCHAR column (#107)
### Changed ### Changed
- Upstream source connectivity test no longer follows redirects, fixing "Exceeded maximum allowed redirects" error with Artifactory proxies (#107) - Upstream source connectivity test no longer follows redirects, fixing "Exceeded maximum allowed redirects" error with Artifactory proxies (#107)
- Upstream sources table now has dedicated "Test" column with OK/Error status badges (#107)
- Test runs automatically after saving a new or updated upstream source (#107) - Test runs automatically after saving a new or updated upstream source (#107)
- Error states in upstream sources table are now clickable to show full error details in a modal (#107) - Test status now shows as colored dots (green=success, red=error) instead of text badges (#107)
- Clicking red dot shows error details in a modal (#107)
- Source name column no longer wraps text for better table layout (#107) - Source name column no longer wraps text for better table layout (#107)
- Renamed "Cache Management" page to "Upstream Sources" (#107)
- Moved Delete button from table row to edit modal for cleaner table layout (#107)
### Removed ### Removed
- Removed `is_public` field from upstream sources - all sources are now treated as internal/private (#107) - Removed `is_public` field from upstream sources - all sources are now treated as internal/private (#107)
@@ -19,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed seeding of public registry URLs (npm-public, pypi-public, maven-central, docker-hub) (#107) - Removed seeding of public registry URLs (npm-public, pypi-public, maven-central, docker-hub) (#107)
- Removed "Public" badge and checkbox from upstream sources UI (#107) - Removed "Public" badge and checkbox from upstream sources UI (#107)
- Removed "Allow Public Internet" toggle from cache settings UI (#107) - Removed "Allow Public Internet" toggle from cache settings UI (#107)
- Removed "Global Settings" section from cache management UI - auto-create system projects is always enabled (#107)
- Removed unused CacheSettings frontend types and API functions (#107)
### Added ### Added
- Added `ORCHARD_PURGE_SEED_DATA` environment variable support to stage helm values to remove seed data from long-running deployments (#107) - Added `ORCHARD_PURGE_SEED_DATA` environment variable support to stage helm values to remove seed data from long-running deployments (#107)

View File

@@ -194,7 +194,8 @@ def purge_seed_data(db: Session) -> dict:
synchronize_session=False synchronize_session=False
) )
# Delete any access permissions for this user # Delete any access permissions for this user
db.query(AccessPermission).filter(AccessPermission.user_id == user.id).delete( # Note: AccessPermission.user_id is VARCHAR (username), not UUID
db.query(AccessPermission).filter(AccessPermission.user_id == user.username).delete(
synchronize_session=False synchronize_session=False
) )
db.delete(user) db.delete(user)

View File

@@ -46,8 +46,6 @@ import {
UpstreamSourceCreate, UpstreamSourceCreate,
UpstreamSourceUpdate, UpstreamSourceUpdate,
UpstreamSourceTestResult, UpstreamSourceTestResult,
CacheSettings,
CacheSettingsUpdate,
} from './types'; } from './types';
const API_BASE = '/api/v1'; const API_BASE = '/api/v1';
@@ -748,21 +746,3 @@ export async function testUpstreamSource(id: string): Promise<UpstreamSourceTest
}); });
return handleResponse<UpstreamSourceTestResult>(response); return handleResponse<UpstreamSourceTestResult>(response);
} }
// Cache Settings Admin API
export async function getCacheSettings(): Promise<CacheSettings> {
const response = await fetch(`${API_BASE}/admin/cache-settings`, {
credentials: 'include',
});
return handleResponse<CacheSettings>(response);
}
export async function updateCacheSettings(data: CacheSettingsUpdate): Promise<CacheSettings> {
const response = await fetch(`${API_BASE}/admin/cache-settings`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include',
});
return handleResponse<CacheSettings>(response);
}

View File

@@ -34,74 +34,6 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* Settings Section */
.settings-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.settings-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.toggle-label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.setting-name {
font-weight: 500;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.setting-description {
font-size: 0.85rem;
color: var(--text-secondary);
}
.toggle-button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
min-width: 100px;
}
.toggle-button.on {
background-color: #28a745;
color: white;
}
.toggle-button.off {
background-color: #dc3545;
color: white;
}
.toggle-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Sources Section */ /* Sources Section */
.sources-section { .sources-section {
background: var(--bg-secondary); background: var(--bg-secondary);
@@ -207,35 +139,37 @@
margin-right: 0; margin-right: 0;
} }
.test-result { .test-cell {
display: inline-flex; text-align: center;
align-items: center; width: 2rem;
gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
} }
.test-result.success { .test-dot {
background-color: #e8f5e9; font-size: 1rem;
cursor: default;
}
.test-dot.success {
color: #2e7d32; color: #2e7d32;
} }
.test-result.failure { .test-dot.failure {
background-color: #ffebee;
color: #c62828; color: #c62828;
cursor: pointer; cursor: pointer;
} }
.test-result.failure:hover { .test-dot.failure:hover {
background-color: #ffcdd2; color: #b71c1c;
} }
.test-result.testing { .test-dot.testing {
background-color: #e3f2fd;
color: #1976d2; color: #1976d2;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
} }
/* Error Modal */ /* Error Modal */
@@ -406,9 +340,14 @@
.form-actions { .form-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
gap: 0.5rem; align-items: center;
margin-top: 1.5rem; margin-top: 1.5rem;
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
} }
.form-actions-right {
display: flex;
gap: 0.5rem;
}

View File

@@ -7,10 +7,8 @@ import {
updateUpstreamSource, updateUpstreamSource,
deleteUpstreamSource, deleteUpstreamSource,
testUpstreamSource, testUpstreamSource,
getCacheSettings,
updateCacheSettings,
} from '../api'; } from '../api';
import { UpstreamSource, CacheSettings, SourceType, AuthType } from '../types'; import { UpstreamSource, SourceType, AuthType } from '../types';
import './AdminCachePage.css'; import './AdminCachePage.css';
const SOURCE_TYPES: SourceType[] = ['npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic']; const SOURCE_TYPES: SourceType[] = ['npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic'];
@@ -25,11 +23,6 @@ function AdminCachePage() {
const [loadingSources, setLoadingSources] = useState(true); const [loadingSources, setLoadingSources] = useState(true);
const [sourcesError, setSourcesError] = useState<string | null>(null); const [sourcesError, setSourcesError] = useState<string | null>(null);
// Cache settings state
const [settings, setSettings] = useState<CacheSettings | null>(null);
const [loadingSettings, setLoadingSettings] = useState(true);
const [settingsError, setSettingsError] = useState<string | null>(null);
// Create/Edit form state // Create/Edit form state
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingSource, setEditingSource] = useState<UpstreamSource | null>(null); const [editingSource, setEditingSource] = useState<UpstreamSource | null>(null);
@@ -53,9 +46,6 @@ function AdminCachePage() {
// Delete confirmation state // Delete confirmation state
const [deletingId, setDeletingId] = useState<string | null>(null); const [deletingId, setDeletingId] = useState<string | null>(null);
// Settings update state
const [updatingSettings, setUpdatingSettings] = useState(false);
// Success message // Success message
const [successMessage, setSuccessMessage] = useState<string | null>(null); const [successMessage, setSuccessMessage] = useState<string | null>(null);
@@ -72,7 +62,6 @@ function AdminCachePage() {
useEffect(() => { useEffect(() => {
if (user && user.is_admin) { if (user && user.is_admin) {
loadSources(); loadSources();
loadSettings();
} }
}, [user]); }, [user]);
@@ -96,19 +85,6 @@ function AdminCachePage() {
} }
} }
async function loadSettings() {
setLoadingSettings(true);
setSettingsError(null);
try {
const data = await getCacheSettings();
setSettings(data);
} catch (err) {
setSettingsError(err instanceof Error ? err.message : 'Failed to load settings');
} finally {
setLoadingSettings(false);
}
}
function openCreateForm() { function openCreateForm() {
setEditingSource(null); setEditingSource(null);
setFormData({ setFormData({
@@ -255,30 +231,6 @@ function AdminCachePage() {
setShowErrorModal(true); setShowErrorModal(true);
} }
async function handleSettingsToggle(field: 'auto_create_system_projects') {
if (!settings) return;
// Check if env override is active
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.');
return;
}
setUpdatingSettings(true);
try {
const update = { [field]: !settings[field] };
const newSettings = await updateCacheSettings(update);
setSettings(newSettings);
setSuccessMessage(`Setting "${field}" updated`);
} catch (err) {
setSettingsError(err instanceof Error ? err.message : 'Failed to update settings');
} finally {
setUpdatingSettings(false);
}
}
if (authLoading) { if (authLoading) {
return <div className="admin-cache-page">Loading...</div>; return <div className="admin-cache-page">Loading...</div>;
} }
@@ -293,49 +245,13 @@ function AdminCachePage() {
return ( return (
<div className="admin-cache-page"> <div className="admin-cache-page">
<h1>Cache Management</h1> <h1>Upstream Sources</h1>
{successMessage && <div className="success-message">{successMessage}</div>} {successMessage && <div className="success-message">{successMessage}</div>}
{/* Cache Settings Section */}
<section className="settings-section">
<h2>Global Settings</h2>
{loadingSettings ? (
<p>Loading settings...</p>
) : settingsError ? (
<div className="error-message">{settingsError}</div>
) : settings ? (
<div className="settings-grid">
<div className="setting-item">
<label className="toggle-label">
<span className="setting-name">
Auto-create System Projects
{settings.auto_create_system_projects_env_override !== null && (
<span className="env-badge" title="Overridden by environment variable">
ENV
</span>
)}
</span>
<span className="setting-description">
Automatically create system projects (_npm, _pypi, etc.) on first cache request.
</span>
</label>
<button
className={`toggle-button ${settings.auto_create_system_projects ? 'on' : 'off'}`}
onClick={() => handleSettingsToggle('auto_create_system_projects')}
disabled={updatingSettings || settings.auto_create_system_projects_env_override !== null}
>
{settings.auto_create_system_projects ? 'Enabled' : 'Disabled'}
</button>
</div>
</div>
) : null}
</section>
{/* Upstream Sources Section */} {/* Upstream Sources Section */}
<section className="sources-section"> <section className="sources-section">
<div className="section-header"> <div className="section-header">
<h2>Upstream Sources</h2>
<button className="btn btn-primary" onClick={openCreateForm}> <button className="btn btn-primary" onClick={openCreateForm}>
Add Source Add Source
</button> </button>
@@ -357,7 +273,7 @@ function AdminCachePage() {
<th>Priority</th> <th>Priority</th>
<th>Status</th> <th>Status</th>
<th>Source</th> <th>Source</th>
<th>Test</th> <th></th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -384,26 +300,20 @@ function AdminCachePage() {
'Database' 'Database'
)} )}
</td> </td>
<td> <td className="test-cell">
{testingId === source.id ? ( {testingId === source.id ? (
<span className="test-result testing">Testing...</span> <span className="test-dot testing" title="Testing..."></span>
) : testResults[source.id] ? ( ) : testResults[source.id] ? (
testResults[source.id].success ? ( testResults[source.id].success ? (
<span className="test-result success" title={testResults[source.id].message}> <span className="test-dot success" title={testResults[source.id].message}></span>
OK
</span>
) : ( ) : (
<span <span
className="test-result failure" className="test-dot failure"
title="Click to see details" title="Click to see error"
onClick={() => showError(source.name, testResults[source.id].message)} onClick={() => showError(source.name, testResults[source.id].message)}
> ></span>
Error
</span>
) )
) : ( ) : null}
<span className="test-result" style={{ opacity: 0.5 }}></span>
)}
</td> </td>
<td className="actions-cell"> <td className="actions-cell">
<button <button
@@ -414,18 +324,9 @@ function AdminCachePage() {
Test Test
</button> </button>
{source.source !== 'env' && ( {source.source !== 'env' && (
<>
<button className="btn btn-sm" onClick={() => openEditForm(source)}> <button className="btn btn-sm" onClick={() => openEditForm(source)}>
Edit Edit
</button> </button>
<button
className="btn btn-sm btn-danger"
onClick={() => handleDelete(source)}
disabled={deletingId === source.id}
>
{deletingId === source.id ? 'Deleting...' : 'Delete'}
</button>
</>
)} )}
</td> </td>
</tr> </tr>
@@ -561,6 +462,20 @@ function AdminCachePage() {
)} )}
<div className="form-actions"> <div className="form-actions">
{editingSource && (
<button
type="button"
className="btn btn-danger"
onClick={() => {
handleDelete(editingSource);
setShowForm(false);
}}
disabled={deletingId === editingSource.id}
>
{deletingId === editingSource.id ? 'Deleting...' : 'Delete'}
</button>
)}
<div className="form-actions-right">
<button type="button" className="btn" onClick={() => setShowForm(false)}> <button type="button" className="btn" onClick={() => setShowForm(false)}>
Cancel Cancel
</button> </button>
@@ -568,6 +483,7 @@ function AdminCachePage() {
{isSaving ? 'Saving...' : editingSource ? 'Update' : 'Create'} {isSaving ? 'Saving...' : editingSource ? 'Update' : 'Create'}
</button> </button>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -557,15 +557,3 @@ export interface UpstreamSourceTestResult {
source_id: string; source_id: string;
source_name: string; source_name: string;
} }
// Cache Settings types
export interface CacheSettings {
auto_create_system_projects: boolean;
auto_create_system_projects_env_override: boolean | null;
created_at: string | null;
updated_at: string | null;
}
export interface CacheSettingsUpdate {
auto_create_system_projects?: boolean;
}