feat(admin): finalizing modular refactoring with testing suite and UI polish
This commit is contained in:
338
frontend/hooks/useAdmin.ts
Normal file
338
frontend/hooks/useAdmin.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { inventoryApi } from '@/lib/api';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function useAdmin() {
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [ldapConfig, setLdapConfig] = useState<any>({
|
||||
ldap_enabled: false,
|
||||
server_uri: '',
|
||||
base_dn: '',
|
||||
user_template: '',
|
||||
groups_dn: '',
|
||||
use_tls: false,
|
||||
role_mappings: []
|
||||
});
|
||||
const [testingLdap, setTestingLdap] = useState(false);
|
||||
|
||||
const [editingUser, setEditingUser] = useState<any | null>(null);
|
||||
const [editUserForm, setEditUserForm] = useState({ username: '', password: '', role: 'user' });
|
||||
|
||||
const [editingCategory, setEditingCategory] = useState<any | null>(null);
|
||||
const [editCatForm, setEditCatForm] = useState({ name: '', description: '' });
|
||||
|
||||
const [backups, setBackups] = useState<any[]>([]);
|
||||
const [dbStats, setDbStats] = useState({ backup_count: 0, total_size_bytes: 0 });
|
||||
const [dbSettings, setDbSettings] = useState({ retention_count: 10, schedule_hour: 3, schedule_freq_days: 1 });
|
||||
const [isBackingUp, setIsBackingUp] = useState(false);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
|
||||
const [aiPrompt, setAiPrompt] = useState("");
|
||||
const [aiConfig, setAiConfig] = useState<any>(null);
|
||||
const [aiKeys, setAiKeys] = useState({ gemini: '', claude: '' });
|
||||
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
|
||||
const [isSavingKeys, setIsSavingKeys] = useState(false);
|
||||
const [isTestingKeys, setIsTestingKeys] = useState<{gemini: boolean, claude: boolean}>(
|
||||
{gemini: false, claude: false}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [u, c, l, b, s, st, promptRes, ai] = await Promise.all([
|
||||
inventoryApi.getUsers(),
|
||||
inventoryApi.getCategories(),
|
||||
inventoryApi.getLdapConfig(),
|
||||
inventoryApi.getDbBackups(),
|
||||
inventoryApi.getDbStats(),
|
||||
inventoryApi.getDbSettings(),
|
||||
inventoryApi.getAiPrompt(),
|
||||
inventoryApi.getAiConfig()
|
||||
]);
|
||||
setUsers(u);
|
||||
setCategories(c);
|
||||
if (l && l.server_uri) setLdapConfig(l);
|
||||
setBackups(b);
|
||||
setDbStats(s);
|
||||
setDbSettings(st);
|
||||
setAiPrompt(promptRes.value);
|
||||
setAiConfig(ai);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to load admin data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddUser = async () => {
|
||||
const name = window.prompt("Enter new username:");
|
||||
if (!name) return;
|
||||
const pwd = window.prompt("Enter password for " + name + ":");
|
||||
if (!pwd) return;
|
||||
|
||||
try {
|
||||
await inventoryApi.createUser({ username: name, password: pwd, role: 'user' });
|
||||
toast.success("User created successfully");
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to create user");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (id: number, username: string) => {
|
||||
if (username === 'Admin') return;
|
||||
if (!window.confirm(`Delete user ${username}?`)) return;
|
||||
|
||||
try {
|
||||
await inventoryApi.deleteUser(id);
|
||||
toast.success("User removed");
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Delete failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateUserSubmit = async () => {
|
||||
if (!editingUser) return;
|
||||
try {
|
||||
const payload: any = { username: editUserForm.username, role: editUserForm.role };
|
||||
if (editUserForm.password) payload.password = editUserForm.password;
|
||||
|
||||
await inventoryApi.updateUser(editingUser.id, payload);
|
||||
toast.success("User updated successfully");
|
||||
setEditingUser(null);
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Update failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAiProvider = async (provider: string) => {
|
||||
try {
|
||||
await inventoryApi.updateAiProvider(provider);
|
||||
toast.success(`AI provider switched to ${provider}`);
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to switch AI provider");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAiKeys = async () => {
|
||||
if (!aiKeys.gemini && !aiKeys.claude) {
|
||||
toast.error("Enter at least one API key to save");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSavingKeys(true);
|
||||
try {
|
||||
await inventoryApi.updateAiKeys({
|
||||
gemini_api_key: aiKeys.gemini || undefined,
|
||||
claude_api_key: aiKeys.claude || undefined
|
||||
});
|
||||
toast.success("AI Configuration keys updated successfully");
|
||||
setAiKeys({ gemini: '', claude: '' });
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update AI keys");
|
||||
} finally {
|
||||
setIsSavingKeys(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestAiKey = async (provider: 'gemini' | 'claude') => {
|
||||
const keyToTest = provider === 'gemini' ? aiKeys.gemini : aiKeys.claude;
|
||||
|
||||
setIsTestingKeys(prev => ({ ...prev, [provider]: true }));
|
||||
try {
|
||||
const res = await inventoryApi.testAiKey(provider, keyToTest);
|
||||
toast.success(res.message || `${provider.toUpperCase()} connection successful!`);
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.detail || `Failed to test ${provider} key`;
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
setIsTestingKeys(prev => ({ ...prev, [provider]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddCategory = async () => {
|
||||
const name = window.prompt("New category name:");
|
||||
if (!name) return;
|
||||
const desc = window.prompt("Description (optional):");
|
||||
|
||||
try {
|
||||
await inventoryApi.createCategory({ name, description: desc });
|
||||
toast.success("Category added");
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to add category");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateCategorySubmit = async () => {
|
||||
if (!editingCategory) return;
|
||||
try {
|
||||
await inventoryApi.updateCategory(editingCategory.id, editCatForm);
|
||||
toast.success("Category updated");
|
||||
setEditingCategory(null);
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Update failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCategory = async (id: number, name: string) => {
|
||||
if (!window.confirm(`Delete category ${name}?`)) return;
|
||||
|
||||
try {
|
||||
await inventoryApi.deleteCategory(id);
|
||||
toast.success("Category removed");
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.detail || "Delete failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBackup = async () => {
|
||||
setIsBackingUp(true);
|
||||
try {
|
||||
await inventoryApi.triggerBackup();
|
||||
toast.success("Snapshot created successfully");
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
toast.error("Backup failed");
|
||||
} finally {
|
||||
setIsBackingUp(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDbSettings = async (newSettings: any) => {
|
||||
try {
|
||||
await inventoryApi.updateDbSettings(newSettings);
|
||||
toast.success("System policy updated");
|
||||
setDbSettings(newSettings);
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update settings");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePrompt = async () => {
|
||||
setIsSavingPrompt(true);
|
||||
try {
|
||||
await inventoryApi.updateAiPrompt(aiPrompt);
|
||||
toast.success("AI Extraction Prompt updated");
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update AI prompt");
|
||||
} finally {
|
||||
setIsSavingPrompt(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateLdap = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await inventoryApi.updateLdapConfig(ldapConfig);
|
||||
toast.success("Enterprise configuration updated");
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update LDAP config");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestLdap = async () => {
|
||||
setTestingLdap(true);
|
||||
try {
|
||||
const res = await inventoryApi.testLdapConnection(ldapConfig);
|
||||
if (res.status === 'success') {
|
||||
toast.success(res.message || "LDAP Connection Successful!");
|
||||
} else {
|
||||
toast.error(`LDAP Error: ${res.message || "Unknown error"}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(`Connection failed: ${err.response?.data?.detail || err.message}`);
|
||||
} finally {
|
||||
setTestingLdap(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async (filename: string) => {
|
||||
if (!window.confirm(`DANGEROUS: Restore database from ${filename}? Current data will be replaced. A rollback snapshot will be created automatically.`)) return;
|
||||
const loadingToast = toast.loading("Restoring database...");
|
||||
try {
|
||||
await inventoryApi.restoreDatabase(filename);
|
||||
toast.success("Database restored! Reloading system...", { id: loadingToast });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (err: any) {
|
||||
toast.error("Restore failed", { id: loadingToast });
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportDb = async () => {
|
||||
try {
|
||||
const blob = await inventoryApi.exportDb();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `inventory_backup_${new Date().toISOString().split('T')[0]}.db`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
toast.success("Database exported successfully");
|
||||
} catch (err: any) {
|
||||
toast.error("Export failed");
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportDb = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (!window.confirm("DANGEROUS: You are about to REPLACE the entire database with this file. All current data will be lost. Proceed?")) {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
setIsImporting(true);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const loadingToast = toast.loading("Importing database...");
|
||||
try {
|
||||
await inventoryApi.importDb(formData);
|
||||
toast.success("Database imported! Reloading system...", { id: loadingToast });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (err: any) {
|
||||
toast.error("Import failed: " + (err.response?.data?.detail || err.message), { id: loadingToast });
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
import('@/lib/auth').then(m => m.clearAuth());
|
||||
window.location.href = '/login';
|
||||
};
|
||||
|
||||
return {
|
||||
users, categories, loading,
|
||||
ldapConfig, setLdapConfig, testingLdap,
|
||||
editingUser, setEditingUser, editUserForm, setEditUserForm,
|
||||
editingCategory, setEditingCategory, editCatForm, setEditCatForm,
|
||||
backups, dbStats, dbSettings, isBackingUp, isImporting,
|
||||
aiPrompt, setAiPrompt, aiConfig, aiKeys, setAiKeys,
|
||||
isSavingPrompt, isSavingKeys, isTestingKeys,
|
||||
refreshData: loadData,
|
||||
handleAddUser, handleDeleteUser, handleUpdateUserSubmit,
|
||||
handleUpdateLdap, handleTestLdap,
|
||||
handleUpdateAiProvider, handleSaveAiKeys, handleTestAiKey, handleUpdatePrompt,
|
||||
handleAddCategory, handleUpdateCategorySubmit, handleDeleteCategory,
|
||||
handleCreateBackup, handleUpdateDbSettings, handleRestore, handleExportDb, handleImportDb,
|
||||
handleLogout
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user