diff --git a/webui/README.md b/webui/README.md index 49526e4..3c01dd3 100644 --- a/webui/README.md +++ b/webui/README.md @@ -68,17 +68,19 @@ npm run build # what `make admin-ui` runs, plus webui/generate.mjs ## Structure - `src/stores/` — Pinia: `auth` (the sessionStorage key gate), `dashboard` - (Overview/Usage, 5s poll), `history` (Charts, fetch-on-selection + 30s + (Providers/Usage, 5s poll), `history` (Charts, fetch-on-selection + 30s refresh). -- `src/components/` — `AuthGate`, `OverviewView`, `UsageView`, +- `src/components/` — `AuthGate`, `ProvidersView` (the "Providers" tab — + named for its UI label; it still fetches GET /admin/api/overview, whose + own name is unchanged API surface), `UsageView`, `ChartsView`/`UsageChart`, `UsageTable`, `DataTable` (the shared sortable-table wrapper — shadcn-vue's DataTable pattern, `@tanstack/ vue-table`, generic over row type), `SearchInput` (the shared search-box — icon + shadcn `Input` + shadcn `Button` clear button — every search filter in the panel renders); `ui/` holds the shadcn-vue primitives this panel actually uses (Accordion, Card, Table, Tabs, Button, Input, Alert, - Select, Badge) — nothing installed and unused. Providers (Overview) and - Groups (Usage) render as a real Accordion — expanding a group shows its + Select, Badge) — nothing installed and unused. Providers and Groups + (Usage) render as a real Accordion — expanding a group shows its member users' own usage rows, a client-side join on `AdminUsageEntryView.groupName`. - `src/composables/` — `useThemeColors` (Chart.js axis/grid/legend colors @@ -87,13 +89,14 @@ npm run build # what `make admin-ui` runs, plus webui/generate.mjs - `src/lib/` — `api.ts` (the `/admin/api/*` fetch wrapper), `format.ts`, `key-storage.ts`, `chart-setup.ts`, `search-expand.ts` (search-aware expand state, adapted to drive Accordion's v-model — shared by - OverviewView's providers and UsageView's groups), `usage-search.ts` + ProvidersView's providers and UsageView's groups), `usage-search.ts` (shared user/group query-matching helpers, used by UsageView and ChartsView), `usage-columns.ts` (shared `ColumnDef`s for every usage table). - `src/types/api.ts` — TypeScript mirrors of admin.go's JSON response shapes (`adminOverviewResponse`, `adminUsageResponse`, - `usageHistoryResponse`). + `usageHistoryResponse`) — these keep their backend names; only the + frontend tab label/component changed (see `ProvidersView` above). - `generate.mjs` — reads `dist/` after `vite build` and writes `../admin_assets_gen.go`; see its own header comment for why every asset is base64-encoded rather than embedded as a raw string literal. diff --git a/webui/src/App.vue b/webui/src/App.vue index bdd38b6..d70fc6f 100644 --- a/webui/src/App.vue +++ b/webui/src/App.vue @@ -4,7 +4,7 @@ import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons' import { computed, defineAsyncComponent, onMounted, ref } from 'vue' import AuthGate from '@/components/AuthGate.vue' -import OverviewView from '@/components/OverviewView.vue' +import ProvidersView from '@/components/ProvidersView.vue' import { Button } from '@/components/ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import UsageView from '@/components/UsageView.vue' @@ -13,7 +13,7 @@ import { useDashboardStore } from '@/stores/dashboard' // Lazy: Chart.js (UsageChart.vue's dependency, pulled in transitively) is // the single largest piece of this bundle, and most admin sessions open on -// Overview/Usage. Splitting it into its own chunk keeps the initial load +// Providers/Usage. Splitting it into its own chunk keeps the initial load // lean; it fetches once, on first visit to the Charts tab. const ChartsView = defineAsyncComponent(() => import('@/components/ChartsView.vue')) @@ -24,7 +24,11 @@ const GITHUB_REPO_URL = 'https://github.com/lukaszraczylo/traefik-llmgateway' const auth = useAuthStore() const dashboard = useDashboardStore() -const activeView = ref<'overview' | 'usage' | 'charts'>('overview') +// The tab is labeled "Providers" (operator rename); the backend endpoint it +// fetches from, GET /admin/api/overview, and the store's own `overview` +// field (AdminOverviewResponse) keep their original names unchanged — the +// rename is UI-only, not an API surface change. +const activeView = ref<'providers' | 'usage' | 'charts'>('providers') const statusText = computed(() => { if (dashboard.error) return `refresh failed: ${dashboard.error}` @@ -65,12 +69,12 @@ onMounted(() => { - Overview + Providers Usage Charts - - + + diff --git a/webui/src/components/ModelChip.vue b/webui/src/components/ModelChip.vue index 9b2698e..cb742f7 100644 --- a/webui/src/components/ModelChip.vue +++ b/webui/src/components/ModelChip.vue @@ -7,7 +7,7 @@ import { copyText } from '@/lib/clipboard' /** * ModelChip is one clickable, copyable model-id chip in a provider's - * expanded model list (OverviewView.vue's provider-model accordion). id + * expanded model list (ProvidersView.vue's provider-model accordion). id * is the full routable id (lib/format.ts's routableModelId) — what * actually gets copied, so a pasted value works straight into a * `model: "..."` request field. diff --git a/webui/src/components/OverviewView.vue b/webui/src/components/ProvidersView.vue similarity index 97% rename from webui/src/components/OverviewView.vue rename to webui/src/components/ProvidersView.vue index b53fba9..93a45c1 100644 --- a/webui/src/components/OverviewView.vue +++ b/webui/src/components/ProvidersView.vue @@ -25,6 +25,11 @@ import { type ExpandState, clearExpandOverrides, computeExpandedItems, toggleIte import { useDashboardStore } from '@/stores/dashboard' import type { AdminAliasView, AdminProviderView } from '@/types/api' +// This component backs the "Providers" tab (App.vue) — a UI-label rename +// only. The data it renders still comes from GET /admin/api/overview +// (admin.go's adminOverviewResponse) and the store field below keeps that +// same "overview" name, since the backend route/type names are unchanged +// API surface. const dashboard = useDashboardStore() const overview = computed(() => dashboard.overview) diff --git a/webui/src/components/SearchInput.vue b/webui/src/components/SearchInput.vue index 18d55b6..4d27606 100644 --- a/webui/src/components/SearchInput.vue +++ b/webui/src/components/SearchInput.vue @@ -9,7 +9,7 @@ import { cn } from '@/lib/utils' /** * SearchInput is the one search-box control every filter in this panel - * uses — OverviewView's model search, UsageView's user/group search, and + * uses — ProvidersView's model search, UsageView's user/group search, and * ChartsView's scope-picker search each grew an identical ~22-line * magnifying-glass-icon + Input + clear-button block; extracted into one * component (vue.md: "if you've written it twice, you owe an diff --git a/webui/src/components/UsageView.vue b/webui/src/components/UsageView.vue index 54858cf..ddbe42d 100644 --- a/webui/src/components/UsageView.vue +++ b/webui/src/components/UsageView.vue @@ -36,7 +36,7 @@ function memberCountOf(entry: AdminUsageEntryView): string { } // --- user/group search filter (operator feature, mirrors -// OverviewView.vue's model search via the shared SearchInput component and +// ProvidersView.vue's model search via the shared SearchInput component and // useSearchQuery composable — same styling, same clear-button behavior. // Matching logic lives in lib/usage-search.ts, which ChartsView.vue's own // scope-picker filter calls directly too (groupMatches, userMatches) — no @@ -64,9 +64,9 @@ const filteredGroups = computed(() => { * ONLY via a member, never via their own name — the auto-expand target * set (search-expand.ts's computeExpandedItems `matchingIds`). * - * This is a DELIBERATE divergence from OverviewView.vue, not an - * oversight, and NOT because Overview lacks a provider-name match — - * it doesn't lack one: providerMatches there (OverviewView.vue) checks + * This is a DELIBERATE divergence from ProvidersView.vue, not an + * oversight, and NOT because Providers lacks a provider-name match — + * it doesn't lack one: providerMatches there (ProvidersView.vue) checks * each model's full ROUTABLE id, `${provider}/${model}` * (lib/format.ts's routableModelId), so a query like "openai" matches * every "openai/..." model id and the whole provider auto-expands with @@ -104,7 +104,7 @@ function visibleMembers(group: AdminUsageEntryView): AdminUsageEntryView[] { return matchingMembersOfGroup(group, users, normalizedQuery.value) } -/** groupsEmptyMessage mirrors OverviewView.vue's aliasEmptyMessage three-way pattern: distinguishes nothing configured from nothing matching the active query, so an empty Groups card never reads the same regardless of why it's empty. */ +/** groupsEmptyMessage mirrors ProvidersView.vue's aliasEmptyMessage three-way pattern: distinguishes nothing configured from nothing matching the active query, so an empty Groups card never reads the same regardless of why it's empty. */ const groupsEmptyMessage = computed(() => !usage.value?.groups.length ? 'none configured' : hasQuery.value ? `no groups match "${userQuery.value}"` : 'none', ) @@ -137,7 +137,7 @@ const usersEmptyMessage = computed(() => // --- Groups accordion (operator directive) --- // // Groups render as a shadcn-vue Accordion, same component and rationale -// as Providers (OverviewView.vue) — expanding a group shows its member +// as Providers (ProvidersView.vue) — expanding a group shows its member // users' own usage rows below it. A group's row cannot ALSO be a literal // row for the identical reason Providers' old expandable row was // retired: AccordionItem wraps its trigger+content in a
, illegal @@ -188,7 +188,7 @@ const toolbarHeaders = computed(() => /** * Which groups are open — driven by the SAME tested lib/search-expand.ts - * state OverviewView.vue uses for providers (generalized from + * state ProvidersView.vue uses for providers (generalized from * provider-expand.ts specifically so this view could reuse it, rather * than fork a second copy — see that module's own doc comment). The * adapter shape is the identical get/set computed pattern @@ -221,7 +221,7 @@ const expandedGroupValues = computed({ // A stale manuallyCollapsed suppression from one search must never // silently carry into a later, unrelated one — see ExpandState's own doc // comment. Watching userQuery (not just the clear button) covers -// backspacing to empty too — same convention as OverviewView.vue. +// backspacing to empty too — same convention as ProvidersView.vue. watch(userQuery, (value) => { if (value.trim() === '') clearExpandOverrides(groupExpandState) }) diff --git a/webui/src/composables/useSearchQuery.ts b/webui/src/composables/useSearchQuery.ts index 7df1dcf..c061449 100644 --- a/webui/src/composables/useSearchQuery.ts +++ b/webui/src/composables/useSearchQuery.ts @@ -2,12 +2,12 @@ import { computed, ref } from 'vue' /** * useSearchQuery is the one raw-query → {normalized, hasQuery} adapter - * every search filter in this panel uses — OverviewView's model search, + * every search filter in this panel uses — ProvidersView's model search, * UsageView's user/group search, ChartsView's scope-picker search all had * their own copy of this identical query/normalizedQuery/hasQuery * pairing; extracted so it exists once (vue.md: "if you've written it * twice, you owe an abstraction"). Callers still own their own matching - * logic entirely (OverviewView's modelMatches/providerMatches, + * logic entirely (ProvidersView's modelMatches/providerMatches, * lib/usage-search.ts's helpers) — this only normalizes the raw input the * same way every call site already agreed on: trimmed, lowercased. * diff --git a/webui/src/lib/search-expand.spec.ts b/webui/src/lib/search-expand.spec.ts index d0d0da9..fdcb44a 100644 --- a/webui/src/lib/search-expand.spec.ts +++ b/webui/src/lib/search-expand.spec.ts @@ -118,12 +118,12 @@ describe('an item manually opened before search, still matches, clicked closed d }) // This module is domain-agnostic on what an "id" represents — it started as -// OverviewView.vue's provider names, but UsageView.vue now feeds it group +// ProvidersView.vue's provider names, but UsageView.vue now feeds it group // ids too (see that view's own doc comment: matchingIds there is every // group that matches ONLY via a member, never via its own name). These two // cases exist to pin that the module behaves identically for that second // domain, not because anything here differs by id shape. -describe('generic ids (Usage group ids, not just Overview provider names)', () => { +describe('generic ids (Usage group ids, not just Providers provider names)', () => { it('auto-expands and can be collapsed/overridden identically for a group id', () => { const state = newState() const matches = ['team-a'] diff --git a/webui/src/lib/search-expand.ts b/webui/src/lib/search-expand.ts index 4b27761..cbc0f36 100644 --- a/webui/src/lib/search-expand.ts +++ b/webui/src/lib/search-expand.ts @@ -1,6 +1,6 @@ /** * Pure expand/collapse state for a search-filterable shadcn-vue Accordion — - * shared by OverviewView.vue (providers) and UsageView.vue (groups), and + * shared by ProvidersView.vue (providers) and UsageView.vue (groups), and * generalized (originally provider-expand.ts) so both consume ONE tested * implementation instead of two copies of the same logic (vue.md: "if * you've written it twice, you owe an abstraction"). Extracted out of the @@ -10,7 +10,7 @@ * directly, including the full flagged sequence. * * Two sets track state, keyed on each accordion item's own string id (a - * provider name in Overview, a group id in Usage — this module has no + * provider name in Providers, a group id in Usage — this module has no * opinion on what the id means or on what counts as a "match"; callers * decide that and pass the resulting id list in): * - manuallyExpanded: items the user explicitly opened, independent of any @@ -34,7 +34,7 @@ export interface ExpandState { * manuallyExpanded plus every id in matchingIds that is not in * manuallyCollapsed (auto-expand every match, unless the user explicitly * closed that one). The caller decides what "matching" means for its own - * view, and this module has no opinion on it either way — OverviewView.vue + * view, and this module has no opinion on it either way — ProvidersView.vue * passes every provider that itself matched at all (every match there * implies at least one matching model, so every match auto-expands); * UsageView.vue instead passes only the subset of its own matches that diff --git a/webui/src/lib/usage-search.ts b/webui/src/lib/usage-search.ts index c8ce5af..d2ceeb9 100644 --- a/webui/src/lib/usage-search.ts +++ b/webui/src/lib/usage-search.ts @@ -6,7 +6,7 @@ import type { AdminUsageEntryView } from '@/types/api' * identical semantics, ONE implementation — no second copy of this logic). * * Every `query` parameter here is expected to already be trimmed and - * lowercased by the caller, mirroring OverviewView.vue's own + * lowercased by the caller, mirroring ProvidersView.vue's own * normalizedQuery convention (modelMatches/aliasMatches there never * re-normalize either) — these functions stay pure string/array * operations, with the caller owning the single source of truth for "is a diff --git a/webui/src/stores/dashboard.ts b/webui/src/stores/dashboard.ts index c8c7e09..35d33f5 100644 --- a/webui/src/stores/dashboard.ts +++ b/webui/src/stores/dashboard.ts @@ -4,12 +4,14 @@ import { AdminApiError, adminFetch } from '@/lib/api' import { useAuthStore } from '@/stores/auth' import type { AdminOverviewResponse, AdminUsageResponse } from '@/types/api' -/** How often the Overview/Usage views poll (matches the replaced vanilla-JS page). */ +/** How often the Providers/Usage views poll (matches the replaced vanilla-JS page). */ const POLL_MS = 5000 /** * useDashboardStore polls GET /admin/api/overview and GET /admin/api/usage - * together every POLL_MS, feeding the Overview and Usage views. A single + * together every POLL_MS, feeding the Providers and Usage views (the store's + * own `overview` field keeps the backend's name — see ProvidersView.vue's + * doc comment for the UI-label/API-name split). A single * store (not two) because both routes are always fetched together — the * old vanilla-JS page's own Promise.all([overview, usage]) pattern. */