Craete UserManagement page
This commit is contained in:
386
webui/src/pages/UserManagement/UserManagement.jsx
Normal file
386
webui/src/pages/UserManagement/UserManagement.jsx
Normal 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;
|
1
webui/src/pages/UserManagement/index.js
Normal file
1
webui/src/pages/UserManagement/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './UserManagement.jsx';
|
39
webui/src/pages/UserManagement/styles.sass
Normal file
39
webui/src/pages/UserManagement/styles.sass
Normal 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
|
Reference in New Issue
Block a user