feat(ui): add create object — inline type selector
小橘 🍊(NEKO Team)
This commit is contained in:
parent
4d63e1943f
commit
b6935fc311
File diff suppressed because one or more lines are too long
@ -44,6 +44,12 @@ export const getObjects = (type?: string, limit = 50, offset = 0) =>
|
|||||||
type ? `/objects?type=${type}&limit=${limit}&offset=${offset}` : `/objects?limit=${limit}&offset=${offset}`,
|
type ? `/objects?type=${type}&limit=${limit}&offset=${offset}` : `/objects?limit=${limit}&offset=${offset}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const createObject = (type: string) =>
|
||||||
|
api<{ id: number; type: string; created_at: string }>('/objects', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ type }),
|
||||||
|
})
|
||||||
|
|
||||||
// Event Defs
|
// Event Defs
|
||||||
export const getEventDefs = () =>
|
export const getEventDefs = () =>
|
||||||
api<{ event_defs: Array<{ name: string; hash: string; parent_hash: string | null; schema: any }> }>('/event-defs')
|
api<{ event_defs: Array<{ name: string; hash: string; parent_hash: string | null; schema: any }> }>('/event-defs')
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Listbox } from '@headlessui/react'
|
import { Listbox } from '@headlessui/react'
|
||||||
import { getObjects, getObjectDefs } from '../api'
|
import { getObjects, getObjectDefs, createObject } from '../api'
|
||||||
import { formatRelativeTime } from '../utils'
|
import { formatRelativeTime } from '../utils'
|
||||||
import { Spinner, EmptyState, Pagination } from './Common'
|
import { Spinner, EmptyState, Pagination } from './Common'
|
||||||
|
|
||||||
@ -13,6 +13,10 @@ export default function Objects() {
|
|||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [limit, setLimit] = useState(50)
|
const [limit, setLimit] = useState(50)
|
||||||
const [offset, setOffset] = useState(0)
|
const [offset, setOffset] = useState(0)
|
||||||
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
|
const [createType, setCreateType] = useState('')
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [createMsg, setCreateMsg] = useState<{ ok: boolean; text: string } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getObjectDefs()
|
getObjectDefs()
|
||||||
@ -36,13 +40,80 @@ export default function Objects() {
|
|||||||
setOffset(0) // reset to first page
|
setOffset(0) // reset to first page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
setLoading(true)
|
||||||
|
getObjects(filter || undefined, limit, offset)
|
||||||
|
.then((res) => { setData(res.objects); setTotal(res.total) })
|
||||||
|
.catch((e) => setError(e.message))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!createType) return
|
||||||
|
setCreating(true)
|
||||||
|
setCreateMsg(null)
|
||||||
|
try {
|
||||||
|
const obj = await createObject(createType)
|
||||||
|
setCreateMsg({ ok: true, text: `Created object #${obj.id} (${obj.type})` })
|
||||||
|
setShowCreate(false)
|
||||||
|
setCreateType('')
|
||||||
|
refresh()
|
||||||
|
setTimeout(() => setCreateMsg(null), 3000)
|
||||||
|
} catch (err: any) {
|
||||||
|
setCreateMsg({ ok: false, text: err.message || 'Failed to create object' })
|
||||||
|
} finally {
|
||||||
|
setCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <Spinner />
|
if (loading) return <Spinner />
|
||||||
if (error) return <div className="text-red-500">Error: {error}</div>
|
if (error) return <div className="text-red-500">Error: {error}</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-2xl font-bold">Objects</h2>
|
<div className="flex items-center gap-3">
|
||||||
|
<h2 className="text-2xl font-bold">Objects</h2>
|
||||||
|
{!showCreate ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowCreate(true); setCreateType(types[0] || '') }}
|
||||||
|
className="px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-sm rounded-lg transition-colors"
|
||||||
|
disabled={types.length === 0}
|
||||||
|
>
|
||||||
|
+ Create
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{types.map((t) => (
|
||||||
|
<option key={t} value={t}>{t}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={handleCreate}
|
||||||
|
disabled={creating || !createType}
|
||||||
|
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 text-white text-sm rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{creating ? '...' : '✓'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowCreate(false); setCreateMsg(null) }}
|
||||||
|
className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{createMsg && (
|
||||||
|
<span className={`text-sm ${createMsg.ok ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{createMsg.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Listbox value={filter} onChange={handleFilterChange}>
|
<Listbox value={filter} onChange={handleFilterChange}>
|
||||||
<div className="relative w-64">
|
<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-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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user