mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Migrate admin dashboard to ts
This commit is contained in:
@@ -6,13 +6,23 @@ import AdminUsersTab from './AdminUsersTab';
|
||||
import AdminWorkspacesTab from './AdminWorkspacesTab';
|
||||
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 [activeTab, setActiveTab] = useState('users');
|
||||
const [activeTab, setActiveTab] = useState<AdminTabValue>('users');
|
||||
|
||||
return (
|
||||
<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.Tab value="users" leftSection={<IconUsers size={16} />}>
|
||||
Users
|
||||
@@ -26,7 +36,7 @@ const AdminDashboard = ({ opened, onClose }) => {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="users" pt="md">
|
||||
<AdminUsersTab currentUser={currentUser} />
|
||||
{currentUser && <AdminUsersTab currentUser={currentUser} />}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="workspaces" pt="md">
|
||||
@@ -4,8 +4,13 @@ import { IconAlertCircle } from '@tabler/icons-react';
|
||||
import { useAdminData } from '../../../hooks/useAdminData';
|
||||
import { formatBytes } from '../../../utils/formatBytes';
|
||||
|
||||
const AdminStatsTab = () => {
|
||||
const { data: stats, loading, error } = useAdminData('stats');
|
||||
interface StatsRow {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const AdminStatsTab: React.FC = () => {
|
||||
const { data: stats, loading, error } = useAdminData<'stats'>('stats');
|
||||
|
||||
if (loading) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
@@ -19,7 +24,7 @@ const AdminStatsTab = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const statsRows = [
|
||||
const statsRows: StatsRow[] = [
|
||||
{ label: 'Total Users', value: stats.totalUsers },
|
||||
{ label: 'Active Users', value: stats.activeUsers },
|
||||
{ label: 'Total Workspaces', value: stats.totalWorkspaces },
|
||||
@@ -33,7 +38,7 @@ const AdminStatsTab = () => {
|
||||
System Statistics
|
||||
</Text>
|
||||
|
||||
<Table striped highlightOnHover withBorder>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Metric</Table.Th>
|
||||
@@ -20,8 +20,14 @@ import { useUserAdmin } from '../../../hooks/useUserAdmin';
|
||||
import CreateUserModal from '../../modals/user/CreateUserModal';
|
||||
import EditUserModal from '../../modals/user/EditUserModal';
|
||||
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 {
|
||||
users,
|
||||
loading,
|
||||
@@ -31,19 +37,24 @@ const AdminUsersTab = ({ currentUser }) => {
|
||||
delete: deleteUser,
|
||||
} = useUserAdmin();
|
||||
|
||||
const [createModalOpened, setCreateModalOpened] = useState(false);
|
||||
const [editModalData, setEditModalData] = useState(null);
|
||||
const [deleteModalData, setDeleteModalData] = useState(null);
|
||||
const [createModalOpened, setCreateModalOpened] = useState<boolean>(false);
|
||||
const [editModalData, setEditModalData] = useState<User | null>(null);
|
||||
const [deleteModalData, setDeleteModalData] = useState<User | null>(null);
|
||||
|
||||
const handleCreateUser = async (userData) => {
|
||||
const handleCreateUser = async (
|
||||
userData: CreateUserRequest
|
||||
): Promise<boolean> => {
|
||||
return await create(userData);
|
||||
};
|
||||
|
||||
const handleEditUser = async (id, userData) => {
|
||||
const handleEditUser = async (
|
||||
id: number,
|
||||
userData: UpdateUserRequest
|
||||
): Promise<boolean> => {
|
||||
return await update(id, userData);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (user) => {
|
||||
const handleDeleteClick = (user: User): void => {
|
||||
if (user.id === currentUser.id) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
@@ -55,20 +66,20 @@ const AdminUsersTab = ({ currentUser }) => {
|
||||
setDeleteModalData(user);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
const handleDeleteConfirm = async (): Promise<void> => {
|
||||
if (!deleteModalData) return;
|
||||
const result = await deleteUser(deleteModalData.id);
|
||||
if (result.success) {
|
||||
const success = await deleteUser(deleteModalData.id);
|
||||
if (success) {
|
||||
setDeleteModalData(null);
|
||||
}
|
||||
};
|
||||
|
||||
const rows = users.map((user) => (
|
||||
const renderUserRow = (user: User) => (
|
||||
<Table.Tr key={user.id}>
|
||||
<Table.Td>{user.email}</Table.Td>
|
||||
<Table.Td>{user.displayName}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text transform="capitalize">{user.role}</Text>
|
||||
<Text style={{ textTransform: 'capitalize' }}>{user.role}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{new Date(user.createdAt).toLocaleDateString()}</Table.Td>
|
||||
<Table.Td>
|
||||
@@ -91,7 +102,7 @@ const AdminUsersTab = ({ currentUser }) => {
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
);
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
@@ -130,7 +141,7 @@ const AdminUsersTab = ({ currentUser }) => {
|
||||
<Table.Th style={{ width: 100 }}>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>{rows}</Table.Tbody>
|
||||
<Table.Tbody>{users.map(renderUserRow)}</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<CreateUserModal
|
||||
@@ -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;
|
||||
73
app/src/components/settings/admin/AdminWorkspacesTab.tsx
Normal file
73
app/src/components/settings/admin/AdminWorkspacesTab.tsx
Normal 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;
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { getUsers, getWorkspaces, getSystemStats } from '@/api/admin';
|
||||
import { SystemStats } from '@/types/adminApi';
|
||||
import { Workspace } from '@/types/workspace';
|
||||
import { User } from '@/types/authApi';
|
||||
import { SystemStats, UserStats, WorkspaceStats } from '@/types/adminApi';
|
||||
|
||||
// Possible types of admin data
|
||||
type AdminDataType = 'stats' | 'workspaces' | 'users';
|
||||
@@ -12,9 +10,9 @@ type AdminDataType = 'stats' | 'workspaces' | 'users';
|
||||
type AdminData<T extends AdminDataType> = T extends 'stats'
|
||||
? SystemStats
|
||||
: T extends 'workspaces'
|
||||
? Workspace[]
|
||||
? WorkspaceStats[]
|
||||
: T extends 'users'
|
||||
? User[]
|
||||
? UserStats[]
|
||||
: never;
|
||||
|
||||
// Define the return type of the hook
|
||||
@@ -34,9 +32,9 @@ export const useAdminData = <T extends AdminDataType>(
|
||||
if (type === 'stats') {
|
||||
return {} as SystemStats as AdminData<T>;
|
||||
} else if (type === 'workspaces') {
|
||||
return [] as Workspace[] as AdminData<T>;
|
||||
return [] as WorkspaceStats[] as AdminData<T>;
|
||||
} else if (type === 'users') {
|
||||
return [] as User[] as AdminData<T>;
|
||||
return [] as UserStats[] as AdminData<T>;
|
||||
} else {
|
||||
// This case should never happen due to type constraints,
|
||||
// but TypeScript requires us to handle it
|
||||
|
||||
Reference in New Issue
Block a user