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:
null 2026-07-04 21:17:16 -05:00
parent bb3e9f551c
commit a9a58d8aa2
3 changed files with 89 additions and 36 deletions

View File

@ -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>

View File

@ -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);
}

View File

@ -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 */ }
}, []);