refactor(webui): rename Overview tab to Providers

Mechanical rename: OverviewView.vue -> ProvidersView.vue (file, component
import, tab label/value in App.vue), plus doc-comment/README references
across webui/ that named the view by its old tab label.

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