Add PUT /api/clients/{id} with dynamic UPDATE for name, description,
is_active, and rate_limit_per_minute. Expose description and
rate_limit_per_minute in client list/detail responses. Replace the
frontend editClient stub with a modal that fetches, edits, and saves
client data.
274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
// Clients Page Module
|
|
|
|
class ClientsPage {
|
|
constructor() {
|
|
this.clients = [];
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
// Load data
|
|
await Promise.all([
|
|
this.loadClients(),
|
|
this.loadClientUsageChart()
|
|
]);
|
|
|
|
// Setup event listeners
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
async loadClients() {
|
|
try {
|
|
const data = await window.api.get('/clients');
|
|
this.clients = data;
|
|
this.renderClientsTable();
|
|
} catch (error) {
|
|
console.error('Error loading clients:', error);
|
|
window.authManager.showToast('Failed to load clients', 'error');
|
|
}
|
|
}
|
|
|
|
renderClientsTable() {
|
|
const tableBody = document.querySelector('#clients-table tbody');
|
|
if (!tableBody) return;
|
|
|
|
if (this.clients.length === 0) {
|
|
tableBody.innerHTML = '<tr><td colspan="8" class="text-center">No clients configured</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tableBody.innerHTML = this.clients.map(client => {
|
|
const statusClass = client.status === 'active' ? 'success' : 'secondary';
|
|
const statusIcon = client.status === 'active' ? 'check-circle' : 'clock';
|
|
const created = luxon.DateTime.fromISO(client.created_at).toFormat('MMM dd, yyyy');
|
|
|
|
return `
|
|
<tr>
|
|
<td><span class="badge-client">${client.id}</span></td>
|
|
<td><strong>${client.name}</strong></td>
|
|
<td>
|
|
<code class="token-display">sk-••••${client.id.substring(client.id.length - 4)}</code>
|
|
</td>
|
|
<td>${created}</td>
|
|
<td>${client.last_used ? window.api.formatTimeAgo(client.last_used) : 'Never'}</td>
|
|
<td>${client.requests_count.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" onclick="window.clientsPage.editClient('${client.id}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn-action danger" title="Delete" onclick="window.clientsPage.deleteClient('${client.id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async loadClientUsageChart() {
|
|
try {
|
|
const cm = window.chartManager || await window.waitForChartManager();
|
|
if (!cm) { this.showEmptyChart('client-usage-chart', 'Chart system unavailable'); return; }
|
|
|
|
const data = await window.api.get('/usage/clients');
|
|
|
|
if (!data || data.length === 0) {
|
|
this.showEmptyChart('client-usage-chart', 'No client usage data yet');
|
|
return;
|
|
}
|
|
|
|
const chartData = {
|
|
labels: data.map(item => item.client_id),
|
|
datasets: [{
|
|
label: 'Requests',
|
|
data: data.map(item => item.requests),
|
|
color: '#6366f1'
|
|
}]
|
|
};
|
|
|
|
cm.createHorizontalBarChart('client-usage-chart', chartData);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading client usage chart:', error);
|
|
this.showEmptyChart('client-usage-chart', 'Failed to load usage data');
|
|
}
|
|
}
|
|
|
|
showEmptyChart(canvasId, message) {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas) return;
|
|
const container = canvas.closest('.chart-container');
|
|
if (container) {
|
|
canvas.style.display = 'none';
|
|
let msg = container.querySelector('.empty-chart-msg');
|
|
if (!msg) {
|
|
msg = document.createElement('div');
|
|
msg.className = 'empty-chart-msg';
|
|
msg.style.cssText = 'display:flex;align-items:center;justify-content:center;height:200px;color:var(--fg4);font-size:0.9rem;';
|
|
container.appendChild(msg);
|
|
}
|
|
msg.textContent = message;
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const addBtn = document.getElementById('add-client');
|
|
if (addBtn) {
|
|
addBtn.onclick = () => this.showAddClientModal();
|
|
}
|
|
}
|
|
|
|
showAddClientModal() {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal active';
|
|
modal.innerHTML = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 class="modal-title">Create New API Client</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal').remove()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="new-client-name">Display Name</label>
|
|
<input type="text" id="new-client-name" placeholder="e.g. My Coding Assistant" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="new-client-id">Custom ID (Optional)</label>
|
|
<input type="text" id="new-client-id" placeholder="e.g. personal-app">
|
|
<small>Leave empty to generate automatically</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
|
|
<button class="btn btn-primary" id="confirm-create-client">Create Client</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('#confirm-create-client').onclick = async () => {
|
|
const name = modal.querySelector('#new-client-name').value;
|
|
const id = modal.querySelector('#new-client-id').value;
|
|
|
|
if (!name) {
|
|
window.authManager.showToast('Name is required', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await window.api.post('/clients', { name, client_id: id || null });
|
|
window.authManager.showToast(`Client "${name}" created`, 'success');
|
|
modal.remove();
|
|
this.loadClients();
|
|
} catch (error) {
|
|
window.authManager.showToast(error.message, 'error');
|
|
}
|
|
};
|
|
}
|
|
|
|
async deleteClient(id) {
|
|
if (!confirm(`Are you sure you want to delete client ${id}? This cannot be undone.`)) return;
|
|
|
|
try {
|
|
await window.api.delete(`/clients/${id}`);
|
|
window.authManager.showToast('Client deleted', 'success');
|
|
this.loadClients();
|
|
} catch (error) {
|
|
window.authManager.showToast(error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async editClient(id) {
|
|
try {
|
|
const client = await window.api.get(`/clients/${id}`);
|
|
this.showEditClientModal(client);
|
|
} catch (error) {
|
|
window.authManager.showToast(`Failed to load client: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
showEditClientModal(client) {
|
|
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.id}</h3>
|
|
<button class="modal-close" onclick="this.closest('.modal').remove()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-control">
|
|
<label for="edit-client-name">Display Name</label>
|
|
<input type="text" id="edit-client-name" value="${client.name || ''}" placeholder="e.g. My Coding Assistant">
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="edit-client-description">Description</label>
|
|
<textarea id="edit-client-description" rows="3" placeholder="Optional description">${client.description || ''}</textarea>
|
|
</div>
|
|
<div class="form-control">
|
|
<label for="edit-client-rate-limit">Rate Limit (requests/minute)</label>
|
|
<input type="number" id="edit-client-rate-limit" min="0" value="${client.rate_limit_per_minute || ''}" placeholder="Leave empty for unlimited">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="toggle-label">
|
|
<input type="checkbox" id="edit-client-active" ${client.is_active ? 'checked' : ''}>
|
|
<span>Active</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">Cancel</button>
|
|
<button class="btn btn-primary" id="confirm-edit-client">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
modal.querySelector('#confirm-edit-client').onclick = async () => {
|
|
const name = modal.querySelector('#edit-client-name').value.trim();
|
|
const description = modal.querySelector('#edit-client-description').value.trim();
|
|
const rateLimitVal = modal.querySelector('#edit-client-rate-limit').value;
|
|
const isActive = modal.querySelector('#edit-client-active').checked;
|
|
|
|
if (!name) {
|
|
window.authManager.showToast('Name is required', 'error');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name,
|
|
description: description || null,
|
|
is_active: isActive,
|
|
rate_limit_per_minute: rateLimitVal ? parseInt(rateLimitVal, 10) : null,
|
|
};
|
|
|
|
try {
|
|
await window.api.put(`/clients/${client.id}`, payload);
|
|
window.authManager.showToast(`Client "${name}" updated`, 'success');
|
|
modal.remove();
|
|
this.loadClients();
|
|
} catch (error) {
|
|
window.authManager.showToast(error.message, 'error');
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
window.initClients = async () => {
|
|
window.clientsPage = new ClientsPage();
|
|
};
|