小糯 🐱 32d85223f2 feat(ui): responsive layout — mobile sidebar drawer + scrollable tables
- Sidebar becomes off-canvas drawer on mobile (<lg), slides in/out
- Mobile top bar with hamburger menu and current page title
- Backdrop overlay when drawer is open, ESC to close
- All tables wrapped in overflow-x-auto with min-width for scroll
- Search/filter bars stack vertically on small screens
- Content padding scales: p-4 (mobile) → p-6 (tablet) → p-8 (desktop)
- Body scroll locked when mobile drawer is open
2026-04-13 18:00:10 +08:00

139 lines
5.4 KiB
TypeScript

import { useState, useEffect } from 'react'
import { getEventDefs } from '../api'
import { Spinner, EmptyState, HashBadge } from './Common'
import EmitEventModal from './EmitEventModal'
export default function EventDefs() {
const [data, setData] = useState<Array<{ name: string; hash: string; parent_hash: string | null; schema: any }>>([])
const [expanded, setExpanded] = useState<Set<string>>(new Set())
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [emitTarget, setEmitTarget] = useState<{ name: string; schema: any } | null>(null)
const fetchData = () => {
setLoading(true)
getEventDefs()
.then((res) => setData(res.event_defs))
.catch((e) => setError(e.message))
.finally(() => setLoading(false))
}
useEffect(() => {
fetchData()
}, [])
const toggleExpand = (hash: string) => {
setExpanded((prev) => {
const next = new Set(prev)
if (next.has(hash)) {
next.delete(hash)
} else {
next.add(hash)
}
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-6xl mx-auto">
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Event Definitions</h2>
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? (
<EmptyState message="No event definitions found" />
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead className="bg-surface-3/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">
Name
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Hash
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Parent
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Schema
</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{data.map((def, i) => (
<tr
key={i}
className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-surface-3/60`}
>
<td className="px-4 py-3 font-mono text-gray-100">{def.name}</td>
<td className="px-4 py-3">
<HashBadge hash={def.hash} />
</td>
<td className="px-4 py-3">
{def.parent_hash ? <HashBadge hash={def.parent_hash} /> : <span className="text-gray-600">-</span>}
</td>
<td className="px-4 py-3">
<button
onClick={() => toggleExpand(def.hash)}
className="text-blue-400 hover:text-blue-300 text-sm font-medium transition-colors"
>
{expanded.has(def.hash) ? 'Hide' : 'Show'}
</button>
{expanded.has(def.hash) && (
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
{JSON.stringify(def.schema, null, 2)
.split('\n')
.map((line, i) => {
if (line.includes(':')) {
const [key, ...rest] = line.split(':')
return (
<div key={i}>
<span className="text-blue-400">{key}:</span>
<span className="text-green-300">{rest.join(':')}</span>
</div>
)
}
return (
<div key={i} className="text-gray-300">
{line}
</div>
)
})}
</pre>
)}
</td>
<td className="px-4 py-3">
<button
onClick={() => setEmitTarget({ name: def.name, schema: def.schema })}
className="px-3 py-1.5 bg-green-700/30 hover:bg-green-600/40 text-green-300 hover:text-green-200 border border-green-700/50 rounded-lg text-sm font-medium transition-colors"
>
Emit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{emitTarget && (
<EmitEventModal
eventDef={emitTarget}
onClose={() => setEmitTarget(null)}
onSuccess={() => {}}
/>
)}
</div>
)
}