ograph/packages/engine/ui/src/components/ReactionLogs.tsx

213 lines
8.9 KiB
TypeScript

import { useState, useEffect } from 'react'
import { getReactionLogs } from '../api'
import { formatRelativeTime } from '../utils'
import { Spinner, EmptyState, Pagination } from './Common'
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
success: 'bg-green-500/20 text-green-300 border-green-500/30',
failed: 'bg-red-500/20 text-red-300 border-red-500/30',
skipped: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
}
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border ${colors[status] || colors.skipped}`}
>
{status}
</span>
)
}
function truncate(value: any, maxLen = 60): string {
if (value === null || value === undefined) return '—'
const str = typeof value === 'string' ? value : JSON.stringify(value)
return str.length > maxLen ? str.slice(0, maxLen) + '...' : str
}
export default function ReactionLogs() {
const [data, setData] = useState<any[]>([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [limit, setLimit] = useState(50)
const [offset, setOffset] = useState(0)
const [filterReactionId, setFilterReactionId] = useState('')
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set())
const load = () => {
setLoading(true)
const rid = filterReactionId.trim() ? parseInt(filterReactionId, 10) : undefined
getReactionLogs(limit, offset, rid)
.then((res) => {
setData(res.reaction_logs)
setTotal(res.total)
})
.catch((e) => setError(e.message))
.finally(() => setLoading(false))
}
useEffect(() => {
load()
}, [limit, offset])
const toggleExpand = (id: number) => {
setExpandedRows((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
if (loading) return <Spinner />
if (error) return <div className="text-red-500 text-center p-8">Error: {error}</div>
return (
<div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Reaction Logs</h2>
{/* Filter */}
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6">
<div className="flex items-end gap-3">
<div>
<label className="block text-xs text-gray-400 mb-1">Filter by Reaction ID</label>
<input
type="number"
value={filterReactionId}
onChange={(e) => setFilterReactionId(e.target.value)}
placeholder="Reaction ID"
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-40"
/>
</div>
<button
onClick={() => {
setOffset(0)
load()
}}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm font-medium transition-colors"
>
Filter
</button>
{filterReactionId && (
<button
onClick={() => {
setFilterReactionId('')
setOffset(0)
setTimeout(load, 0)
}}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm font-medium transition-colors"
>
Clear
</button>
)}
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
{data.length === 0 ? (
<EmptyState message="No reaction logs found" />
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700">
<tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
ID
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Reaction
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Trigger Event
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Projection
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Old Value
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
New Value
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Action
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Output
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Duration
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Created
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{data.map((log, i) => (
<tr
key={log.id}
className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
} hover:bg-gray-800/60`}
>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.id}</td>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.reaction_id}</td>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.trigger_event_id}</td>
<td className="px-4 py-3 text-sm text-gray-400 max-w-[120px] truncate">
{truncate(log.projection_def, 30)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 font-mono max-w-[120px] truncate">
{truncate(log.old_value)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 font-mono max-w-[120px] truncate">
{truncate(log.new_value)}
</td>
<td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-500/20 text-blue-300 border border-blue-500/30">
{log.action}
</span>
</td>
<td className="px-4 py-3">
<StatusBadge status={log.status} />
</td>
<td className="px-4 py-3 text-sm">
{log.handler_output ? (
<div>
<button
onClick={() => toggleExpand(log.id)}
className="text-blue-400 hover:text-blue-300 text-xs underline"
>
{expandedRows.has(log.id) ? 'Hide' : 'Show'}
</button>
{expandedRows.has(log.id) && (
<pre className="mt-2 p-2 bg-gray-800 rounded text-xs text-gray-300 max-w-xs overflow-auto whitespace-pre-wrap">
{typeof log.handler_output === 'string'
? log.handler_output
: JSON.stringify(log.handler_output, null, 2)}
</pre>
)}
</div>
) : (
<span className="text-gray-600"></span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-400 font-mono">
{log.duration_ms != null ? `${log.duration_ms}ms` : '—'}
</td>
<td className="px-4 py-3 text-sm text-gray-400">{formatRelativeTime(log.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Pagination total={total} limit={limit} offset={offset} onPageChange={setOffset} onLimitChange={setLimit} />
</div>
</div>
)
}