addition of new dashboard
This commit is contained in:
108
frontend/src/components/UsageChart.astro
Normal file
108
frontend/src/components/UsageChart.astro
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
---
|
||||
|
||||
<section class="bg-white border border-surface-border rounded-lg overflow-hidden flex flex-col">
|
||||
<div class="p-lg border-b border-surface-border flex justify-between items-center flex-wrap gap-md">
|
||||
<h3 class="font-headline-sm text-headline-sm text-deep-indigo">API Usage & Storage</h3>
|
||||
<div id="usage-legend" class="flex gap-md flex-wrap"></div>
|
||||
</div>
|
||||
<div id="usage-chart" class="h-80 w-full relative p-lg"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
import { timeSeriesChart } from '../resources/lineChart.js';
|
||||
import { formatBytes } from '../resources/formatters.js';
|
||||
import { getFilters, activeModules, onFilters, toQuery } from '../resources/filters.js';
|
||||
import { toast } from '../resources/toast.js';
|
||||
|
||||
let chart: { destroy: () => void } | null = null;
|
||||
|
||||
function buildSeries(coreOn: boolean, auditOn: boolean) {
|
||||
const series = [];
|
||||
// Left axis: API call counts. Right axis ('right'): cumulative disk bytes.
|
||||
if (coreOn || auditOn) series.push({ key: 'total', label: 'Total API', color: '#4F378B' });
|
||||
if (coreOn) series.push({ key: 'core', label: 'Core API', color: '#8B6FD8' });
|
||||
if (auditOn) series.push({ key: 'audit', label: 'Audit API', color: '#cfbcff', dashed: true });
|
||||
if (coreOn) series.push({ key: 'coreDisk', label: 'Core Disk', color: '#31A9BA', axis: 'right' });
|
||||
if (auditOn) series.push({ key: 'auditDisk', label: 'Audit Disk', color: '#006874', axis: 'right', dashed: true });
|
||||
return series;
|
||||
}
|
||||
|
||||
function renderLegend(series: Array<{ label: string; color: string; dashed?: boolean }>) {
|
||||
const el = document.getElementById('usage-legend');
|
||||
if (!el) return;
|
||||
el.replaceChildren();
|
||||
for (const s of series) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'flex items-center gap-xs';
|
||||
const dot = document.createElement('span');
|
||||
dot.className = s.dashed ? 'w-3 h-0.5 border-t-2 border-dashed' : 'w-3 h-3 rounded-full';
|
||||
if (s.dashed) dot.style.borderColor = s.color;
|
||||
else dot.style.background = s.color;
|
||||
const label = document.createElement('span');
|
||||
label.className = 'text-label-sm font-label-sm';
|
||||
label.textContent = s.label;
|
||||
item.append(dot, label);
|
||||
el.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
function message(el: HTMLElement, text: string) {
|
||||
chart?.destroy();
|
||||
chart = null;
|
||||
el.replaceChildren();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'h-full flex items-center justify-center text-label-sm font-label-sm text-on-surface-variant/60 italic';
|
||||
div.textContent = text;
|
||||
el.appendChild(div);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const el = document.getElementById('usage-chart');
|
||||
if (!el) return;
|
||||
try {
|
||||
// One request for both usage counts and storage bytes (server runs a single
|
||||
// query and returns { usage: [...], storage: [...] }).
|
||||
const res = await fetch(`/api/dashboard/series?${toQuery()}`, { credentials: 'include' });
|
||||
if (!res.ok) return message(el, 'Sign in to view usage');
|
||||
|
||||
const payload = await res.json();
|
||||
const usage = payload.usage ?? [];
|
||||
const storage = payload.storage ?? [];
|
||||
const storageByDate = new Map(
|
||||
storage.map((d: { date: string; coreDisk: number; auditDisk: number }) => [d.date, d])
|
||||
);
|
||||
|
||||
const mods = activeModules();
|
||||
const coreOn = mods.includes('core');
|
||||
const auditOn = mods.includes('audit');
|
||||
|
||||
const rows = usage.map((d: { date: string; core: number; audit: number }) => {
|
||||
const sd = storageByDate.get(d.date) || { coreDisk: 0, auditDisk: 0 };
|
||||
return {
|
||||
date: d.date,
|
||||
core: d.core,
|
||||
audit: d.audit,
|
||||
total: (coreOn ? d.core : 0) + (auditOn ? d.audit : 0),
|
||||
coreDisk: sd.coreDisk || 0,
|
||||
auditDisk: sd.auditDisk || 0,
|
||||
};
|
||||
});
|
||||
|
||||
const series = buildSeries(coreOn, auditOn);
|
||||
renderLegend(series);
|
||||
|
||||
el.replaceChildren();
|
||||
chart?.destroy();
|
||||
chart = timeSeriesChart(el, { data: rows, series, formatRight: formatBytes });
|
||||
} catch (e) {
|
||||
console.error('usage chart', e);
|
||||
message(el, 'Failed to load usage');
|
||||
toast('Could not load the usage chart');
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render on filter changes; the date-range fetch + module toggles flow through here.
|
||||
onFilters(() => load());
|
||||
load();
|
||||
</script>
|
||||
Reference in New Issue
Block a user