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:
Jannis Braun
2026-03-22 04:04:46 +01:00
parent 437873c823
commit 4f143bb289
4 changed files with 152 additions and 123 deletions
@@ -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)}