TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/format';45export const ADMIN_NAV = [6 { href: '/admin', label: 'Overview' },7 { href: '/admin/connectors', label: 'Connectors' },8 { href: '/admin/coverage', label: 'Coverage' },9 { href: '/admin/data-quality', label: 'Data quality' },10 { href: '/admin/costs', label: 'Costs' },11 { href: '/admin/taxonomy', label: 'Taxonomy' },12 { href: '/admin/audit', label: 'Audit log' },13 { href: '/admin/events', label: 'Events' },14];1516export function AdminShell({ children, title, subtitle, actions, current }: { children: ReactNode; title: string; subtitle?: ReactNode; actions?: ReactNode; current: string }) {17 return (18 <div className="grid gap-6 lg:grid-cols-[180px_1fr]">19 <aside className="lg:sticky lg:top-20 lg:self-start">20 <p className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-subtle">Admin</p>21 <nav className="flex gap-1 overflow-x-auto scrollbar-none lg:flex-col">22 {ADMIN_NAV.map((n) => (23 <Link key={n.href} href={n.href} className={cn('whitespace-nowrap rounded-md px-2.5 py-1.5 text-[13px]', current === n.href ? 'bg-inset font-medium text-fg' : 'text-muted hover:bg-inset hover:text-fg')}>24 {n.label}25 </Link>26 ))}27 </nav>28 </aside>29 <div className="min-w-0">30 <header className="mb-4 flex flex-wrap items-end justify-between gap-3">31 <div>32 <h1 className="text-xl font-semibold tracking-tight">{title}</h1>33 {subtitle ? <p className="mt-0.5 text-xs text-muted">{subtitle}</p> : null}34 </div>35 {actions}36 </header>37 {children}38 </div>39 </div>40 );41}4243export function StatusPill({ status }: { status: string | null | undefined }) {44 const s = status ?? 'unknown';45 const tone: Record<string, string> = { maintenance: 'bg-index-bg text-index', completed: 'bg-gain-bg text-gain', healthy: 'bg-gain-bg text-gain', active: 'bg-gain-bg text-gain', success: 'bg-gain-bg text-gain', valid: 'bg-gain-bg text-gain', degraded: 'bg-alert-bg text-alert', partial: 'bg-alert-bg text-alert', paused: 'bg-alert-bg text-alert', flagged: 'bg-alert-bg text-alert', pending: 'bg-alert-bg text-alert', running: 'bg-index-bg text-index', failing: 'bg-loss-bg text-loss', failed: 'bg-loss-bg text-loss', excluded: 'bg-loss-bg text-loss', disabled: 'bg-inset text-muted', unknown: 'bg-inset text-muted' };46 return <span className={cn('inline-flex rounded-sm px-1.5 py-0.5 text-[11px] font-medium', tone[s] ?? 'bg-inset text-muted')}>{s}</span>;47}4849export function ActionButton({ label, tone = 'neutral', small = true }: { label: string; tone?: 'neutral' | 'danger' | 'primary'; small?: boolean }) {50 return (51 <button type="submit" className={cn('rounded-md border font-medium', small ? 'px-2 py-0.5 text-[11px]' : 'px-3 py-1.5 text-xs', tone === 'danger' ? 'border-loss/40 text-loss hover:bg-loss-bg' : tone === 'primary' ? 'border-accent bg-accent text-accent-fg' : 'border-border hover:bg-inset')}>52 {label}53 </button>54 );55}5657export function Kpi({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone?: 'gain' | 'loss' | 'alert' }) {58 return (59 <div className="card px-3 py-2.5">60 <p className="text-[10px] font-semibold uppercase tracking-wider text-subtle">{label}</p>61 <p className={cn('num mt-0.5 text-lg font-semibold', tone === 'gain' && 'text-gain', tone === 'loss' && 'text-loss', tone === 'alert' && 'text-alert')}>{value}</p>62 {sub ? <p className="text-[11px] text-muted">{sub}</p> : null}63 </div>64 );65}6667/** Single-series bar chart (one hue, hover tooltips via <title>, no legend needed). */68export function Bars({ points, format, className }: { points: Array<{ label: string; value: number }>; format?: (v: number) => string; className?: string }) {69 if (!points.length) return <p className="text-xs text-subtle">No data in this window.</p>;70 const max = Math.max(...points.map((p) => p.value), 0) || 1;71 const w = 600;72 const h = 120;73 const gap = 2;74 const bw = Math.max(2, (w - gap * points.length) / points.length);75 const fmt = format ?? ((v: number) => String(Math.round(v * 100) / 100));76 return (77 <svg viewBox={`0 0 ${w} ${h + 16}`} className={cn('h-auto w-full', className)} role="img" aria-label="Daily values">78 <line x1={0} x2={w} y1={h} y2={h} stroke="var(--ri-border)" />79 {points.map((p, i) => {80 const bh = Math.max(p.value > 0 ? 1 : 0, (p.value / max) * (h - 8));81 return (82 <g key={p.label}>83 <rect x={i * (bw + gap)} y={h - bh} width={bw} height={bh} rx={bw > 6 ? 2 : 0} fill="var(--ri-index)" opacity={0.85}>84 <title>{`${p.label}: ${fmt(p.value)}`}</title>85 </rect>86 {points.length <= 31 && (i === 0 || i === points.length - 1) ? (87 <text x={i * (bw + gap) + bw / 2} y={h + 12} textAnchor="middle" fontSize={9} fill="var(--ri-fg-subtle)">{p.label.slice(5)}</text>88 ) : null}89 </g>90 );91 })}92 </svg>93 );94}9596export function fmtTs(v: unknown): string {97 if (!v) return '—';98 const d = v instanceof Date ? v : new Date(String(v));99 if (Number.isNaN(d.getTime())) return String(v);100 return d.toISOString().replace('T', ' ').slice(0, 16) + 'Z';101}102103export function n(v: unknown, digits = 0): string {104 const x = Number(v);105 return Number.isFinite(x) ? x.toLocaleString('en-US', { maximumFractionDigits: digits }) : '—';106}107