mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-15 05:29:28 +00:00
feat(webui): vue admin panel source
Vue 3 + TypeScript + Pinia + Tailwind v4 + shadcn-vue + Chart.js admin panel source under webui/, built with Vite. Views: Overview (providers, redis/cache/retry status, model aliases), Usage (per-user/per-group/total tables with tokensIn/tokensOut/cost + limits), Charts (scope selector, req/tokens(stacked in-out)/cost metric tabs, 24h/30d/12mo window switcher, fed by GET /admin/api/usage/history). Pinia stores: auth (sessionStorage key gate, x-api-key header, 401/403 re-prompt), dashboard (5s poll), history (fetch-on-selection + 30s refresh). Design: deliberate neutral warm-neutral palette with a signature teal accent (no default Inter/purple-gradient/glassmorphism), real typographic hierarchy via the platform UI/mono font stacks (zero external assets - no Google Fonts), dark+light via prefers-color-scheme (no class toggle, no theme switcher). Charts: zero-based bars, honest axes, Okabe-Ito colorblind-safe series, tokens-in/out as a distinguishable stacked pair. vite.config.ts: base '/admin/' (matches admin.go's route layout) and build.modulePreload.polyfill:false (no inline bootstrap script, so the tightened admin CSP needs no 'unsafe-inline'). Code-split: Chart.js (ChartsView) loads lazily, not in the initial bundle. generate.mjs reads dist/ after `vite build` and writes ../admin_assets_gen.go (not committed by this change) - base64-encodes every asset, decoded once at package-var init; documented rationale in the script's own header comment. Hygiene: webui/go.mod walls webui/ off from the repo root's `go build`/ `vet`/`test ./...` (npm's node_modules bundles a stray .go file that would otherwise be swept in); tools/yaegi-check/main.go excludes webui/ from its GOPATH copy for the same reason. webui/ export-ignored (.gitattributes) from release source tarballs; webui/node_modules/ gitignored. tsconfig.app.json/tsconfig.node.json's JSONC comments dropped (TS itself is fine with either; the global pre-commit JSON validator is not) - no config values changed.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { formatCost, formatLimits } from '@/lib/format'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableEmpty,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import type { AdminUsageEntryView } from '@/types/api'
|
||||
|
||||
/**
|
||||
* UsageTable renders one scope kind's usage rows (users or groups) — the
|
||||
* shared table shape both need (vue.md: extract before you paste it
|
||||
* twice). The second column differs per kind: a user's row shows the group
|
||||
* it belongs to; a group's row shows its member count (looked up from the
|
||||
* Overview response, which is the only place member counts live).
|
||||
*/
|
||||
defineProps<{
|
||||
idLabel: string
|
||||
entries: AdminUsageEntryView[]
|
||||
secondaryColumnLabel: string
|
||||
secondaryValue: (entry: AdminUsageEntryView) => string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ idLabel }}</TableHead>
|
||||
<TableHead>{{ secondaryColumnLabel }}</TableHead>
|
||||
<TableHead>Limits</TableHead>
|
||||
<TableHead class="text-right">req/min</TableHead>
|
||||
<TableHead class="text-right">req/day</TableHead>
|
||||
<TableHead class="text-right">tokIn/day</TableHead>
|
||||
<TableHead class="text-right">tokOut/day</TableHead>
|
||||
<TableHead class="text-right">tokIn/month</TableHead>
|
||||
<TableHead class="text-right">tokOut/month</TableHead>
|
||||
<TableHead class="text-right">cost/day</TableHead>
|
||||
<TableHead class="text-right">cost/month</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableEmpty v-if="entries.length === 0" :colspan="11" class="text-muted-foreground">
|
||||
none
|
||||
</TableEmpty>
|
||||
<TableRow
|
||||
v-for="entry in entries"
|
||||
:key="entry.id"
|
||||
:class="entry.storeDown ? 'bg-destructive/10' : undefined"
|
||||
>
|
||||
<TableCell class="font-medium">{{ entry.id }}</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{ secondaryValue(entry) }}</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{ formatLimits(entry.limits) }}</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.requestsPerMinute }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.requestsPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensInPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensOutPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensInPerMonth }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensOutPerMonth }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : formatCost(entry.costPerDayMicroUsd) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : formatCost(entry.costPerMonthMicroUsd) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</template>
|
||||
Reference in New Issue
Block a user