Files
traefik-llmgateway/webui/src/components/UsageBar.vue
T
lukaszraczylo ee49467e64 feat(webui): redesign the admin dashboard around admin tasks
A sidebar replaces the flat tabs with seven pages, and one filter bar
sets the time range, comparison period and scope for all of them.

- Home: spend against budget, forecast, request rate, error rate,
  open breakers, top users and models, recent events and warnings.
- Spend: spend over time with comparison, budget burn-down, an
  attribution drill-down from group to user to model, pricing health
  and cost avoided by free models.
- Consumers: a directory with last seen and limit bars, an access
  matrix, and a detail page per user.
- Models: the catalog with billing prices, fleet latency and error
  rates, and provider health.
- Reliability: error, timeout, failover, 429 and 402 trends, events,
  and latency per provider.
- Config: the redacted config and a change helper that emits YAML or
  users.json snippets to paste into git.
- MCP & Agents: callers per target.

Page state lives in the URL hash, and old tab links map to the new
pages. The replaced views are removed. admin_assets_gen.go is
regenerated.

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

75 lines
2.9 KiB
Vue

<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { computed } from 'vue'
import { cn } from '@/lib/utils'
import { ratioTier } from '@/lib/usage-bars'
/**
* UsageBar renders one BudgetRatio (lib/usage-bars.ts) as a compact
* horizontal meter — F1, usage-vs-limit bars. A real `role="meter"`
* (not a `<progress>` element: a meter reports a measurement against a
* range, which is exactly a used/limit reading, where `<progress>`
* implies a task moving toward completion) carries the accessible
* reading via `aria-valuenow`/`aria-valuemin`/`aria-valuemax`/
* `aria-valuetext`, so a screen reader announces the same number a
* sighted reader sees in the fill width and the `title` tooltip.
*
* Used across the panel (usage-columns.ts's numeric cells,
* ConsumerDirectory.vue's group detail grid, UserDetail.vue's headroom
* bars) — every caller passes a fully-computed BudgetRatio plus its own
* human-readable `valueText`, so this component has no formatting
* opinion of its own (formatCost/formatExactInt stay the caller's job,
* same "logic lives in lib/, not the component" split every other
* shared component in this panel follows).
*/
const props = defineProps<{
/** used / limit — may exceed 1 (a scope's usage window can roll past its limit before the next request is rejected). */
ratio: number
/** The accessible name for this meter, e.g. "req/day" or "cost/month". */
label: string
/** The human-readable "used of limit" text — both the `title` tooltip and `aria-valuetext`, e.g. "500 / 1,000" or "$1.20 / $5.00". */
valueText: string
class?: HTMLAttributes['class']
}>()
const tier = computed(() => ratioTier(props.ratio))
/**
* widthPct clamps the visible fill (and, via valueNow below, the
* announced aria-valuenow) to 100% — a ratio above 1.0 (over the limit)
* still reads as a full bar, with the RED tier and valueText's own real
* "used / limit" numbers carrying the "how far over" detail instead of an
* overflowing bar or an aria-valuenow exceeding its own declared
* aria-valuemax (WAI-ARIA meter role: valuenow must stay within
* valuemin/valuemax).
*/
const widthPct = computed(() => Math.min(100, Math.max(0, props.ratio * 100)))
const valueNow = computed(() => Math.round(widthPct.value))
const TIER_FILL_CLASS: Record<ReturnType<typeof ratioTier>, string> = {
normal: 'bg-primary',
amber: 'bg-bar-amber',
red: 'bg-destructive',
}
</script>
<template>
<span
role="meter"
:aria-label="label"
:aria-valuenow="valueNow"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuetext="valueText"
:title="valueText"
:class="cn('relative inline-block h-1.5 w-14 shrink-0 overflow-hidden rounded-full bg-muted align-middle', props.class)"
>
<span
class="absolute inset-y-0 left-0 rounded-full"
:class="TIER_FILL_CLASS[tier]"
:style="{ width: `${widthPct}%` }"
/>
</span>
</template>