SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
5.7 KB · 141 lines tsx
Raw Blame History
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