feat: Add CSV export buttons to all dashboard tabs
Each tab now has an Export CSV button that downloads the currently displayed data as a CSV file, respecting the active time filter. Exports: usage-over-time, top-users, top-models, top-agents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
203ba7197e
commit
80a7e0ecee
3 changed files with 113 additions and 3 deletions
|
|
@ -329,6 +329,9 @@ body {
|
|||
.table-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
|
|
@ -376,6 +379,35 @@ tr:hover td {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Export Button */
|
||||
.btn-export {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 196, 7, 0.1);
|
||||
color: var(--accent-primary);
|
||||
border: 1px solid rgba(255, 196, 7, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
font-family: "Montserrat", system-ui, sans-serif;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: rgba(255, 196, 7, 0.2);
|
||||
border-color: rgba(255, 196, 7, 0.4);
|
||||
}
|
||||
|
||||
.tab-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@
|
|||
|
||||
<!-- Overview Tab -->
|
||||
<div class="tab-content active" id="tab-overview">
|
||||
<div class="tab-title-row">
|
||||
<div></div>
|
||||
<button class="btn-export" onclick="exportOverview()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button>
|
||||
</div>
|
||||
<div class="stats-grid" id="summaryCards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon cost"><i data-lucide="dollar-sign"></i></div>
|
||||
|
|
@ -106,7 +110,7 @@
|
|||
<!-- Users Tab -->
|
||||
<div class="tab-content" id="tab-users">
|
||||
<div class="table-container">
|
||||
<div class="table-header"><h3>Top Users by Cost</h3></div>
|
||||
<div class="table-header"><h3>Top Users by Cost</h3><button class="btn-export" onclick="exportUsers()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -126,7 +130,7 @@
|
|||
<!-- Models Tab -->
|
||||
<div class="tab-content" id="tab-models">
|
||||
<div class="table-container">
|
||||
<div class="table-header"><h3>Top Models by Cost</h3></div>
|
||||
<div class="table-header"><h3>Top Models by Cost</h3><button class="btn-export" onclick="exportModels()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -153,7 +157,7 @@
|
|||
<!-- Agents Tab -->
|
||||
<div class="tab-content" id="tab-agents">
|
||||
<div class="table-container">
|
||||
<div class="table-header"><h3>Top Agents by Cost</h3></div>
|
||||
<div class="table-header"><h3>Top Agents by Cost</h3><button class="btn-export" onclick="exportAgents()"><i data-lucide="download" style="width:16px;height:16px"></i> Export CSV</button></div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ const REFRESH_INTERVAL = 60000;
|
|||
let currentPeriod = '24h';
|
||||
let charts = {};
|
||||
|
||||
// --- Cached data for CSV export ---
|
||||
let cachedData = {
|
||||
usageOverTime: null,
|
||||
topUsers: null,
|
||||
topModels: null,
|
||||
topAgents: null,
|
||||
};
|
||||
|
||||
// --- API Helpers ---
|
||||
function apiHeaders() {
|
||||
const h = { 'Content-Type': 'application/json' };
|
||||
|
|
@ -59,6 +67,68 @@ const CHART_COLORS = [
|
|||
'#a78bfa', '#4ade80', '#fb923c', '#e879f9', '#22d3ee',
|
||||
];
|
||||
|
||||
// --- CSV Export ---
|
||||
function escCSV(val) {
|
||||
const s = String(val == null ? '' : val);
|
||||
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function downloadCSV(headers, rows, filename) {
|
||||
const lines = [headers.map(escCSV).join(',')];
|
||||
for (const row of rows) {
|
||||
lines.push(row.map(escCSV).join(','));
|
||||
}
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
function exportOverview() {
|
||||
const d = cachedData.usageOverTime;
|
||||
if (!d || !d.data.length) return alert('No data to export');
|
||||
downloadCSV(
|
||||
['Time', 'Tokens', 'Cost (USD)'],
|
||||
d.data.map(r => [r.time, r.tokens, r.cost.toFixed(6)]),
|
||||
`usage-over-time-${currentPeriod}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
function exportUsers() {
|
||||
const d = cachedData.topUsers;
|
||||
if (!d || !d.length) return alert('No data to export');
|
||||
downloadCSV(
|
||||
['Rank', 'Name', 'Email', 'Tokens', 'Cost (USD)', 'Conversations'],
|
||||
d.map((u, i) => [i + 1, u.name, u.email, u.totalTokens, u.totalCost.toFixed(4), u.conversationCount]),
|
||||
`top-users-${currentPeriod}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
function exportModels() {
|
||||
const d = cachedData.topModels;
|
||||
if (!d || !d.length) return alert('No data to export');
|
||||
downloadCSV(
|
||||
['Rank', 'Model', 'Prompt Tokens', 'Completion Tokens', 'Prompt Cost (USD)', 'Completion Cost (USD)', 'Total Cost (USD)'],
|
||||
d.map((m, i) => [i + 1, m.model, m.promptTokens, m.completionTokens, m.promptCost.toFixed(4), m.completionCost.toFixed(4), m.totalCost.toFixed(4)]),
|
||||
`top-models-${currentPeriod}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
function exportAgents() {
|
||||
const d = cachedData.topAgents;
|
||||
if (!d || !d.length) return alert('No data to export');
|
||||
downloadCSV(
|
||||
['Rank', 'Agent Name', 'Underlying Model', 'Provider', 'Tokens', 'Cost (USD)', 'Conversations'],
|
||||
d.map((a, i) => [i + 1, a.agentName, a.underlyingModel, a.provider, a.totalTokens, a.totalCost.toFixed(4), a.conversationCount]),
|
||||
`top-agents-${currentPeriod}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
// --- Tab Navigation ---
|
||||
document.querySelectorAll('.nav-links li').forEach(li => {
|
||||
li.addEventListener('click', () => {
|
||||
|
|
@ -99,6 +169,7 @@ async function loadSummary() {
|
|||
async function loadUsageOverTime() {
|
||||
try {
|
||||
const d = await fetchAPI('usage-over-time');
|
||||
cachedData.usageOverTime = d;
|
||||
const labels = d.data.map(p => {
|
||||
if (d.bucketType === 'hour') {
|
||||
const dt = new Date(p.time);
|
||||
|
|
@ -210,6 +281,7 @@ async function loadCostBreakdown() {
|
|||
async function loadTopUsers() {
|
||||
try {
|
||||
const d = await fetchAPI('top-users?limit=20');
|
||||
cachedData.topUsers = d;
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
tbody.innerHTML = d.map((u, i) => `
|
||||
<tr>
|
||||
|
|
@ -227,6 +299,7 @@ async function loadTopUsers() {
|
|||
async function loadTopModels() {
|
||||
try {
|
||||
const d = await fetchAPI('top-models?limit=20');
|
||||
cachedData.topModels = d;
|
||||
const tbody = document.getElementById('modelsTableBody');
|
||||
tbody.innerHTML = d.map((m, i) => `
|
||||
<tr>
|
||||
|
|
@ -270,6 +343,7 @@ async function loadTopModels() {
|
|||
async function loadTopAgents() {
|
||||
try {
|
||||
const d = await fetchAPI('top-agents?limit=20');
|
||||
cachedData.topAgents = d;
|
||||
const tbody = document.getElementById('agentsTableBody');
|
||||
if (d.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--text-muted)">No agent usage found in this period</td></tr>';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue