- 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>
97 lines
3.5 KiB
JavaScript
97 lines
3.5 KiB
JavaScript
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>
|
||
)
|
||
}
|