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%
3.2 KB · 71 lines tsx
Raw Blame History
1'use client';23import { useState } from 'react';4import { adminFetch } from '@/lib/admin-fetch';5import { Time } from '@/lib/time';6import type { AdminAnnotation } from '@/lib/types';7import { AdminPage, ErrorNote, Field, Panel, Toast, btnPrimary, inputCls, useAdmin } from './shared';89export function Annotations() {10  const { data, err, reload } = useAdmin<{ annotations: AdminAnnotation[] }>('/annotations');11  const [form, setForm] = useState(() => ({ ts: new Date().toISOString().slice(0, 19) + 'Z', scope_type: 'global', scope_id: '', text: '' }));12  const [msg, setMsg] = useState<string | null>(null);13  const [actErr, setActErr] = useState<string | null>(null);14  return (15    <AdminPage title="Annotations" desc="Human context attached to a moment and a scope (maintenance windows, confirmed causes, external news). Shown on incident pages and charts.">16      <Panel title="New annotation" className="mb-3">17        <form18          className="grid grid-cols-2 gap-3 md:grid-cols-[180px_130px_160px_minmax(0,1fr)_auto]"19          onSubmit={(e) => {20            e.preventDefault();21            setActErr(null);22            adminFetch('/annotations', { method: 'POST', body: { ...form, scope_id: form.scope_id || null } })23              .then(() => {24                setMsg('Annotation saved');25                setForm({ ...form, text: '' });26                reload();27              })28              .catch((er: unknown) => setActErr(String(er)))29              .finally(() => setTimeout(() => setMsg(null), 3000));30          }}31        >32          <Field label="ts (UTC ISO)">33            <input value={form.ts} onChange={(e) => setForm({ ...form, ts: e.target.value })} className={`${inputCls} num w-full`} required />34          </Field>35          <Field label="scope_type">36            <select value={form.scope_type} onChange={(e) => setForm({ ...form, scope_type: e.target.value })} className={`${inputCls} w-full`}>37              {['global', 'region', 'country', 'asn', 'service', 'incident'].map((s) => (38                <option key={s}>{s}</option>39              ))}40            </select>41          </Field>42          <Field label="scope_id">43            <input value={form.scope_id} onChange={(e) => setForm({ ...form, scope_id: e.target.value })} className={`${inputCls} num w-full`} />44          </Field>45          <Field label="text">46            <input value={form.text} onChange={(e) => setForm({ ...form, text: e.target.value })} className={`${inputCls} w-full`} required />47          </Field>48          <div className="flex items-end">49            <button type="submit" className={btnPrimary}>50              Add51            </button>52          </div>53        </form>54      </Panel>55      <ErrorNote err={err ?? actErr} />56      <Toast msg={msg} />57      <ul className="divide-y divide-line">58        {(data?.annotations ?? []).map((a, i) => (59          <li key={a.id ?? i} className="py-2 text-[13px]">60            <p className="text-ink">{a.text}</p>61            <p className="num mt-0.5 text-[11px] text-ink-3">62              <Time ts={a.ts} /> · {a.scope_type}63              {a.scope_id ? `:${a.scope_id}` : ''} · {a.author ?? 'admin'}64            </p>65          </li>66        ))}67      </ul>68    </AdminPage>69  );70}71