feat(ui): visual refresh — custom theme, refined layout, fixed bugs
- Custom color system (surface-0..4, accent, mint) replacing raw gray-xxx - Inter + JetBrains Mono fonts via Google Fonts - Refined sidebar: compact logo, geometric icons, subtle active states - Fixed bg-gray-850 bug (invalid Tailwind class) in 9 components - Polished login page with centered card + gradient logo - Unified card/table/button/input styling across all components - Subtle grain texture overlay for depth - Smoother animations (fade-in, slide-up)
This commit is contained in:
parent
bc12a4bb18
commit
f950654827
File diff suppressed because one or more lines are too long
@ -48,7 +48,6 @@ function App() {
|
||||
const [tokenInput, setTokenInput] = useState('')
|
||||
const [checking, setChecking] = useState(true)
|
||||
|
||||
// Sync hash → state on popstate (browser back/forward)
|
||||
useEffect(() => {
|
||||
const onHashChange = () => setPageState(getPageFromHash())
|
||||
window.addEventListener('hashchange', onHashChange)
|
||||
@ -59,7 +58,6 @@ function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Navigate: update hash + state
|
||||
const setPage = (p: Page) => {
|
||||
setHash(p)
|
||||
setPageState(p)
|
||||
@ -83,7 +81,7 @@ function App() {
|
||||
|
||||
if (checking) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-gray-950">
|
||||
<div className="flex items-center justify-center h-screen bg-surface-0">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
@ -91,37 +89,43 @@ function App() {
|
||||
|
||||
if (needsAuth) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen bg-gray-950 text-gray-100 px-4">
|
||||
<div className="bg-gray-900/80 backdrop-blur border border-gray-800 p-8 rounded-xl shadow-2xl max-w-md w-full animate-fadeIn">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-3xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
|
||||
OGraph UI
|
||||
</h2>
|
||||
<p className="text-gray-400 text-sm">Enter your API token to continue</p>
|
||||
<div className="flex items-center justify-center h-screen bg-surface-0 text-gray-100 px-4 grain">
|
||||
<div className="animate-slide-up">
|
||||
<div className="bg-surface-1 border border-white/[0.06] p-8 rounded-2xl shadow-2xl max-w-sm w-full">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-lg font-bold mx-auto mb-4 shadow-lg shadow-accent/25">
|
||||
G
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-white tracking-tight">OGraph</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Enter your API token</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="API Token"
|
||||
type="password"
|
||||
className="w-full px-4 py-2.5 bg-surface-3 border border-white/[0.08] rounded-lg text-sm placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent/40 transition-all"
|
||||
placeholder="Bearer token"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAuth()}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleAuth}
|
||||
className="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold shadow-lg shadow-blue-500/30 hover:shadow-blue-500/50 transition-all"
|
||||
disabled={!tokenInput.trim()}
|
||||
className="w-full mt-3 px-4 py-2.5 bg-accent hover:bg-accent-dim rounded-lg text-sm font-semibold text-white shadow-lg shadow-accent/20 hover:shadow-accent/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout page={page} onPageChange={setPage}>
|
||||
<div className="animate-fadeIn">
|
||||
{page === 'health' && <Health />}
|
||||
{page === 'object-defs' && <ObjectDefs />}
|
||||
{page === 'objects' && <Objects />}
|
||||
@ -133,7 +137,6 @@ function App() {
|
||||
{page === 'api-keys' && <ApiKeys />}
|
||||
{page === 'reaction-logs' && <ReactionLogs />}
|
||||
{page === 'request-logs' && <RequestLogs />}
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ export default function ApiKeys() {
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">API Keys</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">API Keys</h2>
|
||||
|
||||
{/* Key reveal modal */}
|
||||
{shownKey && (
|
||||
@ -91,7 +91,7 @@ export default function ApiKeys() {
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
This key will only be shown once. Copy it now and store it securely.
|
||||
</p>
|
||||
<div className="bg-gray-800 rounded-lg p-3 font-mono text-sm text-green-400 break-all mb-4">{shownKey}</div>
|
||||
<div className="bg-surface-3 rounded-lg p-3 font-mono text-sm text-green-400 break-all mb-4">{shownKey}</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
@ -111,7 +111,7 @@ export default function ApiKeys() {
|
||||
)}
|
||||
|
||||
{/* Create form */}
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6">
|
||||
<div className="bg-surface-1 rounded-lg border border-white/[0.06] p-4 mb-6">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Name</label>
|
||||
@ -120,7 +120,7 @@ export default function ApiKeys() {
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="my-service"
|
||||
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"
|
||||
className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -130,7 +130,7 @@ export default function ApiKeys() {
|
||||
value={allowedEvents}
|
||||
onChange={(e) => setAllowedEvents(e.target.value)}
|
||||
placeholder="order.created, user.signed_up"
|
||||
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-64"
|
||||
className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-64"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -140,7 +140,7 @@ export default function ApiKeys() {
|
||||
value={rateLimit}
|
||||
onChange={(e) => setRateLimit(e.target.value)}
|
||||
placeholder="1000"
|
||||
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-24"
|
||||
className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-24"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@ -153,12 +153,12 @@ export default function ApiKeys() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No API keys found" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
@ -184,8 +184,8 @@ export default function ApiKeys() {
|
||||
<tr
|
||||
key={key.id}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{key.id}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td>
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
export function Spinner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
<div className="flex items-center justify-center p-16">
|
||||
<div className="relative">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-surface-3" />
|
||||
<div className="absolute inset-0 w-8 h-8 rounded-full border-2 border-transparent border-t-accent animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function EmptyState({ message = 'No data found' }: { message?: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12 text-gray-500">
|
||||
<div className="flex items-center justify-center py-16 text-gray-500">
|
||||
<div className="text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-600 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
<p>{message}</p>
|
||||
<div className="mx-auto w-10 h-10 rounded-xl bg-surface-3 flex items-center justify-center mb-3">
|
||||
<span className="text-lg text-gray-600">∅</span>
|
||||
</div>
|
||||
<p className="text-sm">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -27,10 +25,79 @@ export function EmptyState({ message = 'No data found' }: { message?: string })
|
||||
export function HashBadge({ hash, short = true }: { hash: string; short?: boolean }) {
|
||||
const display = short && hash.length > 8 ? hash.slice(0, 8) : hash
|
||||
return (
|
||||
<span className="inline-block px-2 py-1 bg-gray-800/50 rounded font-mono text-xs text-gray-400">{display}</span>
|
||||
<span
|
||||
className="inline-block px-1.5 py-0.5 bg-surface-3/60 rounded font-mono text-[11px] text-gray-500 cursor-default select-all"
|
||||
title={hash}
|
||||
>
|
||||
{display}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function SectionHeader({ title, count, action }: { title: string; count?: number; action?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight">{title}</h2>
|
||||
{count !== undefined && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-surface-3 text-[11px] font-medium text-gray-400">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={`bg-surface-1 border border-white/[0.06] rounded-xl overflow-hidden ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ErrorBanner({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="rounded-lg bg-red-500/[0.08] border border-red-500/20 px-4 py-3 text-sm text-red-400 mb-4">
|
||||
{message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function JsonBlock({ data }: { data: unknown }) {
|
||||
const text = JSON.stringify(data, null, 2)
|
||||
return (
|
||||
<pre className="p-3 bg-surface-0 rounded-lg text-xs font-mono overflow-x-auto border border-white/[0.04] text-gray-400 leading-relaxed">
|
||||
{text.split('\n').map((line, i) => {
|
||||
const match = line.match(/^(\s*)"([^"]+)":\s*(.*)/)
|
||||
if (match) {
|
||||
return (
|
||||
<div key={i}>
|
||||
<span>{match[1]}</span>
|
||||
<span className="text-blue-400">"{match[2]}"</span>
|
||||
<span className="text-gray-600">: </span>
|
||||
<span className="text-emerald-400">{match[3]}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div key={i} className="text-gray-500">{line}</div>
|
||||
})}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
export function StatusDot({ status }: { status: 'ok' | 'error' | 'warning' | 'idle' }) {
|
||||
const colors = {
|
||||
ok: 'bg-emerald-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-amber-500',
|
||||
idle: 'bg-gray-600',
|
||||
}
|
||||
return <div className={`w-2 h-2 rounded-full ${colors[status]}`} />
|
||||
}
|
||||
|
||||
export function Pagination({
|
||||
total,
|
||||
limit,
|
||||
@ -53,44 +120,41 @@ export function Pagination({
|
||||
const canNext = offset + limit < total
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between border-t border-gray-800 bg-gray-900/50 px-4 py-3 mt-4">
|
||||
<div className="flex items-center justify-between border-t border-white/[0.06] px-4 py-3 mt-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-400">
|
||||
{startItem}-{endItem} of {total}
|
||||
<span className="text-xs text-gray-500">
|
||||
{startItem}–{endItem} of {total}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-500">Per page:</label>
|
||||
<select
|
||||
value={limit}
|
||||
onChange={(e) => {
|
||||
onLimitChange(parseInt(e.target.value, 10))
|
||||
onPageChange(0) // reset to first page
|
||||
onPageChange(0)
|
||||
}}
|
||||
className="bg-gray-800 text-gray-300 border border-gray-700 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="bg-surface-3 text-gray-300 border border-white/[0.06] rounded-md px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-accent/50"
|
||||
>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => onPageChange(offset - limit)}
|
||||
disabled={!canPrev}
|
||||
className="px-3 py-1 bg-gray-800 text-gray-300 rounded border border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-700 text-sm"
|
||||
className="px-2.5 py-1 bg-surface-3 text-gray-400 rounded-md border border-white/[0.06] disabled:opacity-30 disabled:cursor-not-allowed hover:bg-surface-4 hover:text-gray-200 text-xs font-medium"
|
||||
>
|
||||
Previous
|
||||
‹ Prev
|
||||
</button>
|
||||
<span className="text-sm text-gray-400">
|
||||
Page {currentPage} / {totalPages || 1}
|
||||
<span className="text-xs text-gray-500 px-2">
|
||||
{currentPage}/{totalPages || 1}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPageChange(offset + limit)}
|
||||
disabled={!canNext}
|
||||
className="px-3 py-1 bg-gray-800 text-gray-300 rounded border border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-700 text-sm"
|
||||
className="px-2.5 py-1 bg-surface-3 text-gray-400 rounded-md border border-white/[0.06] disabled:opacity-30 disabled:cursor-not-allowed hover:bg-surface-4 hover:text-gray-200 text-xs font-medium"
|
||||
>
|
||||
Next
|
||||
Next ›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,7 +77,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
|
||||
type="checkbox"
|
||||
checked={!!values[name]}
|
||||
onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.checked }))}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
|
||||
className="w-4 h-4 rounded border-gray-600 bg-surface-3 text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-gray-200 font-mono">{name}</span>
|
||||
</label>
|
||||
@ -94,7 +94,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
|
||||
type={isNumeric ? 'number' : 'text'}
|
||||
value={values[name]}
|
||||
onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 text-sm font-mono placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-gray-100 text-sm font-mono placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder={def.type === 'ref' ? 'Enter object ID' : `Enter ${def.type}`}
|
||||
/>
|
||||
</div>
|
||||
@ -108,7 +108,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-800">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.06]">
|
||||
<h3 className="text-lg font-semibold text-gray-100">
|
||||
Emit <span className="text-blue-400 font-mono">{eventDef.name}</span>
|
||||
</h3>
|
||||
|
||||
@ -39,13 +39,13 @@ export default function EventDefs() {
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Event Definitions</h2>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<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" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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
|
||||
@ -69,8 +69,8 @@ export default function EventDefs() {
|
||||
<tr
|
||||
key={i}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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">
|
||||
@ -87,7 +87,7 @@ export default function EventDefs() {
|
||||
{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-gray-800">
|
||||
<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) => {
|
||||
|
||||
@ -51,29 +51,29 @@ export default function Events() {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">Events</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight">Events</h2>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||
className="px-4 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
|
||||
placeholder="Filter by ref..."
|
||||
value={refFilter}
|
||||
onChange={(e) => setRefFilter(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold transition-all shadow-lg shadow-blue-500/20 hover:shadow-blue-500/40"
|
||||
className="px-4 py-2 bg-accent hover:bg-accent-dim rounded-lg text-sm font-medium transition-all"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No events yet" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
@ -92,8 +92,8 @@ export default function Events() {
|
||||
<tr
|
||||
key={i}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{event.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
@ -108,7 +108,7 @@ export default function Events() {
|
||||
{expanded.has(event.id) ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
{expanded.has(event.id) && (
|
||||
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800">
|
||||
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
|
||||
{JSON.stringify(event.payload, null, 2)
|
||||
.split('\n')
|
||||
.map((line, i) => {
|
||||
|
||||
@ -19,8 +19,8 @@ export default function Health() {
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Health Check</h2>
|
||||
<div className="bg-gray-900/50 backdrop-blur border border-gray-800 rounded-lg p-8">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Health Check</h2>
|
||||
<div className="bg-surface-1 border border-white/[0.06] rounded-lg p-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="relative">
|
||||
<div className="w-4 h-4 bg-green-500 rounded-full"></div>
|
||||
@ -30,7 +30,7 @@ export default function Health() {
|
||||
</div>
|
||||
<div className="text-gray-400 mt-4 flex items-baseline gap-2">
|
||||
<span>Version:</span>
|
||||
<span className="text-white font-mono bg-gray-800/50 px-3 py-1 rounded">{data?.version}</span>
|
||||
<span className="text-white font-mono bg-surface-3/50 px-3 py-1 rounded">{data?.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -23,85 +23,98 @@ const navGroups: Array<{ label: string; items: Array<{ id: Page; label: string;
|
||||
{
|
||||
label: 'Schema',
|
||||
items: [
|
||||
{ id: 'object-defs', label: 'Object Defs', icon: '📦' },
|
||||
{ id: 'event-defs', label: 'Event Defs', icon: '📋' },
|
||||
{ id: 'projection-defs', label: 'Projection Defs', icon: '📊' },
|
||||
{ id: 'object-defs', label: 'Object Defs', icon: '◻' },
|
||||
{ id: 'event-defs', label: 'Event Defs', icon: '◆' },
|
||||
{ id: 'projection-defs', label: 'Projection Defs', icon: '▤' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Data',
|
||||
items: [
|
||||
{ id: 'objects', label: 'Objects', icon: '🗂️' },
|
||||
{ id: 'objects', label: 'Objects', icon: '⊞' },
|
||||
{ id: 'events', label: 'Events', icon: '⚡' },
|
||||
{ id: 'projections', label: 'Projections', icon: '📈' },
|
||||
{ id: 'reactions', label: 'Reactions', icon: '🔔' },
|
||||
{ id: 'projections', label: 'Projections', icon: '◎' },
|
||||
{ id: 'reactions', label: 'Reactions', icon: '⟳' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Observability',
|
||||
label: 'Logs',
|
||||
items: [
|
||||
{ id: 'reaction-logs', label: 'Reaction Logs', icon: '📜' },
|
||||
{ id: 'request-logs', label: 'Request Logs', icon: '📝' },
|
||||
{ id: 'reaction-logs', label: 'Reaction Logs', icon: '▸' },
|
||||
{ id: 'request-logs', label: 'Request Logs', icon: '▸' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
items: [{ id: 'api-keys', label: 'API Keys', icon: '🔑' }],
|
||||
},
|
||||
{
|
||||
label: 'System',
|
||||
items: [{ id: 'health', label: 'Health', icon: '🏠' }],
|
||||
label: 'Admin',
|
||||
items: [
|
||||
{ id: 'api-keys', label: 'API Keys', icon: '⚿' },
|
||||
{ id: 'health', label: 'System', icon: '●' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function Layout({ page, onPageChange, children }: Props) {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-950 text-gray-100">
|
||||
<div className="flex h-screen bg-surface-0 text-gray-200 grain">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-gray-900/50 backdrop-blur border-r border-gray-800 flex flex-col">
|
||||
<div className="p-6 border-b border-gray-800">
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
|
||||
OGraph UI
|
||||
</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">Event Sourcing Dashboard</p>
|
||||
<aside className="w-56 bg-surface-1 border-r border-white/[0.06] flex flex-col shrink-0">
|
||||
{/* Logo */}
|
||||
<div className="px-5 py-5 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-xs font-bold shadow-lg shadow-accent/20">
|
||||
G
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto p-3">
|
||||
<div>
|
||||
<h1 className="text-sm font-bold text-white tracking-tight">OGraph</h1>
|
||||
<p className="text-[10px] text-gray-500 font-medium">Event Engine</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 overflow-y-auto py-3 px-2.5">
|
||||
{navGroups.map((group) => (
|
||||
<div key={group.label} className="mb-4">
|
||||
<div className="px-4 py-1 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<div key={group.label} className="mb-3">
|
||||
<div className="px-2.5 py-1.5 text-[10px] font-semibold text-gray-500 uppercase tracking-[0.08em]">
|
||||
{group.label}
|
||||
</div>
|
||||
{group.items.map((item) => (
|
||||
{group.items.map((item) => {
|
||||
const isActive = page === item.id
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onPageChange(item.id)}
|
||||
className={`
|
||||
w-full text-left px-4 py-2.5 rounded-lg mb-1 flex items-center gap-3 font-medium transition-all
|
||||
${
|
||||
page === item.id
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30'
|
||||
: 'text-gray-400 hover:bg-gray-800/60 hover:text-gray-100'
|
||||
w-full text-left px-2.5 py-[7px] rounded-md mb-0.5 flex items-center gap-2.5 text-[13px] font-medium transition-all
|
||||
${isActive
|
||||
? 'bg-accent/[0.12] text-accent-glow'
|
||||
: 'text-gray-400 hover:bg-white/[0.04] hover:text-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span className="text-sm">{item.label}</span>
|
||||
<span className={`text-xs w-4 text-center ${isActive ? 'text-accent-glow' : 'text-gray-600'}`}>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-gray-800 text-xs text-gray-600">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-3 border-t border-white/[0.06]">
|
||||
<div className="flex items-center gap-2 text-[11px] text-gray-500">
|
||||
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse-slow" />
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-y-auto bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
|
||||
<div className="p-8 max-w-[1600px] mx-auto">{children}</div>
|
||||
{/* Main */}
|
||||
<main className="flex-1 overflow-y-auto bg-surface-0">
|
||||
<div className="p-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -19,13 +19,13 @@ export default function ObjectDefs() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Object Definitions</h2>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Object Definitions</h2>
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No object definitions found" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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
|
||||
@ -37,8 +37,8 @@ export default function ObjectDefs() {
|
||||
<tr
|
||||
key={i}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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>
|
||||
</tr>
|
||||
|
||||
@ -73,7 +73,7 @@ export default function Objects() {
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold">Objects</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight">Objects</h2>
|
||||
{!showCreate ? (
|
||||
<button
|
||||
onClick={() => { setShowCreate(true); setCreateType(types[0] || '') }}
|
||||
@ -87,7 +87,7 @@ export default function Objects() {
|
||||
<select
|
||||
value={createType}
|
||||
onChange={(e) => setCreateType(e.target.value)}
|
||||
className="px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-blue-500 focus:outline-none"
|
||||
className="px-3 py-1.5 bg-surface-3 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
{types.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
@ -116,7 +116,7 @@ export default function Objects() {
|
||||
</div>
|
||||
<Listbox value={filter} onChange={handleFilterChange}>
|
||||
<div className="relative w-64">
|
||||
<Listbox.Button className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
|
||||
<Listbox.Button className="w-full px-4 py-2 bg-surface-3 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
|
||||
<span className={filter ? 'text-gray-100' : 'text-gray-500'}>{filter || 'All Types'}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
@ -128,7 +128,7 @@ export default function Objects() {
|
||||
</svg>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
<Listbox.Option
|
||||
value=""
|
||||
className={({ active }) =>
|
||||
@ -156,12 +156,12 @@ export default function Objects() {
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No objects found" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
@ -177,8 +177,8 @@ export default function Objects() {
|
||||
<tr
|
||||
key={i}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{obj.id}</td>
|
||||
<td className="px-4 py-3 text-gray-100">
|
||||
|
||||
@ -32,7 +32,7 @@ export default function ProjectionDefs() {
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Projection Definitions</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Projection Definitions</h2>
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No projection definitions found" />
|
||||
) : (
|
||||
@ -40,7 +40,7 @@ export default function ProjectionDefs() {
|
||||
{data.map((def, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-gray-900/50 backdrop-blur rounded-lg p-5 border border-gray-800 hover:border-gray-700 transition-colors"
|
||||
className="bg-surface-1 rounded-xl p-5 border border-white/[0.06] hover:border-white/[0.1] transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
@ -57,7 +57,7 @@ export default function ProjectionDefs() {
|
||||
</button>
|
||||
</div>
|
||||
{expanded.has(def.hash || i) && (
|
||||
<div className="mt-4 space-y-3 text-sm bg-gray-800/30 rounded-lg p-4 border border-gray-700/50">
|
||||
<div className="mt-4 space-y-3 text-sm bg-surface-3/30 rounded-lg p-4 border border-gray-700/50">
|
||||
{def.sources && (
|
||||
<div>
|
||||
<span className="text-gray-400 font-medium">Sources:</span>
|
||||
@ -81,7 +81,7 @@ export default function ProjectionDefs() {
|
||||
{def.params && (
|
||||
<div>
|
||||
<span className="text-gray-400 font-medium">Params:</span>
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800">
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
|
||||
{JSON.stringify(def.params, null, 2)
|
||||
.split('\n')
|
||||
.map((line, i) => {
|
||||
@ -106,7 +106,7 @@ export default function ProjectionDefs() {
|
||||
{def.value_schema && (
|
||||
<div>
|
||||
<span className="text-gray-400 font-medium">Value Schema:</span>
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800 text-purple-300">
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06] text-purple-300">
|
||||
{JSON.stringify(def.value_schema, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@ -114,7 +114,7 @@ export default function ProjectionDefs() {
|
||||
{def.initial_value !== undefined && (
|
||||
<div>
|
||||
<span className="text-gray-400 font-medium">Initial Value:</span>
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800 text-blue-300">
|
||||
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06] text-blue-300">
|
||||
{JSON.stringify(def.initial_value, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@ -81,14 +81,14 @@ export default function Projections() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Query Projections</h2>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg p-6 space-y-6 border border-gray-800">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Query Projections</h2>
|
||||
<div className="bg-surface-1 rounded-xl p-6 space-y-6 border border-white/[0.06]">
|
||||
{/* Projection selector with Listbox */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-2">Projection</label>
|
||||
<Listbox value={selectedDef} onChange={handleSelectDef}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
|
||||
<Listbox.Button className="w-full px-4 py-2.5 bg-surface-3 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
|
||||
<span className={selectedDef ? 'text-gray-100' : 'text-gray-500'}>
|
||||
{selectedDef?.name || 'Select a projection...'}
|
||||
</span>
|
||||
@ -102,7 +102,7 @@ export default function Projections() {
|
||||
</svg>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
{defs.map((d) => (
|
||||
<Listbox.Option
|
||||
key={d.name}
|
||||
@ -133,7 +133,7 @@ export default function Projections() {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3 bg-gray-800/30 rounded-lg p-4 border border-gray-700/50">
|
||||
<div className="space-y-3 bg-surface-3/30 rounded-lg p-4 border border-gray-700/50">
|
||||
{Object.entries(selectedDef.params).map(([key, schema]) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1.5">
|
||||
@ -155,7 +155,7 @@ export default function Projections() {
|
||||
) : (
|
||||
<input
|
||||
type={schema.type === 'number' ? 'number' : 'text'}
|
||||
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||
className="w-full px-3 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
|
||||
placeholder={`Enter ${schema.type} value...`}
|
||||
value={params[key] || ''}
|
||||
onChange={(e) => setParams({ ...params, [key]: e.target.value })}
|
||||
@ -169,7 +169,7 @@ export default function Projections() {
|
||||
|
||||
{/* Projection info */}
|
||||
{selectedDef && (
|
||||
<div className="text-xs space-y-2 bg-gray-800/20 rounded-lg p-4 border border-gray-700/30">
|
||||
<div className="text-xs space-y-2 bg-surface-3/20 rounded-lg p-4 border border-gray-700/30">
|
||||
<div>
|
||||
<span className="text-gray-500 font-medium">sources:</span>
|
||||
<div className="mt-1 space-y-2">
|
||||
@ -216,7 +216,7 @@ export default function Projections() {
|
||||
{result !== null && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Result</h3>
|
||||
<pre className="p-4 bg-gray-950 rounded-lg overflow-x-auto text-sm text-green-300 border border-gray-800">
|
||||
<pre className="p-4 bg-gray-950 rounded-lg overflow-x-auto text-sm text-green-300 border border-white/[0.06]">
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@ -280,7 +280,7 @@ function RefCombobox({
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className="w-full px-3 py-2 pr-10 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||
className="w-full px-3 py-2 pr-10 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
|
||||
placeholder="Type object ID or select..."
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
displayValue={(val: string) => val}
|
||||
@ -295,13 +295,13 @@ function RefCombobox({
|
||||
</svg>
|
||||
</Combobox.Button>
|
||||
</div>
|
||||
<Combobox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
<Combobox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
{Object.entries(relevantByType).map(([type, ids]) => {
|
||||
const filteredIds = ids.filter((id) => !query || id.toLowerCase().includes(query.toLowerCase()))
|
||||
if (filteredIds.length === 0) return null
|
||||
return (
|
||||
<div key={type}>
|
||||
<div className="sticky top-0 px-3 py-1.5 text-xs font-medium text-gray-500 bg-gray-850 border-b border-gray-700">
|
||||
<div className="sticky top-0 px-3 py-1.5 text-xs font-medium text-gray-500 bg-surface-3 border-b border-white/[0.06]">
|
||||
{type}
|
||||
</div>
|
||||
{filteredIds.map((id) => (
|
||||
|
||||
@ -64,10 +64,10 @@ export default function ReactionLogs() {
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Reaction Logs</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Reaction Logs</h2>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6">
|
||||
<div className="bg-surface-1 rounded-lg border border-white/[0.06] 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>
|
||||
@ -76,7 +76,7 @@ export default function ReactionLogs() {
|
||||
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"
|
||||
className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-40"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@ -103,13 +103,13 @@ export default function ReactionLogs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{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">
|
||||
<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">
|
||||
ID
|
||||
@ -151,8 +151,8 @@ export default function ReactionLogs() {
|
||||
<tr
|
||||
key={log.id}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{log.id}</td>
|
||||
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.reaction_id}</td>
|
||||
@ -184,7 +184,7 @@ export default function ReactionLogs() {
|
||||
{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">
|
||||
<pre className="mt-2 p-2 bg-surface-3 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)}
|
||||
|
||||
@ -41,13 +41,13 @@ export default function Reactions() {
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Reactions</h2>
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Reactions</h2>
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No reactions found" />
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-800/80 border-b border-gray-700">
|
||||
<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">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
@ -73,8 +73,8 @@ export default function Reactions() {
|
||||
<tr
|
||||
key={i}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{reaction.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
|
||||
@ -60,10 +60,10 @@ export default function RequestLogs() {
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Request Logs</h2>
|
||||
<h2 className="text-lg font-semibold text-white tracking-tight mb-5">Request Logs</h2>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6">
|
||||
<div className="bg-surface-1 rounded-lg border border-white/[0.06] p-4 mb-6">
|
||||
<div className="flex items-end gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Filter by API Key ID</label>
|
||||
@ -99,7 +99,7 @@ export default function RequestLogs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800">
|
||||
<div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
|
||||
{data.length === 0 ? (
|
||||
<EmptyState message="No request logs found" />
|
||||
) : (
|
||||
@ -138,8 +138,8 @@ export default function RequestLogs() {
|
||||
<tr
|
||||
key={log.id}
|
||||
className={`transition-colors ${
|
||||
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30'
|
||||
} hover:bg-gray-800/60`}
|
||||
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-sm text-gray-300">{log.id}</td>
|
||||
<td className="px-4 py-3">
|
||||
|
||||
@ -1,60 +1,64 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@layer base {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
background: linear-gradient(to bottom, #050505, #0a0a0f);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: #07080a;
|
||||
color: #e5e7eb;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for dark theme */
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1f;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #374151;
|
||||
border-radius: 4px;
|
||||
background: #2a2d36;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4b5563;
|
||||
background: #3a3d46;
|
||||
}
|
||||
|
||||
/* Smooth transitions for interactive elements */
|
||||
button,
|
||||
a,
|
||||
input,
|
||||
select {
|
||||
transition: all 0.2s ease;
|
||||
/* Global transitions */
|
||||
button, a, input, select, textarea {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
/* Subtle noise grain overlay */
|
||||
.grain::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
/* Table row animation */
|
||||
@keyframes rowFadeIn {
|
||||
from { opacity: 0; transform: translateX(-4px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.animate-row {
|
||||
animation: rowFadeIn 0.2s ease-out both;
|
||||
}
|
||||
|
||||
@ -2,7 +2,45 @@
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
surface: {
|
||||
0: '#07080a',
|
||||
1: '#0d0f12',
|
||||
2: '#13161b',
|
||||
3: '#1a1d24',
|
||||
4: '#22262f',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: '#3b82f6',
|
||||
dim: '#2563eb',
|
||||
glow: '#60a5fa',
|
||||
},
|
||||
mint: {
|
||||
DEFAULT: '#34d399',
|
||||
dim: '#059669',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Inter"', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.25s ease-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user