SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
8.7 KB · 187 lines tsx
Raw Blame History
1'use client';23import { useState } from 'react';4import { PNum } from '@/components/ui/primitives';5import { adminFetch } from '@/lib/admin-fetch';6import { fmtInt, fmtPct } from '@/lib/format';7import type { AdminTarget } from '@/lib/types';8import { AdminPage, ErrorNote, Field, Panel, Toast, btnCls, btnPrimary, inputCls, useAdmin } from './shared';910const EMPTY: Partial<AdminTarget> = { target_id: '', name: '', hostname: '', category: 'cloud', provider: '', service_id: '', country: 'US', region: 'na-east', importance: 3, tier: 2 };11const CATEGORIES = ['dns', 'cdn', 'cloud', 'search', 'messaging', 'social', 'finance', 'government', 'news', 'developer', 'ai', 'streaming', 'commerce', 'infrastructure'];1213export function Targets() {14  const { data, err, reload } = useAdmin<{ targets: AdminTarget[] }>('/targets');15  const [q, setQ] = useState('');16  const [editing, setEditing] = useState<Partial<AdminTarget> | null>(null);17  const [isNew, setIsNew] = useState(false);18  const [msg, setMsg] = useState<string | null>(null);19  const [busy, setBusy] = useState(false);20  const [actErr, setActErr] = useState<string | null>(null);2122  const rows = (data?.targets ?? []).filter((t) => !q || t.hostname.includes(q) || t.name.toLowerCase().includes(q.toLowerCase()) || t.target_id.includes(q));2324  const run = async (fn: () => Promise<unknown>, ok: string) => {25    setBusy(true);26    setActErr(null);27    try {28      await fn();29      setMsg(ok);30      setEditing(null);31      reload();32    } catch (e) {33      setActErr(String(e));34    } finally {35      setBusy(false);36      setTimeout(() => setMsg(null), 3000);37    }38  };3940  return (41    <AdminPage42      title="Targets"43      desc="Registry of measured endpoints. Frequencies come from tiers (scheduler); importance weights the aggregates. Changes apply on the next config refresh (≤ 5 min on probes)."44      right={45        <div className="flex gap-2">46          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="filter…" className={inputCls} aria-label="Filter" />47          <button48            type="button"49            className={btnPrimary}50            onClick={() => {51              setEditing({ ...EMPTY });52              setIsNew(true);53            }}54          >55            + target56          </button>57        </div>58      }59    >60      <ErrorNote err={err ?? actErr} />61      <Toast msg={msg} />62      {editing && (63        <Panel title={isNew ? 'Create target' : `Edit ${editing.target_id}`} className="mb-3">64          <form65            className="grid grid-cols-2 gap-3 md:grid-cols-5"66            onSubmit={(e) => {67              e.preventDefault();68              const body = { ...editing, importance: Number(editing.importance), tier: Number(editing.tier) };69              void run(() => (isNew ? adminFetch('/targets', { method: 'POST', body }) : adminFetch(`/targets/${encodeURIComponent(editing.target_id!)}`, { method: 'PATCH', body })), isNew ? 'Target created' : 'Target updated');70            }}71          >72            <Field label="target_id">73              <input required disabled={!isNew} value={editing.target_id ?? ''} onChange={(e) => setEditing({ ...editing, target_id: e.target.value })} className={`${inputCls} num w-full`} />74            </Field>75            <Field label="name">76              <input required value={editing.name ?? ''} onChange={(e) => setEditing({ ...editing, name: e.target.value })} className={`${inputCls} w-full`} />77            </Field>78            <Field label="hostname">79              <input required value={editing.hostname ?? ''} onChange={(e) => setEditing({ ...editing, hostname: e.target.value })} className={`${inputCls} num w-full`} />80            </Field>81            <Field label="category">82              <select value={editing.category ?? ''} onChange={(e) => setEditing({ ...editing, category: e.target.value })} className={`${inputCls} w-full`}>83                {CATEGORIES.map((c) => (84                  <option key={c}>{c}</option>85                ))}86              </select>87            </Field>88            <Field label="provider">89              <input value={editing.provider ?? ''} onChange={(e) => setEditing({ ...editing, provider: e.target.value })} className={`${inputCls} w-full`} />90            </Field>91            <Field label="service_id">92              <input value={editing.service_id ?? ''} onChange={(e) => setEditing({ ...editing, service_id: e.target.value })} className={`${inputCls} num w-full`} />93            </Field>94            <Field label="country">95              <input value={editing.country ?? ''} maxLength={2} onChange={(e) => setEditing({ ...editing, country: e.target.value.toUpperCase() })} className={`${inputCls} num w-full`} />96            </Field>97            <Field label="region">98              <input value={editing.region ?? ''} onChange={(e) => setEditing({ ...editing, region: e.target.value })} className={`${inputCls} num w-full`} />99            </Field>100            <Field label="importance 1–5">101              <input type="number" min={1} max={5} value={editing.importance ?? 3} onChange={(e) => setEditing({ ...editing, importance: Number(e.target.value) })} className={`${inputCls} num w-full`} />102            </Field>103            <Field label="tier 1–3">104              <input type="number" min={1} max={3} value={editing.tier ?? 2} onChange={(e) => setEditing({ ...editing, tier: Number(e.target.value) })} className={`${inputCls} num w-full`} />105            </Field>106            <div className="col-span-2 flex gap-2 md:col-span-5">107              <button type="submit" className={btnPrimary} disabled={busy}>108                {isNew ? 'Create' : 'Save'}109              </button>110              <button type="button" className={btnCls} onClick={() => setEditing(null)}>111                Cancel112              </button>113            </div>114          </form>115        </Panel>116      )}117      <div className="scroll-x">118        <table className="tbl">119          <thead>120            <tr>121              <th>Target</th>122              <th>Hostname</th>123              <th>Category</th>124              <th>Anchor</th>125              <th className="r">Imp.</th>126              <th className="r">Tier</th>127              <th className="r">Pressure</th>128              <th className="r">OK 1h</th>129              <th>Enabled</th>130              <th></th>131            </tr>132          </thead>133          <tbody>134            {rows.map((t) => (135              <tr key={t.target_id} className={t.enabled === false ? 'opacity-50' : ''}>136                <td>137                  <span className="text-ink">{t.name}</span> <span className="num text-[10.5px] text-ink-3">{t.target_id}</span>138                </td>139                <td className="num text-ink-2">{t.hostname}</td>140                <td className="text-ink-2">{t.category}</td>141                <td className="num text-ink-2">142                  {t.country} · {t.region}143                </td>144                <td className="num r">{t.importance}</td>145                <td className="num r">{t.tier}</td>146                <td className="r">147                  <PNum value={t.pressure} />148                </td>149                <td className="num r">{fmtPct(t.ok_ratio_1h, 1)}</td>150                <td>151                  <button type="button" className="text-[11px] uppercase tracking-[0.1em]" style={{ color: t.enabled === false ? 'var(--ink-3)' : 'var(--ok)' }} onClick={() => void run(() => adminFetch(`/targets/${encodeURIComponent(t.target_id)}`, { method: 'PATCH', body: { enabled: t.enabled === false } }), t.enabled === false ? 'Enabled' : 'Disabled')}>152                    {t.enabled === false ? 'disabled' : 'enabled'}153                  </button>154                </td>155                <td className="r">156                  <div className="flex justify-end gap-1">157                    <button158                      type="button"159                      className="text-[11px] text-ink-2 hover:text-ink"160                      onClick={() => {161                        setEditing({ ...t });162                        setIsNew(false);163                      }}164                    >165                      edit166                    </button>167                    <button168                      type="button"169                      className="text-[11px] text-bad/80 hover:text-bad"170                      onClick={() => {171                        if (confirm(`Delete target ${t.target_id}? History is kept; the target stops being measured.`)) void run(() => adminFetch(`/targets/${encodeURIComponent(t.target_id)}`, { method: 'DELETE' }), 'Target deleted');172                      }}173                    >174                      delete175                    </button>176                  </div>177                </td>178              </tr>179            ))}180          </tbody>181        </table>182      </div>183      <p className="num mt-2 text-[11px] text-ink-3">{fmtInt(rows.length)} targets</p>184    </AdminPage>185  );186}187