mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
feat(webui): MCP & Agents admin tab
New 'MCP & Agents' tab (after Charts) fed by GET /admin/api/targets: two Card panels, each a sortable DataTable (reusing DataTable/ SortHeaderButton — no hand-rolled table markup) listing configured MCP servers and agents with name, url (muted, truncated with a title tooltip), access (Badge per allowed group, muted 'All groups' when unrestricted — the same idiom UsageView's own group Access block already uses), and req/min, req/day, req/month. - types/api.ts: AdminTargetCountersView/AdminTargetView/ AdminTargetsResponse matching admin.go's JSON exactly. - stores/dashboard.ts: targets fetched in the SAME Promise.all/5s poll as overview and usage — one store, one timer, not a third. - lib/target-columns.ts: shared ColumnDef set both DataTables use. - components/TargetsView.vue: SearchInput + useSearchQuery name filter (inline matching, mirroring ProvidersView's own single-field aliasMatches — not every filter needs a lib/*-search.ts module).
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
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 { 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).
|
||||
const dashboard = useDashboardStore()
|
||||
const targets = computed(() => dashboard.targets)
|
||||
|
||||
// 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.
|
||||
const { query, normalized: normalizedQuery, hasQuery } = useSearchQuery()
|
||||
|
||||
function nameMatches(t: AdminTargetView): boolean {
|
||||
return t.name.toLowerCase().includes(normalizedQuery.value)
|
||||
}
|
||||
|
||||
const filteredMCPServers = computed<AdminTargetView[]>(() => {
|
||||
const all = targets.value?.mcpServers ?? []
|
||||
return hasQuery.value ? all.filter(nameMatches) : all
|
||||
})
|
||||
const filteredAgents = computed<AdminTargetView[]>(() => {
|
||||
const all = targets.value?.agents ?? []
|
||||
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. */
|
||||
function emptyMessage(configuredCount: number, filteredCount: number): string {
|
||||
if (!configuredCount) return 'none configured'
|
||||
if (hasQuery.value && filteredCount === 0) return `no targets match "${query.value}"`
|
||||
return 'none'
|
||||
}
|
||||
|
||||
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()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<SearchInput v-model="query" placeholder="Filter MCP servers or agents" class="max-w-sm" />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>MCP servers</CardTitle>
|
||||
<CardDescription>Configured MCP proxy targets, the groups allowed to reach them, and their request counters.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable :columns="columns" :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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable :columns="columns" :data="filteredAgents" :empty-message="agentsEmptyMessage" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user