feat(admin): expose group provider/model access

GET /admin/api/usage's group rows now carry the group's configured
Providers/Models/MCPServers/Agents (GroupConfig, llmgateway.go), echoed
exactly as configured -- never resolved/expanded to the full catalog
server-side. Empty/unset stays omitted (omitempty), matching
GroupConfig's own JSON convention and group.allowsX's empty-means-all
semantics (auth.go): the dashboard can tell 'restricted to these N' apart
from 'unrestricted' without the payload growing with catalog size.

groupSummary (auth.go) gains providers/models/mcpServers/agents, carried
through from authStore.snapshot unchanged. adminUsageEntryView (admin.go)
gains the matching JSON fields, populated only for group entries -- a
user or the total scope row never carries them, mirroring GroupName's
own user-only convention.

The Usage view's group accordion (webui/UsageView.vue) renders an Access
block above the existing stat grid: Badge chips for explicit providers,
ModelChip for explicit models (both shadcn/existing components, no
hand-rolled chips), or muted 'All providers'/'All models' text when
unrestricted. Display-only this round -- not wired into the user/group
search filter. types/api.ts's AdminUsageEntryView mirrors the new
optional fields.

Tests: admin_test.go's new table-driven TestAdminUsage_GroupAccessLists
covers a restricted group's exact lists, an unrestricted group's
empty/omitted lists, and that a user/total row never carries them.
This commit is contained in:
2026-08-21 13:21:58 +01:00
parent bb4fa4420e
commit 03bc67a5f0
5 changed files with 174 additions and 2 deletions
+33
View File
@@ -3,9 +3,11 @@ import type { SortingState } from '@tanstack/vue-table'
import { getCoreRowModel, getSortedRowModel, useVueTable } from '@tanstack/vue-table'
import { computed, reactive, ref, watch } from 'vue'
import ModelChip from '@/components/ModelChip.vue'
import SearchInput from '@/components/SearchInput.vue'
import SortHeaderButton from '@/components/SortHeaderButton.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { valueUpdater } from '@/components/ui/table'
import UsageTable from '@/components/UsageTable.vue'
@@ -284,6 +286,37 @@ watch(userQuery, (value) => {
</span>
</AccordionTrigger>
<AccordionContent>
<!--
Access block (group-access-display task): the group's
configured provider/model access, display-only — not part
of the user/group search filter above (row.original.providers/
models are plain display data, never touched by
groupMatches/userMatches in lib/usage-search.ts).
-->
<div class="mb-3 grid gap-3 sm:grid-cols-2">
<div>
<p class="mb-1.5 text-xs font-medium text-muted-foreground">Providers</p>
<div v-if="row.original.providers?.length" class="flex flex-wrap gap-1.5">
<Badge
v-for="providerName in row.original.providers"
:key="providerName"
as="span"
variant="secondary"
class="font-normal"
>
{{ providerName }}
</Badge>
</div>
<p v-else class="text-sm text-muted-foreground">All providers</p>
</div>
<div>
<p class="mb-1.5 text-xs font-medium text-muted-foreground">Models</p>
<div v-if="row.original.models?.length" class="flex flex-wrap gap-1.5">
<ModelChip v-for="modelId in row.original.models" :key="modelId" :id="modelId" />
</div>
<p v-else class="text-sm text-muted-foreground">All models</p>
</div>
</div>
<dl class="mb-3 grid grid-cols-2 gap-x-6 gap-y-1.5 text-sm sm:grid-cols-4">
<div>
<dt class="text-xs text-muted-foreground">Limits</dt>
+13
View File
@@ -67,6 +67,19 @@ export interface AdminUsageEntryView {
kind: string
id: string
groupName?: string
/**
* The group's configured access lists (admin.go: adminUsageEntryView's
* Providers/Models/MCPServers/Agents) — set only on a group entry (kind
* === 'group'), never on a user or the total entry, mirroring groupName's
* own user-only convention above. Undefined/empty means unrestricted
* (GroupConfig's own empty-means-all semantics, llmgateway.go) — this is
* the group's configured glob list exactly as written, never expanded to
* the full provider/model catalog.
*/
providers?: string[]
models?: string[]
mcpServers?: string[]
agents?: string[]
requestsPerMinute: number
requestsPerDay: number
tokensInPerDay: number