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:
Oleksandr Bezdieniezhnykh
2026-03-25 03:10:15 +02:00
parent e407308284
commit 157a33096a
112 changed files with 6530 additions and 17843 deletions
+208
View File
@@ -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>
)
}