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 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">
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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 { 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
|
||||||
|
|||||||
Reference in New Issue
Block a user