Files
traefik-llmgateway/webui/src/lib/download.ts
T
lukaszraczylo 4261efc41b feat(webui): add limit bars, forecasts, events, exports and URL state
The Usage tab shows a bar for each configured limit. It turns amber at
80% and red at 100%. The tab also shows rejected requests per day, a
projected month-end cost with budget headroom, and CSV export for users
and groups. Names link to Charts with the matching scope.

The Charts tab ranks models over the full 24h, 30d or 12mo window. A
provider filter is applied on the server, and the ranking can be
exported as CSV. A new Events tab lists recent failures, filtered by
kind and user. The selected tab, filters and scope live in the URL
hash. Polling stops while the page is hidden. Views show which replica
answered, and a banner lists configuration warnings.

CSV cells that start with a formula character are neutralised. Links
sit outside the accordion buttons, so both controls keep their
accessible names. admin_assets_gen.go is regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
2026-09-23 13:09:57 +01:00

59 lines
3.0 KiB
TypeScript

import { copyText } from '@/lib/clipboard'
/** CSV_MIME_TYPE is the Blob type every export in this panel downloads as — text/csv with an explicit UTF-8 charset, so a spreadsheet importer opening the file directly (not pasting it) reads non-ASCII bytes correctly. */
export const CSV_MIME_TYPE = 'text/csv;charset=utf-8;'
/** The outcome downloadText actually achieved, so a caller (CsvExportButton.vue) can show the right feedback: a real file save, or the clipboard/selection fallback (copyText's own two success modes), or neither. */
export type DownloadOutcome = 'downloaded' | 'copied' | 'selected' | 'failed'
/**
* REVOKE_DELAY_MS (P5 review fix) defers URL.revokeObjectURL past the
* click() call below — revoking SYNCHRONOUSLY, right after click(), can
* cancel the download in some WebKit and older Firefox builds: click()
* only STARTS the download, it does not wait for the browser to finish
* reading the blob, so revoking on the very next line raced the read. A
* full second is generous rather than the minimal `setTimeout(fn, 0)`
* (which only guarantees one macrotask, not "download started") — the
* blob itself is only released a little later either way, and this is a
* one-off per export click, never a hot path.
*/
const REVOKE_DELAY_MS = 1000
/**
* downloadText saves `content` as a local file named `filename` (Q8,
* coordinator decision: Blob + `<a download>`, the standard no-server-
* round-trip browser download idiom — no File System Access API, which
* is Chromium-only and needs a user-activation prompt this panel's CSP
* would complicate for no benefit here).
*
* Guarded for a non-DOM environment (this module is never unit-tested —
* see WP-B1's own test plan, lib/download.ts has no .spec.ts — but a
* defensive `typeof document` check keeps it from throwing if it is ever
* imported somewhere `document` is not global, matching useTabHash.ts's
* own `hasWindow` convention). The clipboard fallback (copyText, Q8) runs
* ONLY when the Blob/URL/anchor machinery itself is unavailable or throws
* (a Blob/URL API genuinely unsupported, or the synthetic click rejected)
* — never merely because a download was silently blocked by the browser,
* which raises no exception here to catch. Returns the actual outcome
* (never discarded — CsvExportButton.vue renders it as brief feedback) so
* a caller can tell "downloaded" from the fallback's own "copied"/
* "selected"/"failed".
*/
export async function downloadText(filename: string, content: string, mimeType = CSV_MIME_TYPE): Promise<DownloadOutcome> {
if (typeof document === 'undefined') return 'failed'
try {
const blob = new Blob([content], { type: mimeType })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY_MS)
return 'downloaded'
} catch {
return copyText(content)
}
}