Migrate admin dashboard to ts

This commit is contained in:
2025-05-18 11:53:17 +02:00
parent c478e8e8a1
commit 7044e42e94
6 changed files with 126 additions and 96 deletions

View File

@@ -6,13 +6,23 @@ import AdminUsersTab from './AdminUsersTab';
import AdminWorkspacesTab from './AdminWorkspacesTab'; import AdminWorkspacesTab from './AdminWorkspacesTab';
import AdminStatsTab from './AdminStatsTab'; import AdminStatsTab from './AdminStatsTab';
const AdminDashboard = ({ opened, onClose }) => { interface AdminDashboardProps {
opened: boolean;
onClose: () => void;
}
type AdminTabValue = 'users' | 'workspaces' | 'stats';
const AdminDashboard: React.FC<AdminDashboardProps> = ({ opened, onClose }) => {
const { user: currentUser } = useAuth(); const { user: currentUser } = useAuth();
const [activeTab, setActiveTab] = useState('users'); const [activeTab, setActiveTab] = useState<AdminTabValue>('users');
return ( return (
<Modal opened={opened} onClose={onClose} size="xl" title="Admin Dashboard"> <Modal opened={opened} onClose={onClose} size="xl" title="Admin Dashboard">
<Tabs value={activeTab} onChange={setActiveTab}> <Tabs
value={activeTab}
onChange={(value) => setActiveTab(value as AdminTabValue)}
>
<Tabs.List> <Tabs.List>
<Tabs.Tab value="users" leftSection={<IconUsers size={16} />}> <Tabs.Tab value="users" leftSection={<IconUsers size={16} />}>
Users Users
@@ -26,7 +36,7 @@ const AdminDashboard = ({ opened, onClose }) => {
</Tabs.List> </Tabs.List>
<Tabs.Panel value="users" pt="md"> <Tabs.Panel value="users" pt="md">
<AdminUsersTab currentUser={currentUser} /> {currentUser && <AdminUsersTab currentUser={currentUser} />}
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="workspaces" pt="md"> <Tabs.Panel value="workspaces" pt="md">

View File

@@ -4,8 +4,13 @@ import { IconAlertCircle } from '@tabler/icons-react';
import { useAdminData } from '../../../hooks/useAdminData'; import { useAdminData } from '../../../hooks/useAdminData';
import { formatBytes } from '../../../utils/formatBytes'; import { formatBytes } from '../../../utils/formatBytes';
const AdminStatsTab = () => { interface StatsRow {
const { data: stats, loading, error } = useAdminData('stats'); label: string;
value: string | number;
}
const AdminStatsTab: React.FC = () => {
const { data: stats, loading, error } = useAdminData<'stats'>('stats');
if (loading) { if (loading) {
return <LoadingOverlay visible={true} />; return <LoadingOverlay visible={true} />;
@@ -19,7 +24,7 @@ const AdminStatsTab = () => {
); );
} }
const statsRows = [ const statsRows: StatsRow[] = [
{ label: 'Total Users', value: stats.totalUsers }, { label: 'Total Users', value: stats.totalUsers },
{ label: 'Active Users', value: stats.activeUsers }, { label: 'Active Users', value: stats.activeUsers },
{ label: 'Total Workspaces', value: stats.totalWorkspaces }, { label: 'Total Workspaces', value: stats.totalWorkspaces },
@@ -33,7 +38,7 @@ const AdminStatsTab = () => {
System Statistics System Statistics
</Text> </Text>
<Table striped highlightOnHover withBorder> <Table striped highlightOnHover>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>Metric</Table.Th> <Table.Th>Metric</Table.Th>

View File

@@ -20,8 +20,14 @@ import { useUserAdmin } from '../../../hooks/useUserAdmin';
import CreateUserModal from '../../modals/user/CreateUserModal'; import CreateUserModal from '../../modals/user/CreateUserModal';
import EditUserModal from '../../modals/user/EditUserModal'; import EditUserModal from '../../modals/user/EditUserModal';
import DeleteUserModal from '../../modals/user/DeleteUserModal'; import DeleteUserModal from '../../modals/user/DeleteUserModal';
import { User } from '../../../types/authApi';
import { CreateUserRequest, UpdateUserRequest } from '../../../types/adminApi';
const AdminUsersTab = ({ currentUser }) => { interface AdminUsersTabProps {
currentUser: User;
}
const AdminUsersTab: React.FC<AdminUsersTabProps> = ({ currentUser }) => {
const { const {
users, users,
loading, loading,
@@ -31,19 +37,24 @@ const AdminUsersTab = ({ currentUser }) => {
delete: deleteUser, delete: deleteUser,
} = useUserAdmin(); } = useUserAdmin();
const [createModalOpened, setCreateModalOpened] = useState(false); const [createModalOpened, setCreateModalOpened] = useState<boolean>(false);
const [editModalData, setEditModalData] = useState(null); const [editModalData, setEditModalData] = useState<User | null>(null);
const [deleteModalData, setDeleteModalData] = useState(null); const [deleteModalData, setDeleteModalData] = useState<User | null>(null);
const handleCreateUser = async (userData) => { const handleCreateUser = async (
userData: CreateUserRequest
): Promise<boolean> => {
return await create(userData); return await create(userData);
}; };
const handleEditUser = async (id, userData) => { const handleEditUser = async (
id: number,
userData: UpdateUserRequest
): Promise<boolean> => {
return await update(id, userData); return await update(id, userData);
}; };
const handleDeleteClick = (user) => { const handleDeleteClick = (user: User): void => {
if (user.id === currentUser.id) { if (user.id === currentUser.id) {
notifications.show({ notifications.show({
title: 'Error', title: 'Error',
@@ -55,20 +66,20 @@ const AdminUsersTab = ({ currentUser }) => {
setDeleteModalData(user); setDeleteModalData(user);
}; };
const handleDeleteConfirm = async () => { const handleDeleteConfirm = async (): Promise<void> => {
if (!deleteModalData) return; if (!deleteModalData) return;
const result = await deleteUser(deleteModalData.id); const success = await deleteUser(deleteModalData.id);
if (result.success) { if (success) {
setDeleteModalData(null); setDeleteModalData(null);
} }
}; };
const rows = users.map((user) => ( const renderUserRow = (user: User) => (
<Table.Tr key={user.id}> <Table.Tr key={user.id}>
<Table.Td>{user.email}</Table.Td> <Table.Td>{user.email}</Table.Td>
<Table.Td>{user.displayName}</Table.Td> <Table.Td>{user.displayName}</Table.Td>
<Table.Td> <Table.Td>
<Text transform="capitalize">{user.role}</Text> <Text style={{ textTransform: 'capitalize' }}>{user.role}</Text>
</Table.Td> </Table.Td>
<Table.Td>{new Date(user.createdAt).toLocaleDateString()}</Table.Td> <Table.Td>{new Date(user.createdAt).toLocaleDateString()}</Table.Td>
<Table.Td> <Table.Td>
@@ -91,7 +102,7 @@ const AdminUsersTab = ({ currentUser }) => {
</Group> </Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
)); );
return ( return (
<Box pos="relative"> <Box pos="relative">
@@ -130,7 +141,7 @@ const AdminUsersTab = ({ currentUser }) => {
<Table.Th style={{ width: 100 }}>Actions</Table.Th> <Table.Th style={{ width: 100 }}>Actions</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody>{rows}</Table.Tbody> <Table.Tbody>{users.map(renderUserRow)}</Table.Tbody>
</Table> </Table>
<CreateUserModal <CreateUserModal

View File

@@ -1,67 +0,0 @@
import React from 'react';
import {
Table,
Group,
Text,
ActionIcon,
Box,
LoadingOverlay,
Alert,
} from '@mantine/core';
import { IconTrash, IconEdit, IconAlertCircle } from '@tabler/icons-react';
import { useAdminData } from '../../../hooks/useAdminData';
import { formatBytes } from '../../../utils/formatBytes';
const AdminWorkspacesTab = () => {
const { data: workspaces, loading, error } = useAdminData('workspaces');
const rows = workspaces.map((workspace) => (
<Table.Tr key={workspace.id}>
<Table.Td>{workspace.userEmail}</Table.Td>
<Table.Td>{workspace.workspaceName}</Table.Td>
<Table.Td>
{new Date(workspace.workspaceCreatedAt).toLocaleDateString()}
</Table.Td>
<Table.Td>{workspace.totalFiles}</Table.Td>
<Table.Td>{formatBytes(workspace.totalSize)}</Table.Td>
</Table.Tr>
));
return (
<Box pos="relative">
<LoadingOverlay visible={loading} />
{error && (
<Alert
icon={<IconAlertCircle size={16} />}
title="Error"
color="red"
mb="md"
>
{error}
</Alert>
)}
<Group justify="space-between" mb="md">
<Text size="xl" fw={700}>
Workspace Management
</Text>
</Group>
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Owner</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Created At</Table.Th>
<Table.Th>Total Files</Table.Th>
<Table.Th>Total Size</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Box>
);
};
export default AdminWorkspacesTab;

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { Table, Group, Text, Box, LoadingOverlay, Alert } from '@mantine/core';
import { IconAlertCircle } from '@tabler/icons-react';
import { useAdminData } from '../../../hooks/useAdminData';
import { formatBytes } from '../../../utils/formatBytes';
import { FileCountStats, WorkspaceStats } from '../../../types/adminApi';
const AdminWorkspacesTab: React.FC = () => {
const {
data: workspaces,
loading,
error,
} = useAdminData<'workspaces'>('workspaces');
const renderWorkspaceRow = (workspace: WorkspaceStats) => {
const fileStats: FileCountStats = workspace.fileCountStats || {
totalFiles: 0,
totalSize: 0,
};
return (
<Table.Tr key={workspace.workspaceID}>
<Table.Td>{workspace.userEmail}</Table.Td>
<Table.Td>{workspace.workspaceName}</Table.Td>
<Table.Td>
{new Date(workspace.workspaceCreatedAt).toLocaleDateString()}
</Table.Td>
<Table.Td>{fileStats.totalFiles}</Table.Td>
<Table.Td>{formatBytes(fileStats.totalSize)}</Table.Td>
</Table.Tr>
);
};
return (
<Box pos="relative">
<LoadingOverlay visible={loading} />
{error && (
<Alert
icon={<IconAlertCircle size={16} />}
title="Error"
color="red"
mb="md"
>
{error}
</Alert>
)}
<Group justify="space-between" mb="md">
<Text size="xl" fw={700}>
Workspace Management
</Text>
</Group>
<Table striped highlightOnHover withTableBorder>
<Table.Thead>
<Table.Tr>
<Table.Th>Owner</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Created At</Table.Th>
<Table.Th>Total Files</Table.Th>
<Table.Th>Total Size</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{!loading && !error && workspaces.map(renderWorkspaceRow)}
</Table.Tbody>
</Table>
</Box>
);
};
export default AdminWorkspacesTab;

View File

@@ -1,9 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { getUsers, getWorkspaces, getSystemStats } from '@/api/admin'; import { getUsers, getWorkspaces, getSystemStats } from '@/api/admin';
import { SystemStats } from '@/types/adminApi'; import { SystemStats, UserStats, WorkspaceStats } from '@/types/adminApi';
import { Workspace } from '@/types/workspace';
import { User } from '@/types/authApi';
// Possible types of admin data // Possible types of admin data
type AdminDataType = 'stats' | 'workspaces' | 'users'; type AdminDataType = 'stats' | 'workspaces' | 'users';
@@ -12,9 +10,9 @@ type AdminDataType = 'stats' | 'workspaces' | 'users';
type AdminData<T extends AdminDataType> = T extends 'stats' type AdminData<T extends AdminDataType> = T extends 'stats'
? SystemStats ? SystemStats
: T extends 'workspaces' : T extends 'workspaces'
? Workspace[] ? WorkspaceStats[]
: T extends 'users' : T extends 'users'
? User[] ? UserStats[]
: never; : never;
// Define the return type of the hook // Define the return type of the hook
@@ -34,9 +32,9 @@ export const useAdminData = <T extends AdminDataType>(
if (type === 'stats') { if (type === 'stats') {
return {} as SystemStats as AdminData<T>; return {} as SystemStats as AdminData<T>;
} else if (type === 'workspaces') { } else if (type === 'workspaces') {
return [] as Workspace[] as AdminData<T>; return [] as WorkspaceStats[] as AdminData<T>;
} else if (type === 'users') { } else if (type === 'users') {
return [] as User[] as AdminData<T>; return [] as UserStats[] as AdminData<T>;
} else { } else {
// This case should never happen due to type constraints, // This case should never happen due to type constraints,
// but TypeScript requires us to handle it // but TypeScript requires us to handle it