refactor(ts): convert CleanupPanel + UsersTable admin cards to TSX (B17)
CleanupPanel (CleanupForm/CleanupStatus/CleanupResult; keyed toggle array typed [keyof CleanupForm,string][]). UsersTable (AdminUser type exported + reused by AdminPage's users state; ResetForm; typed row handlers). typecheck 0, build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bb3e9f551c
commit
a9a58d8aa2
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Play, Wrench } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { errMessage } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
|
@ -9,9 +10,42 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { FieldRow, Toggle, formatDateTime } from './adminShared';
|
||||
|
||||
interface CleanupForm {
|
||||
import_sessions_enabled: boolean;
|
||||
temp_exports_enabled: boolean;
|
||||
temp_export_max_age_hours: number;
|
||||
backup_partials_enabled: boolean;
|
||||
import_history_enabled: boolean;
|
||||
import_history_max_age_days: number;
|
||||
}
|
||||
|
||||
interface CleanupTask {
|
||||
pruned?: number;
|
||||
removed?: number;
|
||||
}
|
||||
|
||||
interface CleanupResult {
|
||||
import_sessions?: CleanupTask;
|
||||
temp_export_files?: CleanupTask;
|
||||
backup_partials?: CleanupTask;
|
||||
import_history?: CleanupTask;
|
||||
}
|
||||
|
||||
interface CleanupStatus {
|
||||
last_run_at?: string | null;
|
||||
last_result?: CleanupResult | null;
|
||||
settings?: CleanupForm;
|
||||
}
|
||||
|
||||
const TASK_TOGGLES: [keyof CleanupForm, string][] = [
|
||||
['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'],
|
||||
['temp_exports_enabled', 'Remove stale SQLite export temp files'],
|
||||
['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'],
|
||||
];
|
||||
|
||||
export default function CleanupPanel() {
|
||||
const [status, setStatus] = useState(null);
|
||||
const [form, setForm] = useState({
|
||||
const [status, setStatus] = useState<CleanupStatus | null>(null);
|
||||
const [form, setForm] = useState<CleanupForm>({
|
||||
import_sessions_enabled: true,
|
||||
temp_exports_enabled: true,
|
||||
temp_export_max_age_hours: 2,
|
||||
|
|
@ -25,11 +59,11 @@ export default function CleanupPanel() {
|
|||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.adminCleanup();
|
||||
const data = await api.adminCleanup() as CleanupStatus;
|
||||
setStatus(data);
|
||||
if (data.settings) setForm(data.settings);
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to load cleanup settings.');
|
||||
toast.error(errMessage(err, 'Failed to load cleanup settings.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -37,16 +71,16 @@ export default function CleanupPanel() {
|
|||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const set = (k, v) => setForm(prev => ({ ...prev, [k]: v }));
|
||||
const set = (k: keyof CleanupForm, v: boolean | number) => setForm(prev => ({ ...prev, [k]: v }) as CleanupForm);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const next = await api.saveAdminCleanup(form);
|
||||
const next = await api.saveAdminCleanup(form) as CleanupForm | null;
|
||||
if (next) setForm(next);
|
||||
toast.success('Cleanup settings saved.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to save cleanup settings.');
|
||||
toast.error(errMessage(err, 'Failed to save cleanup settings.'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
|
@ -55,7 +89,7 @@ export default function CleanupPanel() {
|
|||
async function handleRunNow() {
|
||||
setRunning(true);
|
||||
try {
|
||||
const result = await api.runAdminCleanup();
|
||||
const result = await api.runAdminCleanup() as { ran_at?: string; tasks?: CleanupResult };
|
||||
setStatus(prev => ({
|
||||
...prev,
|
||||
last_run_at: result.ran_at,
|
||||
|
|
@ -63,13 +97,13 @@ export default function CleanupPanel() {
|
|||
}));
|
||||
toast.success('Cleanup tasks completed.');
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Cleanup run failed.');
|
||||
toast.error(errMessage(err, 'Cleanup run failed.'));
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
function resultLine(label, task, countKey) {
|
||||
function resultLine(label: string, task: CleanupTask | undefined, countKey: keyof CleanupTask): string | null {
|
||||
if (!task || task[countKey] == null) return null;
|
||||
return `${label}: ${task[countKey]}`;
|
||||
}
|
||||
|
|
@ -137,11 +171,7 @@ export default function CleanupPanel() {
|
|||
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium">Task Settings</p>
|
||||
{[
|
||||
['import_sessions_enabled', 'Prune expired import sessions (24h TTL)'],
|
||||
['temp_exports_enabled', 'Remove stale SQLite export temp files'],
|
||||
['backup_partials_enabled', 'Remove orphaned backup .partial / .upload files'],
|
||||
].map(([key, label]) => (
|
||||
{TASK_TOGGLES.map(([key, label]) => (
|
||||
<FieldRow key={key} label={label}>
|
||||
<Toggle checked={!!form[key]} onChange={v => set(key, v)} label={label} />
|
||||
</FieldRow>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn, errMessage } from '@/lib/utils';
|
||||
import { Button, buttonVariants } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
|
@ -11,19 +11,41 @@ import {
|
|||
AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
|
||||
AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { User } from '@/hooks/useAuth';
|
||||
|
||||
export default function UsersTable({ users, onRefresh, currentUser }) {
|
||||
const [resetForms, setResetForms] = useState({});
|
||||
const [deleting, setDeleting] = useState(null);
|
||||
const [resetting, setResetting] = useState(null);
|
||||
const [roleUpdating, setRoleUpdating] = useState(null);
|
||||
const [activeUpdating, setActiveUpdating] = useState(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
export interface AdminUser {
|
||||
id: number;
|
||||
username: string;
|
||||
role?: string;
|
||||
is_default_admin?: boolean;
|
||||
active?: boolean | number;
|
||||
must_change_password?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const setReset = (id, v) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } }));
|
||||
const getForm = (id) => resetForms[id] || { pw: '', open: false };
|
||||
interface ResetForm {
|
||||
pw?: string;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const handleReset = async (user) => {
|
||||
interface UsersTableProps {
|
||||
users?: AdminUser[];
|
||||
onRefresh: () => void;
|
||||
currentUser?: User | null;
|
||||
}
|
||||
|
||||
export default function UsersTable({ users, onRefresh, currentUser }: UsersTableProps) {
|
||||
const [resetForms, setResetForms] = useState<Record<number, ResetForm>>({});
|
||||
const [deleting, setDeleting] = useState<number | null>(null);
|
||||
const [resetting, setResetting] = useState<number | null>(null);
|
||||
const [roleUpdating, setRoleUpdating] = useState<number | null>(null);
|
||||
const [activeUpdating, setActiveUpdating] = useState<number | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<AdminUser | null>(null);
|
||||
|
||||
const setReset = (id: number, v: ResetForm) => setResetForms(p => ({ ...p, [id]: { ...(p[id] || {}), ...v } }));
|
||||
const getForm = (id: number): ResetForm => resetForms[id] || { pw: '', open: false };
|
||||
|
||||
const handleReset = async (user: AdminUser) => {
|
||||
const form = getForm(user.id);
|
||||
if (!form.pw || form.pw.length < 8) { toast.error('Password must be at least 8 characters.'); return; }
|
||||
setResetting(user.id);
|
||||
|
|
@ -32,7 +54,7 @@ export default function UsersTable({ users, onRefresh, currentUser }) {
|
|||
toast.success(`Password reset for ${user.username}.`);
|
||||
setReset(user.id, { pw: '', open: false });
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to reset password.');
|
||||
toast.error(errMessage(err, 'Failed to reset password.'));
|
||||
} finally {
|
||||
setResetting(null);
|
||||
}
|
||||
|
|
@ -47,13 +69,13 @@ export default function UsersTable({ users, onRefresh, currentUser }) {
|
|||
setDeleteTarget(null);
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to delete user.');
|
||||
toast.error(errMessage(err, 'Failed to delete user.'));
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = async (user, role) => {
|
||||
const handleRoleChange = async (user: AdminUser, role: string) => {
|
||||
if (user.role === role) return;
|
||||
setRoleUpdating(user.id);
|
||||
try {
|
||||
|
|
@ -61,20 +83,20 @@ export default function UsersTable({ users, onRefresh, currentUser }) {
|
|||
toast.success(`${user.username} is now ${role === 'admin' ? 'an admin' : 'a user'}.`);
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to update user role.');
|
||||
toast.error(errMessage(err, 'Failed to update user role.'));
|
||||
} finally {
|
||||
setRoleUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveChange = async (user, active) => {
|
||||
const handleActiveChange = async (user: AdminUser, active: boolean) => {
|
||||
setActiveUpdating(user.id);
|
||||
try {
|
||||
await api.updateUserActive(user.id, { active });
|
||||
toast.success(`${user.username} is now ${active ? 'active' : 'inactive'}.`);
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
toast.error(err.message || 'Failed to update user status.');
|
||||
toast.error(errMessage(err, 'Failed to update user status.'));
|
||||
} finally {
|
||||
setActiveUpdating(null);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import UsersTable from '@/components/admin/UsersTable';
|
|||
import AddUserCard from '@/components/admin/AddUserCard';
|
||||
import BackupManagementCard from '@/components/admin/BackupManagementCard';
|
||||
import CleanupPanel from '@/components/admin/CleanupPanel';
|
||||
import type { AdminUser } from '@/components/admin/UsersTable';
|
||||
import type { User } from '@/hooks/useAuth';
|
||||
|
||||
export default function AdminPage() {
|
||||
|
|
@ -20,7 +21,7 @@ export default function AdminPage() {
|
|||
const [me, setMe] = useState<User | null>(null);
|
||||
const [hasUsers, setHasUsers] = useState<boolean | null>(null);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [users, setUsers] = useState<unknown[]>([]);
|
||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||
const [authMode, setAuthMode] = useState('multi');
|
||||
|
||||
const loadMe = useCallback(async () => {
|
||||
|
|
@ -34,7 +35,7 @@ export default function AdminPage() {
|
|||
|
||||
const loadUsers = useCallback(async () => {
|
||||
try {
|
||||
const d = await api.adminUsers() as { users?: unknown[] } | unknown[];
|
||||
const d = await api.adminUsers() as { users?: AdminUser[] } | AdminUser[];
|
||||
setUsers(Array.isArray(d) ? d : (d.users || []));
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
|
|
|
|||
Loading…
Reference in New Issue