fix(admin): removeNode URL bug, container empty-state text, PasswordResetModal replaces prompt()
This commit is contained in:
parent
004bdd0778
commit
854775e322
1 changed files with 81 additions and 13 deletions
|
|
@ -85,6 +85,7 @@ function Users() {
|
|||
const [tab, setTab] = React.useState("users");
|
||||
const [showInvite, setShowInvite] = React.useState(false);
|
||||
const [editingUser, setEditingUser] = React.useState(null);
|
||||
const [resetUser, setResetUser] = React.useState(null);
|
||||
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
|
||||
|
||||
const refreshUsers = React.useCallback(() => {
|
||||
|
|
@ -139,15 +140,7 @@ function Users() {
|
|||
.catch(e => alert('Delete failed: ' + e.message));
|
||||
};
|
||||
|
||||
const resetPassword = (u) => {
|
||||
setMenuFor(null);
|
||||
const pw = prompt(`Reset password for ${u.username}\n\nNew password (≥ 8 characters):`);
|
||||
if (!pw) return;
|
||||
if (pw.length < 8) { alert('Password must be at least 8 characters.'); return; }
|
||||
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'PATCH', body: JSON.stringify({ password: pw }) })
|
||||
.then(() => alert('Password reset for ' + u.username))
|
||||
.catch(e => alert('Reset failed: ' + e.message));
|
||||
};
|
||||
const resetPassword = (u) => { setMenuFor(null); setResetUser(u); };
|
||||
|
||||
const changeRole = (u, newRole) => {
|
||||
if (u.role === newRole) return;
|
||||
|
|
@ -253,6 +246,12 @@ function Users() {
|
|||
onSaved={() => { setEditingUser(null); refreshUsers(); }}
|
||||
/>
|
||||
)}
|
||||
{resetUser && (
|
||||
<PasswordResetModal user={resetUser}
|
||||
onClose={() => setResetUser(null)}
|
||||
onSaved={() => setResetUser(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -293,6 +292,77 @@ function EditUserModal({ user, onClose, onSaved }) {
|
|||
);
|
||||
}
|
||||
|
||||
function PasswordResetModal({ user, onClose, onSaved }) {
|
||||
const [pw, setPw] = React.useState('');
|
||||
const [pw2, setPw2] = React.useState('');
|
||||
const [show, setShow] = React.useState(false);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [err, setErr] = React.useState(null);
|
||||
const [done, setDone] = React.useState(false);
|
||||
|
||||
const valid = pw.length >= 8 && pw === pw2;
|
||||
const submit = () => {
|
||||
if (!valid) return;
|
||||
setSaving(true); setErr(null);
|
||||
window.ZAMPP_API.fetch('/users/' + user.id, { method: 'PATCH', body: JSON.stringify({ password: pw }) })
|
||||
.then(() => { setSaving(false); setDone(true); setTimeout(onSaved, 1200); })
|
||||
.catch(e => { setSaving(false); setErr(e.message); });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal" style={{ width: 400 }} onClick={e => e.stopPropagation()}>
|
||||
<div className="modal-head">
|
||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Reset password · @{user.username}</div>
|
||||
<button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{done ? (
|
||||
<div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--success)', fontSize: 13 }}>
|
||||
<Icon name="check" size={16} /> Password updated.
|
||||
</div>
|
||||
) : (<>
|
||||
<div className="field">
|
||||
<label className="field-label">New password</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input className="field-input" autoFocus type={show ? 'text' : 'password'}
|
||||
value={pw} onChange={e => setPw(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }}
|
||||
style={{ paddingRight: 36 }} />
|
||||
<button className="icon-btn" style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
|
||||
onClick={() => setShow(s => !s)} type="button" tabIndex={-1}>
|
||||
<Icon name={show ? 'eye-off' : 'eye'} size={13} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: pw.length > 0 && pw.length < 8 ? 'var(--danger)' : 'var(--text-3)', marginTop: 4 }}>
|
||||
Minimum 8 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">Confirm password</label>
|
||||
<input className="field-input" type={show ? 'text' : 'password'}
|
||||
value={pw2} onChange={e => setPw2(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && valid) submit(); }} />
|
||||
{pw2.length > 0 && pw !== pw2 && (
|
||||
<div style={{ fontSize: 11, color: 'var(--danger)', marginTop: 4 }}>Passwords do not match</div>
|
||||
)}
|
||||
</div>
|
||||
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 4 }}>{err}</div>}
|
||||
</>)}
|
||||
</div>
|
||||
{!done && (
|
||||
<div className="modal-foot">
|
||||
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
|
||||
<button className="btn primary sm" onClick={submit} disabled={saving || !valid}>
|
||||
{saving ? 'Saving…' : 'Reset password'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupsPanel({ groups, users, onChange }) {
|
||||
const [creating, setCreating] = React.useState(false);
|
||||
const [newName, setNewName] = React.useState('');
|
||||
|
|
@ -428,7 +498,6 @@ function GroupsPanel({ groups, users, onChange }) {
|
|||
function Tokens() {
|
||||
const [burned, setBurned] = React.useState(14340);
|
||||
const [rate, setRate] = React.useState(2.4);
|
||||
const [showCalc, setShowCalc] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const i = setInterval(() => {
|
||||
|
|
@ -776,8 +845,8 @@ function Containers() {
|
|||
{containers !== null && containers.length === 0 && (
|
||||
<div style={{ textAlign: 'center', padding: '64px 0', color: 'var(--text-3)' }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 12 }}>🐳</div>
|
||||
<div style={{ fontWeight: 500, fontSize: 14 }}>No container data available</div>
|
||||
<div style={{ fontSize: 12, marginTop: 6 }}>Container metrics endpoint not yet wired</div>
|
||||
<div style={{ fontWeight: 500, fontSize: 14 }}>No containers returned</div>
|
||||
<div style={{ fontSize: 12, marginTop: 6 }}>Confirm <code>/var/run/docker.sock</code> is mounted in the mam-api container</div>
|
||||
</div>
|
||||
)}
|
||||
{containers !== null && containers.length > 0 && (
|
||||
|
|
@ -1226,7 +1295,6 @@ function GpuSettingsCard() {
|
|||
};
|
||||
|
||||
if (!cfg) return <SettingsCard icon="gpu" title="Proxy encoding" sub="Global proxy encoder applied to every ingested file"><div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading…</div></SettingsCard>;
|
||||
|
||||
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
|
||||
const gpuEnabled = cfg.gpu_transcode_enabled === 'true' || cfg.gpu_transcode_enabled === true;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue