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 (
<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">
Configure your Backspace instance. These settings affect all users.
</div>
@@ -74,6 +74,7 @@ export function StoragePanel() {
return (
<div className="space-y-5">
<h2 className="text-lg font-semibold text-txt-primary">Storage</h2>
<div className="text-xs text-txt-tertiary">
Monitor disk usage and clean up orphaned files left behind by deleted content or replaced avatars/banners.
</div>
@@ -162,6 +162,7 @@ export function StreamingPanel() {
return (
<div className="space-y-5">
<h2 className="text-lg font-semibold text-txt-primary">Streaming</h2>
<div className="text-xs text-txt-tertiary">
These limits apply to all users on this instance. Users can pick values within these bounds.
</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('');
setTempPassword(null);
try {
const result = await api.admin.resetUserPassword(user.id);
setTempPassword({ userId: user.id, password: result.temporaryPassword });
const result = await api.admin.resetUserPassword(resetConfirmUser.id);
setTempPassword({ userId: resetConfirmUser.id, password: result.temporaryPassword });
setResetConfirmUser(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to reset password');
setResetConfirmUser(null);
} finally {
setResetLoading(false);
}
};
@@ -111,6 +124,7 @@ export function UsersPanel() {
return (
<div className="space-y-4">
<h2 className="text-lg font-semibold text-txt-primary">Users</h2>
<div className="text-xs text-txt-tertiary">
View and manage user accounts on this instance.
</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>
)}
{/* 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 && !data && (
<div className="text-sm text-txt-tertiary py-4">Loading users...</div>
@@ -187,8 +170,11 @@ export function UsersPanel() {
const isFederated = !!user.homeInstance;
const isDeleted = user.isDeleted;
const hasTempPassword = tempPassword?.userId === user.id;
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 */}
<div className={isDeleted ? 'opacity-50' : ''}>
<Avatar
@@ -286,6 +272,36 @@ export function UsersPanel() {
</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>
@@ -315,6 +331,16 @@ export function UsersPanel() {
)}
{/* 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
isOpen={confirmAction?.type === 'demote'}
onClose={() => setConfirmAction(null)}