SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
6.8 KB · 121 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ActionButton, AdminTitle, Bool, Mono, Notice, StatusChip, Trunc } from '@/components/admin/ui';4import { TierBadge } from '@/components/ui/badges';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { LiveAgo } from '@/components/ui/live';7import { Unavailable } from '@/components/ui/unavailable';8import { runConnectorAction, toggleConnectorAction } from '@/lib/admin/actions';9import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';10import { fmtDateTime, fmtDuration, fmtInt, num } from '@/lib/format';1112export const metadata: Metadata = { title: 'Connectors', robots: { index: false, follow: false } };13export const dynamic = 'force-dynamic';1415const RETURN = '/admin/connectors';1617export default async function AdminConnectorsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {18  await requireAdmin();19  const sp = await searchParams;20  const res = await load(adminApi.connectors());21  if (!res.ok) {22    return (23      <>24        <AdminTitle title="Connectors" />25        <Unavailable what="Connectors" reason={res.error} />26      </>27    );28  }29  const items = [...res.data.items].sort((a, b) => (num(a.priority) ?? 9) - (num(b.priority) ?? 9) || a.name.localeCompare(b.name));30  const counts = items.reduce<Record<string, number>>((acc, c) => ((acc[c.health] = (acc[c.health] ?? 0) + 1), acc), {});31  return (32    <>33      <AdminTitle title="Connectors" count={fmtInt(items.length)} lede={Object.entries(counts).map(([k, n]) => `${k} ${n}`).join(' · ')} />34      <Notice notice={sp.notice} level={sp.level} />35      <DataTable compact scroll caption="Connectors">36        <thead>37          <tr>38            <Th>Connector</Th>39            <Th>Source</Th>40            <Th>Health</Th>41            <Th>Enabled</Th>42            <Th num>Prio</Th>43            <Th num>Interval</Th>44            <Th>Last attempt</Th>45            <Th>Last success</Th>46            <Th>Next run</Th>47            <Th>Last run</Th>48            <Th num>Docs / snaps</Th>49            <Th num>Fails</Th>50            <Th>Circuit</Th>51            <Th>Actions</Th>52          </tr>53        </thead>54        <tbody>55          {items.length === 0 && <EmptyRow cols={14}>No connectors registered.</EmptyRow>}56          {items.map((c) => {57            const lr = c.last_run;58            return (59              <tr key={c.name}>60                <Td>61                  <div className="flex items-center gap-1.5">62                    <Link href={`/admin/runs?connector=${encodeURIComponent(c.name)}`} className="mono text-xs font-medium text-ink hover:text-accent">{c.name}</Link>63                    {c.run_now_pending && <span className="text-[10px] uppercase tracking-wide text-accent" title="Run requested; waiting for the scheduler tick">queued</span>}64                    {c.in_code === false && <span className="text-[10px] uppercase tracking-wide text-warning" title="Registered in the database but not present in code">no code</span>}65                  </div>66                  <span className="block max-w-[16rem] truncate text-[11px] text-ink-3" title={c.label}>{c.label}</span>67                </Td>68                <Td className="text-xs">69                  <span className="text-ink-2">{c.source_name ?? c.source_key ?? '—'}</span> {c.source_tier !== undefined && c.source_tier !== null && <TierBadge tier={num(c.source_tier)} />}70                </Td>71                <Td><StatusChip value={c.health} /></Td>72                <Td><Bool v={c.enabled} /></Td>73                <Td num className="tnum text-xs">{fmtInt(c.priority)}</Td>74                <Td num className="tnum text-xs">{fmtDuration(c.interval_seconds)}</Td>75                <Td className="text-xs" title={fmtDateTime(c.last_attempt_at)}><LiveAgo at={c.last_attempt_at} /></Td>76                <Td className="text-xs" title={fmtDateTime(c.last_success_at)}><LiveAgo at={c.last_success_at} /></Td>77                <Td className="text-xs text-ink-2" title={fmtDateTime(c.next_run_at)}>{c.next_run_at ? fmtDateTime(c.next_run_at).replace(/ UTC$/, '') : '—'}</Td>78                <Td className="text-xs">79                  {lr ? (80                    <span className="inline-flex flex-wrap items-center gap-1.5">81                      <StatusChip value={lr.status} />82                      <span className="tnum text-ink-3">{num(lr.duration_ms) === null ? '' : fmtDuration(Math.round((num(lr.duration_ms) ?? 0) / 1000))}</span>83                      <span className="tnum text-ink-3" title="docs changed / fetched">{fmtInt(lr.docs_changed)}/{fmtInt(lr.docs_fetched)}</span>84                      {lr.error && <Trunc text={lr.error} max={40} className="text-danger" />}85                    </span>86                  ) : (87                    <span className="text-ink-3">—</span>88                  )}89                </Td>90                <Td num className="tnum text-xs text-ink-2">{c.documents === undefined && c.snapshots === undefined ? '—' : `${fmtInt(c.documents)} / ${fmtInt(c.snapshots)}`}</Td>91                <Td num className={`tnum text-xs ${num(c.consecutive_failures) ? 'text-danger' : 'text-ink-2'}`}>{fmtInt(c.consecutive_failures)}</Td>92                <Td className="text-xs">{c.circuit_open_until ? <span className="text-warning" title={fmtDateTime(c.circuit_open_until)}>open · <LiveAgo at={c.circuit_open_until} /></span> : <span className="text-ink-3">closed</span>}</Td>93                <Td>94                  <div className="flex items-center gap-1.5">95                    <form action={runConnectorAction}>96                      <input type="hidden" name="name" value={c.name} />97                      <input type="hidden" name="return" value={RETURN} />98                      <ActionButton tone="accent" disabled={!!c.run_now_pending} title="Enqueue an immediate run (force)">Run now</ActionButton>99                    </form>100                    <form action={toggleConnectorAction}>101                      <input type="hidden" name="name" value={c.name} />102                      <input type="hidden" name="enabled" value={c.enabled ? 'false' : 'true'} />103                      <input type="hidden" name="return" value={RETURN} />104                      <ActionButton tone={c.enabled ? 'danger' : 'positive'}>{c.enabled ? 'Disable' : 'Enable'}</ActionButton>105                    </form>106                  </div>107                </Td>108              </tr>109            );110          })}111        </tbody>112      </DataTable>113      {res.data.unregistered_in_db && res.data.unregistered_in_db.length > 0 && (114        <p className="mt-4 text-xs text-ink-3">115          In code but not in the database ({res.data.unregistered_in_db.length}): {res.data.unregistered_in_db.map((n) => <Mono key={n} className="mr-1.5">{n}</Mono>)} — run <Mono>aia seed</Mono> to register them.116        </p>117      )}118    </>119  );120}121