pgkiln 0.49.0 → 0.50.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 +1 -0
- package/docs/apex-feature-parity.md +3 -3
- package/docs/guide/01-installation.md +3 -0
- package/docs/guide/03-builder.md +49 -6
- package/docs/guide/12-development.md +21 -8
- package/package.json +1 -1
- package/public/builder.css +61 -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/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +1 -1
- package/src/builder/db-users.ts +2 -2
- 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/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/table-editor.ts +2 -2
- package/src/builder/worksheet.ts +171 -23
- package/src/session.ts +5 -0
- package/src/sqlworksheet.ts +9 -6
- package/src/worksheet-txn.ts +270 -0
package/public/code-editor.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// - "Check" plans the SQL like the Advisor (POST /builder/code/check)
|
|
16
16
|
// - screen readers keep the plain textarea; the list is announced through
|
|
17
17
|
// aria-activedescendant and a polite live region
|
|
18
|
+
// - (sprint 108) read-only code, <pre data-code-view="sql|plpgsql|json|html">, is
|
|
19
|
+
// highlighted with the same lexer and colours (DDL, generated SQL)
|
|
18
20
|
//
|
|
19
21
|
// Strict CSP: no inline styles; geometry is set through the CSSOM. Suggestions
|
|
20
22
|
// and highlighted text are built as text nodes, never parsed as HTML.
|
|
@@ -437,15 +439,20 @@
|
|
|
437
439
|
gutter.setAttribute('aria-hidden', 'true');
|
|
438
440
|
const nums = document.createElement('div');
|
|
439
441
|
nums.className = 'ce-nums';
|
|
440
|
-
gutter
|
|
442
|
+
// the caret's line: a band behind the text, its number over the gutter's
|
|
443
|
+
const curNum = document.createElement('div');
|
|
444
|
+
curNum.className = 'ce-cur-num';
|
|
445
|
+
gutter.append(nums, curNum);
|
|
441
446
|
const main = document.createElement('div');
|
|
442
447
|
main.className = 'ce-main';
|
|
443
448
|
const hl = document.createElement('div');
|
|
444
449
|
hl.className = 'ce-hl';
|
|
445
450
|
hl.setAttribute('aria-hidden', 'true');
|
|
451
|
+
const cur = document.createElement('div');
|
|
452
|
+
cur.className = 'ce-cur';
|
|
446
453
|
const inner = document.createElement('pre');
|
|
447
454
|
inner.className = 'ce-code';
|
|
448
|
-
hl.append(inner);
|
|
455
|
+
hl.append(cur, inner);
|
|
449
456
|
ta.parentNode.insertBefore(wrap, ta);
|
|
450
457
|
main.append(hl, ta);
|
|
451
458
|
wrap.append(gutter, main);
|
|
@@ -502,7 +509,7 @@
|
|
|
502
509
|
ta.setAttribute('aria-autocomplete', 'list');
|
|
503
510
|
ta.setAttribute('aria-haspopup', 'listbox');
|
|
504
511
|
ta.setAttribute('aria-controls', list.id);
|
|
505
|
-
Object.assign(this, { wrap, gutter, nums, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
|
|
512
|
+
Object.assign(this, { wrap, gutter, nums, cur, curNum, main, hl, inner, list, bar, langEl: lang, hint, pos, msg, check, live, suggest });
|
|
506
513
|
this.applyLang();
|
|
507
514
|
}
|
|
508
515
|
|
|
@@ -661,9 +668,27 @@
|
|
|
661
668
|
this.digits = digits;
|
|
662
669
|
this.gutter.style.setProperty('--ce-digits', String(Math.max(2, digits)));
|
|
663
670
|
}
|
|
671
|
+
this.placeLine();
|
|
664
672
|
if (this.pop) this.place();
|
|
665
673
|
}
|
|
666
674
|
|
|
675
|
+
/** The current-line band and number: on the caret's line; hidden while a selection spans lines. */
|
|
676
|
+
placeLine() {
|
|
677
|
+
const ta = this.ta;
|
|
678
|
+
const lines = this.lineStarts;
|
|
679
|
+
const a = lowerBound(lines, ta.selectionStart + 1);
|
|
680
|
+
const b = lowerBound(lines, ta.selectionEnd + 1);
|
|
681
|
+
const hide = a !== b;
|
|
682
|
+
this.cur.hidden = hide;
|
|
683
|
+
this.curNum.hidden = hide;
|
|
684
|
+
if (hide) return;
|
|
685
|
+
const { lh, padTop } = this.metrics();
|
|
686
|
+
const y = padTop + (a - 1) * lh - ta.scrollTop;
|
|
687
|
+
this.cur.style.transform = `translateY(${y}px)`;
|
|
688
|
+
this.curNum.style.transform = `translateY(${y}px)`;
|
|
689
|
+
this.curNum.textContent = String(a);
|
|
690
|
+
}
|
|
691
|
+
|
|
667
692
|
tokenAt(off) {
|
|
668
693
|
this.sync();
|
|
669
694
|
const { T, S, E } = this.toks;
|
|
@@ -703,6 +728,7 @@
|
|
|
703
728
|
this.sync();
|
|
704
729
|
if (this.pop && this.ta.selectionStart !== this.ta.selectionEnd) this.close();
|
|
705
730
|
if (this.updateMatch()) this.schedule(true);
|
|
731
|
+
else this.placeLine();
|
|
706
732
|
const c = this.ta.selectionStart;
|
|
707
733
|
const line = lowerBound(this.lineStarts, c + 1);
|
|
708
734
|
this.pos.textContent = `Ln ${line}, Col ${c - this.lineStarts[line - 1] + 1}`;
|
|
@@ -997,12 +1023,41 @@
|
|
|
997
1023
|
return lo;
|
|
998
1024
|
}
|
|
999
1025
|
|
|
1026
|
+
/** Highlight a read-only <pre data-code-view>: its text, as spans (text nodes only). Long ones stay plain. */
|
|
1027
|
+
function colorize(pre) {
|
|
1028
|
+
const lang = pre.dataset.codeView in LANG_LABEL ? pre.dataset.codeView : 'text';
|
|
1029
|
+
pre.classList.add('ce-view');
|
|
1030
|
+
const text = pre.textContent;
|
|
1031
|
+
if (lang === 'text' || text.length > 200000) return;
|
|
1032
|
+
const { T, S, E } = lex(lang, text);
|
|
1033
|
+
const frag = document.createDocumentFragment();
|
|
1034
|
+
let pos = 0;
|
|
1035
|
+
for (let k = 0; k < T.length; k++) {
|
|
1036
|
+
const st = Math.max(S[k], pos);
|
|
1037
|
+
if (E[k] <= st) continue;
|
|
1038
|
+
if (st > pos) frag.append(text.slice(pos, st));
|
|
1039
|
+
const span = document.createElement('span');
|
|
1040
|
+
span.className = `t-${T[k]}`;
|
|
1041
|
+
span.textContent = text.slice(st, E[k]);
|
|
1042
|
+
frag.append(span);
|
|
1043
|
+
pos = E[k];
|
|
1044
|
+
}
|
|
1045
|
+
if (pos < text.length) frag.append(text.slice(pos));
|
|
1046
|
+
pre.replaceChildren(frag);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1000
1049
|
const editors = new WeakMap();
|
|
1050
|
+
const viewed = new WeakSet();
|
|
1001
1051
|
function enhance(root) {
|
|
1002
1052
|
for (const ta of (root || document).querySelectorAll('textarea[data-code]')) {
|
|
1003
1053
|
if (editors.has(ta) || ta.closest('.ce')) continue;
|
|
1004
1054
|
editors.set(ta, new Editor(ta));
|
|
1005
1055
|
}
|
|
1056
|
+
for (const pre of (root || document).querySelectorAll('pre[data-code-view]')) {
|
|
1057
|
+
if (viewed.has(pre)) continue;
|
|
1058
|
+
viewed.add(pre);
|
|
1059
|
+
colorize(pre);
|
|
1060
|
+
}
|
|
1006
1061
|
}
|
|
1007
1062
|
window.pgkilnCodeEditor = { enhance, editorOf: (ta) => editors.get(ta) };
|
|
1008
1063
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance());
|
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/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>`)}`;
|
package/src/builder/db-users.ts
CHANGED
|
@@ -223,7 +223,7 @@ function reviewForm(s: Session, spec: RoleSpec, stmts: string[], o: { isNew: boo
|
|
|
223
223
|
return html`${errorList(o.errors)}${o.output ?? ''}
|
|
224
224
|
${region('SQL', stmts.length
|
|
225
225
|
? html`<p class="muted u-mt0">These statements run in one transaction when you apply them: all of them, or none.${spec.password === 'set' ? ' The password shows as \'********\': pgkiln sends PostgreSQL only its SCRAM-SHA-256 verifier.' : ''}</p>
|
|
226
|
-
<pre class="source dbu-sql" aria-label="Generated SQL">${maskSql(stmts)}</pre>`
|
|
226
|
+
<pre class="source dbu-sql" data-code-view="sql" tabindex="0" aria-label="Generated SQL">${maskSql(stmts)}</pre>`
|
|
227
227
|
: html`<p class="muted u-mt0">Nothing to change: the role is as the form says.</p>`)}
|
|
228
228
|
<form method="post" action="${URL_BASE}/save" class="dbu-form">${csrf(s)}${hiddenSpec(spec, o.isNew)}
|
|
229
229
|
<input type="hidden" name="reviewed" value="${reviewDigest(stmts)}">
|
|
@@ -409,7 +409,7 @@ async function dropPage(req: Req, reply: FastifyReply, s: Session, name: string,
|
|
|
409
409
|
<option value=""${sel(to === '')}>${r.owned ? 'Drop them with the role (DROP OWNED)' : 'It owns nothing here: just revoke its privileges'}</option></select>
|
|
410
410
|
<small class="help">DROP OWNED also revokes the privileges granted to the role in this database.</small></div>
|
|
411
411
|
<div class="buttons"><button class="btn">${icon('eye')} Review SQL</button></div></form>`)}
|
|
412
|
-
${region('SQL', html`<pre class="source dbu-sql" aria-label="Generated SQL">${maskSql(stmts)}</pre>
|
|
412
|
+
${region('SQL', html`<pre class="source dbu-sql" data-code-view="sql" tabindex="0" aria-label="Generated SQL">${maskSql(stmts)}</pre>
|
|
413
413
|
<form method="post" action="${URL_BASE}/drop" class="dbu-form">${csrf(s)}
|
|
414
414
|
<input type="hidden" name="name" value="${r.name}"><input type="hidden" name="to" value="${to}"><input type="hidden" name="reviewed" value="${reviewDigest(stmts)}">
|
|
415
415
|
${!to && r.owned ? html`<label class="check u-mb1"><input type="checkbox" name="drop_objects" value="1" required> Drop the ${String(r.owned)} object${r.owned === 1 ? '' : 's'} ${r.name} owns, with their data</label>` : ''}
|
|
@@ -279,22 +279,22 @@ export async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s
|
|
|
279
279
|
const ddlOf = async (match: (x: { kind: string; name: string; table?: string }) => boolean) => {
|
|
280
280
|
const objects = await readSchema(db.pool as pg.Pool, schema).catch(() => []);
|
|
281
281
|
const text = objects.filter(match).map((x) => x.ddl).join('\n\n');
|
|
282
|
-
return text ? html`<pre class="source">${text}</pre>` : html`<p class="muted">No DDL (an extension's object, or not readable with this connection).</p>`;
|
|
282
|
+
return text ? html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL of ${o.name}">${text}</pre>` : html`<p class="muted">No DDL (an extension's object, or not readable with this connection).</p>`;
|
|
283
283
|
};
|
|
284
284
|
const relname = o.ref.includes('.') ? o.ref.slice(o.ref.lastIndexOf('.') + 1).replace(/^"|"$/g, '') : o.ref.replace(/^"|"$/g, '');
|
|
285
285
|
|
|
286
286
|
if (o.type === 'function' || o.type === 'procedure') {
|
|
287
287
|
const src = await db.one<{ def: string }>('select pg_get_functiondef($1::regprocedure) as def', [o.ref]).catch(() => undefined);
|
|
288
|
-
return region(o.name, src ? html`<pre class="source">${src.def}</pre>` : html`<p class="muted">An aggregate or window function: no source.</p>`);
|
|
288
|
+
return region(o.name, src ? html`<pre class="source" data-code-view="plpgsql" tabindex="0" aria-label="Source of ${o.name}">${src.def}</pre>` : html`<p class="muted">An aggregate or window function: no source.</p>`);
|
|
289
289
|
}
|
|
290
290
|
if (o.type === 'trigger') {
|
|
291
291
|
const [rel, name] = o.ref.split('|');
|
|
292
292
|
const t = await db.one<{ def: string }>('select pg_get_triggerdef(oid, true) as def from pg_trigger where tgrelid = $1::regclass and tgname = $2', [rel, name]);
|
|
293
|
-
return region(o.name, html`<pre class="source">${t?.def ?? ''}</pre>`);
|
|
293
|
+
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="Definition of ${o.name}">${t?.def ?? ''}</pre>`);
|
|
294
294
|
}
|
|
295
295
|
if (o.type === 'index') {
|
|
296
296
|
const i = await db.one<{ def: string; size: string }>('select pg_get_indexdef($1::regclass) as def, pg_size_pretty(pg_relation_size($1::regclass)) as size', [o.ref]);
|
|
297
|
-
return region(o.name, html`<pre class="source">${i?.def ?? ''};</pre><p class="muted">Size: ${i?.size ?? ''}</p>`);
|
|
297
|
+
return region(o.name, html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="Definition of ${o.name}">${i?.def ?? ''};</pre><p class="muted">Size: ${i?.size ?? ''}</p>`);
|
|
298
298
|
}
|
|
299
299
|
if (o.type === 'sequence') {
|
|
300
300
|
const q = await db.one<Record<string, unknown>>(
|
|
@@ -361,7 +361,7 @@ export async function queryBuilderRoutes(app: FastifyInstance) {
|
|
|
361
361
|
</form>`;
|
|
362
362
|
|
|
363
363
|
const output = built
|
|
364
|
-
? html`<div class="u-spacer"></div>${region('SQL', html`<pre class="source" tabindex="0" aria-label="Generated SQL">${built.sql}</pre>
|
|
364
|
+
? html`<div class="u-spacer"></div>${region('SQL', html`<pre class="source" data-code-view="sql" tabindex="0" aria-label="Generated SQL">${built.sql}</pre>
|
|
365
365
|
<form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${built.sql}">
|
|
366
366
|
<div class="buttons"><button class="btn btn-hot">${icon('play')} Run in the SQL Worksheet</button></div></form>`)}`
|
|
367
367
|
: '';
|
package/src/builder/quicksql.ts
CHANGED
|
@@ -43,7 +43,7 @@ export async function quickSqlRoutes(app: FastifyInstance) {
|
|
|
43
43
|
${region('PostgreSQL', html`${warnings.length
|
|
44
44
|
? html`<div class="alert alert-error" role="alert"><ul class="u-m0">${warnings.map((w) => html`<li>Line ${w.line}: ${w.message}</li>`)}</ul></div>`
|
|
45
45
|
: ''}
|
|
46
|
-
<pre class="source ddl" tabindex="0" aria-label="Generated SQL">${ddl}</pre>`)}
|
|
46
|
+
<pre class="source ddl" data-code-view="sql" tabindex="0" aria-label="Generated SQL">${ddl}</pre>`)}
|
|
47
47
|
</div>
|
|
48
48
|
<div class="u-spacer"></div>
|
|
49
49
|
${region('Save or run', html`<div class="form-grid"><div class="field"><label class="label" for="f_name">Script name</label>
|
|
@@ -237,7 +237,7 @@ const hiddenSpec = (spec: JobSpec, jobid: string | null) => html`<input type="hi
|
|
|
237
237
|
/** The review step of any change: the SQL, Apply (with the digest), Back. */
|
|
238
238
|
function reviewRegion(s: Session, stmts: string[], action: string, hidden: Raw, o: { back?: Raw | ''; apply?: string; danger?: boolean; extra?: Raw | '' } = {}) {
|
|
239
239
|
return html`${region('SQL', stmts.length
|
|
240
|
-
? html`<p class="muted u-mt0">These statements run in one transaction on pgkiln's owner connection when you apply them: all of them, or none.</p><pre class="source cron-sql" aria-label="Generated SQL">${stmts.join('\n\n')}</pre>`
|
|
240
|
+
? html`<p class="muted u-mt0">These statements run in one transaction on pgkiln's owner connection when you apply them: all of them, or none.</p><pre class="source cron-sql" data-code-view="sql" tabindex="0" aria-label="Generated SQL">${stmts.join('\n\n')}</pre>`
|
|
241
241
|
: html`<p class="muted u-mt0">Nothing to change.</p>`)}
|
|
242
242
|
<form method="post" action="${action}" class="cron-form">${csrf(s)}${hidden}<input type="hidden" name="reviewed" value="${reviewDigest(stmts)}">${o.extra ?? ''}
|
|
243
243
|
<div class="buttons">${stmts.length ? html`<button class="btn ${o.danger ? 'btn-danger' : 'btn-hot'}" name="mode" value="apply">${icon(o.danger ? 'trash' : 'check')} ${o.apply ?? 'Apply'}</button>` : ''}${o.back ?? ''}</div></form>`;
|
|
@@ -320,7 +320,7 @@ async function jobPage(reply: FastifyReply, s: Session, st: Extract<CronState, {
|
|
|
320
320
|
<dt>State</dt><dd>${job.active ? 'Active' : html`<span class="tag">inactive</span>`}</dd>
|
|
321
321
|
<dt>Schedule</dt><dd><code>${job.schedule}</code></dd>
|
|
322
322
|
<dt>Database</dt><dd>${job.database}</dd><dt>Run as</dt><dd>${job.username}</dd><dt>Job id</dt><dd>${job.jobid}</dd>
|
|
323
|
-
</dl><pre class="source cron-sql" aria-label="Command">${job.command}</pre>`)}
|
|
323
|
+
</dl><pre class="source cron-sql" data-code-view="sql" tabindex="0" aria-label="Command">${job.command}</pre>`)}
|
|
324
324
|
${region('Schedule', schedulePreview(job.schedule, st.timeZone))}
|
|
325
325
|
${region('Recent runs', html`${runsTable(runs, false)}<p class="small"><a href="${URL_BASE}/history?job=${job.jobid}">All runs of this job</a> · <a href="${URL_BASE}/history/purge?job=${job.jobid}">Purge its history</a></p>`)}`;
|
|
326
326
|
return send(reply, s, shell(s, name, crumbs([name]), main, 'sql'));
|
package/src/builder/scripts.ts
CHANGED
|
@@ -92,7 +92,7 @@ export function resultsHtml(results: StatementResult[]) {
|
|
|
92
92
|
(r) => html`<li class="script-result">
|
|
93
93
|
<div class="script-result-head">${statusTag(r)} <strong>#${r.n || '-'}</strong>
|
|
94
94
|
<span class="muted small">${r.line ? `line ${r.line}` : ''}${r.status === 'ok' ? ` · ${r.command ?? 'done'}${r.rows !== null && r.rows !== undefined ? `, ${r.rows} row(s)` : ''}` : ''}${r.status === 'ok' || r.status === 'error' ? ` · ${r.ms} ms` : ''}</span></div>
|
|
95
|
-
<details class="script-sql"><summary><code>${r.sql.length > 120 ? `${r.sql.slice(0, 120).replace(/\s+/g, ' ')}…` : r.sql.replace(/\s+/g, ' ')}</code></summary><pre class="source">${r.sql}</pre></details>
|
|
95
|
+
<details class="script-sql"><summary><code>${r.sql.length > 120 ? `${r.sql.slice(0, 120).replace(/\s+/g, ' ')}…` : r.sql.replace(/\s+/g, ' ')}</code></summary><pre class="source" data-code-view="sql" tabindex="0" aria-label="Statement">${r.sql}</pre></details>
|
|
96
96
|
${r.error ? html`<div class="alert ${r.status === 'error' ? 'alert-error' : 'alert-note'}">${r.error}</div>` : ''}
|
|
97
97
|
${r.columns?.length
|
|
98
98
|
? html`<div class="table-wrap"><table class="report"><thead><tr>${r.columns.map((c) => html`<th>${c}</th>`)}</tr></thead>
|
|
@@ -130,7 +130,7 @@ export async function sqlChartRoutes(app: FastifyInstance) {
|
|
|
130
130
|
<div class="field"><label class="label" for="ch-name">Name</label><input id="ch-name" name="name" value="${ch.name}" maxlength="120" required></div>
|
|
131
131
|
${controls}
|
|
132
132
|
<label class="check"><input type="checkbox" name="on_dashboard"${ch.on_dashboard ? raw(' checked') : ''}> On the Charts dashboard</label>
|
|
133
|
-
<details class="u-mt1"><summary>The query</summary><pre class="source">${ch.sql}</pre></details>
|
|
133
|
+
<details class="u-mt1"><summary>The query</summary><pre class="source" data-code-view="sql" tabindex="0" aria-label="The query">${ch.sql}</pre></details>
|
|
134
134
|
<div class="buttons"><button class="btn btn-hot" name="action" value="save">${icon('save')} Save</button>
|
|
135
135
|
<button class="btn" name="action" value="worksheet">${icon('code')} Open in the worksheet</button>
|
|
136
136
|
<button class="btn btn-danger" name="action" value="delete" data-confirm="Delete the chart ${ch.name}?">${icon('trash')} Delete</button></div>
|
|
@@ -369,7 +369,7 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
369
369
|
const main = html`<h1 class="u-mb1">Statement</h1>
|
|
370
370
|
<div class="stat-grid u-mb1">${tile('Calls', num(calls))}${tile('Total ms', num(total))}${tile('Mean ms', num(calls ? total / calls : 0))}${tile('Rows', num(sum('rows')))}
|
|
371
371
|
${tile('Blocks hit', num(sum('hit')))}${tile('Blocks read', num(sum('read')))}${tile('Temp blocks written', num(sum('temp_written')))}</div>
|
|
372
|
-
${region('Text', html`<pre class="source u-break sm-sql">${sql}</pre>
|
|
372
|
+
${region('Text', html`<pre class="source u-break sm-sql" data-code-view="sql" tabindex="0" aria-label="SQL text">${sql}</pre>
|
|
373
373
|
<div class="buttons"><form method="post" action="${URL_BASE}/statements/${queryid}/worksheet">${csrf(s)}<button class="btn">${icon('code')} Open in worksheet</button></form>
|
|
374
374
|
<a class="btn" href="${URL_BASE}/top">Back to Top SQL</a></div>`)}
|
|
375
375
|
${region('By role', html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>Role</th><th class="num">Calls</th><th class="num">Total ms</th><th class="num">Mean ms</th><th class="num">Std dev ms</th><th class="num">Min ms</th><th class="num">Max ms</th><th class="num">Rows</th><th class="num">Hit</th><th class="num">Read</th><th class="num">Temp read</th><th class="num">Temp written</th></tr></thead>
|
|
@@ -121,7 +121,7 @@ function form(s: Session, def: TableDef, opts: { editing: TableDef | null; schem
|
|
|
121
121
|
<div class="field"><label class="label" for="te-part_by">Partition by</label><select id="te-part_by" name="part_by"><option value="">Not partitioned</option>${(['range', 'list', 'hash'] as const).map((p) => html`<option value="${p}"${sel(p, def.partitionBy)}>${p}</option>`)}</select></div>
|
|
122
122
|
<div class="field"><label class="label" for="te-part_key">Key</label>${input('part_key', def.partitionKey, 'Key', 'class="code" placeholder="created_at"')}</div></div>
|
|
123
123
|
<small class="help">The partitions themselves are tables made with CREATE TABLE … PARTITION OF in the worksheet.</small></fieldset>`}`],
|
|
124
|
-
['ddl', 'DDL', opts.ddl.length ? html`<pre class="source te-ddl">${opts.ddl.join('\n\n')}</pre>` : html`<p class="muted">${editing ? 'No changes yet: the table is as the form shows it.' : 'Fill in the table and press Preview DDL.'}</p>`],
|
|
124
|
+
['ddl', 'DDL', opts.ddl.length ? html`<pre class="source te-ddl" data-code-view="sql" tabindex="0" aria-label="DDL">${opts.ddl.join('\n\n')}</pre>` : html`<p class="muted">${editing ? 'No changes yet: the table is as the form shows it.' : 'Fill in the table and press Preview DDL.'}</p>`],
|
|
125
125
|
['output', 'Output', opts.output || html`<p class="muted">Apply shows what happened here.</p>`],
|
|
126
126
|
];
|
|
127
127
|
const tab = sections.some(([k]) => k === opts.tab) ? opts.tab : opts.errors.length ? 'columns' : 'columns';
|
|
@@ -203,7 +203,7 @@ async function page(req: Req, reply: FastifyReply) {
|
|
|
203
203
|
const now = await readTable(db, `${pgIdent(def.schema)}.${pgIdent(def.name)}`);
|
|
204
204
|
return back(reply, s, `${BASE}/sql/objects/table?${new URLSearchParams({ o: now ? `${pgIdent(now.schema)}.${pgIdent(now.name)}` : '' })}`);
|
|
205
205
|
}
|
|
206
|
-
output = html`<div class="alert alert-error" role="alert"><p><strong>Nothing was changed</strong>: the statements ran in one transaction, which was rolled back.</p><p>${failed}</p><pre class="source">${at}</pre></div>`;
|
|
206
|
+
output = html`<div class="alert alert-error" role="alert"><p><strong>Nothing was changed</strong>: the statements ran in one transaction, which was rolled back.</p><p>${failed}</p><pre class="source" data-code-view="sql" tabindex="0" aria-label="Failed statement">${at}</pre></div>`;
|
|
207
207
|
tab = 'output';
|
|
208
208
|
}
|
|
209
209
|
if (errors.length) tab = 'columns';
|