spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { RunStatusPill, fmtDuration } from '@/components/meta/connectors-table';4import { cn } from '@/lib/cn';5import { fmtAgo, fmtDateTime, fmtInt, num } from '@/lib/format';6import type { ConnectorRun } from './types';78/** Server-safe admin building blocks: page header, stat tiles, error state, byte formatting, runs table. */910export function fmtBytes(v: number | string | null | undefined): string {11 const n = num(v);12 if (n === null) return '—';13 if (n < 1024) return `${fmtInt(n)} B`;14 const units = ['KB', 'MB', 'GB', 'TB'];15 let x = n / 1024;16 let i = 0;17 while (x >= 1024 && i < units.length - 1) {18 x /= 1024;19 i++;20 }21 return `${x >= 100 ? x.toFixed(0) : x.toFixed(1)} ${units[i]}`;22}2324export function AdminHeader({ title, lede, action }: { title: ReactNode; lede?: ReactNode; action?: ReactNode }) {25 return (26 <div className="mb-6 flex flex-wrap items-end justify-between gap-3 border-b border-rule pb-4">27 <div className="min-w-0">28 <p className="eyebrow">Admin</p>29 <h1 className="mt-1 text-2xl font-semibold tracking-tight md:text-3xl">{title}</h1>30 {lede && <p className="mt-1.5 max-w-2xl text-sm text-ink-2">{lede}</p>}31 </div>32 {action}33 </div>34 );35}3637export function AdminError({ message, what = 'Admin API' }: { message: string; what?: string }) {38 return (39 <div role="alert" className="rounded-md border border-danger/40 bg-danger-soft px-4 py-3 text-sm text-ink">40 <p className="font-medium text-danger">{what} unavailable</p>41 <p className="mono mt-1 break-words text-xs text-ink-2">{message}</p>42 </div>43 );44}4546export function Tile({ label, value, hint, tone, href }: { label: string; value: ReactNode; hint?: ReactNode; tone?: 'ok' | 'warn' | 'danger'; href?: string }) {47 const color = tone === 'ok' ? 'text-active' : tone === 'warn' ? 'text-warn' : tone === 'danger' ? 'text-danger' : '';48 const body = (49 <>50 <p className="eyebrow">{label}</p>51 <p className={cn('tnum mt-1 text-xl font-semibold tracking-tight md:text-2xl', color)}>{value}</p>52 {hint && <p className="mt-0.5 text-xs text-ink-3">{hint}</p>}53 </>54 );55 return href ? (56 <Link href={href} className="block min-w-0 rounded-md border border-rule px-3 py-2.5 hover:bg-plane-2">57 {body}58 </Link>59 ) : (60 <div className="min-w-0 rounded-md border border-rule px-3 py-2.5">{body}</div>61 );62}6364export function SectionTitle({ children, action }: { children: ReactNode; action?: ReactNode }) {65 return (66 <div className="mb-3 mt-8 flex items-end justify-between gap-3">67 <h2 className="text-base font-semibold tracking-tight md:text-lg">{children}</h2>68 {action}69 </div>70 );71}7273export function RunsTable({ runs, now = Date.now(), showConnector = true, emptyLabel = 'No runs recorded' }: { runs: ConnectorRun[]; now?: number; showConnector?: boolean; emptyLabel?: string }) {74 if (runs.length === 0) return <p className="rounded-md border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3">{emptyLabel}</p>;75 return (76 <div className="overflow-x-auto">77 <table className="data-table stack md:min-w-[860px]">78 <thead>79 <tr>80 {showConnector && <th>Connector</th>}81 <th>Started</th>82 <th>Status</th>83 <th className="num">Duration</th>84 <th className="num">Fetched</th>85 <th className="num">Created</th>86 <th className="num">Updated</th>87 <th>Error</th>88 </tr>89 </thead>90 <tbody>91 {runs.map((r) => (92 <tr key={r.id}>93 {showConnector && (94 <td className="primary">95 <Link href={`/admin/connectors/${encodeURIComponent(r.connector_name)}`} className="mono text-sm text-ink hover:text-accent">96 {r.connector_name}97 </Link>98 </td>99 )}100 <td data-label="Started" title={fmtDateTime(r.started_at)} className={cn(!showConnector && 'primary')}>101 <span className="text-sm">{fmtAgo(r.started_at, now)}</span>102 <span className="mono ml-2 text-xs text-ink-3">{fmtDateTime(r.started_at)}</span>103 </td>104 <td data-label="Status">105 <RunStatusPill status={r.status} />106 </td>107 <td data-label="Duration" className="num tnum text-sm text-ink-2">108 {fmtDuration(r.duration_ms)}109 </td>110 <td data-label="Fetched" className="num tnum text-sm">111 {fmtInt(r.records_fetched)}112 </td>113 <td data-label="Created" className="num tnum text-sm text-ink-2">114 {fmtInt(r.records_created)}115 </td>116 <td data-label="Updated" className="num tnum text-sm text-ink-2">117 {fmtInt(r.records_updated)}118 </td>119 <td data-label="Error" className="max-w-[320px]">120 {r.error ? (121 <span className="mono block truncate text-xs text-danger" title={r.error}>122 {r.error}123 </span>124 ) : (125 <span className="text-xs text-ink-3">—</span>126 )}127 </td>128 </tr>129 ))}130 </tbody>131 </table>132 </div>133 );134}135136/** Small mono JSON preview (config, run meta) — scrolls instead of overflowing. */137export function JsonInline({ value, className }: { value: unknown; className?: string }) {138 if (value === null || value === undefined) return <span className="text-xs text-ink-3">—</span>;139 return <code className={cn('mono block max-w-full overflow-x-auto whitespace-pre text-[11px] text-ink-3', className)}>{JSON.stringify(value)}</code>;140}141