Init repo
This commit is contained in:
471
static/js/pages/clients.js
Normal file
471
static/js/pages/clients.js
Normal file
@@ -0,0 +1,471 @@
|
||||
// Clients Page Module
|
||||
|
||||
class ClientsPage {
|
||||
constructor() {
|
||||
this.clients = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Load data
|
||||
await this.loadClients();
|
||||
await this.loadClientUsageChart();
|
||||
await this.loadRateLimitStatus();
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
async loadClients() {
|
||||
try {
|
||||
// In a real app, this would fetch from /api/clients
|
||||
this.clients = [
|
||||
{ id: 'client-1', name: 'Web Application', token: 'sk-*****abc123', created: '2024-01-01', lastUsed: '2024-01-15', requests: 1245, status: 'active' },
|
||||
{ id: 'client-2', name: 'Mobile App', token: 'sk-*****def456', created: '2024-01-05', lastUsed: '2024-01-15', requests: 890, status: 'active' },
|
||||
{ id: 'client-3', name: 'API Integration', token: 'sk-*****ghi789', created: '2024-01-08', lastUsed: '2024-01-14', requests: 1560, status: 'active' },
|
||||
{ id: 'client-4', name: 'Internal Tools', token: 'sk-*****jkl012', created: '2024-01-10', lastUsed: '2024-01-13', requests: 340, status: 'inactive' },
|
||||
{ id: 'client-5', name: 'Testing Suite', token: 'sk-*****mno345', created: '2024-01-12', lastUsed: '2024-01-12', requests: 120, status: 'active' },
|
||||
{ id: 'client-6', name: 'Backup Service', token: 'sk-*****pqr678', created: '2024-01-14', lastUsed: null, requests: 0, status: 'pending' }
|
||||
];
|
||||
|
||||
this.renderClientsTable();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading clients:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderClientsTable() {
|
||||
const tableBody = document.querySelector('#clients-table tbody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = this.clients.map(client => {
|
||||
const statusClass = client.status === 'active' ? 'success' :
|
||||
client.status === 'inactive' ? 'warning' : 'secondary';
|
||||
const statusIcon = client.status === 'active' ? 'check-circle' :
|
||||
client.status === 'inactive' ? 'exclamation-triangle' : 'clock';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${client.id}</td>
|
||||
<td>${client.name}</td>
|
||||
<td>
|
||||
<code class="token-display">${client.token}</code>
|
||||
<button class="btn-copy-token" data-token="${client.token}" title="Copy token">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>${client.created}</td>
|
||||
<td>${client.lastUsed || 'Never'}</td>
|
||||
<td>${client.requests.toLocaleString()}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">
|
||||
<i class="fas fa-${statusIcon}"></i>
|
||||
${client.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-action" title="Edit" data-action="edit" data-id="${client.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn-action" title="Rotate Token" data-action="rotate" data-id="${client.id}">
|
||||
<i class="fas fa-redo"></i>
|
||||
</button>
|
||||
<button class="btn-action danger" title="Revoke" data-action="revoke" data-id="${client.id}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for action buttons
|
||||
this.addActionStyles();
|
||||
}
|
||||
|
||||
addActionStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.token-display {
|
||||
background-color: var(--bg-secondary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-copy-token {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-copy-token:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-action.danger:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
async loadClientUsageChart() {
|
||||
try {
|
||||
const data = {
|
||||
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: [1245, 890, 1560, 340, 120],
|
||||
color: '#3b82f6'
|
||||
}]
|
||||
};
|
||||
|
||||
window.chartManager.createHorizontalBarChart('client-usage-chart', data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading client usage chart:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadRateLimitStatus() {
|
||||
const container = document.getElementById('rate-limit-status');
|
||||
if (!container) return;
|
||||
|
||||
const rateLimits = [
|
||||
{ client: 'Web Application', limit: 1000, used: 645, remaining: 355 },
|
||||
{ client: 'Mobile App', limit: 500, used: 320, remaining: 180 },
|
||||
{ client: 'API Integration', limit: 2000, used: 1560, remaining: 440 },
|
||||
{ client: 'Internal Tools', limit: 100, used: 34, remaining: 66 },
|
||||
{ client: 'Testing Suite', limit: 200, used: 120, remaining: 80 }
|
||||
];
|
||||
|
||||
container.innerHTML = rateLimits.map(limit => {
|
||||
const percentage = (limit.used / limit.limit) * 100;
|
||||
let color = 'success';
|
||||
if (percentage > 80) color = 'warning';
|
||||
if (percentage > 95) color = 'danger';
|
||||
|
||||
return `
|
||||
<div class="rate-limit-item">
|
||||
<div class="rate-limit-header">
|
||||
<span class="rate-limit-client">${limit.client}</span>
|
||||
<span class="rate-limit-numbers">${limit.used} / ${limit.limit}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${color}" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
<div class="rate-limit-footer">
|
||||
<span class="rate-limit-percentage">${Math.round(percentage)}% used</span>
|
||||
<span class="rate-limit-remaining">${limit.remaining} remaining</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add CSS for rate limit items
|
||||
this.addRateLimitStyles();
|
||||
}
|
||||
|
||||
addRateLimitStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.rate-limit-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rate-limit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rate-limit-client {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.rate-limit-numbers {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.rate-limit-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.rate-limit-percentage {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.rate-limit-remaining {
|
||||
color: var(--success);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Add client button
|
||||
const addBtn = document.getElementById('add-client');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', () => {
|
||||
this.showAddClientModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Copy token buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-copy-token')) {
|
||||
const button = e.target.closest('.btn-copy-token');
|
||||
const token = button.dataset.token;
|
||||
this.copyToClipboard(token);
|
||||
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Token copied to clipboard', 'success');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Action buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-action')) {
|
||||
const button = e.target.closest('.btn-action');
|
||||
const action = button.dataset.action;
|
||||
const clientId = button.dataset.id;
|
||||
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
this.editClient(clientId);
|
||||
break;
|
||||
case 'rotate':
|
||||
this.rotateToken(clientId);
|
||||
break;
|
||||
case 'revoke':
|
||||
this.revokeClient(clientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showAddClientModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Add New Client</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="add-client-form">
|
||||
<div class="form-control">
|
||||
<label for="client-name">Client Name</label>
|
||||
<input type="text" id="client-name" placeholder="e.g., Web Application" required>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="client-description">Description (Optional)</label>
|
||||
<textarea id="client-description" rows="3" placeholder="Describe what this client will be used for..."></textarea>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="rate-limit">Rate Limit (requests per hour)</label>
|
||||
<input type="number" id="rate-limit" value="1000" min="1" max="10000">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary create-client">Create Client</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const createBtn = modal.querySelector('.create-client');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
createBtn.addEventListener('click', () => {
|
||||
const name = modal.querySelector('#client-name').value;
|
||||
if (!name.trim()) {
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Client name is required', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In a real app, this would create the client via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Client "${name}" created successfully`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editClient(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show edit modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Edit Client: ${client.name}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Client editing would be implemented here.</p>
|
||||
<p>In a real implementation, this would include forms for updating client settings.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||
<button class="btn btn-primary save-client">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Setup event listeners
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const closeModalBtn = modal.querySelector('.close-modal');
|
||||
const saveBtn = modal.querySelector('.save-client');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
};
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// In a real app, this would save client changes
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast('Client updated successfully', 'success');
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Close on background click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rotateToken(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show confirmation modal
|
||||
if (confirm(`Are you sure you want to rotate the token for "${client.name}"? The old token will be invalidated.`)) {
|
||||
// In a real app, this would rotate the token via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Token rotated for "${client.name}"`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
}
|
||||
}
|
||||
|
||||
revokeClient(clientId) {
|
||||
const client = this.clients.find(c => c.id === clientId);
|
||||
if (!client) return;
|
||||
|
||||
// Show confirmation modal
|
||||
if (confirm(`Are you sure you want to revoke client "${client.name}"? This action cannot be undone.`)) {
|
||||
// In a real app, this would revoke the client via API
|
||||
if (window.authManager) {
|
||||
window.authManager.showToast(`Client "${client.name}" revoked`, 'success');
|
||||
}
|
||||
|
||||
// Refresh clients list
|
||||
this.loadClients();
|
||||
}
|
||||
}
|
||||
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.loadClients();
|
||||
this.loadClientUsageChart();
|
||||
this.loadRateLimitStatus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize clients page when needed
|
||||
window.initClients = async () => {
|
||||
window.clientsPage = new ClientsPage();
|
||||
};
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ClientsPage;
|
||||
}
|
||||
Reference in New Issue
Block a user