Craete UserManagement page

This commit is contained in:
Mathias Wagner
2025-09-09 13:42:49 +02:00
parent 5908ee0f99
commit 29b32ec317
3 changed files with 426 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
import React, {useState, useEffect, useContext} from 'react';
import {UserContext} from '@/common/contexts/UserContext.jsx';
import {useToast} from '@/common/contexts/ToastContext.jsx';
import {getRequest, postRequest, putRequest, deleteRequest} from '@/common/utils/RequestUtil.js';
import Button from '@/common/components/Button';
import Input from '@/common/components/Input';
import Select from '@/common/components/Select';
import Modal, {ModalActions} from '@/common/components/Modal';
import Card, {CardHeader, CardBody} from '@/common/components/Card';
import Badge from '@/common/components/Badge';
import Grid from '@/common/components/Grid';
import LoadingSpinner from '@/common/components/LoadingSpinner';
import EmptyState from '@/common/components/EmptyState';
import PageHeader from '@/common/components/PageHeader';
import Avatar from '@/common/components/Avatar';
import DetailItem, {DetailList} from '@/common/components/DetailItem';
import FormError from '@/common/components/FormError';
import {
PlusIcon,
PencilIcon,
TrashIcon,
UserIcon,
ShieldCheckIcon,
CalendarIcon,
HardDriveIcon
} from '@phosphor-icons/react';
import './styles.sass';
const UserManagement = () => {
const {user: currentUser} = useContext(UserContext);
const toast = useToast();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [formData, setFormData] = useState({
username: '',
password: '',
role: 'user',
storage_limit_gb: 10
});
const [formErrors, setFormErrors] = useState({});
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
try {
setLoading(true);
const response = await getRequest('admin/users');
setUsers(response);
} catch (error) {
console.error('Failed to fetch users:', error);
toast.error('Failed to load users. Please try again.');
} finally {
setLoading(false);
}
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const openCreateModal = () => {
setEditingUser(null);
setFormData({
username: '',
password: '',
role: 'user',
storage_limit_gb: 10
});
setFormErrors({});
setShowModal(true);
};
const openEditModal = (user) => {
setEditingUser(user);
setFormData({
username: user.username,
password: '',
role: user.role,
storage_limit_gb: user.storage_limit_gb
});
setFormErrors({});
setShowModal(true);
};
const closeModal = () => {
setShowModal(false);
setEditingUser(null);
setFormData({
username: '',
password: '',
role: 'user',
storage_limit_gb: 10
});
setFormErrors({});
};
const validateForm = () => {
const errors = {};
if (!formData.username.trim()) {
errors.username = 'Username is required';
} else if (formData.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}
if (!editingUser && !formData.password) {
errors.password = 'Password is required';
} else if (formData.password && formData.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
if (formData.storage_limit_gb < 0) {
errors.storage_limit_gb = 'Storage limit cannot be negative';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setSubmitting(true);
try {
if (editingUser) {
const updateData = {
username: formData.username,
role: formData.role,
storage_limit_gb: parseInt(formData.storage_limit_gb)
};
if (formData.password) {
updateData.password = formData.password;
}
await putRequest(`admin/users/${editingUser.id}`, updateData);
toast.success(`User "${formData.username}" updated successfully`);
} else {
await postRequest('admin/users', {
username: formData.username,
password: formData.password,
role: formData.role,
storage_limit_gb: parseInt(formData.storage_limit_gb)
});
toast.success(`User "${formData.username}" created successfully`);
}
closeModal();
fetchUsers();
} catch (error) {
console.error('Failed to save user:', error);
const errorMessage = error.error || 'Failed to save user. Please try again.';
toast.error(errorMessage);
if (error.error) {
setFormErrors({general: error.error});
}
} finally {
setSubmitting(false);
}
};
const handleDelete = async (userId, username) => {
if (userId === currentUser.id) {
toast.warning('You cannot delete your own account');
return;
}
if (!window.confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
return;
}
try {
await deleteRequest(`admin/users/${userId}`);
toast.success(`User "${username}" deleted successfully`);
fetchUsers();
} catch (error) {
console.error('Failed to delete user:', error);
toast.error('Failed to delete user. Please try again.');
}
};
const handleInputChange = (e) => {
const {name, value} = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
if (formErrors[name]) {
setFormErrors(prev => ({
...prev,
[name]: ''
}));
}
};
if (loading) {
return (
<div className="content">
<LoadingSpinner centered text="Loading users..."/>
</div>
);
}
return (
<div className="content">
<PageHeader
title="User Management"
subtitle="Manage system users and their permissions"
actions={
<Button
variant="primary"
icon={<PlusIcon size={16}/>}
onClick={openCreateModal}
>
Add User
</Button>
}
/>
<Grid minWidth="400px">
{users.map(user => (
<Card key={user.id} hover>
<CardHeader>
<div className="user-card-header">
<Avatar>
<UserIcon size={24} weight="duotone"/>
</Avatar>
<div className="user-info">
<h3 className="user-name">{user.username}</h3>
<div className="user-role">
<ShieldCheckIcon size={14}/>
<Badge variant={user.role}>
{user.role}
</Badge>
</div>
</div>
<div className="user-actions">
<Button
variant="subtle"
size="sm"
icon={<PencilIcon size={14}/>}
onClick={() => openEditModal(user)}
>
Edit
</Button>
{user.id !== currentUser.id && (
<Button
variant="danger"
size="sm"
icon={<TrashIcon size={14}/>}
onClick={() => handleDelete(user.id, user.username)}
>
Delete
</Button>
)}
</div>
</div>
</CardHeader>
<CardBody>
<DetailList>
<DetailItem icon={<HardDriveIcon size={16}/>}>
Storage Limit: {user.storage_limit_gb}GB
</DetailItem>
<DetailItem icon={<CalendarIcon size={16}/>}>
Created: {formatDate(user.created_at)}
</DetailItem>
</DetailList>
</CardBody>
</Card>
))}
</Grid>
{users.length === 0 && (
<EmptyState
icon={<UserIcon size={48} weight="duotone"/>}
title="No users found"
description="Get started by creating your first user"
action={
<Button
variant="primary"
icon={<PlusIcon size={16}/>}
onClick={openCreateModal}
>
Add User
</Button>
}
/>
)}
<Modal
isOpen={showModal}
onClose={closeModal}
title={editingUser ? 'Edit User' : 'Create New User'}
size="md"
>
<form onSubmit={handleSubmit} className="modal-form">
<FormError>{formErrors.general}</FormError>
<Input
label="Username"
name="username"
value={formData.username}
onChange={handleInputChange}
error={formErrors.username}
placeholder="Enter username"
disabled={submitting}
/>
<Input
label={editingUser ? "New Password (leave empty to keep current)" : "Password"}
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
error={formErrors.password}
placeholder={editingUser ? "Leave empty to keep current password" : "Enter password"}
disabled={submitting}
/>
<Select
label="Role"
name="role"
value={formData.role}
onChange={handleInputChange}
options={[
{value: 'user', label: 'User'},
{value: 'admin', label: 'Admin'}
]}
disabled={submitting}
/>
<Input
label="Storage Limit (GB)"
name="storage_limit_gb"
type="number"
value={formData.storage_limit_gb}
onChange={handleInputChange}
error={formErrors.storage_limit_gb}
placeholder="10"
min="0"
disabled={submitting}
/>
<ModalActions>
<Button
type="button"
variant="subtle"
onClick={closeModal}
disabled={submitting}
>
Cancel
</Button>
<Button
type="submit"
variant="primary"
loading={submitting}
disabled={submitting}
>
{editingUser ? 'Update User' : 'Create User'}
</Button>
</ModalActions>
</form>
</Modal>
</div>
);
};
export default UserManagement;

View File

@@ -0,0 +1 @@
export { default } from './UserManagement.jsx';

View File

@@ -0,0 +1,39 @@
.user-card-header
display: flex
align-items: flex-start
gap: 1rem
.user-info
flex: 1
min-width: 0
.user-name
font-size: 1.1rem
font-weight: 600
color: var(--text)
margin-bottom: 0.5rem
.user-role
display: flex
align-items: center
gap: 0.5rem
color: var(--text-dim)
font-size: 0.85rem
.user-actions
display: flex
gap: 0.5rem
flex-shrink: 0
.modal-form
display: flex
flex-direction: column
gap: 1.25rem
@media (max-width: 768px)
.user-card-header
flex-direction: column
text-align: center
.user-actions
justify-content: center