Update cluster tokens page to match API tokens flow with modals and copy functionality

parent b2af681f
......@@ -26,6 +26,19 @@
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.token-preview { font-family: monospace; background: #f1f5f9; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus { outline: none; border-color: #667eea; }
.token-display { background: #f8fafc; padding: 1rem; border-radius: 8px; border: 1px solid #e5e7eb; margin: 1rem 0; }
.token-display pre { word-break: break-all; white-space: pre-wrap; font-family: monospace; background: white; padding: 1rem; border-radius: 4px; border: 1px solid #d1d5db; }
.copy-btn { margin-top: 0.5rem; padding: 0.5rem 1rem; font-size: 0.9rem; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
.modal-content { background-color: white; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 2rem; width: 90%; max-width: 500px; border-radius: 12px; }
.modal-header { margin-bottom: 1rem; position: relative; }
.modal-body { margin-bottom: 1rem; }
.modal-footer { text-align: right; margin-top: 1.5rem; }
.close { position: absolute; top: 0; right: 0; cursor: pointer; font-size: 1.5rem; line-height: 1; }
</style>
{% endblock %}
......@@ -35,6 +48,7 @@
<div class="card-header">
<h3><i class="fas fa-key"></i> Cluster Tokens</h3>
<p>Manage authentication tokens for worker processes in the cluster.</p>
<button onclick="openAddTokenModal()" class="btn" style="margin-left: auto;"><i class="fas fa-plus"></i> Add Token</button>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
......@@ -45,29 +59,13 @@
{% endif %}
{% endwith %}
<div class="admin-card" style="margin-bottom: 2rem;">
<div class="card-header">
<h3><i class="fas fa-plus"></i> Create New Token</h3>
</div>
<form method="post" action="/admin/cluster_tokens/generate">
<div style="display: flex; gap: 1rem; align-items: end;">
<div style="flex: 1;">
<label for="token_name" style="display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500;">Token Name</label>
<input type="text" id="token_name" name="token_name" required style="width: 100%; padding: 0.5rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem;">
</div>
<button type="submit" class="btn"><i class="fas fa-plus"></i> Generate Token</button>
</div>
</form>
</div>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Token</th>
<th>Status</th>
<th>Created</th>
<th>Last Used</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
......@@ -75,50 +73,162 @@
{% for token in worker_tokens %}
<tr>
<td>{{ token.get('name') }}</td>
<td><span class="token-preview">{{ token.get('token')[:20] }}...</span></td>
<td><span class="status-{{ 'active' if token.get('active') else 'inactive' }}">{{ 'Active' if token.get('active') else 'Inactive' }}</span></td>
<td>{{ token.get('created_at', 'N/A')[:19] if token.get('created_at') else 'N/A' }}</td>
<td>{{ token.get('last_used', 'Never')[:19] if token.get('last_used') else 'Never' }}</td>
<td class="actions-cell">
<td><span class="status-{{ 'active' if token.get('active') else 'inactive' }}">{{ 'Active' if token.get('active') else 'Inactive' }}</span></td>
<td>
{% if token.get('active') %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/deactivate" style="display: inline;">
<button type="submit" class="btn-icon" title="Deactivate"><i class="fas fa-ban"></i></button>
<button type="submit" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem; background: #f59e0b;">Deactivate</button>
</form>
{% else %}
<form method="post" action="/admin/cluster_tokens/{{ token.get('id') }}/activate" style="display: inline;">
<button type="submit" class="btn btn-success" title="Activate" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Activate</button>
<button type="submit" class="btn btn-success" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Activate</button>
</form>
{% endif %}
<button onclick='deleteToken({{ token.get("id") }}, "{{ token.get("name") }}")' class="btn-icon" title="Delete" style="color: #dc2626;"><i class="fas fa-trash"></i></button>
<button onclick='deleteToken({{ token.get("id") }}, "{{ token.get("name") }}")' class="btn btn-danger" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Delete</button>
</td>
</tr>
{% endfor %}
{% if not worker_tokens %}
<tr>
<td colspan="5" style="text-align: center; color: #6b7280;">No cluster tokens found. Generate your first token above.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% if not worker_tokens %}
<p style="text-align: center; color: #64748b; margin-top: 2rem;">No cluster tokens found. Generate your first token above.</p>
{% endif %}
<!-- Add Token Modal -->
<div id="addTokenModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-plus"></i> Generate New Cluster Token</h3>
<span class="close" onclick="closeAddTokenModal()">&times;</span>
</div>
<form method="post" action="/admin/cluster_tokens/generate">
<div class="modal-body">
<div class="form-group">
<label for="modal_token_name">Token Name</label>
<input type="text" id="modal_token_name" name="token_name" placeholder="e.g., Worker Node 1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" onclick="closeAddTokenModal()" class="btn" style="background: #6b7280;">Cancel</button>
<button type="submit" class="btn">Generate Token</button>
</div>
</form>
</div>
</div>
<!-- Token Generated Modal -->
{% if generated_token %}
<div id="tokenModal" class="modal" style="display: block;">
<div class="modal-content">
<div class="modal-header">
<h3>Cluster Token Generated Successfully!</h3>
<span class="close" onclick="closeTokenModal()">&times;</span>
</div>
<div class="modal-body">
<p><strong>Token Name:</strong> {{ token_name }}</p>
<p><strong>Token:</strong></p>
<div class="token-display" style="margin: 1rem 0;">
<pre id="modalTokenText">{{ generated_token }}</pre>
<button class="btn copy-btn" onclick="copyTokenFromModal()">Copy Token</button>
</div>
<p style="color: #dc2626; font-weight: 500;">Copy this token now. You won't be able to see it again!</p>
</div>
<div class="modal-footer">
<button onclick="closeTokenModal()" class="btn">Close</button>
</div>
</div>
</div>
{% endif %}
<!-- Delete Confirmation Modal -->
<div id="deleteModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Delete Cluster Token</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the token "<span id="deleteTokenName"></span>"?</p>
<p style="color: #dc2626; font-weight: 500;">This action cannot be undone. Any worker processes using this token will stop working.</p>
</div>
<div class="modal-footer">
<button onclick="closeDeleteModal()" class="btn" style="background: #6b7280;">Cancel</button>
<form id="deleteForm" method="post" action="" style="display: inline;">
<button type="submit" class="btn btn-danger">Delete Token</button>
</form>
</div>
</div>
</div>
<script>
function deleteToken(tokenId, tokenName) {
document.getElementById('deleteTokenName').textContent = tokenName;
document.getElementById('deleteForm').action = `/admin/cluster_tokens/${tokenId}/delete`;
document.getElementById('deleteModal').style.display = 'block';
}
<script>
function deleteToken(id, name) {
if (confirm(`Are you sure you want to delete the token "${name}"? This action cannot be undone.`)) {
// Create a form and submit it
const form = document.createElement('form');
form.method = 'POST';
form.action = `/admin/cluster_tokens/${id}/delete`;
function closeDeleteModal() {
document.getElementById('deleteModal').style.display = 'none';
}
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirm_delete';
input.value = 'yes';
form.appendChild(input);
function openAddTokenModal() {
const modal = document.getElementById('addTokenModal');
modal.style.display = 'block';
}
document.body.appendChild(form);
form.submit();
function closeAddTokenModal() {
const modal = document.getElementById('addTokenModal');
modal.style.display = 'none';
}
function closeTokenModal() {
document.getElementById('tokenModal').style.display = 'none';
window.location.href = '/admin/cluster_tokens'; // Redirect to avoid POST resubmission
}
</script>
</div>
function copyTokenFromModal() {
const tokenText = document.getElementById('modalTokenText');
const copyBtn = document.querySelector('#tokenModal .copy-btn');
const originalText = copyBtn.textContent;
// Use execCommand for reliable copying
const textArea = document.createElement('textarea');
textArea.value = tokenText.textContent.trim();
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
copyBtn.textContent = 'Copied!';
copyBtn.style.background = '#10b981';
setTimeout(function() {
copyBtn.textContent = originalText;
copyBtn.style.background = '';
}, 2000);
} catch (err) {
alert('Failed to copy token. Please select and copy manually.');
}
document.body.removeChild(textArea);
}
// Close modal when clicking outside
window.onclick = function(event) {
const addModal = document.getElementById('addTokenModal');
if (addModal && event.target == addModal) {
closeAddTokenModal();
}
const deleteModal = document.getElementById('deleteModal');
if (deleteModal && event.target == deleteModal) {
closeDeleteModal();
}
const tokenModal = document.getElementById('tokenModal');
if (tokenModal && event.target == tokenModal) {
closeTokenModal();
}
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -436,8 +436,11 @@ def generate_cluster_token():
from .auth import generate_worker_token
token = generate_worker_token(token_name)
flash(f'New cluster token "{token_name}" generated: {token[:20]}...', 'success')
return redirect(url_for('cluster_tokens'))
# Return with modal data instead of flash
from .database import get_worker_tokens
worker_tokens = get_worker_tokens()
user = get_current_user_session()
return render_template('admin/cluster_tokens.html', user=user, worker_tokens=worker_tokens, generated_token=token, token_name=token_name, show_modal=True, active_page='cluster_tokens')
@app.route('/admin/cluster_tokens/<int:token_id>/deactivate', methods=['POST'])
@admin_required
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment