mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 17:21:10 +00:00
Refactor project structure and dependencies; rename package to azaion-ui, update version to 0.0.1, and remove unused files. Introduce new routing and authentication features in App component.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { api } from '../../api/client'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog'
|
||||
import type { DetectionClass, Aircraft, User } from '../../types'
|
||||
|
||||
export default function AdminPage() {
|
||||
const { t } = useTranslation()
|
||||
const [classes, setClasses] = useState<DetectionClass[]>([])
|
||||
const [aircrafts, setAircrafts] = useState<Aircraft[]>([])
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
const [newClass, setNewClass] = useState({ name: '', shortName: '', color: '#FF0000', maxSizeM: 7 })
|
||||
const [newUser, setNewUser] = useState({ name: '', email: '', password: '', role: 'Annotator' })
|
||||
const [deactivateId, setDeactivateId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.get<DetectionClass[]>('/api/annotations/classes').then(setClasses).catch(() => {})
|
||||
api.get<Aircraft[]>('/api/flights/aircrafts').then(setAircrafts).catch(() => {})
|
||||
api.get<User[]>('/api/admin/users').then(setUsers).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleAddClass = async () => {
|
||||
if (!newClass.name) return
|
||||
await api.post('/api/admin/classes', newClass)
|
||||
const updated = await api.get<DetectionClass[]>('/api/annotations/classes')
|
||||
setClasses(updated)
|
||||
setNewClass({ name: '', shortName: '', color: '#FF0000', maxSizeM: 7 })
|
||||
}
|
||||
|
||||
const handleDeleteClass = async (id: number) => {
|
||||
await api.delete(`/api/admin/classes/${id}`)
|
||||
setClasses(prev => prev.filter(c => c.id !== id))
|
||||
}
|
||||
|
||||
const handleAddUser = async () => {
|
||||
if (!newUser.email || !newUser.password) return
|
||||
await api.post('/api/admin/users', newUser)
|
||||
const updated = await api.get<User[]>('/api/admin/users')
|
||||
setUsers(updated)
|
||||
setNewUser({ name: '', email: '', password: '', role: 'Annotator' })
|
||||
}
|
||||
|
||||
const handleDeactivate = async () => {
|
||||
if (!deactivateId) return
|
||||
await api.patch(`/api/admin/users/${deactivateId}`, { isActive: false })
|
||||
setUsers(prev => prev.map(u => u.id === deactivateId ? { ...u, isActive: false } : u))
|
||||
setDeactivateId(null)
|
||||
}
|
||||
|
||||
const handleToggleDefault = async (a: Aircraft) => {
|
||||
await api.patch(`/api/flights/aircrafts/${a.id}`, { isDefault: !a.isDefault })
|
||||
setAircrafts(prev => prev.map(x => x.id === a.id ? { ...x, isDefault: !x.isDefault } : x))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-y-auto p-4 gap-4">
|
||||
{/* Detection classes */}
|
||||
<div className="w-[340px] shrink-0">
|
||||
<h2 className="text-sm font-semibold text-white mb-2">{t('admin.classes')}</h2>
|
||||
<div className="bg-az-panel border border-az-border rounded overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-az-border text-az-muted">
|
||||
<th className="px-2 py-1 text-left">#</th>
|
||||
<th className="px-2 py-1 text-left">Name</th>
|
||||
<th className="px-2 py-1">Color</th>
|
||||
<th className="px-2 py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{classes.map(c => (
|
||||
<tr key={c.id} className="border-b border-az-border text-az-text">
|
||||
<td className="px-2 py-1">{c.id}</td>
|
||||
<td className="px-2 py-1">{c.name}</td>
|
||||
<td className="px-2 py-1 text-center"><span className="inline-block w-3 h-3 rounded-full" style={{ backgroundColor: c.color }} /></td>
|
||||
<td className="px-2 py-1"><button onClick={() => handleDeleteClass(c.id)} className="text-az-muted hover:text-az-red">×</button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="p-2 flex gap-1 border-t border-az-border">
|
||||
<input value={newClass.name} onChange={e => setNewClass(p => ({ ...p, name: e.target.value }))} placeholder="Name" className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text" />
|
||||
<input type="color" value={newClass.color} onChange={e => setNewClass(p => ({ ...p, color: e.target.value }))} className="w-8 h-7 border-0 bg-transparent cursor-pointer" />
|
||||
<button onClick={handleAddClass} className="bg-az-orange text-white text-xs px-2 py-1 rounded">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: AI + GPS settings */}
|
||||
<div className="flex-1 space-y-4 max-w-md">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-white mb-2">{t('admin.aiSettings')}</h2>
|
||||
<div className="bg-az-panel border border-az-border rounded p-3 space-y-2 text-xs">
|
||||
<div>
|
||||
<label className="text-az-muted">Frame Period Recognition</label>
|
||||
<input type="number" defaultValue={5} className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted">Frame Recognition Seconds</label>
|
||||
<input type="number" defaultValue={1} className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted">Probability Threshold</label>
|
||||
<input type="number" defaultValue={0.5} step={0.05} min={0} max={1} className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text" />
|
||||
</div>
|
||||
<button className="bg-az-orange text-white text-xs px-3 py-1 rounded">{t('common.save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-white mb-2">{t('admin.gpsSettings')}</h2>
|
||||
<div className="bg-az-panel border border-az-border rounded p-3 space-y-2 text-xs">
|
||||
<div>
|
||||
<label className="text-az-muted">Device Address</label>
|
||||
<input defaultValue="192.168.1.100" className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted">Port</label>
|
||||
<input type="number" defaultValue={5535} className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-az-muted">Protocol</label>
|
||||
<select className="w-full bg-az-bg border border-az-border rounded px-2 py-1 mt-0.5 text-az-text">
|
||||
<option>TCP</option>
|
||||
<option>UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
<button className="bg-az-orange text-white text-xs px-3 py-1 rounded">{t('common.save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-white mb-2">{t('admin.users')}</h2>
|
||||
<div className="bg-az-panel border border-az-border rounded overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-az-border text-az-muted">
|
||||
<th className="px-2 py-1 text-left">Name</th>
|
||||
<th className="px-2 py-1 text-left">Email</th>
|
||||
<th className="px-2 py-1">Role</th>
|
||||
<th className="px-2 py-1">Status</th>
|
||||
<th className="px-2 py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(u => (
|
||||
<tr key={u.id} className="border-b border-az-border text-az-text">
|
||||
<td className="px-2 py-1">{u.name}</td>
|
||||
<td className="px-2 py-1">{u.email}</td>
|
||||
<td className="px-2 py-1 text-center">{u.role}</td>
|
||||
<td className="px-2 py-1 text-center">
|
||||
<span className={`px-1 rounded ${u.isActive ? 'text-az-green' : 'text-az-red'}`}>
|
||||
{u.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-1">
|
||||
{u.isActive && (
|
||||
<button onClick={() => setDeactivateId(u.id)} className="text-az-muted hover:text-az-red text-xs">
|
||||
{t('admin.deactivate')}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="p-2 flex gap-1 border-t border-az-border">
|
||||
<input value={newUser.name} onChange={e => setNewUser(p => ({ ...p, name: e.target.value }))} placeholder="Name" className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text" />
|
||||
<input value={newUser.email} onChange={e => setNewUser(p => ({ ...p, email: e.target.value }))} placeholder="Email" className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text" />
|
||||
<input value={newUser.password} onChange={e => setNewUser(p => ({ ...p, password: e.target.value }))} placeholder="Password" type="password" className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text" />
|
||||
<select value={newUser.role} onChange={e => setNewUser(p => ({ ...p, role: e.target.value }))} className="bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text">
|
||||
<option>Annotator</option>
|
||||
<option>Admin</option>
|
||||
<option>Viewer</option>
|
||||
</select>
|
||||
<button onClick={handleAddUser} className="bg-az-orange text-white text-xs px-2 py-1 rounded">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Aircrafts sidebar */}
|
||||
<div className="w-[280px] shrink-0">
|
||||
<h2 className="text-sm font-semibold text-white mb-2">{t('admin.aircrafts')}</h2>
|
||||
<div className="bg-az-panel border border-az-border rounded p-2 space-y-1">
|
||||
{aircrafts.map(a => (
|
||||
<div key={a.id} onClick={() => handleToggleDefault(a)} className="flex items-center gap-2 px-2 py-1 rounded cursor-pointer hover:bg-az-bg text-xs text-az-text">
|
||||
<span className={`px-1 rounded text-[10px] ${a.type === 'Plane' ? 'bg-az-blue/20 text-az-blue' : 'bg-az-green/20 text-az-green'}`}>
|
||||
{a.type === 'Plane' ? 'P' : 'C'}
|
||||
</span>
|
||||
<span className="flex-1">{a.model}</span>
|
||||
<span className={`text-sm ${a.isDefault ? 'text-az-orange' : 'text-az-muted'}`}>★</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deactivateId}
|
||||
title={t('admin.deactivate')}
|
||||
message="Deactivate this user?"
|
||||
onConfirm={handleDeactivate}
|
||||
onCancel={() => setDeactivateId(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user