Files
stellars-jupyterhub-ds/services/jupyterhub/html_templates_enhanced/admin.html
stellarshenson 181e2ac5f4 chore: cleanup templates directory structure
- Remove unused *.html from services/jupyterhub/templates/
- Keep only certs/ subdirectory in templates/
- Rename templates_enhanced to html_templates_enhanced
- Update Dockerfile COPY paths
2026-01-14 17:19:12 +01:00

316 lines
12 KiB
HTML

{% extends "page.html" %}
{% block main %}
<div id="react-admin-hook">
<script id="jupyterhub-admin-config">
window.api_page_limit = parseInt("{{ api_page_limit|safe }}")
window.base_url = "{{ base_url|safe }}"
</script>
<script src={{ static_url("js/admin-react.js") }}></script>
</div>
<!-- User Credentials Modal -->
<div class="modal fade" id="user-credentials-modal" tabindex="-1" role="dialog" aria-labelledby="userCredentialsModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userCredentialsModalLabel">
<i class="fa fa-user-plus me-2"></i>New Users Created
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-success">
<strong>Success:</strong> Users created and authorized.
Share the credentials below with each user.
</div>
<div class="mb-3">
<button type="button" class="btn btn-outline-secondary me-2" id="copy-credentials-btn">
<i class="fa fa-copy me-1"></i>Copy to Clipboard
</button>
<button type="button" class="btn btn-primary" id="download-credentials-btn">
<i class="fa fa-download me-1"></i>Download as TXT
</button>
</div>
<div style="max-height: 300px; overflow-y: auto;">
<table class="table table-striped" id="credentials-table">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th style="width: 40px;"></th>
</tr>
</thead>
<tbody id="credentials-body">
<!-- Populated dynamically -->
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Loading Spinner Modal -->
<div class="modal fade" id="loading-modal" tabindex="-1" role="dialog" data-bs-backdrop="static" data-bs-keyboard="false" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body text-center py-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div>Generating credentials...</div>
</div>
</div>
</div>
</div>
{% endblock main %}
{% block footer %}
<div class="py-2 px-4 bg-body-tertiary small version_footer">JupyterHub {{ server_version }}</div>
{% endblock footer %}
{% block script %}
{{ super() }}
<script type="text/javascript">
(function() {
const baseUrl = "{{ base_url|safe }}";
// Store for tracking newly created users
let pendingUsernames = [];
// Helper to get CSRF token
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// Loading spinner functions
let loadingModal = null;
function showLoadingSpinner() {
const modalEl = document.getElementById('loading-modal');
if (!loadingModal) {
loadingModal = new bootstrap.Modal(modalEl);
}
loadingModal.show();
}
function hideLoadingSpinner() {
if (loadingModal) {
loadingModal.hide();
}
}
// Intercept fetch to detect user creation
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [urlArg, options] = args;
// Convert URL object to string if needed
const url = typeof urlArg === 'string' ? urlArg : urlArg.toString();
const method = options?.method || 'GET';
// console.log('[Admin Fetch]', method, url);
// Check for user creation - POST to api/users (may have ?_xsrf query param)
// Exclude URLs with other query params like include_stopped_servers
const urlPath = url.split('?')[0];
const isUserCreation = method === 'POST' && urlPath.endsWith('api/users');
// Capture request body before fetch (for batch user creation)
let requestUsernames = [];
if (isUserCreation) {
// console.log('[Admin] User creation POST detected!');
try {
if (options.body) {
const bodyData = JSON.parse(options.body);
if (bodyData.usernames && Array.isArray(bodyData.usernames)) {
requestUsernames = bodyData.usernames;
// console.log('[Admin] Batch user creation request:', requestUsernames);
}
}
} catch (e) {
// Body might not be JSON, ignore
}
}
const response = await originalFetch.apply(this, args);
// console.log('[Admin Fetch] Response status:', response.status);
// Check if this is a POST to create users
if (isUserCreation) {
try {
// Only process successful responses
if (response.ok || response.status === 201) {
// Clone response to read body
const clonedResponse = response.clone();
const responseData = await clonedResponse.json();
// Extract created usernames from response
let createdUsers = [];
if (Array.isArray(responseData)) {
// Batch response: array of user objects
createdUsers = responseData.map(u => u.name);
} else if (responseData.name) {
// Single user response
createdUsers = [responseData.name];
}
if (createdUsers.length > 0) {
// console.log('[Admin] Users created:', createdUsers);
pendingUsernames.push(...createdUsers);
// Show loading spinner
showLoadingSpinner();
// Debounce - wait for batch completion then fetch credentials
clearTimeout(window._credentialsFetchTimeout);
window._credentialsFetchTimeout = setTimeout(() => {
if (pendingUsernames.length > 0) {
fetchAndShowCredentials([...pendingUsernames]);
pendingUsernames = [];
}
}, 1000);
}
}
} catch (e) {
// console.error('[Admin] Error processing user creation:', e);
}
}
return response;
};
// Fetch credentials for created users and show modal
async function fetchAndShowCredentials(usernames) {
// console.log('[Admin] Fetching credentials for:', usernames);
try {
const response = await originalFetch(`${baseUrl}api/admin/credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRFToken': getCookie('_xsrf')
},
body: JSON.stringify({ usernames: usernames })
});
if (response.ok) {
const data = await response.json();
// console.log('[Admin] Credentials received:', data);
if (data.credentials && data.credentials.length > 0) {
showCredentialsModal(data.credentials);
}
} else {
// console.error('[Admin] Failed to fetch credentials:', response.status);
hideLoadingSpinner();
}
} catch (e) {
// console.error('[Admin] Error fetching credentials:', e);
hideLoadingSpinner();
}
}
// Show modal with credentials
function showCredentialsModal(credentials) {
hideLoadingSpinner();
const tbody = document.getElementById('credentials-body');
tbody.innerHTML = '';
credentials.forEach(cred => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${escapeHtml(cred.username)}</td>
<td>${escapeHtml(cred.password)}</td>
<td><i class="fa fa-copy copy-row-btn" style="opacity: 0.4; cursor: pointer;" title="Copy to clipboard"></i></td>
`;
// Add click handler for row copy
row.querySelector('.copy-row-btn').addEventListener('click', function() {
const text = 'Username: ' + cred.username + '\nPassword: ' + cred.password;
navigator.clipboard.writeText(text).then(() => {
this.classList.remove('fa-copy');
this.classList.add('fa-check');
this.style.opacity = '1';
setTimeout(() => {
this.classList.remove('fa-check');
this.classList.add('fa-copy');
this.style.opacity = '0.4';
}, 1500);
});
});
tbody.appendChild(row);
});
// Store credentials for download/copy
window._currentCredentials = credentials;
// Show modal
const modalEl = document.getElementById('user-credentials-modal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
}
// HTML escape helper
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Format credentials for text output
function formatCredentialsText(credentials) {
let text = 'JupyterHub User Credentials\n';
text += '===========================\n\n';
credentials.forEach(cred => {
text += `Username: ${cred.username}\n`;
text += `Password: ${cred.password}\n\n`;
});
text += `Generated: ${new Date().toISOString()}\n`;
return text;
}
// Copy to clipboard handler
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('copy-credentials-btn')?.addEventListener('click', function() {
if (window._currentCredentials) {
const text = formatCredentialsText(window._currentCredentials);
navigator.clipboard.writeText(text).then(() => {
const btn = this;
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa fa-check me-1"></i>Copied!';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
}, 2000);
}).catch(err => {
// console.error('[Admin] Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
});
// Download as TXT handler
document.getElementById('download-credentials-btn')?.addEventListener('click', function() {
if (window._currentCredentials) {
const text = formatCredentialsText(window._currentCredentials);
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `jupyterhub-credentials-${new Date().toISOString().slice(0,10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
});
})();
</script>
{% endblock script %}