diff --git a/services/jupyterhub/html_templates_enhanced/admin.html b/services/jupyterhub/html_templates_enhanced/admin.html
index 4642883..58db6e7 100644
--- a/services/jupyterhub/html_templates_enhanced/admin.html
+++ b/services/jupyterhub/html_templates_enhanced/admin.html
@@ -378,64 +378,56 @@
let volumeModal = null;
let currentVolumeUsername = null;
- // Inject "Manage Volumes" button into user action cells
+ // Inject "Manage Volumes" button into user action areas
function injectVolumeButtons() {
- // Find all user action cells (td.actions contains buttons like Start/Stop/Delete)
- const actionCells = document.querySelectorAll('td.actions');
+ // JupyterHub React admin renders Edit buttons for each user
+ // Find all Edit buttons and inject our button before them
+ const editButtons = document.querySelectorAll('button[title="Edit User"], a[title="Edit User"]');
- actionCells.forEach(cell => {
- // Skip if already has our button
- if (cell.querySelector('.admin-manage-volumes-btn')) return;
+ editButtons.forEach(editBtn => {
+ // Skip if already has our button nearby
+ const parent = editBtn.parentNode;
+ if (parent.querySelector('.admin-manage-volumes-btn')) return;
- // Find the username from the row
- const row = cell.closest('tr');
- if (!row) return;
-
- // Try multiple methods to find username:
- // 1. Link to user page: /hub/admin#/users/{username}
- // 2. Link to user server: /user/{username}
- // 3. First cell text content
+ // Find username - look for links in parent containers
let username = null;
- // Method 1: Look for admin user link (e.g., href="#/users/konrad")
- const adminLink = row.querySelector('td a[href*="#/users/"]');
- if (adminLink) {
- const href = adminLink.getAttribute('href');
- const match = href.match(/#\/users\/([^\/]+)/);
- if (match) username = decodeURIComponent(match[1]);
- }
+ // Walk up to find the user row/card container
+ let container = editBtn.closest('tr') || editBtn.closest('.card') || editBtn.closest('[class*="user"]') || editBtn.parentNode.parentNode.parentNode;
- // Method 2: Look for server link (e.g., href="/jupyterhub/user/konrad/")
- if (!username) {
- const serverLink = row.querySelector('td a[href*="/user/"]');
- if (serverLink) {
- const href = serverLink.getAttribute('href');
- const match = href.match(/\/user\/([^\/]+)/);
+ if (container) {
+ // Method 1: Admin user link (href contains #/users/)
+ const adminLink = container.querySelector('a[href*="#/users/"]');
+ if (adminLink) {
+ const href = adminLink.getAttribute('href');
+ const match = href.match(/#\/users\/([^\/\?]+)/);
if (match) username = decodeURIComponent(match[1]);
}
- }
- // Method 3: First td with a link - get text content
- if (!username) {
- const firstLink = row.querySelector('td:first-child a');
- if (firstLink) {
- username = firstLink.textContent.trim();
+ // Method 2: Server link (href contains /user/)
+ if (!username) {
+ const serverLink = container.querySelector('a[href*="/user/"]');
+ if (serverLink) {
+ const href = serverLink.getAttribute('href');
+ const match = href.match(/\/user\/([^\/\?]+)/);
+ if (match) username = decodeURIComponent(match[1]);
+ }
+ }
+
+ // Method 3: Any link that looks like a username (first link text)
+ if (!username) {
+ const anyLink = container.querySelector('a');
+ if (anyLink && anyLink.textContent && !anyLink.textContent.includes('/')) {
+ username = anyLink.textContent.trim();
+ }
}
}
if (!username) return;
- // Find the Edit button (has fa-edit or fa-pencil icon) to match its styling
- const editBtn = cell.querySelector('button[title*="dit"], button .fa-edit, button .fa-pencil')?.closest('button')
- || cell.querySelector('a[title*="dit"], a .fa-edit, a .fa-pencil')?.closest('a');
-
- // Create button with same classes as Edit button, or fallback
+ // Create button with same classes as Edit button
const btn = document.createElement('button');
- if (editBtn) {
- btn.className = editBtn.className.replace(/\s+/g, ' ').trim();
- } else {
- btn.className = 'btn btn-light';
- }
+ btn.className = editBtn.className;
btn.classList.add('admin-manage-volumes-btn');
btn.innerHTML = '';
btn.title = 'Manage Volumes';
@@ -447,18 +439,8 @@
openVolumeModal(username);
});
- // Insert before Edit button if found, otherwise before last button (Delete)
- if (editBtn) {
- editBtn.parentNode.insertBefore(btn, editBtn);
- } else {
- // Find delete button (usually last, has fa-trash icon)
- const deleteBtn = cell.querySelector('button .fa-trash, button .fa-times')?.closest('button');
- if (deleteBtn) {
- deleteBtn.parentNode.insertBefore(btn, deleteBtn);
- } else {
- cell.appendChild(btn);
- }
- }
+ // Insert before Edit button
+ parent.insertBefore(btn, editBtn);
});
}