pgkiln 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +3 -0
- package/db/migrations/119_workspace_home.sql +30 -0
- package/db/migrations/120_datasets.sql +87 -0
- package/docs/apex-feature-parity.md +7 -6
- package/docs/guide/01-installation.md +5 -0
- package/docs/guide/03-builder.md +140 -11
- package/docs/guide/04-pages-and-regions.md +25 -1
- package/docs/guide/12-development.md +47 -11
- package/examples/hr/hr_63_datasets.sql +29 -0
- package/package.json +1 -1
- package/public/app.css +6 -0
- package/public/builder.css +95 -3
- package/public/code-editor.css +32 -79
- package/public/code-editor.js +58 -3
- package/public/data-modeler.css +17 -14
- package/public/worksheet.css +14 -7
- package/public/worksheet.js +119 -3
- package/src/app.ts +5 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +1 -1
- package/src/builder/datasets.ts +472 -0
- package/src/builder/db-users.ts +2 -2
- package/src/builder/home.ts +12 -7
- package/src/builder/instance.ts +2 -0
- package/src/builder/object-browser.ts +4 -4
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/reporter.ts +52 -3
- package/src/builder/routes.ts +3 -3
- package/src/builder/scheduling.ts +2 -2
- package/src/builder/scripts.ts +1 -1
- package/src/builder/sql-charts.ts +1 -1
- package/src/builder/sql-monitor.ts +1 -1
- package/src/builder/subscriptions.ts +1 -0
- package/src/builder/table-editor.ts +2 -2
- package/src/builder/ui.ts +7 -5
- package/src/builder/worksheet.ts +171 -23
- package/src/builder/workspace-home.ts +283 -0
- package/src/builder/workspaces.ts +7 -4
- package/src/datasets.ts +367 -0
- package/src/i18n/ar.ts +16 -0
- package/src/i18n/cs.ts +16 -0
- package/src/i18n/da.ts +16 -0
- package/src/i18n/de.ts +16 -0
- package/src/i18n/el.ts +16 -0
- package/src/i18n/es.ts +16 -0
- package/src/i18n/fi.ts +16 -0
- package/src/i18n/fr.ts +16 -0
- package/src/i18n/he.ts +16 -0
- package/src/i18n/it.ts +16 -0
- package/src/i18n/ja.ts +16 -0
- package/src/i18n/ko.ts +16 -0
- package/src/i18n/nb.ts +16 -0
- package/src/i18n/pl.ts +16 -0
- package/src/i18n/pt.ts +16 -0
- package/src/i18n/ru.ts +16 -0
- package/src/i18n/sv.ts +16 -0
- package/src/i18n/tr.ts +16 -0
- package/src/i18n/uk.ts +16 -0
- package/src/i18n/zh.ts +16 -0
- package/src/i18n.ts +32 -0
- package/src/instance.ts +1 -0
- package/src/release-check.ts +139 -0
- package/src/reporting-app.ts +101 -0
- package/src/runtime/data-reporter.ts +199 -32
- package/src/session.ts +7 -0
- package/src/sqlworksheet.ts +9 -6
- package/src/subscriptions.ts +2 -1
- package/src/worksheet-txn.ts +270 -0
- package/src/workspace-export.ts +24 -4
package/public/data-modeler.css
CHANGED
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
.dm-toolbar .btn[disabled] { opacity: 0.5; }
|
|
9
9
|
.dm-status { min-width: 0; }
|
|
10
10
|
.dm-stage { position: relative; }
|
|
11
|
-
.dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); }
|
|
11
|
+
.dm-canvas { overflow: auto; max-width: 100%; height: 65vh; min-height: 18rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--canvas, var(--bg)); box-shadow: var(--shadow-xs, none); }
|
|
12
12
|
.dm-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
13
13
|
.dm-svg { display: block; }
|
|
14
|
-
.dm-svg
|
|
15
|
-
.dm-svg .dm-
|
|
16
|
-
.dm-
|
|
17
|
-
.dm-
|
|
18
|
-
.dm-
|
|
19
|
-
.dm-
|
|
20
|
-
.dm-
|
|
21
|
-
.dm-
|
|
22
|
-
.dm-
|
|
23
|
-
.dm-
|
|
14
|
+
/* the boxes and lines: no .dm-svg ancestor in the selectors, so the mini-map's <use> copy (a shadow tree) is themed too */
|
|
15
|
+
.dm-svg .dm-bg { fill: var(--canvas, var(--bg)); }
|
|
16
|
+
.dm-frame { fill: var(--block, var(--surface)); stroke: var(--border-strong, var(--border)); }
|
|
17
|
+
.dm-view .dm-frame { fill: var(--surface-2); }
|
|
18
|
+
path.dm-head { fill: var(--accent-soft); stroke: var(--border-strong, var(--border)); }
|
|
19
|
+
.dm-title { fill: var(--text); }
|
|
20
|
+
.dm-col { fill: var(--text); }
|
|
21
|
+
:is(.dm-type, .dm-kind, .dm-more) { fill: var(--muted); }
|
|
22
|
+
.dm-key { fill: var(--gold, #b45309); }
|
|
23
|
+
:is(.dm-line, .dm-end) { stroke: var(--muted); }
|
|
24
|
+
.dm-opt { fill: var(--canvas, var(--bg)); stroke: var(--muted); }
|
|
24
25
|
.dm-svg .dm-fk:hover .dm-line, .dm-svg .dm-fk:hover .dm-end { stroke: var(--accent); stroke-width: 2.5; }
|
|
25
26
|
.dm-svg .dm-link:hover .dm-title, .dm-svg .dm-link:focus .dm-title { fill: var(--accent); text-decoration: underline; }
|
|
26
27
|
.dm-live .dm-box { cursor: grab; touch-action: none; }
|
|
@@ -29,9 +30,9 @@
|
|
|
29
30
|
.dm-svg .dm-box:focus-visible .dm-frame { stroke: var(--accent); stroke-width: 2.5; }
|
|
30
31
|
.dm-svg .dm-dragging .dm-frame { stroke: var(--accent); }
|
|
31
32
|
.dm-svg text { user-select: none; }
|
|
32
|
-
.dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
33
|
-
background: var(--surface); box-shadow: 0 2px 8px
|
|
34
|
-
.dm-minimap-bg { fill: var(--surface); }
|
|
33
|
+
.dm-minimap { position: absolute; right: 12px; bottom: 12px; width: 200px; height: 140px; border: 1px solid var(--border-strong, var(--border)); border-radius: var(--radius);
|
|
34
|
+
background: var(--canvas, var(--surface)); box-shadow: var(--shadow-md, 0 2px 8px rgb(0 0 0 / 20%)); cursor: pointer; touch-action: none; overflow: hidden; }
|
|
35
|
+
.dm-minimap-bg { fill: var(--canvas, var(--surface)); }
|
|
35
36
|
.dm-minimap-view { fill: var(--accent); fill-opacity: 0.15; stroke: var(--accent); stroke-width: 4; vector-effect: non-scaling-stroke; }
|
|
36
37
|
.dm-legend { margin: 6px 0 12px; }
|
|
37
38
|
.dm-search-row { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
@@ -54,3 +55,5 @@
|
|
|
54
55
|
.dm-svg { max-width: 100%; height: auto; }
|
|
55
56
|
.dm-dict-table { break-inside: avoid-page; }
|
|
56
57
|
}
|
|
58
|
+
/* sprint 108: the Mini-map toggle shows that it is on */
|
|
59
|
+
.dm-toolbar .btn[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
|
package/public/worksheet.css
CHANGED
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
.ws-grid .null { color: var(--muted); font-style: italic; }
|
|
61
61
|
.ws-log { margin: 0; padding: 8px; background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); font-size: 12.5px; line-height: 1.45; white-space: pre; overflow: auto; }
|
|
62
62
|
.ws-log-sql { color: var(--muted); }
|
|
63
|
-
.ws-log-ok { color:
|
|
64
|
-
.ws-log-err { color:
|
|
63
|
+
.ws-log-ok { color: var(--success); font-weight: 600; }
|
|
64
|
+
.ws-log-err { color: var(--danger); white-space: pre-wrap; }
|
|
65
65
|
.ws-log-warn { color: var(--warn); white-space: pre-wrap; }
|
|
66
66
|
.ws-notices, .ws-history { list-style: none; margin: 0; padding: 0; font-size: 13px; }
|
|
67
67
|
.ws-notices li, .ws-history li { display: flex; gap: 8px; align-items: baseline; padding: 4px 0; border-bottom: 1px solid var(--border); min-width: 0; }
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
.ws-status { display: flex; align-items: center; gap: 12px; padding: 4px 10px; border-top: 1px solid var(--border); background: var(--surface-2); font-size: 12.5px; min-width: 0; }
|
|
73
73
|
.ws-status .icon { width: 13px; height: 13px; vertical-align: -2px; }
|
|
74
74
|
.ws-count { color: var(--muted); white-space: nowrap; }
|
|
75
|
-
.ws-count.is-err { color:
|
|
75
|
+
.ws-count.is-err { color: var(--danger); }
|
|
76
76
|
.ws-count.is-warn { color: var(--warn); }
|
|
77
77
|
.ws-last { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
78
78
|
.ws-time { margin-left: auto; }
|
|
@@ -156,8 +156,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
|
|
|
156
156
|
.ws-hist-bar { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
157
157
|
.ws-hist-bar input { flex: 1; min-width: 0; }
|
|
158
158
|
.ws-hist-icon .icon { width: 14px; height: 14px; }
|
|
159
|
-
.ws-hist-icon.is-ok { color:
|
|
160
|
-
.ws-hist-icon.is-err { color:
|
|
159
|
+
.ws-hist-icon.is-ok { color: var(--success); }
|
|
160
|
+
.ws-hist-icon.is-err { color: var(--danger); }
|
|
161
161
|
.ws-history li { align-items: center; }
|
|
162
162
|
.ws-history .btn { flex: none; }
|
|
163
163
|
@media (max-width: 640px) { .ws-hist-meta { display: none; } }
|
|
@@ -166,8 +166,8 @@ th[aria-sort=descending] .ws-sort::after { content: ' ▼'; font-size: 0.75em; }
|
|
|
166
166
|
.ws-wtabs { display: none; align-items: stretch; gap: 0; overflow-x: auto; border-bottom: 1px solid var(--border); background: var(--surface-2); flex: none; }
|
|
167
167
|
.js .ws-wtabs { display: flex; }
|
|
168
168
|
.ws-wtab { display: flex; align-items: center; border-right: 1px solid var(--border); }
|
|
169
|
-
.ws-wtab [
|
|
170
|
-
.ws-wtab [
|
|
169
|
+
.ws-wtab [data-ws-wtab] { border: 0; background: none; color: var(--muted); padding: 5px 4px 5px 10px; font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
|
|
170
|
+
.ws-wtab [data-ws-wtab][aria-current=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
|
|
171
171
|
.ws-wtab-close, .ws-wtab-new { border: 0; background: none; color: var(--muted); cursor: pointer; padding: 2px 8px; font-size: 14px; line-height: 1; }
|
|
172
172
|
.ws-wtab-close:hover, .ws-wtab-new:hover { color: var(--text); }
|
|
173
173
|
.ws-file > summary { list-style: none; max-width: 14rem; }
|
|
@@ -291,3 +291,10 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
|
|
|
291
291
|
.ch-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 10px 12px; min-width: 0; }
|
|
292
292
|
.ch-card h2 { font-size: 14px; margin: 0 0 6px; }
|
|
293
293
|
.ch-edit { max-width: 48rem; margin-top: 16px; }
|
|
294
|
+
|
|
295
|
+
/* (sprint 107) transaction mode: Autocommit / Transaction, the badge, Commit and Rollback */
|
|
296
|
+
.ws-txn { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
|
|
297
|
+
.ws-txn-badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); max-width: 22rem; overflow-wrap: anywhere; }
|
|
298
|
+
.ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
|
|
299
|
+
.ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
|
|
300
|
+
.ws-txn-alert { margin: 6px 8px; }
|
package/public/worksheet.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
// - (sprint 93) worksheets as tabs kept in this browser (unsaved text survives a reload), saved as SQL Scripts
|
|
14
14
|
// (Ctrl+S), the Files list; bind and substitution values in a dialog; Format (Ctrl+F7), Convert case (Ctrl+B),
|
|
15
15
|
// find and replace (Ctrl+F, Ctrl+H), go to line (Ctrl+G), comment lines (Ctrl+/), Help (F1)
|
|
16
|
+
// - (sprint 107) transaction mode per worksheet tab: Autocommit / Transaction, Commit and Rollback, a badge with the
|
|
17
|
+
// server's real state (fetched again after a reload and a tab switch, and when the idle timeout is due); leaving the
|
|
18
|
+
// page with an open transaction asks first, closing the tab or switching to Autocommit rolls it back
|
|
16
19
|
// Strict CSP: no inline styles; sizes are set through the CSSOM.
|
|
17
20
|
(() => {
|
|
18
21
|
'use strict';
|
|
@@ -266,8 +269,8 @@
|
|
|
266
269
|
wrap.className = 'ws-wtab';
|
|
267
270
|
const b = document.createElement('button');
|
|
268
271
|
b.type = 'button';
|
|
269
|
-
|
|
270
|
-
b.setAttribute('aria-
|
|
272
|
+
// (sprint 108) buttons in a group, the open one aria-current: a tablist may hold only tabs, not the close buttons
|
|
273
|
+
if (t === cur) b.setAttribute('aria-current', 'true');
|
|
271
274
|
b.dataset.wsWtab = t.key;
|
|
272
275
|
b.textContent = `${t.name}${dirty(t) ? '*' : ''}`;
|
|
273
276
|
b.title = t.scriptId ? `Script “${t.name}”${dirty(t) ? ' (unsaved changes)' : ''}` : 'Not saved as a script';
|
|
@@ -288,11 +291,15 @@
|
|
|
288
291
|
ta.value = t.text;
|
|
289
292
|
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
290
293
|
drawTabs();
|
|
294
|
+
txnTabChanged();
|
|
291
295
|
ta.focus();
|
|
292
296
|
};
|
|
293
297
|
const newTab = () => switchTo(add({ key: newKey(), name: untitled(), scriptId: null, text: '', saved: '' }));
|
|
294
298
|
const closeTab = (t) => {
|
|
295
|
-
|
|
299
|
+
const txnOpen = t === cur ? txnBox.dataset.state !== 'none' : !!t.txnOpen;
|
|
300
|
+
if (txnOpen && !window.confirm(`“${t.name}” has an open transaction. Close it and roll the transaction back?`)) return;
|
|
301
|
+
if (!txnOpen && dirty(t) && t.text.trim() && !window.confirm(`Close “${t.name}” without saving?`)) return;
|
|
302
|
+
if (t.txn) txnPost(t.key, 'rollback'); // the server knows best whether one is open
|
|
296
303
|
const i = book.tabs.indexOf(t);
|
|
297
304
|
book.tabs.splice(i, 1);
|
|
298
305
|
if (t === cur) {
|
|
@@ -340,6 +347,111 @@
|
|
|
340
347
|
form.querySelector('.ws-nav-switch').addEventListener('click', (e) => { const b = e.target.closest('[data-ws-navtab]'); if (b) navTab(b.dataset.wsNavtab); });
|
|
341
348
|
navTab(location.hash === '#ws-files' ? 'files' : 'objects');
|
|
342
349
|
|
|
350
|
+
// ------------------------------------------------------------ (sprint 107) transaction mode
|
|
351
|
+
function txnTabChanged() {
|
|
352
|
+
modeSel.value = cur.txn ? 'on' : '';
|
|
353
|
+
txnApply({ state: 'none', statements: 0, startedMs: null, note: null });
|
|
354
|
+
txnFetch();
|
|
355
|
+
}
|
|
356
|
+
const txnBox = form.querySelector('[data-ws-txn]');
|
|
357
|
+
const modeSel = txnBox.querySelector('[data-ws-txn-mode]');
|
|
358
|
+
const badge = txnBox.querySelector('[data-ws-txn-badge]');
|
|
359
|
+
const commitBtn = txnBox.querySelector('[data-ws-txn-commit]');
|
|
360
|
+
const rollbackBtn = txnBox.querySelector('[data-ws-txn-rollback]');
|
|
361
|
+
const commitNote = form.querySelector('[data-ws-commit-note]');
|
|
362
|
+
const idleSeconds = Number(txnBox.dataset.idle) || 300;
|
|
363
|
+
const idleWords = idleSeconds % 60 ? `${idleSeconds} seconds` : `${idleSeconds / 60} minute${idleSeconds === 60 ? '' : 's'}`;
|
|
364
|
+
let txnInfo = { state: txnBox.dataset.state, statements: Number(txnBox.dataset.statements) || 0, startedMs: Number(txnBox.dataset.started) || null,
|
|
365
|
+
lastMs: Number(txnBox.dataset.last) || null, note: txnBox.dataset.note || null };
|
|
366
|
+
let clockSkew = Date.now() - (Number(txnBox.dataset.now) || Date.now());
|
|
367
|
+
let txnTimer = 0;
|
|
368
|
+
const badgeText = (i) => {
|
|
369
|
+
if (i.state === 'none') return i.note || 'No open transaction';
|
|
370
|
+
const min = i.startedMs ? Math.floor((Date.now() - clockSkew - i.startedMs) / 60000) : 0;
|
|
371
|
+
return `${i.state === 'aborted' ? 'Transaction failed' : 'Transaction open'} · ${i.statements} statement${i.statements === 1 ? '' : 's'} · ${min < 1 ? 'under a minute' : `${min} min`}`;
|
|
372
|
+
};
|
|
373
|
+
const txnApply = (i) => {
|
|
374
|
+
txnInfo = i;
|
|
375
|
+
const on = modeSel.value === 'on';
|
|
376
|
+
txnBox.dataset.state = i.state;
|
|
377
|
+
badge.dataset.state = i.state;
|
|
378
|
+
badge.textContent = badgeText(i);
|
|
379
|
+
badge.hidden = !on && i.state === 'none' && !i.note;
|
|
380
|
+
commitBtn.hidden = rollbackBtn.hidden = !on && i.state === 'none';
|
|
381
|
+
commitBtn.disabled = i.state !== 'open';
|
|
382
|
+
rollbackBtn.disabled = i.state === 'none';
|
|
383
|
+
commitNote.textContent = on
|
|
384
|
+
? `The statements stay in one transaction until Commit or Rollback; it is rolled back after ${idleWords} without a run.`
|
|
385
|
+
: 'Each statement commits on its own.';
|
|
386
|
+
cur.txn = on;
|
|
387
|
+
cur.txnOpen = i.state !== 'none';
|
|
388
|
+
persist();
|
|
389
|
+
clearTimeout(txnTimer);
|
|
390
|
+
if (i.state !== 'none') {
|
|
391
|
+
// the age once a minute; the server again when the idle timeout is due
|
|
392
|
+
const due = i.lastMs ? i.lastMs + idleSeconds * 1000 + 2000 - (Date.now() - clockSkew) : 60000;
|
|
393
|
+
txnTimer = setTimeout(() => (due <= 60000 ? txnFetch() : txnApply(txnInfo)), Math.max(1000, Math.min(due, 60000)));
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
const fromServer = (j) => {
|
|
397
|
+
clockSkew = Date.now() - j.now;
|
|
398
|
+
txnApply({ state: j.state, statements: j.statements, startedMs: j.startedMs, lastMs: j.lastMs, note: j.note });
|
|
399
|
+
};
|
|
400
|
+
async function txnFetch() {
|
|
401
|
+
const key = cur.key;
|
|
402
|
+
try {
|
|
403
|
+
const res = await fetch(`/builder/sql/worksheet/txn?tab=${encodeURIComponent(key)}`, { credentials: 'same-origin', headers: { accept: 'application/json' } });
|
|
404
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
405
|
+
const j = await res.json();
|
|
406
|
+
if (key !== cur.key) return; // switched meanwhile
|
|
407
|
+
if (j.state !== 'none') modeSel.value = 'on';
|
|
408
|
+
fromServer(j);
|
|
409
|
+
} catch {
|
|
410
|
+
badge.textContent = 'Transaction state unknown';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function txnPost(key, action) {
|
|
414
|
+
try {
|
|
415
|
+
const res = await fetch('/builder/sql/worksheet/txn', { method: 'POST', credentials: 'same-origin', headers: { accept: 'application/json' },
|
|
416
|
+
body: new URLSearchParams({ __csrf: form.elements.__csrf.value, tab: key, action }) });
|
|
417
|
+
const j = await res.json();
|
|
418
|
+
if (key === cur.key) {
|
|
419
|
+
fromServer(j);
|
|
420
|
+
if (j.message) announce(j.message);
|
|
421
|
+
}
|
|
422
|
+
return j;
|
|
423
|
+
} catch {
|
|
424
|
+
announce('The transaction could not be ended: reload the page.');
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
modeSel.addEventListener('change', async () => {
|
|
429
|
+
if (modeSel.value !== 'on' && txnInfo.state !== 'none') {
|
|
430
|
+
if (!window.confirm('Switching to Autocommit rolls back the open transaction. Roll it back?')) { modeSel.value = 'on'; return; }
|
|
431
|
+
await txnPost(cur.key, 'rollback');
|
|
432
|
+
modeSel.value = '';
|
|
433
|
+
}
|
|
434
|
+
txnApply(txnInfo);
|
|
435
|
+
});
|
|
436
|
+
// a page post (a run, Commit, Rollback) is not leaving: the transaction stays
|
|
437
|
+
let submitting = false;
|
|
438
|
+
form.addEventListener('submit', () => {
|
|
439
|
+
submitting = true;
|
|
440
|
+
setTimeout(() => { submitting = false; }, 3000); // a download doesn't leave the page
|
|
441
|
+
});
|
|
442
|
+
window.addEventListener('pageshow', () => { submitting = false; });
|
|
443
|
+
window.addEventListener('beforeunload', (e) => {
|
|
444
|
+
if (submitting || txnInfo.state === 'none') return;
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
e.returnValue = ''; // the browser asks "Leave site?"; an idle transaction is rolled back after the timeout
|
|
447
|
+
});
|
|
448
|
+
if (!form.dataset.tab || form.dataset.tab !== cur.key) txnTabChanged();
|
|
449
|
+
else {
|
|
450
|
+
if (txnInfo.state !== 'none') modeSel.value = 'on';
|
|
451
|
+
else if (!form.querySelector('[data-ws-txn-mode] option[selected]')) modeSel.value = cur.txn ? 'on' : '';
|
|
452
|
+
txnApply(txnInfo);
|
|
453
|
+
}
|
|
454
|
+
|
|
343
455
|
// ------------------------------------------------------------ (sprint 97) a chart's settings draw it again
|
|
344
456
|
form.querySelector('.ch-controls')?.addEventListener('change', () => run('chart'));
|
|
345
457
|
|
|
@@ -513,6 +625,7 @@
|
|
|
513
625
|
const size = (key, px) => {
|
|
514
626
|
const v = key === 'nav' ? Math.max(160, Math.min(px, window.innerWidth * 0.5)) : Math.max(80, Math.min(px, window.innerHeight * 0.75));
|
|
515
627
|
form.style.setProperty(key === 'nav' ? '--ws-nav' : '--ws-out', `${Math.round(v)}px`);
|
|
628
|
+
form.querySelector(`[data-ws-split="${key}"]`)?.setAttribute('aria-valuenow', String(Math.round(v)));
|
|
516
629
|
store.set(key, String(Math.round(v)));
|
|
517
630
|
return v;
|
|
518
631
|
};
|
|
@@ -520,6 +633,9 @@
|
|
|
520
633
|
for (const sp of form.querySelectorAll('[data-ws-split]')) {
|
|
521
634
|
const key = sp.dataset.wsSplit;
|
|
522
635
|
const current = () => (key === 'nav' ? form.querySelector('.ws-nav').getBoundingClientRect().width : out.getBoundingClientRect().height);
|
|
636
|
+
// (sprint 108) a focusable separator states its position (in pixels)
|
|
637
|
+
sp.setAttribute('aria-valuemin', '0');
|
|
638
|
+
if (!sp.hasAttribute('aria-valuenow')) sp.setAttribute('aria-valuenow', String(Math.round(current())));
|
|
523
639
|
sp.addEventListener('pointerdown', (e) => {
|
|
524
640
|
e.preventDefault();
|
|
525
641
|
sp.setPointerCapture(e.pointerId);
|
package/src/app.ts
CHANGED
|
@@ -52,7 +52,9 @@ import { blueprintRoutes } from './builder/blueprints.ts';
|
|
|
52
52
|
import { accountRoutes } from './runtime/account.ts';
|
|
53
53
|
import { taskRoutes } from './runtime/tasks.ts';
|
|
54
54
|
import { dataReporterRoutes } from './runtime/data-reporter.ts';
|
|
55
|
+
import { datasetRoutes } from './builder/datasets.ts';
|
|
55
56
|
import { reporterBuilderRoutes } from './builder/reporter.ts';
|
|
57
|
+
import { workspaceHomeRoutes } from './builder/workspace-home.ts';
|
|
56
58
|
import { formDesignerRoutes } from './runtime/form-designer.ts';
|
|
57
59
|
import { formDesignerBuilderRoutes } from './builder/form-designer.ts';
|
|
58
60
|
import { builderErrorPages, builderServerError } from './builder/errors.ts';
|
|
@@ -239,6 +241,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
239
241
|
await app.register(codeEditorRoutes);
|
|
240
242
|
await app.register(dataReporterRoutes);
|
|
241
243
|
await app.register(reporterBuilderRoutes);
|
|
244
|
+
await app.register(datasetRoutes);
|
|
242
245
|
await app.register(formDesignerRoutes);
|
|
243
246
|
await app.register(formDesignerBuilderRoutes);
|
|
244
247
|
await app.register(sampleDataRoutes);
|
|
@@ -247,6 +250,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
247
250
|
await app.register(assistantBuilderRoutes);
|
|
248
251
|
await app.register(aiBuilderRoutes);
|
|
249
252
|
await app.register(blueprintRoutes);
|
|
250
|
-
app.
|
|
253
|
+
await app.register(workspaceHomeRoutes);
|
|
254
|
+
app.get('/', async (_req, reply) => reply.redirect('/builder/home'));
|
|
251
255
|
return app;
|
|
252
256
|
}
|
package/src/builder/changelog.ts
CHANGED
|
@@ -69,7 +69,7 @@ async function listPage(s: Session, q: Record<string, unknown>) {
|
|
|
69
69
|
${list.map((c) => html`<tr>
|
|
70
70
|
<td data-label="#">${c.seq}</td>
|
|
71
71
|
<td data-label="Change set" class="wrap-anywhere"><details><summary><code>${flywayName(c)}</code>${c.object_name ? html` <span class="muted small">${c.object_name}</span>` : ''}</summary>
|
|
72
|
-
<pre class="source wrap-pre">${c.sql}</pre>
|
|
72
|
+
<pre class="source wrap-pre" data-code-view="sql" tabindex="0" aria-label="SQL">${c.sql}</pre>
|
|
73
73
|
<form method="post" action="${CHANGES}/${c.id}" class="search u-mwnone">${csrf(s)}
|
|
74
74
|
<div class="field"><label class="label" for="f_name_${c.id}">Name</label><input id="f_name_${c.id}" name="name" value="${c.name}" pattern="[a-z0-9][a-z0-9_]{0,59}" required></div>
|
|
75
75
|
<button class="btn btn-sm u-selfend">Rename</button></form>
|
|
@@ -355,7 +355,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
355
355
|
return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}.sql"`).type('application/sql; charset=utf-8').send(script);
|
|
356
356
|
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
|
|
357
357
|
${region(`DDL of ${d.name}`, html`<p class="muted u-mt0">The CREATE statements of the diagram's ${m.tables.length} objects in schema ${d.schema_name}: tables, their keys, indexes, triggers, row level security, policies, comments and grants, and views. Review before running.</p>
|
|
358
|
-
<pre class="source" tabindex="0" aria-label="DDL">${script}</pre>
|
|
358
|
+
<pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL">${script}</pre>
|
|
359
359
|
<form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${script}">
|
|
360
360
|
<div class="buttons"><a class="btn" href="${M}/${d.id}">${bicon('back')} Back to the diagram</a><a class="btn" href="${M}/${d.id}/ddl?download=1">${icon('download')} Download</a>
|
|
361
361
|
<button class="btn btn-hot">${icon('play')} Open in the SQL Worksheet</button></div></form>`)}`;
|