Files
traefik-llmgateway/webui/src/components/SnippetBlock.vue
T
lukaszraczylo ee49467e64 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
2026-09-23 16:41:01 +01:00

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>