From 24f877e50bac81368488316723781774be6c79ca Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 6 Nov 2024 23:34:37 +0100 Subject: [PATCH 01/11] Initial admin dashboard layout --- .../src/components/navigation/UserMenu.jsx | 39 +++- .../settings/admin/AdminDashboard.jsx | 182 ++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/settings/admin/AdminDashboard.jsx diff --git a/frontend/src/components/navigation/UserMenu.jsx b/frontend/src/components/navigation/UserMenu.jsx index c5d0e68..0002eb0 100644 --- a/frontend/src/components/navigation/UserMenu.jsx +++ b/frontend/src/components/navigation/UserMenu.jsx @@ -8,12 +8,19 @@ import { Text, Divider, } from '@mantine/core'; -import { IconUser, IconLogout, IconSettings } from '@tabler/icons-react'; +import { + IconUser, + IconUsers, + IconLogout, + IconSettings, +} from '@tabler/icons-react'; import { useAuth } from '../../contexts/AuthContext'; import AccountSettings from '../settings/account/AccountSettings'; +import AdminDashboard from '../settings/admin/AdminDashboard'; const UserMenu = () => { const [accountSettingsOpened, setAccountSettingsOpened] = useState(false); + const [adminDashboardOpened, setAdminDashboardOpened] = useState(false); const [opened, setOpened] = useState(false); const { user, logout } = useAuth(); @@ -81,6 +88,31 @@ const UserMenu = () => { + {user.role === 'admin' && ( + { + setAdminDashboardOpened(true); + setOpened(false); + }} + px="sm" + py="xs" + style={(theme) => ({ + borderRadius: theme.radius.sm, + '&:hover': { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[5] + : theme.colors.gray[0], + }, + })} + > + + + Admin Dashboard + + + )} + { opened={accountSettingsOpened} onClose={() => setAccountSettingsOpened(false)} /> + + setAdminDashboardOpened(false)} + /> ); }; diff --git a/frontend/src/components/settings/admin/AdminDashboard.jsx b/frontend/src/components/settings/admin/AdminDashboard.jsx new file mode 100644 index 0000000..74f468b --- /dev/null +++ b/frontend/src/components/settings/admin/AdminDashboard.jsx @@ -0,0 +1,182 @@ +import React, { useState } from 'react'; +import { + Modal, + Table, + Button, + Group, + TextInput, + PasswordInput, + Select, + Stack, + Text, + ActionIcon, + Box, +} from '@mantine/core'; +import { IconTrash, IconEdit, IconPlus } from '@tabler/icons-react'; + +// Dummy data - replace with actual API calls in production +const DUMMY_USERS = [ + { + id: 1, + email: 'admin@example.com', + displayName: 'Admin User', + role: 'admin', + createdAt: '2024-01-01', + }, + { + id: 2, + email: 'editor@example.com', + displayName: 'Editor User', + role: 'editor', + createdAt: '2024-01-02', + }, + { + id: 3, + email: 'viewer@example.com', + displayName: 'Viewer User', + role: 'viewer', + createdAt: '2024-01-03', + }, +]; + +const CreateUserModal = ({ opened, onClose, onCreateUser }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [displayName, setDisplayName] = useState(''); + const [role, setRole] = useState('viewer'); + + const handleSubmit = () => { + onCreateUser({ email, password, displayName, role }); + 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" + /> + + + + + + + + ); +}; + +export default CreateUserModal; diff --git a/frontend/src/components/modals/user/DeleteUserModal.jsx b/frontend/src/components/modals/user/DeleteUserModal.jsx new file mode 100644 index 0000000..5763a0a --- /dev/null +++ b/frontend/src/components/modals/user/DeleteUserModal.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Modal, Text, Button, Group, Stack } from '@mantine/core'; + +const DeleteUserModal = ({ opened, onClose, onConfirm, user, loading }) => ( + + + + Are you sure you want to delete user "{user?.email}"? This action cannot + be undone and all associated data will be permanently deleted. + + + + + + + +); + +export default DeleteUserModal; diff --git a/frontend/src/components/settings/admin/AdminDashboard.jsx b/frontend/src/components/settings/admin/AdminDashboard.jsx index 3345ce2..e095dc5 100644 --- a/frontend/src/components/settings/admin/AdminDashboard.jsx +++ b/frontend/src/components/settings/admin/AdminDashboard.jsx @@ -1,194 +1,42 @@ import React, { useState } from 'react'; -import { - Modal, - Table, - Button, - Group, - TextInput, - PasswordInput, - Select, - Stack, - Text, - ActionIcon, - Box, - LoadingOverlay, - Alert, -} from '@mantine/core'; -import { - IconTrash, - IconEdit, - IconPlus, - IconAlertCircle, -} from '@tabler/icons-react'; -import { useAdmin } from '../../../hooks/useAdmin'; +import { Modal, Tabs } from '@mantine/core'; +import { IconUsers, IconFolders, IconChartBar } from '@tabler/icons-react'; import { useAuth } from '../../../contexts/AuthContext'; - -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" - /> - setFormData({ ...formData, role: value })} + data={[ + { value: 'admin', label: 'Admin' }, + { value: 'editor', label: 'Editor' }, + { value: 'viewer', label: 'Viewer' }, + ]} + /> + + setFormData({ ...formData, password: e.currentTarget.value }) + } + placeholder="Enter new password (leave empty to keep current)" + /> + + Leave password empty to keep the current password + + + + + + + + ); +}; + +export default EditUserModal; diff --git a/frontend/src/components/settings/admin/AdminUsersTab.jsx b/frontend/src/components/settings/admin/AdminUsersTab.jsx index 0960058..a5df0bb 100644 --- a/frontend/src/components/settings/admin/AdminUsersTab.jsx +++ b/frontend/src/components/settings/admin/AdminUsersTab.jsx @@ -18,6 +18,7 @@ import { import { notifications } from '@mantine/notifications'; import { useAdmin } from '../../../hooks/useAdmin'; import CreateUserModal from '../../modals/user/CreateUserModal'; +import EditUserModal from '../../modals/user/EditUserModal'; import DeleteUserModal from '../../modals/user/DeleteUserModal'; const AdminUsersTab = ({ currentUser }) => { @@ -26,15 +27,22 @@ const AdminUsersTab = ({ currentUser }) => { loading, error, create, + update, delete: deleteUser, } = useAdmin('users'); + const [createModalOpened, setCreateModalOpened] = useState(false); + const [editModalData, setEditModalData] = useState(null); const [deleteModalData, setDeleteModalData] = useState(null); const handleCreateUser = async (userData) => { return await create(userData); }; + const handleEditUser = async (id, userData) => { + return await update(id, userData); + }; + const handleDeleteClick = (user) => { if (user.id === currentUser.id) { notifications.show({ @@ -65,7 +73,11 @@ const AdminUsersTab = ({ currentUser }) => { {new Date(user.createdAt).toLocaleDateString()} - + setEditModalData(user)} + > { loading={loading} /> + setEditModalData(null)} + onEditUser={handleEditUser} + user={editModalData} + loading={loading} + /> + setDeleteModalData(null)} From 5e2d434b4b1308a0fd585a809324c4d0b21ac581 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 10 Nov 2024 15:03:51 +0100 Subject: [PATCH 11/11] Implement admin dash workspaces tab --- backend/internal/api/routes.go | 4 + backend/internal/db/workspaces.go | 40 +++++ backend/internal/handlers/admin_handlers.go | 53 ++++++ .../settings/admin/AdminStatsTab.jsx | 16 +- .../settings/admin/AdminUsersTab.jsx | 6 +- .../settings/admin/AdminWorkspacesTab.jsx | 28 ++-- frontend/src/hooks/useAdmin.js | 155 ------------------ frontend/src/hooks/useAdminData.js | 48 ++++++ frontend/src/hooks/useUserAdmin.js | 79 +++++++++ frontend/src/services/adminApi.js | 8 +- frontend/src/utils/formatBytes.js | 10 ++ 11 files changed, 258 insertions(+), 189 deletions(-) delete mode 100644 frontend/src/hooks/useAdmin.js create mode 100644 frontend/src/hooks/useAdminData.js create mode 100644 frontend/src/hooks/useUserAdmin.js create mode 100644 frontend/src/utils/formatBytes.js diff --git a/backend/internal/api/routes.go b/backend/internal/api/routes.go index ddb350c..9b37986 100644 --- a/backend/internal/api/routes.go +++ b/backend/internal/api/routes.go @@ -50,6 +50,10 @@ func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem, authMiddlew r.Put("/{userId}", handler.AdminUpdateUser()) r.Delete("/{userId}", handler.AdminDeleteUser()) }) + // Workspace management + r.Route("/workspaces", func(r chi.Router) { + r.Get("/", handler.AdminListWorkspaces()) + }) // System stats r.Get("/stats", handler.AdminGetSystemStats()) }) diff --git a/backend/internal/db/workspaces.go b/backend/internal/db/workspaces.go index e3f07d9..f2d1815 100644 --- a/backend/internal/db/workspaces.go +++ b/backend/internal/db/workspaces.go @@ -249,3 +249,43 @@ func (db *DB) GetLastOpenedFile(workspaceID int) (string, error) { } return filePath.String, nil } + +// GetAllWorkspaces retrieves all workspaces in the database +func (db *DB) GetAllWorkspaces() ([]*models.Workspace, error) { + rows, err := db.Query(` + SELECT + id, user_id, name, created_at, + theme, auto_save, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template + FROM workspaces`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var workspaces []*models.Workspace + for rows.Next() { + workspace := &models.Workspace{} + var encryptedToken string + err := rows.Scan( + &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, + &workspace.Theme, &workspace.AutoSave, + &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, + &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + ) + if err != nil { + return nil, err + } + + // Decrypt token + workspace.GitToken, err = db.decryptToken(encryptedToken) + if err != nil { + return nil, fmt.Errorf("failed to decrypt token: %w", err) + } + + workspaces = append(workspaces, workspace) + } + return workspaces, nil +} diff --git a/backend/internal/handlers/admin_handlers.go b/backend/internal/handlers/admin_handlers.go index 69ba0c6..dc9ab0c 100644 --- a/backend/internal/handlers/admin_handlers.go +++ b/backend/internal/handlers/admin_handlers.go @@ -8,6 +8,7 @@ import ( "novamd/internal/httpcontext" "novamd/internal/models" "strconv" + "time" "github.com/go-chi/chi/v5" "golang.org/x/crypto/bcrypt" @@ -204,6 +205,58 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc { } } +// WorkspaceStats holds workspace statistics +type WorkspaceStats struct { + UserID int `json:"userID"` + UserEmail string `json:"userEmail"` + WorkspaceID int `json:"workspaceID"` + WorkspaceName string `json:"workspaceName"` + WorkspaceCreatedAt time.Time `json:"workspaceCreatedAt"` + *filesystem.FileCountStats +} + +// AdminListWorkspaces returns a list of all workspaces and their stats +func (h *Handler) AdminListWorkspaces() http.HandlerFunc { + return func(w http.ResponseWriter, _ *http.Request) { + workspaces, err := h.DB.GetAllWorkspaces() + if err != nil { + http.Error(w, "Failed to list workspaces", http.StatusInternalServerError) + return + } + + workspacesStats := make([]*WorkspaceStats, 0, len(workspaces)) + + for _, ws := range workspaces { + + workspaceData := &WorkspaceStats{} + + user, err := h.DB.GetUserByID(ws.UserID) + if err != nil { + http.Error(w, "Failed to get user", http.StatusInternalServerError) + return + } + + workspaceData.UserID = ws.UserID + workspaceData.UserEmail = user.Email + workspaceData.WorkspaceID = ws.ID + workspaceData.WorkspaceName = ws.Name + workspaceData.WorkspaceCreatedAt = ws.CreatedAt + + fileStats, err := h.FS.GetFileStats(ws.UserID, ws.ID) + if err != nil { + http.Error(w, "Failed to get file stats", http.StatusInternalServerError) + return + } + + workspaceData.FileCountStats = fileStats + + workspacesStats = append(workspacesStats, workspaceData) + } + + respondJSON(w, workspacesStats) + } +} + // SystemStats holds system-wide statistics type SystemStats struct { *db.UserStats diff --git a/frontend/src/components/settings/admin/AdminStatsTab.jsx b/frontend/src/components/settings/admin/AdminStatsTab.jsx index b8564da..3b7346f 100644 --- a/frontend/src/components/settings/admin/AdminStatsTab.jsx +++ b/frontend/src/components/settings/admin/AdminStatsTab.jsx @@ -1,21 +1,11 @@ import React from 'react'; import { Table, Text, Box, LoadingOverlay, Alert } from '@mantine/core'; import { IconAlertCircle } from '@tabler/icons-react'; -import { useAdmin } from '../../../hooks/useAdmin'; - -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]}`; -}; +import { useAdminData } from '../../../hooks/useAdminData'; +import { formatBytes } from '../../../utils/formatBytes'; const AdminStatsTab = () => { - const { data: stats, loading, error } = useAdmin('stats'); + const { data: stats, loading, error } = useAdminData('stats'); if (loading) { return ; diff --git a/frontend/src/components/settings/admin/AdminUsersTab.jsx b/frontend/src/components/settings/admin/AdminUsersTab.jsx index a5df0bb..f6d5b99 100644 --- a/frontend/src/components/settings/admin/AdminUsersTab.jsx +++ b/frontend/src/components/settings/admin/AdminUsersTab.jsx @@ -16,20 +16,20 @@ import { IconAlertCircle, } from '@tabler/icons-react'; import { notifications } from '@mantine/notifications'; -import { useAdmin } from '../../../hooks/useAdmin'; +import { useUserAdmin } from '../../../hooks/useUserAdmin'; import CreateUserModal from '../../modals/user/CreateUserModal'; import EditUserModal from '../../modals/user/EditUserModal'; import DeleteUserModal from '../../modals/user/DeleteUserModal'; const AdminUsersTab = ({ currentUser }) => { const { - data: users, + users, loading, error, create, update, delete: deleteUser, - } = useAdmin('users'); + } = useUserAdmin(); const [createModalOpened, setCreateModalOpened] = useState(false); const [editModalData, setEditModalData] = useState(null); diff --git a/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx index 8de5f2f..5c2cf9d 100644 --- a/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx +++ b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx @@ -9,27 +9,21 @@ import { Alert, } from '@mantine/core'; import { IconTrash, IconEdit, IconAlertCircle } from '@tabler/icons-react'; -import { useAdmin } from '../../../hooks/useAdmin'; +import { useAdminData } from '../../../hooks/useAdminData'; +import { formatBytes } from '../../../utils/formatBytes'; const AdminWorkspacesTab = () => { - const { data: workspaces, loading, error } = useAdmin('workspaces'); + const { data: workspaces, loading, error } = useAdminData('workspaces'); const rows = workspaces.map((workspace) => ( - {workspace.name} - {workspace.owner?.email} - {new Date(workspace.createdAt).toLocaleDateString()} - {workspace.gitEnabled ? 'Yes' : 'No'} + {workspace.userEmail} + {workspace.workspaceName} - - - - - - - - + {new Date(workspace.workspaceCreatedAt).toLocaleDateString()} + {workspace.totalFiles} + {formatBytes(workspace.totalSize)} )); @@ -57,11 +51,11 @@ const AdminWorkspacesTab = () => { - Name Owner + Name Created At - Git Enabled - Actions + Total Files + Total Size {rows} diff --git a/frontend/src/hooks/useAdmin.js b/frontend/src/hooks/useAdmin.js deleted file mode 100644 index 08d3cc4..0000000 --- a/frontend/src/hooks/useAdmin.js +++ /dev/null @@ -1,155 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import { notifications } from '@mantine/notifications'; -import * as adminApi from '../services/adminApi'; - -export const useAdmin = (resource) => { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchData = useCallback(async () => { - setLoading(true); - try { - let result; - switch (resource) { - case 'users': - result = await adminApi.listUsers(); - break; - case 'stats': - result = await adminApi.getSystemStats(); - break; - default: - throw new Error(`Unknown resource type: ${resource}`); - } - setData(result); - setError(null); - } catch (err) { - console.error(`Failed to fetch ${resource}:`, err); - setError(err.message); - notifications.show({ - title: 'Error', - message: `Failed to load ${resource}. Please try again.`, - color: 'red', - }); - } finally { - setLoading(false); - } - }, [resource]); - - const createItem = useCallback( - async (itemData) => { - try { - let newItem; - switch (resource) { - case 'users': - newItem = await adminApi.createUser(itemData); - break; - default: - throw new Error(`Create not supported for resource: ${resource}`); - } - setData((prevData) => - Array.isArray(prevData) ? [...prevData, newItem] : prevData - ); - notifications.show({ - title: 'Success', - message: `${resource} created successfully`, - color: 'green', - }); - return { success: true, data: newItem }; - } catch (err) { - console.error(`Failed to create ${resource}:`, err); - notifications.show({ - title: 'Error', - message: err.message || `Failed to create ${resource}`, - color: 'red', - }); - return { success: false, error: err.message }; - } - }, - [resource] - ); - - const deleteItem = useCallback( - async (itemId) => { - try { - switch (resource) { - case 'users': - await adminApi.deleteUser(itemId); - break; - default: - throw new Error(`Delete not supported for resource: ${resource}`); - } - setData((prevData) => - Array.isArray(prevData) - ? prevData.filter((item) => item.id !== itemId) - : prevData - ); - notifications.show({ - title: 'Success', - message: `${resource} deleted successfully`, - color: 'green', - }); - return { success: true }; - } catch (err) { - console.error(`Failed to delete ${resource}:`, err); - notifications.show({ - title: 'Error', - message: err.message || `Failed to delete ${resource}`, - color: 'red', - }); - return { success: false, error: err.message }; - } - }, - [resource] - ); - - const updateItem = useCallback( - async (itemId, itemData) => { - try { - let updatedItem; - switch (resource) { - case 'users': - updatedItem = await adminApi.updateUser(itemId, itemData); - break; - default: - throw new Error(`Update not supported for resource: ${resource}`); - } - setData((prevData) => - Array.isArray(prevData) - ? prevData.map((item) => (item.id === itemId ? updatedItem : item)) - : prevData - ); - notifications.show({ - title: 'Success', - message: `${resource} updated successfully`, - color: 'green', - }); - return { success: true, data: updatedItem }; - } catch (err) { - console.error(`Failed to update ${resource}:`, err); - notifications.show({ - title: 'Error', - message: err.message || `Failed to update ${resource}`, - color: 'red', - }); - return { success: false, error: err.message }; - } - }, - [resource] - ); - - // Fetch data on mount - useEffect(() => { - fetchData(); - }, [fetchData]); - - return { - data, - loading, - error, - refresh: fetchData, - create: createItem, - delete: deleteItem, - update: updateItem, - }; -}; diff --git a/frontend/src/hooks/useAdminData.js b/frontend/src/hooks/useAdminData.js new file mode 100644 index 0000000..9669ad3 --- /dev/null +++ b/frontend/src/hooks/useAdminData.js @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react'; +import { notifications } from '@mantine/notifications'; +import { getUsers, getWorkspaces, getSystemStats } from '../services/adminApi'; + +// Hook for admin data fetching (stats and workspaces) +export const useAdminData = (type) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadData = async () => { + setLoading(true); + setError(null); + try { + let response; + switch (type) { + case 'stats': + response = await getSystemStats(); + break; + case 'workspaces': + response = await getWorkspaces(); + break; + case 'users': + response = await getUsers(); + break; + default: + throw new Error('Invalid data type'); + } + setData(response); + } catch (err) { + const message = err.response?.data?.error || err.message; + setError(message); + notifications.show({ + title: 'Error', + message: `Failed to load ${type}: ${message}`, + color: 'red', + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, [type]); + + return { data, loading, error, reload: loadData }; +}; diff --git a/frontend/src/hooks/useUserAdmin.js b/frontend/src/hooks/useUserAdmin.js new file mode 100644 index 0000000..f8e0a1d --- /dev/null +++ b/frontend/src/hooks/useUserAdmin.js @@ -0,0 +1,79 @@ +import { useAdminData } from './useAdminData'; +import { createUser, updateUser, deleteUser } from '../services/adminApi'; +import { notifications } from '@mantine/notifications'; + +export const useUserAdmin = () => { + const { data: users, loading, error, reload } = useAdminData('users'); + + const handleCreate = async (userData) => { + try { + await createUser(userData); + notifications.show({ + title: 'Success', + message: 'User created successfully', + color: 'green', + }); + reload(); + return { success: true }; + } catch (err) { + const message = err.response?.data?.error || err.message; + notifications.show({ + title: 'Error', + message: `Failed to create user: ${message}`, + color: 'red', + }); + return { success: false, error: message }; + } + }; + + const handleUpdate = async (userId, userData) => { + try { + await updateUser(userId, userData); + notifications.show({ + title: 'Success', + message: 'User updated successfully', + color: 'green', + }); + reload(); + return { success: true }; + } catch (err) { + const message = err.response?.data?.error || err.message; + notifications.show({ + title: 'Error', + message: `Failed to update user: ${message}`, + color: 'red', + }); + return { success: false, error: message }; + } + }; + + const handleDelete = async (userId) => { + try { + await deleteUser(userId); + notifications.show({ + title: 'Success', + message: 'User deleted successfully', + color: 'green', + }); + reload(); + return { success: true }; + } catch (err) { + const message = err.response?.data?.error || err.message; + notifications.show({ + title: 'Error', + message: `Failed to delete user: ${message}`, + color: 'red', + }); + return { success: false, error: message }; + } + }; + + return { + users, + loading, + error, + create: handleCreate, + update: handleUpdate, + delete: handleDelete, + }; +}; diff --git a/frontend/src/services/adminApi.js b/frontend/src/services/adminApi.js index 3c3bec3..3c011d0 100644 --- a/frontend/src/services/adminApi.js +++ b/frontend/src/services/adminApi.js @@ -4,7 +4,7 @@ import { API_BASE_URL } from '../utils/constants'; const ADMIN_BASE_URL = `${API_BASE_URL}/admin`; // User Management -export const listUsers = async () => { +export const getUsers = async () => { const response = await apiCall(`${ADMIN_BASE_URL}/users`); return response.json(); }; @@ -36,6 +36,12 @@ export const updateUser = async (userId, userData) => { return response.json(); }; +// Workspace Management +export const getWorkspaces = async () => { + const response = await apiCall(`${ADMIN_BASE_URL}/workspaces`); + return response.json(); +}; + // System Statistics export const getSystemStats = async () => { const response = await apiCall(`${ADMIN_BASE_URL}/stats`); diff --git a/frontend/src/utils/formatBytes.js b/frontend/src/utils/formatBytes.js new file mode 100644 index 0000000..dde9f21 --- /dev/null +++ b/frontend/src/utils/formatBytes.js @@ -0,0 +1,10 @@ +export 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]}`; +};