- Add import_id column to records (links each record to its import batch) - import_records() now stores readable dedup field values (not hashes) in info.inserted_keys / info.excluded_keys, and stamps import_id on insert - delete_import() simplified to delete log row; ON DELETE CASCADE removes records - Add get_import_log() and get_all_import_logs() DB functions - Add DELETE /api/sources/:name/import-log/:id endpoint - Add GET /api/sources/import-log global log endpoint - Import route now auto-applies transformations to new records after import - Import page: show ID column, expandable key detail, checkbox delete - New Log page: global view of all imports across sources - Update README API reference and workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.3 KiB
JavaScript
101 lines
3.3 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { api } from '../api'
|
|
|
|
function KeyList({ keys, label, color }) {
|
|
if (!keys || keys.length === 0) return null
|
|
return (
|
|
<div className="mb-2">
|
|
<div className={`text-xs font-medium mb-1 ${color}`}>{label} ({keys.length})</div>
|
|
<div className="max-h-32 overflow-y-auto bg-gray-50 rounded p-2 font-mono text-xs text-gray-500 space-y-0.5">
|
|
{keys.map((k, i) => (
|
|
<div key={i}>
|
|
{typeof k === 'object' && k !== null
|
|
? Object.entries(k).map(([field, val]) => `${field}: ${val}`).join(' · ')
|
|
: k}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LogRow({ entry }) {
|
|
const [expanded, setExpanded] = useState(false)
|
|
const info = entry.info || {}
|
|
const insertedKeys = info.inserted_keys || []
|
|
const excludedKeys = info.excluded_keys || []
|
|
const hasKeys = insertedKeys.length > 0 || excludedKeys.length > 0
|
|
|
|
return (
|
|
<>
|
|
<tr className="border-b border-gray-50 hover:bg-gray-50">
|
|
<td className="py-1.5 text-xs text-gray-400 font-mono pr-3">{entry.id}</td>
|
|
<td className="py-1.5 text-gray-700 pr-3">{entry.source_name}</td>
|
|
<td className="py-1.5 text-gray-500 pr-3">{new Date(entry.imported_at).toLocaleString()}</td>
|
|
<td className="py-1.5 text-gray-800 pr-3">{entry.records_imported}</td>
|
|
<td className="py-1.5 text-gray-400 pr-3">{entry.records_duplicate}</td>
|
|
<td className="py-1.5">
|
|
{hasKeys && (
|
|
<button
|
|
onClick={() => setExpanded(e => !e)}
|
|
className="text-xs text-blue-400 hover:text-blue-600"
|
|
>
|
|
{expanded ? '▲ hide' : '▼ keys'}
|
|
</button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
{expanded && (
|
|
<tr className="bg-gray-50">
|
|
<td colSpan={6} className="px-4 py-3">
|
|
<KeyList keys={insertedKeys} label="Inserted" color="text-green-600" />
|
|
<KeyList keys={excludedKeys} label="Excluded" color="text-gray-500" />
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default function Log() {
|
|
const [log, setLog] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
api.getAllImportLog()
|
|
.then(setLog)
|
|
.catch(() => {})
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<h1 className="text-xl font-semibold text-gray-800 mb-6">Import Log</h1>
|
|
|
|
{loading && <p className="text-sm text-gray-400">Loading…</p>}
|
|
|
|
{!loading && log.length === 0 && (
|
|
<p className="text-sm text-gray-400">No imports yet.</p>
|
|
)}
|
|
|
|
{log.length > 0 && (
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="text-left text-xs text-gray-400 border-b border-gray-100">
|
|
<th className="pb-1 font-medium pr-3">ID</th>
|
|
<th className="pb-1 font-medium pr-3">Source</th>
|
|
<th className="pb-1 font-medium pr-3">Date</th>
|
|
<th className="pb-1 font-medium pr-3">Imported</th>
|
|
<th className="pb-1 font-medium pr-3">Duplicates</th>
|
|
<th className="pb-1 w-16"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{log.map(entry => <LogRow key={entry.id} entry={entry} />)}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|