mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
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
61 lines
2.3 KiB
Vue
61 lines
2.3 KiB
Vue
<script setup lang="ts">
|
|
import { faCheck, faCopy, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
|
|
import { onUnmounted, ref, useTemplateRef } from 'vue'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import { copyText } from '@/lib/clipboard'
|
|
|
|
/**
|
|
* SnippetBlock (redesign-plan.md section 3.4) is the one "here is text to
|
|
* paste into a file" surface every ChangeHelper form and ConfigTree.vue
|
|
* share — a `<pre>` plus a copy button (lib/clipboard.ts's copyText, same
|
|
* Clipboard-API-with-selection-fallback convention CsvExportButton.vue's
|
|
* download path and ModelChip.vue's own click-to-copy already use).
|
|
*/
|
|
const props = defineProps<{
|
|
/** The exact text to display and copy — YAML (lib/snippets.ts) or a single-line JSON fragment, verbatim, never re-formatted by this component. */
|
|
text: string
|
|
/** A short caption above the block, e.g. "Paste into middleware.yaml" or "Add to the user's users.json line". */
|
|
label?: string
|
|
}>()
|
|
|
|
const preRef = useTemplateRef<HTMLElement>('pre')
|
|
const state = ref<'idle' | 'copied' | 'selected' | 'failed'>('idle')
|
|
let resetTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
|
async function onCopy(): Promise<void> {
|
|
state.value = await copyText(props.text, preRef.value ?? undefined)
|
|
clearTimeout(resetTimer)
|
|
resetTimer = setTimeout(() => {
|
|
state.value = 'idle'
|
|
}, 1500)
|
|
}
|
|
|
|
const BUTTON_LABEL: Record<typeof state.value, string> = {
|
|
idle: 'Copy',
|
|
copied: 'Copied',
|
|
selected: 'Selected — press ⌘/Ctrl+C',
|
|
failed: 'Copy failed',
|
|
}
|
|
|
|
onUnmounted(() => clearTimeout(resetTimer))
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-1.5">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<span v-if="label" class="text-xs font-medium text-muted-foreground">{{ label }}</span>
|
|
<Button type="button" variant="outline" size="sm" class="ml-auto" @click="onCopy">
|
|
<FontAwesomeIcon
|
|
:icon="state === 'idle' ? faCopy : state === 'failed' ? faTriangleExclamation : faCheck"
|
|
class="size-3.5"
|
|
:class="state === 'failed' ? 'text-destructive' : undefined"
|
|
aria-hidden="true"
|
|
/>
|
|
{{ BUTTON_LABEL[state] }}
|
|
</Button>
|
|
</div>
|
|
<pre ref="pre" class="overflow-x-auto rounded-md border bg-muted p-3 font-mono text-xs select-text whitespace-pre">{{ text }}</pre>
|
|
</div>
|
|
</template>
|