mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
feat(webui): shadcn-vue accordion + sortable DataTable, full-width panel
Component-first admin-panel rework (operator directive), replacing hand-rolled patterns with the actual shadcn-vue library components rather than working around them. - Providers (OverviewView) and Groups (UsageView) now render as a real shadcn-vue Accordion: trigger is the summary line, content holds the detail (base URL, last refresh/error, model chips for providers; full stat grid + member users for groups) — the earlier hand-rolled second-<TableRow> toggle is retired. lib/provider-expand.ts's tested two-set expand-state module is unchanged; only the template now adapts it to Accordion's type="multiple" v-model via a writable computed instead of driving a plain v-if. - Usage view: expanding a group shows its member users' own usage rows — a client-side join on AdminUsageEntryView.groupName, which admin.go already sets per user (verified via admin_test.go's existing TestAdminUsage_MathAgainstSeededCounters; no backend change needed). - New DataTable.vue: the shadcn-vue DataTable pattern (@tanstack/vue-table, pinned to ^8.21.3 — the version whose API matches the documented pattern; npm's default "latest" resolves to v9, a breaking rewrite with a different API) factored into one reusable, generic component so every real table (users usage, aliases, a group's nested member list) gets clickable sortable headers with asc/desc indicators for free. usage-columns.ts holds the shared ColumnDef set; UsageTable.vue now delegates to DataTable. Groups themselves cannot be a literal <table> (Accordion wraps content in a <div>, illegal inside <tbody>), so their own sortability is driven by the same columns headlessly, surfaced as a small header-button toolbar above the Accordion. - Aliases table: alias id gets the ModelChip copy treatment (the alias name is the routable string); target stays plain text. - App.vue: dropped the max-w-6xl container — the panel now uses the full viewport width with responsive gutters (px-4 sm:px-6 lg:px-8). Table.vue already wraps every table in its own overflow-x-auto container, so a wide table still scrolls itself, never the page. shadcn-vue add accordion re-added @lucide/vue and required swapping its Chevron icons for FontAwesome (webui/README.md's own documented trap); components.json's iconLibrary/font keys stayed absent (verified main.css byte-identical after the add). Browser-verified against the real integration stack (real /admin over the Yaegi plugin, strict CSP incl. style-src 'self'): provider/group accordion expand, search auto-expand through the new Accordion adapter, sort toggles on aliases/users/groups-toolbar/nested-member tables, alias copy-to-clipboard, full-width at 1920px. Zero console errors (one unrelated MetaMask extension log).
This commit is contained in:
@@ -1,85 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { formatCost, formatLimits } from '@/lib/format'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableEmpty,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import DataTable from '@/components/DataTable.vue'
|
||||
import { usageColumns } from '@/lib/usage-columns'
|
||||
import type { AdminUsageEntryView } from '@/types/api'
|
||||
|
||||
/**
|
||||
* UsageTable renders one scope kind's usage rows (users or groups) — the
|
||||
* shared table shape both need (vue.md: extract before you paste it
|
||||
* twice). The second column differs per kind: a user's row shows the group
|
||||
* it belongs to; a group's row shows its member count (looked up from the
|
||||
* Overview response, which is the only place member counts live).
|
||||
* UsageTable renders one scope kind's usage rows (users, or a group's
|
||||
* member users) as a sortable DataTable (shadcn-vue's DataTable pattern,
|
||||
* via the shared usageColumns() defs — vue.md: extract before you paste
|
||||
* it twice). The second column differs per kind: a user's row shows the
|
||||
* group it belongs to; a group's row shows its member count (looked up
|
||||
* from the Overview response, which is the only place member counts
|
||||
* live).
|
||||
*/
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
idLabel: string
|
||||
entries: AdminUsageEntryView[]
|
||||
secondaryColumnLabel: string
|
||||
secondaryValue: (entry: AdminUsageEntryView) => string
|
||||
}>()
|
||||
|
||||
const columns = computed(() => usageColumns(props.idLabel, props.secondaryColumnLabel, props.secondaryValue))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ idLabel }}</TableHead>
|
||||
<TableHead>{{ secondaryColumnLabel }}</TableHead>
|
||||
<TableHead>Limits</TableHead>
|
||||
<TableHead class="text-right">req/min</TableHead>
|
||||
<TableHead class="text-right">req/day</TableHead>
|
||||
<TableHead class="text-right">tokIn/day</TableHead>
|
||||
<TableHead class="text-right">tokOut/day</TableHead>
|
||||
<TableHead class="text-right">tokIn/month</TableHead>
|
||||
<TableHead class="text-right">tokOut/month</TableHead>
|
||||
<TableHead class="text-right">cost/day</TableHead>
|
||||
<TableHead class="text-right">cost/month</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableEmpty v-if="entries.length === 0" :colspan="11" class="text-muted-foreground">
|
||||
none
|
||||
</TableEmpty>
|
||||
<TableRow
|
||||
v-for="entry in entries"
|
||||
:key="entry.id"
|
||||
:class="entry.storeDown ? 'bg-destructive/10' : undefined"
|
||||
>
|
||||
<TableCell class="font-medium">{{ entry.id }}</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{ secondaryValue(entry) }}</TableCell>
|
||||
<TableCell class="text-muted-foreground">{{ formatLimits(entry.limits) }}</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.requestsPerMinute }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.requestsPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensInPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensOutPerDay }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensInPerMonth }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : entry.tokensOutPerMonth }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : formatCost(entry.costPerDayMicroUsd) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ entry.storeDown ? '?' : formatCost(entry.costPerMonthMicroUsd) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="entries"
|
||||
:row-class="(entry) => (entry.storeDown ? 'bg-destructive/10' : undefined)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user