From dd3ea9f65f54c07f66a18fb1f9d8cdb2bea770da Mon Sep 17 00:00:00 2001 From: LordMathis Date: Fri, 8 Nov 2024 23:49:12 +0100 Subject: [PATCH] Improve admin dashboard --- backend/internal/db/users.go | 22 +- backend/internal/handlers/admin_handlers.go | 9 +- backend/internal/user/user.go | 57 +---- .../modals/user/CreateUserModal.jsx | 76 +++++++ .../modals/user/DeleteUserModal.jsx | 29 +++ .../settings/admin/AdminDashboard.jsx | 208 +++--------------- .../settings/admin/AdminStatsTab.jsx | 122 ++++++++++ .../settings/admin/AdminUsersTab.jsx | 142 ++++++++++++ .../settings/admin/AdminWorkspacesTab.jsx | 73 ++++++ .../components/settings/admin/StatCard.jsx | 20 ++ frontend/src/services/adminApi.js | 6 +- 11 files changed, 517 insertions(+), 247 deletions(-) create mode 100644 frontend/src/components/modals/user/CreateUserModal.jsx create mode 100644 frontend/src/components/modals/user/DeleteUserModal.jsx create mode 100644 frontend/src/components/settings/admin/AdminStatsTab.jsx create mode 100644 frontend/src/components/settings/admin/AdminUsersTab.jsx create mode 100644 frontend/src/components/settings/admin/AdminWorkspacesTab.jsx create mode 100644 frontend/src/components/settings/admin/StatCard.jsx diff --git a/backend/internal/db/users.go b/backend/internal/db/users.go index 9ce15ab..3c7e40f 100644 --- a/backend/internal/db/users.go +++ b/backend/internal/db/users.go @@ -6,10 +6,10 @@ import ( ) // CreateUser inserts a new user record into the database -func (db *DB) CreateUser(user *models.User) error { +func (db *DB) CreateUser(user *models.User) (*models.User, error) { tx, err := db.Begin() if err != nil { - return err + return nil, err } defer tx.Rollback() @@ -18,15 +18,21 @@ func (db *DB) CreateUser(user *models.User) error { VALUES (?, ?, ?, ?)`, user.Email, user.DisplayName, user.PasswordHash, user.Role) if err != nil { - return err + return nil, err } userID, err := result.LastInsertId() if err != nil { - return err + return nil, err } user.ID = int(userID) + // Retrieve the created_at timestamp + err = tx.QueryRow("SELECT created_at FROM users WHERE id = ?", user.ID).Scan(&user.CreatedAt) + if err != nil { + return nil, err + } + // Create default workspace with default settings defaultWorkspace := &models.Workspace{ UserID: user.ID, @@ -37,22 +43,22 @@ func (db *DB) CreateUser(user *models.User) error { // Create workspace with settings err = db.createWorkspaceTx(tx, defaultWorkspace) if err != nil { - return err + return nil, err } // Update user's last workspace ID _, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", defaultWorkspace.ID, user.ID) if err != nil { - return err + return nil, err } err = tx.Commit() if err != nil { - return err + return nil, err } user.LastWorkspaceID = defaultWorkspace.ID - return nil + return user, nil } // Helper function to create a workspace in a transaction diff --git a/backend/internal/handlers/admin_handlers.go b/backend/internal/handlers/admin_handlers.go index 6b3c4a9..4c78ed1 100644 --- a/backend/internal/handlers/admin_handlers.go +++ b/backend/internal/handlers/admin_handlers.go @@ -75,18 +75,19 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { Role: req.Role, } - if err := h.DB.CreateUser(user); err != nil { + insertedUser, err := h.DB.CreateUser(user) + if err != nil { http.Error(w, "Failed to create user", http.StatusInternalServerError) return } // Initialize user workspace - if err := h.FS.InitializeUserWorkspace(user.ID, user.LastWorkspaceID); err != nil { + if err := h.FS.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil { http.Error(w, "Failed to initialize user workspace", http.StatusInternalServerError) return } - respondJSON(w, user) + respondJSON(w, insertedUser) } } @@ -197,7 +198,7 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc { return } - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNoContent) } } diff --git a/backend/internal/user/user.go b/backend/internal/user/user.go index dbfce37..fb83cc1 100644 --- a/backend/internal/user/user.go +++ b/backend/internal/user/user.go @@ -47,69 +47,18 @@ func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models. Role: models.RoleAdmin, } - err = s.DB.CreateUser(adminUser) + createdUser, err := s.DB.CreateUser(adminUser) if err != nil { return nil, fmt.Errorf("failed to create admin user: %w", err) } // Initialize workspace directory - err = s.FS.InitializeUserWorkspace(adminUser.ID, adminUser.LastWorkspaceID) + err = s.FS.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) if err != nil { return nil, fmt.Errorf("failed to initialize admin workspace: %w", err) } - log.Printf("Created admin user with ID: %d and default workspace with ID: %d", adminUser.ID, adminUser.LastWorkspaceID) + log.Printf("Created admin user with ID: %d and default workspace with ID: %d", createdUser.ID, createdUser.LastWorkspaceID) return adminUser, nil } - -func (s *UserService) CreateUser(user *models.User) error { - err := s.DB.CreateUser(user) - if err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - - err = s.FS.InitializeUserWorkspace(user.ID, user.LastWorkspaceID) - if err != nil { - return fmt.Errorf("failed to initialize user workspace: %w", err) - } - - return nil -} - -func (s *UserService) GetUserByID(id int) (*models.User, error) { - return s.DB.GetUserByID(id) -} - -func (s *UserService) GetUserByEmail(email string) (*models.User, error) { - return s.DB.GetUserByEmail(email) -} - -func (s *UserService) UpdateUser(user *models.User) error { - return s.DB.UpdateUser(user) -} - -func (s *UserService) DeleteUser(id int) error { - // First, get the user to check if they exist - user, err := s.DB.GetUserByID(id) - if err != nil { - return fmt.Errorf("failed to get user: %w", err) - } - - // Get user's workspaces - workspaces, err := s.DB.GetWorkspacesByUserID(id) - if err != nil { - return fmt.Errorf("failed to get user's workspaces: %w", err) - } - - // Delete workspace directories - for _, workspace := range workspaces { - err = s.FS.DeleteUserWorkspace(user.ID, workspace.ID) - if err != nil { - return fmt.Errorf("failed to delete workspace files: %w", err) - } - } - - // Delete user from database (this will cascade delete workspaces) - return s.DB.DeleteUser(id) -} diff --git a/frontend/src/components/modals/user/CreateUserModal.jsx b/frontend/src/components/modals/user/CreateUserModal.jsx new file mode 100644 index 0000000..23d5b5d --- /dev/null +++ b/frontend/src/components/modals/user/CreateUserModal.jsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { + Modal, + Stack, + TextInput, + PasswordInput, + Select, + Button, + Group, +} from '@mantine/core'; + +const CreateUserModal = ({ opened, onClose, onCreateUser, loading }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [role, setRole] = useState('viewer'); + + const handleSubmit = async () => { + const result = await onCreateUser({ email, password, displayName, role }); + if (result.success) { + setEmail(''); + setPassword(''); + setDisplayName(''); + setRole('viewer'); + onClose(); + } + }; + + return ( + + + setEmail(e.currentTarget.value)} + placeholder="user@example.com" + /> + setDisplayName(e.currentTarget.value)} + placeholder="John Doe" + /> + setPassword(e.currentTarget.value)} + placeholder="Enter password" + /> + - - - - - - - ); -}; +import AdminUsersTab from './AdminUsersTab'; +import AdminWorkspacesTab from './AdminWorkspacesTab'; +import AdminStatsTab from './AdminStatsTab'; const AdminDashboard = ({ opened, onClose }) => { - const { - data: users, - loading, - error, - create, - delete: deleteUser, - } = useAdmin('users'); - const [createModalOpened, setCreateModalOpened] = useState(false); const { user: currentUser } = useAuth(); - - const handleCreateUser = async (userData) => { - return await create(userData); - }; - - const handleDeleteUser = async (userId) => { - if (userId === currentUser.id) { - notifications.show({ - title: 'Error', - message: 'You cannot delete your own account', - color: 'red', - }); - return; - } - - return await deleteUser(userId); - }; - - const rows = users.map((user) => ( - - {user.email} - {user.displayName} - - {user.role} - - {new Date(user.createdAt).toLocaleDateString()} - - - - - - handleDeleteUser(user.id)} - disabled={user.id === currentUser.id} - > - - - - - - )); + const [activeTab, setActiveTab] = useState('users'); return ( - - + + + }> + Users + + }> + Workspaces + + }> + Statistics + + - {error && ( - } - title="Error" - color="red" - mb="md" - > - {error} - - )} + + + - - - User Management - - - + + + - - - - Email - Display Name - Role - Created At - Actions - - - {rows} -
- - setCreateModalOpened(false)} - onCreateUser={handleCreateUser} - loading={loading} - /> -
+ + + +
); }; diff --git a/frontend/src/components/settings/admin/AdminStatsTab.jsx b/frontend/src/components/settings/admin/AdminStatsTab.jsx new file mode 100644 index 0000000..31c13eb --- /dev/null +++ b/frontend/src/components/settings/admin/AdminStatsTab.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { + Grid, + Card, + Stack, + Text, + Title, + LoadingOverlay, + Alert, + RingProgress, +} from '@mantine/core'; +import { + IconUsers, + IconFolders, + IconServer, + IconFiles, + IconAlertCircle, +} from '@tabler/icons-react'; +import { useAdmin } from '../../../hooks/useAdmin'; +import StatCard from './StatCard'; + +const formatBytes = (bytes) => { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + return `${size.toFixed(1)} ${units[unitIndex]}`; +}; + +const AdminStatsTab = () => { + const { data: stats, loading, error } = useAdmin('stats'); + + if (loading) { + return ; + } + + if (error) { + return ( + } title="Error" color="red"> + {error} + + ); + } + + return ( + + + System Statistics + + + + + + + + + + + + + + + + + + + + + + Active Users + + {((stats.activeUsers / stats.totalUsers) * 100).toFixed(1)}% + + } + /> + + {stats.activeUsers} out of {stats.totalUsers} users active in + last 30 days + + + + + + + ); +}; + +export default AdminStatsTab; diff --git a/frontend/src/components/settings/admin/AdminUsersTab.jsx b/frontend/src/components/settings/admin/AdminUsersTab.jsx new file mode 100644 index 0000000..0960058 --- /dev/null +++ b/frontend/src/components/settings/admin/AdminUsersTab.jsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; +import { + Table, + Button, + Group, + Text, + ActionIcon, + Box, + LoadingOverlay, + Alert, +} from '@mantine/core'; +import { + IconTrash, + IconEdit, + IconPlus, + IconAlertCircle, +} from '@tabler/icons-react'; +import { notifications } from '@mantine/notifications'; +import { useAdmin } from '../../../hooks/useAdmin'; +import CreateUserModal from '../../modals/user/CreateUserModal'; +import DeleteUserModal from '../../modals/user/DeleteUserModal'; + +const AdminUsersTab = ({ currentUser }) => { + const { + data: users, + loading, + error, + create, + delete: deleteUser, + } = useAdmin('users'); + const [createModalOpened, setCreateModalOpened] = useState(false); + const [deleteModalData, setDeleteModalData] = useState(null); + + const handleCreateUser = async (userData) => { + return await create(userData); + }; + + const handleDeleteClick = (user) => { + if (user.id === currentUser.id) { + notifications.show({ + title: 'Error', + message: 'You cannot delete your own account', + color: 'red', + }); + return; + } + setDeleteModalData(user); + }; + + const handleDeleteConfirm = async () => { + if (!deleteModalData) return; + const result = await deleteUser(deleteModalData.id); + if (result.success) { + setDeleteModalData(null); + } + }; + + const rows = users.map((user) => ( + + {user.email} + {user.displayName} + + {user.role} + + {new Date(user.createdAt).toLocaleDateString()} + + + + + + handleDeleteClick(user)} + disabled={user.id === currentUser.id} + > + + + + + + )); + + return ( + + + + {error && ( + } + title="Error" + color="red" + mb="md" + > + {error} + + )} + + + + User Management + + + + + + + + Email + Display Name + Role + Created At + Actions + + + {rows} +
+ + setCreateModalOpened(false)} + onCreateUser={handleCreateUser} + loading={loading} + /> + + setDeleteModalData(null)} + onConfirm={handleDeleteConfirm} + user={deleteModalData} + loading={loading} + /> +
+ ); +}; + +export default AdminUsersTab; diff --git a/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx new file mode 100644 index 0000000..8de5f2f --- /dev/null +++ b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + Table, + Group, + Text, + ActionIcon, + Box, + LoadingOverlay, + Alert, +} from '@mantine/core'; +import { IconTrash, IconEdit, IconAlertCircle } from '@tabler/icons-react'; +import { useAdmin } from '../../../hooks/useAdmin'; + +const AdminWorkspacesTab = () => { + const { data: workspaces, loading, error } = useAdmin('workspaces'); + + const rows = workspaces.map((workspace) => ( + + {workspace.name} + {workspace.owner?.email} + {new Date(workspace.createdAt).toLocaleDateString()} + {workspace.gitEnabled ? 'Yes' : 'No'} + + + + + + + + + + + + )); + + return ( + + + + {error && ( + } + title="Error" + color="red" + mb="md" + > + {error} + + )} + + + + Workspace Management + + + + + + + Name + Owner + Created At + Git Enabled + Actions + + + {rows} +
+
+ ); +}; + +export default AdminWorkspacesTab; diff --git a/frontend/src/components/settings/admin/StatCard.jsx b/frontend/src/components/settings/admin/StatCard.jsx new file mode 100644 index 0000000..9a2a379 --- /dev/null +++ b/frontend/src/components/settings/admin/StatCard.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Card, Group, Box, Text } from '@mantine/core'; + +const StatCard = ({ title, value, icon: Icon, color = 'blue' }) => ( + + + + + {title} + + + {value} + + + + + +); + +export default StatCard; diff --git a/frontend/src/services/adminApi.js b/frontend/src/services/adminApi.js index 3c6c4a0..3c3bec3 100644 --- a/frontend/src/services/adminApi.js +++ b/frontend/src/services/adminApi.js @@ -21,7 +21,11 @@ export const deleteUser = async (userId) => { const response = await apiCall(`${ADMIN_BASE_URL}/users/${userId}`, { method: 'DELETE', }); - return response.json(); + if (response.status === 204) { + return; + } else { + throw new Error('Failed to delete user with status: ', response.status); + } }; export const updateUser = async (userId, userData) => {