"use client"; import { useState } from "react"; import { Badge, Button, EmptyState, Table, TBody, Td, Th, THead, Tr } from "@/components/ui"; import { cn } from "@/components/ui/cn"; import { DASH, fmtInt, fmtValue } from "@/lib/format"; import { adminFetch, type DevtoolPreviewResult } from "@/lib/admin-api"; import { ErrorNote, Field, fmtMs, Input, JsonView, LevelBadge, Mono, Panel, Spinner } from "../primitives"; import { useToast } from "../toast"; import { defaultsToConfig, extractorToConfig, fieldUnion, isEmptyValue, type DevtoolShared } from "./state"; export function StepPreview({ s }: { s: DevtoolShared }) { const { draft, update, fetchResult, previewResult: r, setPreviewResult } = s; const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [details, setDetails] = useState(null); const toast = useToast(); const d = draft.defaults; const setDefaults = (patch: Partial) => update({ defaults: { ...d, ...patch } }); const canReuseHtml = Boolean(fetchResult?.html && fetchResult.url === draft.fetch.url && !fetchResult.error); const run = async () => { const url = draft.fetch.url.trim(); if (!url) return setError("No URL — go back to step 1."); const extractor = extractorToConfig(draft.extractor); if (!extractor.fields && !extractor.parser) return setError("Define at least one field rule (or a parser) in step 2."); setBusy(true); setError(null); setDetails(null); const body: Record = { url, extractor, defaults: defaultsToConfig(d), connectorId: draft.meta.id || undefined, kind: draft.extractor.kind, level: draft.fetch.level, maxLevel: Math.max(draft.fetch.level, draft.fetch.maxLevel) }; if (canReuseHtml) body.html = fetchResult!.html; const res = await adminFetch("devtool/preview", { method: "POST", body, timeoutMs: 120_000 }); setBusy(false); if (!res.ok) { setError(res.error); setDetails(res.details ?? null); toast({ tone: "danger", title: "Preview failed", description: res.error }); return; } setPreviewResult(res.data); const v = res.data.validation; toast({ tone: v.rejected > 0 ? "warn" : res.data.entities.length ? "ok" : "warn", title: `${fmtInt(res.data.records.length)} record(s) → ${fmtInt(res.data.entities.length)} entit${res.data.entities.length === 1 ? "y" : "ies"}`, description: `${fmtInt(v.valid)} valid · ${fmtInt(v.rejected)} rejected · ${fmtInt(v.issues.length)} issue(s)` }); }; const entityFields = r ? fieldUnion(r.entities, ["entityType", "key", "name", "operatorName", "city", "countryIso2", "lat", "lng", "status", "facilityType", "itCapacityMw", "totalPowerMw", "plannedPowerMw"]) : []; const recordFields = r ? fieldUnion(r.records) : []; const ruleNames = draft.extractor.fields.map((f) => f.name.trim()).filter(Boolean); const emptyRules = r && r.records.length ? ruleNames.filter((n) => r.records.every((rec) => isEmptyValue(rec[n]))) : []; return (
setDefaults({ operatorName: e.target.value })} placeholder="Digital Realty" /> setDefaults({ operatorKind: e.target.value })} placeholder="colocation" mono /> setDefaults({ countryIso2: e.target.value.toUpperCase().slice(0, 2) })} placeholder="US" mono /> setDefaults({ facilityType: e.target.value })} placeholder="colocation" mono /> setDefaults({ status: e.target.value })} placeholder="operational" mono />
{details ? : null}
{r && ( <>
HTTP {r.fetch.status} {r.fetch.fetcher} {fmtMs(r.fetch.durationMs)} {fmtInt(r.records.length)} records → {fmtInt(r.entities.length)} entities → {fmtInt(r.validation.valid)} valid / {fmtInt(r.validation.rejected)} rejected {emptyRules.length > 0 && ( empty on every record: {emptyRules.join(", ")} )}
{r.records.length === 0 ? ( ) : (
{r.records.slice(0, 6).map((_, i) => ( ))} {[...new Set([...ruleNames, ...recordFields])].map((k) => ( {r.records.slice(0, 6).map((rec, i) => ( ))} ))}
Field#{i + 1}
{k} {isEmptyValue(rec[k]) ? "empty" : fmtValue(rec[k])}
{r.records.length > 6 &&

Showing 6 of {fmtInt(r.records.length)} records.

}
)}
{r.entities.length === 0 ? ( ) : (
{r.entities.slice(0, 6).map((_, i) => ( ))} {entityFields.map((k) => ( {r.entities.slice(0, 6).map((e, i) => ( ))} ))}
Field#{i + 1}
{k} {isEmptyValue(e[k]) ? "empty" : fmtValue(e[k])}
)}
{r.validation.issues.length === 0 ? ( ) : (
    {r.validation.issues.map((i, k) => { const level = String(i.level ?? i.severity ?? "error"); return (
  • {level} {(i.field ?? i.path) && {String(i.field ?? i.path)}} {String(i.message ?? JSON.stringify(i))} {(i.entity ?? i.key) != null && {String(i.entity ?? i.key)}}
  • ); })}
)}
{r.logs.length === 0 ? ( ) : (
{r.logs.map((l, i) => (
{l.level} {l.msg}
))}
)}
{r.entities.length > 0 && (
Raw JSON (first entity)
)} )}
); }