fix: admin volume button for all users

- Find Edit buttons directly instead of looking for td.actions
- Inject button before each Edit User button found
- Works with JupyterHub React admin panel structure
- Walks up DOM to find user container and extract username
This commit is contained in:
stellarshenson
2026-01-26 13:31:30 +01:00
parent a7ec100878
commit a67af7a090

View File

@@ -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 = '<i class="fa fa-database"></i>';
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);
});
}