dataflow/ui/src/pages/Records.jsx
Paul Trowbridge 928a54932d Add multi-capture regex, computed view fields, collapsible rules, and live preview
- Support multi-capture-group regex: mappings.input_value changed to JSONB,
  regexp_match() result stored as scalar or array JSONB in transformed column
- Computed expression fields in generated views: {fieldname} refs substituted
  with (transformed->>'fieldname')::numeric for arithmetic in view columns
- Fix generate_source_view to DROP VIEW before CREATE (avoids column drop error)
- Collapsible rule cards that open directly to inline edit form
- Debounced live regex preview (extract + replace) with popout modal for 50 rows
- Records page now shows dfv.<source> view output instead of raw records
- Unified field table in Sources: single table with In view, Seq, expression columns
- Fix "Rule already exists" error when editing by passing rule.id directly to submit
- Fix Sources page clearing on F5 by watching sourceObj?.name in useEffect dep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 16:37:15 -04:00

97 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { api } from '../api'
export default function Records({ source }) {
const [rows, setRows] = useState([])
const [exists, setExists] = useState(null)
const [offset, setOffset] = useState(0)
const [loading, setLoading] = useState(false)
const LIMIT = 100
useEffect(() => {
if (!source) return
setOffset(0)
load(0)
}, [source])
async function load(off) {
setLoading(true)
try {
const res = await api.getViewData(source, LIMIT, off)
setExists(res.exists)
setRows(res.rows)
} catch (err) {
console.error(err)
} finally {
setLoading(false)
}
}
function prev() { const o = Math.max(0, offset - LIMIT); setOffset(o); load(o) }
function next() { const o = offset + LIMIT; setOffset(o); load(o) }
if (!source) return <div className="p-6 text-sm text-gray-400">Select a source first.</div>
return (
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold text-gray-800">Records {source}</h1>
{exists && rows.length > 0 && (
<span className="text-xs text-gray-400 font-mono">dfv.{source}</span>
)}
</div>
{loading && <p className="text-sm text-gray-400">Loading</p>}
{!loading && exists === false && (
<p className="text-sm text-gray-400">
No view generated yet. Go to <span className="font-medium text-gray-600">Sources</span>, check fields as <span className="font-medium text-gray-600">In view</span>, then click <span className="font-medium text-gray-600">Generate view</span>.
</p>
)}
{!loading && exists && rows.length === 0 && (
<p className="text-sm text-gray-400">View exists but no transformed records yet. Import data and run a transform first.</p>
)}
{!loading && exists && rows.length > 0 && (
<>
<div className="bg-white border border-gray-200 rounded overflow-auto mb-4">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-gray-400 border-b border-gray-100 bg-gray-50">
{Object.keys(rows[0]).map(col => (
<th key={col} className="px-3 py-2 font-medium whitespace-nowrap">{col}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="border-t border-gray-50 hover:bg-gray-50">
{Object.values(row).map((val, j) => (
<td key={j} className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap max-w-48 truncate">
{val === null ? <span className="text-gray-300"></span> : String(val)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-center gap-3 text-sm text-gray-500">
<button onClick={prev} disabled={offset === 0}
className="px-3 py-1 border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-40">
Prev
</button>
<span>{offset + 1}{offset + rows.length}</span>
<button onClick={next} disabled={rows.length < LIMIT}
className="px-3 py-1 border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-40">
Next
</button>
</div>
</>
)}
</div>
)
}