ReviewOS

also looking at this

stacks/ts-cloud

refactor(dashboard): one confirm bar instead of five copies

#131
Closed glennmichael123 wants to merge fix/consolidate-op-confirm into main
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.

packages/ui/pages/partials/op-confirm.stxmodified+5-3
Changes to packages/ui/pages/partials/op-confirm.stx
@@ -1,7 +1,9 @@
11<div class="op-confirm panel" @show="pending() !== null">
22 <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>
66</div>
7@if (output ?? true)
78<pre class="action-output op-output" @show="opShown()">{{ opOutput() }}</pre>
9@endif
packages/ui/pages/server/firewall.stxmodified+7-10
Changes to packages/ui/pages/server/firewall.stx
@@ -50,13 +50,15 @@ async function removePort(port) {
5050 setMessage('Removed port ' + port + ' from cloud config.' + applyLine(data.apply), 'ok')
5151}
5252
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.
5455const pending = state(null); const typed = state('')
55const confirmTok = derived(() => { const p = pending(); return p ? String(p) : '' })
56const confirmTok = derived(() => { const p = pending(); return p !== null ? String(p) : '' })
57const confirmVerb = derived(() => { const p = pending(); return p !== null ? 'remove port ' + p : '' })
5658const canRun = derived(() => { const p = pending(); return p !== null && typed() === String(p) })
5759function askRemove(port) { pending.set(port); typed.set('') }
58function cancelRemove() { pending.set(null); typed.set('') }
59async function runRemove() {
60function cancelOp() { pending.set(null); typed.set('') }
61async function runOp() {
6062 const p = pending(); if (p === null || typed() !== String(p)) return
6163 pending.set(null); typed.set('')
6264 await removePort(p)
@@ -120,12 +122,7 @@ onMount(() => { load() })
120122 </template>
121123 </div>
122124 <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 })
129126 <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>
130127 </div>
131128 </div>
packages/ui/pages/server/ssh-keys.stxmodified+12-15
Changes to packages/ui/pages/server/ssh-keys.stx
@@ -41,15 +41,17 @@ async function removeKey(n) {
4141// Removal runs through a typed-confirm bar (same pattern as firewall/team):
4242// type the key name to confirm. Removing the wrong key can lock every operator
4343// out of the box, so it should never be one misclick away.
44const pendingRemove = state(null)
45const typedRemove = state('')
46const canRemove = derived(() => { const k = pendingRemove(); return k !== null && typedRemove() === String(k.name) })
47function askRemove(key) { pendingRemove.set(key); typedRemove.set('') }
48function cancelRemove() { pendingRemove.set(null); typedRemove.set('') }
49async function confirmRemove() {
50 const k = pendingRemove()
51 if (!k || typedRemove() !== String(k.name)) return
52 pendingRemove.set(null); typedRemove.set('')
44const pending = state(null)
45const typed = state('')
46const canRun = derived(() => { const k = pending(); return k !== null && typed() === String(k.name) })
47const confirmTok = derived(() => { const k = pending(); return k ? String(k.name) : '' })
48const confirmVerb = derived(() => 'remove this key from the box')
49function askRemove(key) { pending.set(key); typed.set('') }
50function cancelOp() { pending.set(null); typed.set('') }
51async function runOp() {
52 const k = pending()
53 if (!k || typed() !== String(k.name)) return
54 pending.set(null); typed.set('')
5355 await removeKey(k.name)
5456}
5557
@@ -100,12 +102,7 @@ onMount(() => { load() })
100102 </tbody>
101103 </table>
102104 <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 })
109106 <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>
110107 </div>
111108 </div>
packages/ui/pages/server/team.stxmodified+16-19
Changes to packages/ui/pages/server/team.stx
@@ -67,19 +67,21 @@ async function revoke(name) {
6767 load()
6868}
6969
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.
74const pendingRevoke = state(null)
75const typedRevoke = state('')
76const canRevoke = derived(() => { const u = pendingRevoke(); return u !== null && typedRevoke() === String(u.username) })
77function askRevoke(user) { pendingRevoke.set(user); typedRevoke.set('') }
78function cancelRevoke() { pendingRevoke.set(null); typedRevoke.set('') }
79async 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.
74const pending = state(null)
75const typed = state('')
76const confirmTok = derived(() => { const u = pending(); return u ? String(u.username) : '' })
77const confirmVerb = derived(() => { const u = pending(); return u ? 'remove ' + u.name + ' from every site' : '' })
78const canRun = derived(() => { const u = pending(); return u !== null && typed() === String(u.username) })
79function askRevoke(user) { pending.set(user); typed.set('') }
80function cancelOp() { pending.set(null); typed.set('') }
81async function runOp() {
82 const u = pending()
83 if (!u || typed() !== String(u.username)) return
84 pending.set(null); typed.set('')
8385 await revoke(u.username)
8486}
8587
@@ -157,12 +159,7 @@ onMount(() => { load() })
157159 </tbody>
158160 </table>
159161 <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 })
166163 <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>
167164 </div>
168165 </div>
packages/ui/pages/serverless/alarms.stxmodified+12-15
Changes to packages/ui/pages/serverless/alarms.stx
@@ -29,9 +29,11 @@ const msgOk = state(false)
2929const busy = state(false)
3030
3131// Delete flow uses a typed-confirm (the alarm name) before firing.
32const pendingDelete = state(null)
33const typedDelete = state('')
34const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
32const pending = state(null)
33const typed = state('')
34const canRun = derived(() => { const p = pending(); return !!p && typed() === p })
35const confirmTok = derived(() => pending() ?? '')
36const confirmVerb = derived(() => 'delete this alarm')
3537
3638function toneOf(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') }
3739
@@ -60,12 +62,12 @@ async function createAlarm() {
6062 finally { busy.set(false) }
6163}
6264
63function askDelete(name) { pendingDelete.set(name); typedDelete.set('') }
64function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
65async function confirmDelete() {
66 const name = pendingDelete()
67 if (!name || typedDelete() !== name) return
68 pendingDelete.set(null); typedDelete.set('')
65function askDelete(name) { pending.set(name); typed.set('') }
66function cancelOp() { pending.set(null); typed.set('') }
67async function runOp() {
68 const name = pending()
69 if (!name || typed() !== name) return
70 pending.set(null); typed.set('')
6971 msg.set('Deleting ' + name + '...'); msgOk.set(false)
7072 try {
7173 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() {
115117 </tbody>
116118 </table>
117119
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 })
124121 </div>
125122 </div>
126123
packages/ui/pages/serverless/secrets.stxmodified+12-15
Changes to packages/ui/pages/serverless/secrets.stx
@@ -22,9 +22,11 @@ const msgOk = state(false)
2222const busy = state(false)
2323
2424// Delete flow uses a typed-confirm (the secret id) before firing.
25const pendingDelete = state(null)
26const typedDelete = state('')
27const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
25const pending = state(null)
26const typed = state('')
27const canRun = derived(() => { const p = pending(); return !!p && typed() === p })
28const confirmTok = derived(() => pending() ?? '')
29const confirmVerb = derived(() => 'delete this secret')
2830
2931async function refresh() {
3032 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() {
4547 finally { busy.set(false) }
4648}
4749function pickSource(src) { secretId.set(src) }
48function askDelete(src) { pendingDelete.set(src); typedDelete.set('') }
49function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
50async function confirmDelete() {
51 const id = pendingDelete()
52 if (!id || typedDelete() !== id) return
53 pendingDelete.set(null); typedDelete.set('')
50function askDelete(src) { pending.set(src); typed.set('') }
51function cancelOp() { pending.set(null); typed.set('') }
52async function runOp() {
53 const id = pending()
54 if (!id || typed() !== id) return
55 pending.set(null); typed.set('')
5456 msg.set('Deleting ' + id + '...'); msgOk.set(false)
5557 try {
5658 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() {
101103 </tbody>
102104 </table>
103105
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 })
110107 </div>
111108 </div>
112109