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