feat: add pause and delete actions for users
This commit is contained in:
127
src/App.tsx
127
src/App.tsx
@@ -26,6 +26,10 @@ const servicesById = new Map(
|
||||
dashboardSnapshot.system.services.map((service) => [service.id, service] as const),
|
||||
);
|
||||
|
||||
type UserRow = (typeof dashboardSnapshot.userRecords)[number] & {
|
||||
paused?: boolean;
|
||||
};
|
||||
|
||||
function LoginGate({ onUnlock }: { onUnlock: () => void }) {
|
||||
const [login, setLogin] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -161,6 +165,60 @@ function AddUserModal({ onClose }: { onClose: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmDeleteModal({
|
||||
username,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
username: string;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
const stopPropagation = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" role="presentation" onClick={onClose}>
|
||||
<section
|
||||
aria-labelledby="delete-user-title"
|
||||
aria-modal="true"
|
||||
className="modal-card confirm-card"
|
||||
role="dialog"
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h2 id="delete-user-title">Delete user</h2>
|
||||
</div>
|
||||
<p className="confirm-copy">
|
||||
Remove profile <strong>{username}</strong>? This action will delete the user entry from the
|
||||
current panel state.
|
||||
</p>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="button-secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className="button-danger" onClick={onConfirm}>
|
||||
Delete user
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardTab() {
|
||||
const serviceTone = getServiceTone(dashboardSnapshot.service.status);
|
||||
|
||||
@@ -257,6 +315,8 @@ function DashboardTab() {
|
||||
function UsersTab() {
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [users, setUsers] = useState<UserRow[]>(() => dashboardSnapshot.userRecords);
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
const handleCopy = async (userId: string, proxyLink: string) => {
|
||||
await navigator.clipboard.writeText(proxyLink);
|
||||
@@ -264,6 +324,32 @@ function UsersTab() {
|
||||
window.setTimeout(() => setCopiedId((value) => (value === userId ? null : value)), 1200);
|
||||
};
|
||||
|
||||
const handleTogglePause = (userId: string) => {
|
||||
setUsers((current) =>
|
||||
current.map((user) => (user.id === userId ? { ...user, paused: !user.paused } : user)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!deleteTargetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUsers((current) => current.filter((user) => user.id !== deleteTargetId));
|
||||
setDeleteTargetId(null);
|
||||
};
|
||||
|
||||
const deleteTarget = users.find((user) => user.id === deleteTargetId) ?? null;
|
||||
const liveUsers = users.filter((user) => !user.paused && user.status === 'live').length;
|
||||
const nearQuotaUsers = users.filter((user) => {
|
||||
if (user.paused || user.quotaBytes === null || isQuotaExceeded(user.usedBytes, user.quotaBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return user.usedBytes / user.quotaBytes >= 0.8;
|
||||
}).length;
|
||||
const exceededUsers = users.filter((user) => isQuotaExceeded(user.usedBytes, user.quotaBytes)).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-grid single-column">
|
||||
@@ -271,13 +357,13 @@ function UsersTab() {
|
||||
<div className="table-toolbar">
|
||||
<div className="toolbar-title">
|
||||
<h2>Users</h2>
|
||||
<p>{dashboardSnapshot.userRecords.length} accounts in current profile</p>
|
||||
<p>{users.length} accounts in current profile</p>
|
||||
</div>
|
||||
<div className="toolbar-actions">
|
||||
<div className="summary-pills">
|
||||
<span>{dashboardSnapshot.users.live} live</span>
|
||||
<span>{dashboardSnapshot.users.nearQuota} near quota</span>
|
||||
<span>{dashboardSnapshot.users.exceeded} exceeded</span>
|
||||
<span>{liveUsers} live</span>
|
||||
<span>{nearQuotaUsers} near quota</span>
|
||||
<span>{exceededUsers} exceeded</span>
|
||||
</div>
|
||||
<button type="button" onClick={() => setIsModalOpen(true)}>
|
||||
New user
|
||||
@@ -295,10 +381,11 @@ function UsersTab() {
|
||||
<th>Remaining</th>
|
||||
<th>Share</th>
|
||||
<th>Proxy</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dashboardSnapshot.userRecords.map((user) => {
|
||||
{users.map((user) => {
|
||||
const service = servicesById.get(user.serviceId);
|
||||
const endpoint = service
|
||||
? `${dashboardSnapshot.system.publicHost}:${service.port}`
|
||||
@@ -313,6 +400,7 @@ function UsersTab() {
|
||||
)
|
||||
: '';
|
||||
const exhausted = isQuotaExceeded(user.usedBytes, user.quotaBytes);
|
||||
const displayStatus = user.paused ? 'paused' : user.status;
|
||||
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
@@ -328,7 +416,9 @@ function UsersTab() {
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-pill ${getServiceTone(user.status)}`}>{user.status}</span>
|
||||
<span className={`status-pill ${getServiceTone(displayStatus)}`}>
|
||||
{displayStatus}
|
||||
</span>
|
||||
</td>
|
||||
<td>{formatBytes(user.usedBytes)}</td>
|
||||
<td>{formatQuotaState(user.usedBytes, user.quotaBytes)}</td>
|
||||
@@ -343,6 +433,24 @@ function UsersTab() {
|
||||
{copiedId === user.id ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="button-secondary button-small"
|
||||
onClick={() => handleTogglePause(user.id)}
|
||||
>
|
||||
{user.paused ? 'Resume' : 'Pause'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button-danger button-small"
|
||||
onClick={() => setDeleteTargetId(user.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -353,6 +461,13 @@ function UsersTab() {
|
||||
</section>
|
||||
|
||||
{isModalOpen ? <AddUserModal onClose={() => setIsModalOpen(false)} /> : null}
|
||||
{deleteTarget ? (
|
||||
<ConfirmDeleteModal
|
||||
username={deleteTarget.username}
|
||||
onClose={() => setDeleteTargetId(null)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user