also looking at this
refactor(dashboard): one confirm bar instead of five copies
#131
0 files
+0
-0
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -1,7 +1,9 @@ | ||
| 1 | 1 | <div class="op-confirm panel" @show="pending() !== null"> |
| 2 | 2 | <span>Type <b class="mono">{{ confirmTok() }}</b> to {{ confirmVerb() }}:</span> |
| 3 | <input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runOp()" placeholder="confirm" autocomplete="off"> | |
| 4 | <button class="btn sm" :disabled="!canRun()" @click="runOp()">Run</button> | |
| 5 | <button class="btn ghost sm" @click="cancelOp()">Cancel</button> | |
| 3 | <input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runOp()" placeholder="confirm" autocomplete="off" aria-label="Type the confirmation token to proceed"> | |
| 4 | <button type="button" class="btn sm" :disabled="!canRun()" @click="runOp()">{{ label ?? 'Run' }}</button> | |
| 5 | <button type="button" class="btn ghost sm" @click="cancelOp()">Cancel</button> | |
| 6 | 6 | </div> |
| 7 | @if (output ?? true) | |
| 7 | 8 | <pre class="action-output op-output" @show="opShown()">{{ opOutput() }}</pre> |
| 9 | @endif | |
| @@ -50,13 +50,15 @@ async function removePort(port) { | ||
| 50 | 50 | setMessage('Removed port ' + port + ' from cloud config.' + applyLine(data.apply), 'ok') |
| 51 | 51 | } |
| 52 | 52 | |
| 53 | // Remove runs through a typed-confirm bar: type the port number to confirm. | |
| 53 | // Remove runs through the shared typed-confirm bar: type the port number to | |
| 54 | // confirm. Names match the contract partials/op-confirm.stx binds to. | |
| 54 | 55 | const pending = state(null); const typed = state('') |
| 55 | const confirmTok = derived(() => { const p = pending(); return p ? String(p) : '' }) | |
| 56 | const confirmTok = derived(() => { const p = pending(); return p !== null ? String(p) : '' }) | |
| 57 | const confirmVerb = derived(() => { const p = pending(); return p !== null ? 'remove port ' + p : '' }) | |
| 56 | 58 | const canRun = derived(() => { const p = pending(); return p !== null && typed() === String(p) }) |
| 57 | 59 | function askRemove(port) { pending.set(port); typed.set('') } |
| 58 | function cancelRemove() { pending.set(null); typed.set('') } | |
| 59 | async function runRemove() { | |
| 60 | function cancelOp() { pending.set(null); typed.set('') } | |
| 61 | async function runOp() { | |
| 60 | 62 | const p = pending(); if (p === null || typed() !== String(p)) return |
| 61 | 63 | pending.set(null); typed.set('') |
| 62 | 64 | await removePort(p) |
| @@ -120,12 +122,7 @@ onMount(() => { load() }) | ||
| 120 | 122 | </template> |
| 121 | 123 | </div> |
| 122 | 124 | <div class="compact empty" @show="ports().length === 0"><strong>No extra ports configured</strong><span>Only 22, 80, and 443 are open. Allow a port above to update <span class="mono">infrastructure.compute.firewall</span>.</span></div> |
| 123 | <div class="op-confirm panel" @show="pending() !== null"> | |
| 124 | <span>Type <b class="mono">{{ confirmTok() }}</b> to remove this port:</span> | |
| 125 | <input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runRemove()" placeholder="confirm" autocomplete="off"> | |
| 126 | <button class="btn sm" :disabled="!canRun()" @click="runRemove()">Run</button> | |
| 127 | <button class="btn ghost sm" @click="cancelRemove()">Cancel</button> | |
| 128 | </div> | |
| 125 | @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false }) | |
| 129 | 126 | <p class="note">Ports are declarative: this page edits <span class="mono">infrastructure.compute.firewall</span> in cloud config and applies <span class="mono">ufw allow</span> / <span class="mono">ufw delete allow</span> live on the box. The full config is reconciled again on the next <span class="mono">cloud deploy</span>.</p> |
| 130 | 127 | </div> |
| 131 | 128 | </div> |
| @@ -41,15 +41,17 @@ async function removeKey(n) { | ||
| 41 | 41 | // Removal runs through a typed-confirm bar (same pattern as firewall/team): |
| 42 | 42 | // type the key name to confirm. Removing the wrong key can lock every operator |
| 43 | 43 | // out of the box, so it should never be one misclick away. |
| 44 | const pendingRemove = state(null) | |
| 45 | const typedRemove = state('') | |
| 46 | const canRemove = derived(() => { const k = pendingRemove(); return k !== null && typedRemove() === String(k.name) }) | |
| 47 | function askRemove(key) { pendingRemove.set(key); typedRemove.set('') } | |
| 48 | function cancelRemove() { pendingRemove.set(null); typedRemove.set('') } | |
| 49 | async function confirmRemove() { | |
| 50 | const k = pendingRemove() | |
| 51 | if (!k || typedRemove() !== String(k.name)) return | |
| 52 | pendingRemove.set(null); typedRemove.set('') | |
| 44 | const pending = state(null) | |
| 45 | const typed = state('') | |
| 46 | const canRun = derived(() => { const k = pending(); return k !== null && typed() === String(k.name) }) | |
| 47 | const confirmTok = derived(() => { const k = pending(); return k ? String(k.name) : '' }) | |
| 48 | const confirmVerb = derived(() => 'remove this key from the box') | |
| 49 | function askRemove(key) { pending.set(key); typed.set('') } | |
| 50 | function cancelOp() { pending.set(null); typed.set('') } | |
| 51 | async function runOp() { | |
| 52 | const k = pending() | |
| 53 | if (!k || typed() !== String(k.name)) return | |
| 54 | pending.set(null); typed.set('') | |
| 53 | 55 | await removeKey(k.name) |
| 54 | 56 | } |
| 55 | 57 | |
| @@ -100,12 +102,7 @@ onMount(() => { load() }) | ||
| 100 | 102 | </tbody> |
| 101 | 103 | </table> |
| 102 | 104 | <div class="compact empty" @show="keys().length === 0"><strong>No SSH keys configured</strong><span>Add a public key above to update <span class="mono">infrastructure.compute.sshKeys</span>.</span></div> |
| 103 | <div class="op-confirm" @show="pendingRemove() !== null" style="margin-top:14px"> | |
| 104 | <span>Type <b class="mono">{{ pendingRemove()?.name }}</b> to remove this key from the box:</span> | |
| 105 | <input class="op-confirm-input" :value="typedRemove()" @input="typedRemove.set($event.target.value)" @keydown.enter="confirmRemove()" placeholder="confirm" autocomplete="off" aria-label="Type the key name to confirm removal"> | |
| 106 | <button type="button" class="btn danger sm" :disabled="!canRemove()" @click="confirmRemove()">Remove</button> | |
| 107 | <button type="button" class="btn ghost sm" @click="cancelRemove()">Cancel</button> | |
| 108 | </div> | |
| 105 | @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false }) | |
| 109 | 106 | <p class="note">Keys are declarative: this page edits <span class="mono">infrastructure.compute.sshKeys</span> in cloud config. Run <span class="mono">cloud deploy</span> to apply changes to the box. Connect with <span class="mono">cloud server:ssh {{ server.name }}</span>.</p> |
| 110 | 107 | </div> |
| 111 | 108 | </div> |
| @@ -67,19 +67,21 @@ async function revoke(name) { | ||
| 67 | 67 | load() |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | // Removal runs through a typed-confirm bar (same pattern as firewall/secrets): | |
| 71 | // type the username to confirm. Revoking pulls someone's access to every site | |
| 72 | // on the box at once and invalidates their password, so it should never be one | |
| 73 | // misclick away. | |
| 74 | const pendingRevoke = state(null) | |
| 75 | const typedRevoke = state('') | |
| 76 | const canRevoke = derived(() => { const u = pendingRevoke(); return u !== null && typedRevoke() === String(u.username) }) | |
| 77 | function askRevoke(user) { pendingRevoke.set(user); typedRevoke.set('') } | |
| 78 | function cancelRevoke() { pendingRevoke.set(null); typedRevoke.set('') } | |
| 79 | async function confirmRevoke() { | |
| 80 | const u = pendingRevoke() | |
| 81 | if (!u || typedRevoke() !== String(u.username)) return | |
| 82 | pendingRevoke.set(null); typedRevoke.set('') | |
| 70 | // Removal runs through the shared typed-confirm bar: type the username to | |
| 71 | // confirm. Revoking pulls someone's access to every site on the box at once and | |
| 72 | // invalidates their password, so it should never be one misclick away. Names | |
| 73 | // match the contract partials/op-confirm.stx binds to. | |
| 74 | const pending = state(null) | |
| 75 | const typed = state('') | |
| 76 | const confirmTok = derived(() => { const u = pending(); return u ? String(u.username) : '' }) | |
| 77 | const confirmVerb = derived(() => { const u = pending(); return u ? 'remove ' + u.name + ' from every site' : '' }) | |
| 78 | const canRun = derived(() => { const u = pending(); return u !== null && typed() === String(u.username) }) | |
| 79 | function askRevoke(user) { pending.set(user); typed.set('') } | |
| 80 | function cancelOp() { pending.set(null); typed.set('') } | |
| 81 | async function runOp() { | |
| 82 | const u = pending() | |
| 83 | if (!u || typed() !== String(u.username)) return | |
| 84 | pending.set(null); typed.set('') | |
| 83 | 85 | await revoke(u.username) |
| 84 | 86 | } |
| 85 | 87 | |
| @@ -157,12 +159,7 @@ onMount(() => { load() }) | ||
| 157 | 159 | </tbody> |
| 158 | 160 | </table> |
| 159 | 161 | <div class="compact empty" @show="users().length === 0"><strong>No one invited yet</strong><span>Invite someone above to give them access to a site.</span></div> |
| 160 | <div class="op-confirm" @show="pendingRevoke() !== null" style="margin-top:14px"> | |
| 161 | <span>Type <b class="mono">{{ pendingRevoke()?.username }}</b> to remove <b>{{ pendingRevoke()?.name }}</b> from every site:</span> | |
| 162 | <input class="op-confirm-input" :value="typedRevoke()" @input="typedRevoke.set($event.target.value)" @keydown.enter="confirmRevoke()" placeholder="confirm" autocomplete="off"> | |
| 163 | <button class="btn danger sm" :disabled="!canRevoke()" @click="confirmRevoke()">Remove</button> | |
| 164 | <button class="btn ghost sm" @click="cancelRevoke()">Cancel</button> | |
| 165 | </div> | |
| 162 | @include('../partials/op-confirm', { label: 'Remove', danger: true, output: false }) | |
| 166 | 163 | <p class="note">Box owners reach everything on this server. Members reach only the sites listed here, and never the shell, SSH keys, firewall or databases. Access is checked on every request.</p> |
| 167 | 164 | </div> |
| 168 | 165 | </div> |
| @@ -29,9 +29,11 @@ const msgOk = state(false) | ||
| 29 | 29 | const busy = state(false) |
| 30 | 30 | |
| 31 | 31 | // Delete flow uses a typed-confirm (the alarm name) before firing. |
| 32 | const pendingDelete = state(null) | |
| 33 | const typedDelete = state('') | |
| 34 | const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p }) | |
| 32 | const pending = state(null) | |
| 33 | const typed = state('') | |
| 34 | const canRun = derived(() => { const p = pending(); return !!p && typed() === p }) | |
| 35 | const confirmTok = derived(() => pending() ?? '') | |
| 36 | const confirmVerb = derived(() => 'delete this alarm') | |
| 35 | 37 | |
| 36 | 38 | function toneOf(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') } |
| 37 | 39 | |
| @@ -60,12 +62,12 @@ async function createAlarm() { | ||
| 60 | 62 | finally { busy.set(false) } |
| 61 | 63 | } |
| 62 | 64 | |
| 63 | function askDelete(name) { pendingDelete.set(name); typedDelete.set('') } | |
| 64 | function cancelDelete() { pendingDelete.set(null); typedDelete.set('') } | |
| 65 | async function confirmDelete() { | |
| 66 | const name = pendingDelete() | |
| 67 | if (!name || typedDelete() !== name) return | |
| 68 | pendingDelete.set(null); typedDelete.set('') | |
| 65 | function askDelete(name) { pending.set(name); typed.set('') } | |
| 66 | function cancelOp() { pending.set(null); typed.set('') } | |
| 67 | async function runOp() { | |
| 68 | const name = pending() | |
| 69 | if (!name || typed() !== name) return | |
| 70 | pending.set(null); typed.set('') | |
| 69 | 71 | msg.set('Deleting ' + name + '...'); msgOk.set(false) |
| 70 | 72 | try { |
| 71 | 73 | const res = await fetch('/api/serverless/alarms', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, confirm: name }) }) |
| @@ -115,12 +117,7 @@ async function confirmDelete() { | ||
| 115 | 117 | </tbody> |
| 116 | 118 | </table> |
| 117 | 119 | |
| 118 | <div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px"> | |
| 119 | <span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this alarm:</span> | |
| 120 | <input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off"> | |
| 121 | <button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button> | |
| 122 | <button class="btn ghost sm" @click="cancelDelete()">Cancel</button> | |
| 123 | </div> | |
| 120 | @include('../partials/op-confirm', { label: 'Delete', danger: true, output: false }) | |
| 124 | 121 | </div> |
| 125 | 122 | </div> |
| 126 | 123 | |
| @@ -22,9 +22,11 @@ const msgOk = state(false) | ||
| 22 | 22 | const busy = state(false) |
| 23 | 23 | |
| 24 | 24 | // Delete flow uses a typed-confirm (the secret id) before firing. |
| 25 | const pendingDelete = state(null) | |
| 26 | const typedDelete = state('') | |
| 27 | const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p }) | |
| 25 | const pending = state(null) | |
| 26 | const typed = state('') | |
| 27 | const canRun = derived(() => { const p = pending(); return !!p && typed() === p }) | |
| 28 | const confirmTok = derived(() => pending() ?? '') | |
| 29 | const confirmVerb = derived(() => 'delete this secret') | |
| 28 | 30 | |
| 29 | 31 | async function refresh() { |
| 30 | 32 | try { const r = await fetch('/api/serverless/secrets'); if (!r.ok) return; const x = await r.json(); if (Array.isArray(x.secrets)) rows.set(x.secrets) } catch {} |
| @@ -45,12 +47,12 @@ async function setSecret() { | ||
| 45 | 47 | finally { busy.set(false) } |
| 46 | 48 | } |
| 47 | 49 | function pickSource(src) { secretId.set(src) } |
| 48 | function askDelete(src) { pendingDelete.set(src); typedDelete.set('') } | |
| 49 | function cancelDelete() { pendingDelete.set(null); typedDelete.set('') } | |
| 50 | async function confirmDelete() { | |
| 51 | const id = pendingDelete() | |
| 52 | if (!id || typedDelete() !== id) return | |
| 53 | pendingDelete.set(null); typedDelete.set('') | |
| 50 | function askDelete(src) { pending.set(src); typed.set('') } | |
| 51 | function cancelOp() { pending.set(null); typed.set('') } | |
| 52 | async function runOp() { | |
| 53 | const id = pending() | |
| 54 | if (!id || typed() !== id) return | |
| 55 | pending.set(null); typed.set('') | |
| 54 | 56 | msg.set('Deleting ' + id + '...'); msgOk.set(false) |
| 55 | 57 | try { |
| 56 | 58 | const res = await fetch('/api/serverless/secrets', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ secretId: id, confirm: id }) }) |
| @@ -101,12 +103,7 @@ async function confirmDelete() { | ||
| 101 | 103 | </tbody> |
| 102 | 104 | </table> |
| 103 | 105 | |
| 104 | <div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px"> | |
| 105 | <span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this secret:</span> | |
| 106 | <input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off"> | |
| 107 | <button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button> | |
| 108 | <button class="btn ghost sm" @click="cancelDelete()">Cancel</button> | |
| 109 | </div> | |
| 106 | @include('../partials/op-confirm', { label: 'Delete', danger: true, output: false }) | |
| 110 | 107 | </div> |
| 111 | 108 | </div> |
| 112 | 109 | |