/* ============================================================ TDS Errors view — backend drill drawer Opens from a backend × env matrix cell. Drill path: matrix cell → grouped patterns → raw rows (grep-able, copyable). Raw rows come from the paginated /api/statistics/errors endpoint, fetched per category (≤3 per backend) through mapLimitSettled(cap 2). Free-text grep is client-side over the loaded pages — server stays ILIKE-free. ============================================================ */ const { useState: edUseState, useEffect: edUseEffect, useMemo: edUseMemo, useRef: edUseRef } = React; function EvDrawerEventRow({ r, openId, setOpenId, copiedKey, copy }) { const isOpen = openId === r.id; const copied = copiedKey === r.id; return (
setOpenId(isOpen ? null : r.id)}> {evFmtTime(r.createdAt)} {r.feature} {EV_STEP_LABEL[r.step] || r.step} {r.errorCode ? r.errorCode + ' · ' : ''}{r.errorMessage}
{isOpen && (
{r.brand} · {r.team} · {r.step} · {r.populationJobId} · {r.createdAt} (UTC)
{r.errorMessage}
)}
); } function EvErrorDrawer({ sel, range, filters, patterns, onClose }) { const open = !!sel; const backend = sel ? EV_BACKENDS.find((b) => b.id === sel.backendId) : null; const cats = sel ? (sel.category ? [sel.category] : backend.cats) : []; const [q, setQ] = edUseState(''); const [state, setState] = edUseState({ rows: [], total: 0, hasMore: false, loading: true, loadingMore: false, error: null }); const pageRef = edUseRef(0); const load = (page, append) => { setState((s) => ({ ...s, loading: !append, loadingMore: append, error: append ? null : s.error })); const api = window.TDS_ERRORS_API; const base = { page, size: 50, feature: filters.feature, step: filters.step, team: filters.team, brand: filters.brand }; api.mapLimitSettled(cats, 2, (cat) => api.loadRawErrors(sel.env, range, { ...base, category: cat })) .then((results) => { const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value); const failed = results.find((r) => r.status === 'rejected'); const rows = ok.flatMap((r) => r.rows).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); setState((s) => ({ rows: append ? s.rows.concat(rows) : rows, total: ok.reduce((n, r) => n + r.totalCount, 0), hasMore: ok.some((r) => r.hasMore), loading: false, loadingMore: false, error: ok.length === 0 && failed ? ((failed.reason && failed.reason.message) || 'Unavailable') : null, })); }); }; edUseEffect(() => { if (!open) return; setQ(''); pageRef.current = 0; setState({ rows: [], total: 0, hasMore: false, loading: true, loadingMore: false, error: null }); load(0, false); }, [open, sel && sel.env, sel && sel.backendId, sel && sel.category]); edUseEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); // Focus the grep box WITHOUT scrolling. autoFocus fired while the drawer was // still translated off-screen (mid slide-in), and browsers scroll // overflow:hidden ancestors to reveal a focused element — which shifted the // entire artboard sideways and left a drawer-wide white band on the right. const searchRef = edUseRef(null); edUseEffect(() => { if (!open) return; const id = setTimeout(() => { if (searchRef.current) searchRef.current.focus({ preventScroll: true }); }, 0); return () => clearTimeout(id); }, [open]); const [copiedKey, setCopiedKey] = edUseState(null); const [openId, setOpenId] = edUseState(null); const copy = (text, key) => { try { navigator.clipboard.writeText(text); } catch (e) {} setCopiedKey(key); setTimeout(() => setCopiedKey(null), 1400); }; const ql = q.trim().toLowerCase(); const matchQ = (s) => !ql || (s || '').toLowerCase().includes(ql); const visibleRows = edUseMemo( () => state.rows.filter((r) => matchQ(r.errorMessage) || matchQ(r.errorCode)), [state.rows, ql] ); const visiblePatterns = edUseMemo( () => (patterns || []).filter((p) => matchQ(p.message) || matchQ(p.errorCode)), [patterns, ql] ); return (
); } window.EvErrorDrawer = EvErrorDrawer;