The process table's columns fit the box they are in, the rate control saves, curves are timed on the viewer's clock and keep to the stride, the power icon is drawn, and a young history draws in from the right instead of being stretched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
54 lines
2.7 KiB
TypeScript
54 lines
2.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { api } from './api'
|
|
import { Btn, Spinner, useEscape } from './ui'
|
|
|
|
// Header power control: a small button, a dialog that needs the hostname typed back.
|
|
// `label` is what the user types (the machine name on a remote); `host` is what the node checks.
|
|
export default function Power({ host, label, rebootNeeded }: { host: string; label: string; rebootNeeded?: boolean }) {
|
|
const [open, setOpen] = useState(false)
|
|
const [typed, setTyped] = useState('')
|
|
const [sent, setSent] = useState<'' | 'reboot' | 'poweroff'>('')
|
|
useEscape(() => { if (!sent) setOpen(false) }, open)
|
|
const [err, setErr] = useState('')
|
|
const ok = typed.trim() === label
|
|
|
|
async function go(action: 'reboot' | 'poweroff') {
|
|
setErr('')
|
|
try { await api.power(action, host); setSent(action) }
|
|
catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<button className={'icon-btn power' + (rebootNeeded ? ' attn' : '')} title={rebootNeeded ? 'reboot required' : 'power'} onClick={() => { setOpen(true); setTyped(''); setSent(''); setErr('') }}>
|
|
{/* drawn, not typed: ⏻ (U+23FB) is missing from Windows' fonts and shows as a box of hex digits */}
|
|
<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><path d="M8 1.8v6" /><path d="M4.6 4.2a5.2 5.2 0 1 0 6.8 0" /></svg>
|
|
</button>
|
|
{open && (
|
|
<div className="modal-back" onClick={() => !sent && setOpen(false)}>
|
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
<h3>Power · {label}</h3>
|
|
{rebootNeeded && <div className="notice">A reboot is pending from package updates.</div>}
|
|
{sent ? (
|
|
<div className="muted"><Spinner /> {sent === 'reboot' ? 'Rebooting. The console will reconnect when the host is back.' : 'Shutting down. Power it back on by hand.'}</div>
|
|
) : (
|
|
<>
|
|
<label className="muted">Type <b>{label}</b> to confirm
|
|
<input autoFocus value={typed} onChange={(e) => setTyped(e.target.value)} placeholder={label} />
|
|
</label>
|
|
{err && <div className="error">{err}</div>}
|
|
<div className="svc-actions">
|
|
<Btn className="btn on" disabled={!ok} onClick={() => go('reboot')}>Reboot</Btn>
|
|
<Btn className="btn danger" disabled={!ok} onClick={() => go('poweroff')}>Shut down</Btn>
|
|
<span className="spacer" />
|
|
<button className="btn" onClick={() => setOpen(false)}>Cancel</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|