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 (
|
||||
<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)}
|
||||
|
||||
Reference in New Issue
Block a user