A × closes a detail pane on a wide screen: Docker, Services, Cron
On a phone the list gives way to the pane and Back returns; on a desktop the list stays beside it and there was no way to put the pane away. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
027a2a575c
commit
b8ea7abcc7
@@ -48,6 +48,7 @@ function SourceEditor({ name, onClose, onSaved }: { name: string; onClose: () =>
|
||||
}
|
||||
return (
|
||||
<div className="cron-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onClose}>×</button>
|
||||
<button className="btn back" onClick={onClose}>‹ Back</button>
|
||||
<h2>{srcLabel(name)}</h2>
|
||||
{!editable && <div className="notice">Read-only: writing this file needs passwordless sudo on the host.</div>}
|
||||
@@ -98,6 +99,7 @@ function JobDetail({ job, logging, onChanged, onBack, onEdit }: { job: CronJob;
|
||||
|
||||
return (
|
||||
<div className="cron-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button>
|
||||
<button className="btn back" onClick={onBack}>‹ Cron</button>
|
||||
<h2>{job.command}</h2>
|
||||
<div className="props">
|
||||
@@ -150,6 +152,7 @@ function TimerDetail({ t, onChanged, onBack }: { t: CronTimer; onChanged: () =>
|
||||
const B = ({ a, label }: { a: string; label: string }) => <button className="btn" disabled={!!busy} onClick={() => act(a)}>{busy === a && <Spinner />}{label}</button>
|
||||
return (
|
||||
<div className="cron-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button>
|
||||
<button className="btn back" onClick={onBack}>‹ Cron</button>
|
||||
<h2>{t.unit}</h2>
|
||||
<div className="muted">{t.description}</div>
|
||||
|
||||
+4
-2
@@ -60,6 +60,7 @@ function Detail({ c, live, onBack, onChanged }: { c: DockerContainer; live: Reco
|
||||
const mounts = (info?.mounts as { Source: string; Destination: string; RW: boolean }[] | undefined) ?? []
|
||||
return (
|
||||
<div className="svc-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button>
|
||||
<div className="svc-head"><button className="btn back" onClick={onBack}>‹ Back</button><span className={'sdot ' + dot(c)} /><span className="dname">{c.name}</span></div>
|
||||
<div className="props">
|
||||
<div><span className="k">Image</span><span className="mono small">{c.image}</span></div>
|
||||
@@ -140,10 +141,11 @@ function StackDetail({ name, startTask, onBack, onChanged, onDuplicate }: { name
|
||||
}
|
||||
const duplicate = async () => { if (!st) return; try { onDuplicate({ name, text: file === 0 ? orig : (await api.stackFile(name, 0)).text, hasEnv: !!st.has_env, files: st.host_files?.length ?? 1 }) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } }
|
||||
const run = (a: 'deploy' | 'update' | 'restart' | 'stop' | 'down') => async () => { try { setTask((await api.stackAction(name, a)).id) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } }
|
||||
if (err && !st) return <div className="svc-detail"><button className="btn back" onClick={onBack}>‹ Back</button><div className="notice">{err}</div></div>
|
||||
if (err && !st) return <div className="svc-detail"><button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button><button className="btn back" onClick={onBack}>‹ Back</button><div className="notice">{err}</div></div>
|
||||
if (!st) return <div className="svc-detail"><Loading label="Reading the compose file…" /></div>
|
||||
return (
|
||||
<div className="svc-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button>
|
||||
<div className="svc-head"><button className="btn back" onClick={onBack}>‹ Back</button><span className={'sdot ' + (st.containers.length === 0 ? '' : st.running === st.containers.length ? 'ok' : st.running ? 'warn' : 'bad')} /><span className="dname">{st.name}</span><span className="muted small">{st.running}/{st.containers.length} running</span></div>
|
||||
<div className="muted small mono" style={{ overflowWrap: 'anywhere' }}>{file === 'env' ? (st.host_file.replace(/[^/]+$/, '') + '.env') : (st.host_files?.[file] ?? st.host_file)}{st.held_by && <span className="muted"> · kept on <b>{st.held_by}</b>, deployed to this machine over {st.remote}</span>}{st.via && <span className="muted"> · compose runs inside <b>{st.via}</b>, as it was deployed</span>}{st.has_env && ' · has a .env beside it'}</div>
|
||||
{((st.host_files?.length ?? 0) > 1 || st.has_env) && <div className="chips" title="this stack is deployed from several files, in this order; Deploy uses all of them">{st.host_files!.map((f, i) => <button key={f} className={'chip' + (i === file ? ' on' : '')} onClick={async () => { if (dirty && !(await askYes('Discard unsaved changes to this file?', 'Discard'))) return; setFile(i) }}>{f.split('/').pop()}</button>)}{st.has_env && <button className={'chip' + (file === 'env' ? ' on' : '')} title="the .env beside the compose file: variables compose substitutes" onClick={async () => { if (dirty && !(await askYes('Discard unsaved changes to this file?', 'Discard'))) return; setFile('env') }}>.env</button>}</div>}
|
||||
@@ -264,7 +266,7 @@ export default function Docker() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{shownTab === 'containers' && !cur && sel && !selStack && <div className="svc-detail"><button className="btn back" onClick={() => setSel('')}>‹ Back</button><div className="notice">There is no container called <b className="mono">{sel}</b> on this machine. It may have been removed, renamed or recreated under another name. <button className="link" onClick={() => setSel('')}>Show them all</button></div></div>}
|
||||
{shownTab === 'containers' && !cur && sel && !selStack && <div className="svc-detail"><button className="icon-btn detail-close" title="Close" aria-label="close" onClick={() => setSel('')}>×</button><button className="btn back" onClick={() => setSel('')}>‹ Back</button><div className="notice">There is no container called <b className="mono">{sel}</b> on this machine. It may have been removed, renamed or recreated under another name. <button className="link" onClick={() => setSel('')}>Show them all</button></div></div>}
|
||||
{shownTab === 'containers' && (cur ? <Detail c={cur} live={sensors} onBack={() => setSel('')} onChanged={load} /> : (sel && !selStack ? null : <div className="svc-empty muted">Select a container.</div>))}
|
||||
{making && <StackNew key={'new:' + (dup?.name ?? making)} mode={making} from={dup ?? undefined} networks={nets} taken={taken} onClose={() => { setMaking(''); setDup(null) }} onCreated={async (name, deploy) => { setMaking(''); setDup(null); setTab('stacks'); if (deploy) { try { setStartTask({ name, id: (await api.stackAction(name, 'deploy')).id }) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } else setStartTask(null); await load(); setSel('stack:' + name) }} />}
|
||||
{shownTab === 'stacks' && (selStack ? <StackDetail key={selStack + (startTask?.name === selStack ? startTask.id : '')} name={selStack} startTask={startTask?.name === selStack ? startTask.id : undefined} onBack={() => setSel('')} onChanged={load} onDuplicate={(f) => { setDup(f); setMaking('stack') }} /> : <div className="svc-empty muted">Select a stack to edit its compose file.</div>)}
|
||||
|
||||
@@ -137,6 +137,7 @@ function Detail({ name, onChanged, onBack }: { name: string; onChanged: () => vo
|
||||
|
||||
return (
|
||||
<div className="svc-detail">
|
||||
<button className="icon-btn detail-close" title="Close" aria-label="close" onClick={onBack}>×</button>
|
||||
<button className="btn back" onClick={onBack}>‹ Services</button>
|
||||
<div className="svc-head">
|
||||
<span className={'sdot ' + stateClass(d)} />
|
||||
|
||||
+7
-1
@@ -149,6 +149,12 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
||||
What is meant to give way (.logs, .stack-editor) says so itself, further down. */
|
||||
.svc-detail > *, .cron-detail > * { flex-shrink: 0; }
|
||||
.svc-detail .back { display: none; align-self: flex-start; }
|
||||
/* The × that closes a detail pane on a wide screen, where the list stays beside it and there is
|
||||
no Back. Sits in the pane's top-right corner; the header keeps clear of it. */
|
||||
.svc-detail, .cron-detail { position: relative; }
|
||||
.detail-close { position: absolute; top: 8px; right: 10px; z-index: 1; width: 28px; height: 28px; line-height: 28px; font-size: 20px; color: var(--muted); border-radius: 6px; }
|
||||
.detail-close:hover { color: var(--fg); background: var(--bg3); }
|
||||
.svc-detail > .svc-head, .cron-detail > h2 { padding-right: 36px; }
|
||||
/* Nothing selected: the list has the window to itself rather than a 360px column beside an
|
||||
empty pane. The names, descriptions and commands that get an ellipsis are what it's for.
|
||||
Picking something puts the list back at its own width, beside the detail. A phone shows one
|
||||
@@ -174,7 +180,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
||||
.log.err .lm { color: var(--bad); } .log.warn .lm { color: var(--s2); } .log.debug .lm { color: var(--muted); }
|
||||
@media (max-width: 800px) {
|
||||
.svc-list { width: 100%; border-right: 0; } .has-sel .svc-list { display: none; } .svc-empty { display: none; }
|
||||
.svc-detail .back { display: inline-block; } .props { grid-template-columns: 1fr; }
|
||||
.svc-detail .back { display: inline-block; } .props { grid-template-columns: 1fr; } .detail-close { display: none; }
|
||||
}
|
||||
|
||||
/* cron */
|
||||
|
||||
Reference in New Issue
Block a user