fix: admin volume button only for stopped servers, positioned last

- Volume management button now only appears for users with stopped servers
- Button dynamically removed when server starts running
- Button positioned as last element in action cell (after Edit User)
- MutationObserver handles React re-renders
This commit is contained in:
stellarshenson
2026-01-26 23:50:36 +01:00
parent ea814b70f8
commit d3913b4d69
2 changed files with 21 additions and 6 deletions

View File

@@ -277,8 +277,8 @@ This journal tracks substantive work on documents, diagrams, and documentation c
91. **Task - Fix Volume Sizes logging and tooltip cleanup**: Fixed invisible VolumeSizeRefresher logs and simplified activity tooltip<br>
**Result**: **Logging Fix**: Module-level `log = logging.getLogger('jupyterhub.custom_handlers')` doesn't inherit JupyterHub's handlers, causing all VolumeSizeRefresher and Volume Sizes logs to be invisible. Added `_get_logger()` helper function that returns `Application.instance().log` for proper integration. Updated `_fetch_volume_sizes()`, `_refresh_volume_sizes_sync()`, `get_volume_sizes_with_refresh()`, and all `VolumeSizeRefresher` methods to use `_get_logger()`. Added cache validation - empty results no longer overwrite valid cached data (logs warning instead). **Tooltip Cleanup**: Removed "(>8h/day)" suffix from activity tooltip when score exceeds 100% - now shows just "Activity: X%"
92. **Task - Admin volume management button**: Added Manage Volumes button to admin panel for all users<br>
**Result**: Added volume management modal to admin.html with checkboxes for each volume type (home, workspace, cache), warning alert, and confirm button. JavaScript uses MutationObserver to inject database icon button into each user's action cell as React renders them. Finds all `tr.user-row` elements, extracts username from `span[data-testid="user-name-div-{username}"]`, locates Edit User button by text content, copies its exact classes (`btn btn-light btn-xs`), and inserts volume button immediately before Edit User button. Modal shows target username, selection count warning, and spinner during reset. Calls existing `DELETE /api/users/{username}/manage-volumes` endpoint with CSRF token. Admins can now reset any user's volumes directly from the admin panel
92. **Task - Admin volume management button**: Added Manage Volumes button to admin panel for users with stopped servers<br>
**Result**: Added volume management modal to admin.html with checkboxes for each volume type (home, workspace, cache), warning alert, and confirm button. JavaScript uses MutationObserver to inject database icon button into each user's action cell as React renders them. Finds all `tr.user-row` elements, checks for "Stop Server" button to detect running servers (skips those), extracts username from `span[data-testid="user-name-div-{username}"]`, copies Edit User button classes (`btn btn-light btn-xs`), and appends volume button as last element in actions cell. Button is removed dynamically when server starts (MutationObserver detects DOM changes). Modal shows target username, selection count warning, and spinner during reset. Calls existing `DELETE /api/users/{username}/manage-volumes` endpoint with CSRF token. Admins can reset volumes only for users with stopped servers
93. **Task - README documentation update**: Updated README with admin volume management and volume sizes tracking<br>
**Result**: Added Admin Volume Management feature to Features list (database icon button in admin panel). Updated Activity Monitor feature description to include volume sizes with per-volume breakdown. Added Volume sizes feature to Activity Monitor Features section with hover tooltip details. Added `JUPYTERHUB_ACTIVITYMON_VOLUMES_UPDATE_INTERVAL` config option (default 1 hour). Added note in User Self-Service Workflow section explaining admin can manage volumes for any user from `/hub/admin`

View File

@@ -388,8 +388,23 @@
const actionsCell = row.querySelector('td.actions');
if (!actionsCell) return;
// Check if server is running (has "Stop Server" button) or stopped (has "Start Server" button)
let serverRunning = false;
actionsCell.querySelectorAll('button').forEach(btn => {
if (btn.textContent.trim() === 'Stop Server') {
serverRunning = true;
}
});
// Remove existing button if server is now running
const existingBtn = actionsCell.querySelector('.admin-manage-volumes-btn');
if (serverRunning) {
if (existingBtn) existingBtn.remove();
return;
}
// Skip if already has our button
if (actionsCell.querySelector('.admin-manage-volumes-btn')) return;
if (existingBtn) return;
// Get username from the user-name-div span
const usernameSpan = row.querySelector('span[data-testid^="user-name-div-"]');
@@ -397,7 +412,7 @@
const username = usernameSpan.textContent.trim();
if (!username) return;
// Find the Edit User button (by text content)
// Find the Edit User button (by text content) to copy its styling
let editBtn = null;
actionsCell.querySelectorAll('button').forEach(btn => {
if (btn.textContent.trim() === 'Edit User') {
@@ -421,8 +436,8 @@
openVolumeModal(username);
});
// Insert before Edit button
editBtn.parentNode.insertBefore(btn, editBtn);
// Append as last button in actions cell
actionsCell.appendChild(btn);
});
}