mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-16 05:37:28 +00:00
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
This commit is contained in:
@@ -5,20 +5,38 @@ import DataTable from '@/components/DataTable.vue'
|
||||
import SearchInput from '@/components/SearchInput.vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useSearchQuery } from '@/composables/useSearchQuery'
|
||||
import { targetColumns } from '@/lib/target-columns'
|
||||
import { composeTargetId, targetColumns } from '@/lib/target-columns'
|
||||
import type { TargetKind } from '@/lib/target-columns'
|
||||
import { useDashboardStore } from '@/stores/dashboard'
|
||||
import type { AdminTargetView } from '@/types/api'
|
||||
|
||||
// This component backs the "MCP & Agents" tab (App.vue) — GET
|
||||
// /admin/api/targets (admin.go: adminTargetsResponse, Feature B v0.21).
|
||||
// This component backs the "MCP & Agents" page (redesign-plan.md section
|
||||
// 3.4, TargetsPage.vue) — GET /admin/api/targets (admin.go:
|
||||
// adminTargetsResponse, Feature B v0.21).
|
||||
const dashboard = useDashboardStore()
|
||||
const targets = computed(() => dashboard.targets)
|
||||
|
||||
/**
|
||||
* select (redesign-plan.md section 3.4's "TargetCallers.vue expander")
|
||||
* fires with the composed "mcp/{name}"/"agent/{name}" id and the target's
|
||||
* own display name — TargetsPage.vue listens and renders TargetCallers.vue
|
||||
* for whichever target the reader last clicked. Two separate
|
||||
* targetColumns() instantiations below (MCP servers, Agents) close over
|
||||
* their own fixed `kind` so a click always composes the correct prefix,
|
||||
* without this component needing to carry a per-row kind field itself.
|
||||
*/
|
||||
const emit = defineEmits<{ select: [id: string, label: string] }>()
|
||||
|
||||
function onSelect(kind: TargetKind): (t: AdminTargetView) => void {
|
||||
return (t) => emit('select', composeTargetId(kind, t.name), t.name)
|
||||
}
|
||||
|
||||
// Simple, single-field substring match on name — no separate lib/*-search
|
||||
// module: ProvidersView.vue's own aliasMatches/providerMatches stay inline
|
||||
// for the identical reason (one field, one comparison), reserving the
|
||||
// extracted-and-tested lib/usage-search.ts treatment for UsageView.vue's
|
||||
// genuinely multi-entity (user-or-group-member) matching semantics.
|
||||
// module: ProviderHealthPanel.vue's own aliasMatches/providerMatches stay
|
||||
// inline for the identical reason (one field, one comparison), reserving
|
||||
// the extracted-and-tested lib/usage-search.ts treatment for
|
||||
// ConsumerDirectory.vue's genuinely multi-entity (user-or-group-member)
|
||||
// matching semantics.
|
||||
const { query, normalized: normalizedQuery, hasQuery } = useSearchQuery()
|
||||
|
||||
function nameMatches(t: AdminTargetView): boolean {
|
||||
@@ -34,7 +52,7 @@ const filteredAgents = computed<AdminTargetView[]>(() => {
|
||||
return hasQuery.value ? all.filter(nameMatches) : all
|
||||
})
|
||||
|
||||
/** emptyMessage mirrors UsageView.vue's groupsEmptyMessage/usersEmptyMessage three-way pattern: distinguishes nothing configured from nothing matching the active query. */
|
||||
/** emptyMessage mirrors ConsumerDirectory.vue's groupsEmptyMessage/usersEmptyMessage three-way pattern: distinguishes nothing configured from nothing matching the active query. */
|
||||
function emptyMessage(configuredCount: number, filteredCount: number): string {
|
||||
if (!configuredCount) return 'none configured'
|
||||
if (hasQuery.value && filteredCount === 0) return `no targets match "${query.value}"`
|
||||
@@ -44,10 +62,13 @@ function emptyMessage(configuredCount: number, filteredCount: number): string {
|
||||
const mcpEmptyMessage = computed(() => emptyMessage(targets.value?.mcpServers.length ?? 0, filteredMCPServers.value.length))
|
||||
const agentsEmptyMessage = computed(() => emptyMessage(targets.value?.agents.length ?? 0, filteredAgents.value.length))
|
||||
|
||||
// One shared column-def set (lib/target-columns.ts) for both tables below —
|
||||
// stateless ColumnDef objects, safe to reuse across DataTable's two
|
||||
// separate useVueTable instances (each owns its own sorting state).
|
||||
const columns = targetColumns()
|
||||
// Two separate column-def sets (lib/target-columns.ts) — MCP servers and
|
||||
// Agents each close over their own fixed `kind` (see onSelect above) so a
|
||||
// name click composes the right "mcp/…"/"agent/…" id. Stateless ColumnDef
|
||||
// objects otherwise, safe to reuse across DataTable's two separate
|
||||
// useVueTable instances (each owns its own sorting state).
|
||||
const mcpColumns = targetColumns(onSelect('mcp'))
|
||||
const agentColumns = targetColumns(onSelect('agent'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -57,20 +78,26 @@ const columns = targetColumns()
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>MCP servers</CardTitle>
|
||||
<CardDescription>Configured MCP proxy targets, the groups allowed to reach them, and their request counters.</CardDescription>
|
||||
<CardDescription>
|
||||
Configured MCP proxy targets, the groups allowed to reach them, and their request counters. Click a name to see its
|
||||
callers.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable :columns="columns" :data="filteredMCPServers" :empty-message="mcpEmptyMessage" />
|
||||
<DataTable :columns="mcpColumns" :data="filteredMCPServers" :empty-message="mcpEmptyMessage" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Agents</CardTitle>
|
||||
<CardDescription>Configured A2A agent proxy targets, the groups allowed to reach them, and their request counters.</CardDescription>
|
||||
<CardDescription>
|
||||
Configured A2A agent proxy targets, the groups allowed to reach them, and their request counters. Click a name to
|
||||
see its callers.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable :columns="columns" :data="filteredAgents" :empty-message="agentsEmptyMessage" />
|
||||
<DataTable :columns="agentColumns" :data="filteredAgents" :empty-message="agentsEmptyMessage" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user