fix: add missing Instance sub-panel titles and overhaul reset password UX
- Add <h2> title headings to General, Streaming, Storage, Users panels - Reset password now shows confirmation dialog before executing - Temp password displays inline below the user row (not at top of list) - Temp password has copy button and dismiss action
This commit is contained in:
@@ -65,6 +65,7 @@ export function GeneralPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={(e) => e.preventDefault()}>
|
<form className="space-y-5" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<h2 className="text-lg font-semibold text-txt-primary">General</h2>
|
||||||
<div className="text-xs text-txt-tertiary">
|
<div className="text-xs text-txt-tertiary">
|
||||||
Configure your Backspace instance. These settings affect all users.
|
Configure your Backspace instance. These settings affect all users.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export function StoragePanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
<h2 className="text-lg font-semibold text-txt-primary">Storage</h2>
|
||||||
<div className="text-xs text-txt-tertiary">
|
<div className="text-xs text-txt-tertiary">
|
||||||
Monitor disk usage and clean up orphaned files left behind by deleted content or replaced avatars/banners.
|
Monitor disk usage and clean up orphaned files left behind by deleted content or replaced avatars/banners.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export function StreamingPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
<h2 className="text-lg font-semibold text-txt-primary">Streaming</h2>
|
||||||
<div className="text-xs text-txt-tertiary">
|
<div className="text-xs text-txt-tertiary">
|
||||||
These limits apply to all users on this instance. Users can pick values within these bounds.
|
These limits apply to all users on this instance. Users can pick values within these bounds.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,14 +67,27 @@ export function UsersPanel() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetPassword = async (user: AdminUser) => {
|
const [resetConfirmUser, setResetConfirmUser] = useState<AdminUser | null>(null);
|
||||||
|
const [resetLoading, setResetLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleResetPassword = (user: AdminUser) => {
|
||||||
|
setResetConfirmUser(user);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetConfirm = async () => {
|
||||||
|
if (!resetConfirmUser) return;
|
||||||
|
setResetLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
setTempPassword(null);
|
setTempPassword(null);
|
||||||
try {
|
try {
|
||||||
const result = await api.admin.resetUserPassword(user.id);
|
const result = await api.admin.resetUserPassword(resetConfirmUser.id);
|
||||||
setTempPassword({ userId: user.id, password: result.temporaryPassword });
|
setTempPassword({ userId: resetConfirmUser.id, password: result.temporaryPassword });
|
||||||
|
setResetConfirmUser(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to reset password');
|
setError(err instanceof Error ? err.message : 'Failed to reset password');
|
||||||
|
setResetConfirmUser(null);
|
||||||
|
} finally {
|
||||||
|
setResetLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +124,7 @@ export function UsersPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold text-txt-primary">Users</h2>
|
||||||
<div className="text-xs text-txt-tertiary">
|
<div className="text-xs text-txt-tertiary">
|
||||||
View and manage user accounts on this instance.
|
View and manage user accounts on this instance.
|
||||||
</div>
|
</div>
|
||||||
@@ -140,37 +154,6 @@ export function UsersPanel() {
|
|||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Temp password banner */}
|
|
||||||
{tempPassword && (
|
|
||||||
<div className="p-3 bg-status-online/10 border border-status-online/30 rounded-lg">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="text-sm text-txt-secondary">
|
|
||||||
Temporary password for <span className="font-medium text-txt-primary">{data?.users.find(u => u.id === tempPassword.userId)?.username ?? 'user'}</span>:
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setTempPassword(null)}
|
|
||||||
className="text-txt-tertiary hover:text-txt-secondary text-xs"
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
|
||||||
<code className="px-2 py-1 bg-black/30 rounded text-sm font-mono text-status-online select-all">
|
|
||||||
{tempPassword.password}
|
|
||||||
</code>
|
|
||||||
<button
|
|
||||||
onClick={() => navigator.clipboard.writeText(tempPassword.password)}
|
|
||||||
className="px-2 py-1 bg-white/[0.06] hover:bg-white/[0.1] text-txt-secondary text-xs rounded transition-colors"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-txt-tertiary mt-1.5">
|
|
||||||
This password is shown once. The user has been disconnected and must log in again.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading */}
|
{/* Loading */}
|
||||||
{loading && !data && (
|
{loading && !data && (
|
||||||
<div className="text-sm text-txt-tertiary py-4">Loading users...</div>
|
<div className="text-sm text-txt-tertiary py-4">Loading users...</div>
|
||||||
@@ -187,8 +170,11 @@ export function UsersPanel() {
|
|||||||
const isFederated = !!user.homeInstance;
|
const isFederated = !!user.homeInstance;
|
||||||
const isDeleted = user.isDeleted;
|
const isDeleted = user.isDeleted;
|
||||||
|
|
||||||
|
const hasTempPassword = tempPassword?.userId === user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={user.id} className="flex items-center gap-3 rounded-lg bg-white/[0.02] p-3.5">
|
<div key={user.id}>
|
||||||
|
<div className={`flex items-center gap-3 rounded-lg bg-white/[0.02] p-3.5 ${hasTempPassword ? 'rounded-b-none' : ''}`}>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className={isDeleted ? 'opacity-50' : ''}>
|
<div className={isDeleted ? 'opacity-50' : ''}>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -286,6 +272,36 @@ export function UsersPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Inline temp password display */}
|
||||||
|
{hasTempPassword && (
|
||||||
|
<div className="p-3 bg-status-online/10 border border-status-online/30 border-t-0 rounded-b-lg">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="text-xs text-txt-secondary">Temporary password:</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTempPassword(null)}
|
||||||
|
className="text-txt-tertiary hover:text-txt-secondary text-xs"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
|
<code className="px-2 py-1 bg-black/30 rounded text-sm font-mono text-status-online select-all">
|
||||||
|
{tempPassword.password}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
onClick={() => navigator.clipboard.writeText(tempPassword.password)}
|
||||||
|
className="px-2 py-1 bg-white/[0.06] hover:bg-white/[0.1] text-txt-secondary text-xs rounded transition-colors"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-txt-tertiary mt-1">
|
||||||
|
Shown once. User has been disconnected and must log in again.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -315,6 +331,16 @@ export function UsersPanel() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Confirm dialogs */}
|
{/* Confirm dialogs */}
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!resetConfirmUser}
|
||||||
|
onClose={() => setResetConfirmUser(null)}
|
||||||
|
onConfirm={handleResetConfirm}
|
||||||
|
title="Reset Password"
|
||||||
|
description={<>Reset the password for <strong>{resetConfirmUser?.username}</strong>? They will be disconnected immediately and must log in with the new temporary password.</>}
|
||||||
|
confirmLabel="Reset Password"
|
||||||
|
variant="warning"
|
||||||
|
loading={resetLoading}
|
||||||
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={confirmAction?.type === 'demote'}
|
isOpen={confirmAction?.type === 'demote'}
|
||||||
onClose={() => setConfirmAction(null)}
|
onClose={() => setConfirmAction(null)}
|
||||||
|
|||||||
Reference in New Issue
Block a user