pgkiln 0.48.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 +3 -2
- package/db/migrations/116_dump_jobs.sql +41 -0
- package/db/migrations/117_rest_handlers.sql +63 -0
- package/db/migrations/118_change_log.sql +54 -0
- package/docs/apex-feature-parity.md +9 -9
- package/docs/guide/01-installation.md +7 -0
- package/docs/guide/03-builder.md +227 -10
- package/docs/guide/08-security.md +11 -0
- package/docs/guide/12-development.md +64 -13
- package/docs/guide/13-rest-api.md +59 -1
- package/docs/guide/15-extensions.md +1 -1
- package/examples/hr/hr_62_rest_module.sql +39 -0
- package/package.json +1 -1
- package/public/app.css +1 -1
- package/public/builder-icons.svg +2 -0
- package/public/builder.css +478 -173
- package/public/builder.js +48 -0
- package/public/code-editor.css +32 -79
- package/public/code-editor.js +58 -3
- package/public/data-modeler.css +17 -14
- package/public/sql-monitor.js +73 -0
- package/public/worksheet.css +14 -7
- package/public/worksheet.js +119 -3
- package/src/app.ts +6 -0
- package/src/automations.ts +6 -0
- package/src/binds.ts +2 -1
- package/src/builder/advisor.ts +24 -2
- package/src/builder/autorest.ts +3 -2
- package/src/builder/changelog.ts +240 -0
- package/src/builder/components.ts +1 -1
- package/src/builder/data-modeler.ts +1 -1
- package/src/builder/datapump.ts +364 -0
- package/src/builder/db-users.ts +2 -2
- package/src/builder/ddl-tools.ts +13 -4
- package/src/builder/find-workspace.ts +2 -2
- package/src/builder/object-browser.ts +4 -4
- package/src/builder/plan-view.ts +25 -0
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +629 -0
- package/src/builder/rest.ts +1 -0
- package/src/builder/routes.ts +4 -2
- package/src/builder/scheduling.ts +495 -0
- package/src/builder/scripts.ts +1 -1
- package/src/builder/sql-charts.ts +1 -1
- package/src/builder/sql-monitor.ts +397 -0
- package/src/builder/sql-tools.ts +6 -2
- package/src/builder/sql-utilities.ts +7 -39
- package/src/builder/sql.ts +6 -0
- package/src/builder/table-editor.ts +6 -3
- package/src/builder/top-sql.ts +24 -6
- package/src/builder/ui.ts +6 -4
- package/src/builder/worksheet.ts +173 -41
- package/src/changelog.ts +109 -0
- package/src/cron-schedule.ts +282 -0
- package/src/datapump.ts +546 -0
- package/src/dumpscript.ts +407 -0
- package/src/pgcron.ts +225 -0
- package/src/runtime/rest-handlers.ts +75 -0
- package/src/runtime/rest.ts +116 -22
- package/src/session.ts +5 -0
- package/src/sqlworksheet.ts +9 -6
- package/src/worksheet-txn.ts +270 -0
package/public/builder.js
CHANGED
|
@@ -1234,3 +1234,51 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
|
|
|
1234
1234
|
// the button is for pages without JavaScript
|
|
1235
1235
|
picker.form.querySelector('button')?.setAttribute('hidden', '');
|
|
1236
1236
|
})();
|
|
1237
|
+
|
|
1238
|
+
// (sprint 101, DA-12) SQL Workshop › Scheduling: the schedule builder's five fields and the common schedules fill the
|
|
1239
|
+
// schedule; the preview (in words, next five runs) follows as you type. Without JavaScript the Use / Preview buttons
|
|
1240
|
+
// post the form and it comes back with the same.
|
|
1241
|
+
(() => {
|
|
1242
|
+
const form = document.querySelector('form[data-cron-form]');
|
|
1243
|
+
const input = form?.querySelector('[data-cron-schedule]');
|
|
1244
|
+
const preview = form?.querySelector('[data-cron-preview]');
|
|
1245
|
+
if (!form || !input || !preview) return;
|
|
1246
|
+
const fields = [...form.querySelectorAll('[data-cron-field]')];
|
|
1247
|
+
let timer = 0;
|
|
1248
|
+
let seq = 0;
|
|
1249
|
+
const refresh = () => {
|
|
1250
|
+
clearTimeout(timer);
|
|
1251
|
+
timer = setTimeout(async () => {
|
|
1252
|
+
const mine = ++seq;
|
|
1253
|
+
try {
|
|
1254
|
+
const res = await fetch(`${preview.dataset.cronPreview}?schedule=${encodeURIComponent(input.value)}`, { headers: { accept: 'text/html' }, credentials: 'same-origin' });
|
|
1255
|
+
if (!res.ok || mine !== seq) return;
|
|
1256
|
+
const doc = new DOMParser().parseFromString(`<div>${await res.text()}</div>`, 'text/html');
|
|
1257
|
+
preview.replaceChildren(...doc.body.firstElementChild.childNodes);
|
|
1258
|
+
} catch {
|
|
1259
|
+
/* the Preview button still works */
|
|
1260
|
+
}
|
|
1261
|
+
}, 250);
|
|
1262
|
+
};
|
|
1263
|
+
const split = () => {
|
|
1264
|
+
const parts = input.value.trim().split(/\s+/);
|
|
1265
|
+
if (parts.length === 5) fields.forEach((f, i) => (f.value = parts[i] === '*' ? '' : parts[i]));
|
|
1266
|
+
};
|
|
1267
|
+
input.addEventListener('input', () => {
|
|
1268
|
+
split();
|
|
1269
|
+
refresh();
|
|
1270
|
+
});
|
|
1271
|
+
for (const f of fields) {
|
|
1272
|
+
f.addEventListener('input', () => {
|
|
1273
|
+
input.value = fields.map((x) => x.value.trim() || '*').join(' ');
|
|
1274
|
+
refresh();
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
form.querySelector('[data-cron-preset]')?.addEventListener('change', (e) => {
|
|
1278
|
+
if (!e.target.value) return;
|
|
1279
|
+
input.value = e.target.value;
|
|
1280
|
+
split();
|
|
1281
|
+
refresh();
|
|
1282
|
+
});
|
|
1283
|
+
for (const b of form.querySelectorAll('[data-cron-nojs]')) b.hidden = true;
|
|
1284
|
+
})();
|
package/public/code-editor.css
CHANGED
|
@@ -2,79 +2,23 @@
|
|
|
2
2
|
transparent and lies exactly over the highlighted copy (.ce-code), so both
|
|
3
3
|
must share font, size, line height, padding and tab size. */
|
|
4
4
|
:root {
|
|
5
|
-
--ce-bg
|
|
6
|
-
--ce-gutter: #f1f3f6;
|
|
7
|
-
--ce-gutter-text: #5d6876;
|
|
8
|
-
--ce-text: #1b2330;
|
|
9
|
-
--ce-kw: #8b2bb9;
|
|
10
|
-
--ce-type: #0b6e8a;
|
|
11
|
-
--ce-fn: #1d5fc4;
|
|
12
|
-
--ce-str: #1d7a3a;
|
|
13
|
-
--ce-num: #b35400;
|
|
14
|
-
--ce-com: #6b7685;
|
|
15
|
-
--ce-bind: #b0306a;
|
|
16
|
-
--ce-qid: #7a5b00;
|
|
17
|
-
--ce-tag: #1d5fc4;
|
|
18
|
-
--ce-attr: #8b2bb9;
|
|
19
|
-
--ce-err: #c23028;
|
|
20
|
-
--ce-match: rgb(11 99 197 / 18%);
|
|
21
|
-
--ce-select: rgb(11 99 197 / 22%);
|
|
5
|
+
/* the colours (--ce-bg, --ce-kw, …, --ce-line, --ce-select) are tokens in builder.css, light and dark */
|
|
22
6
|
--ce-font: 0.84rem;
|
|
23
7
|
--ce-lh: 20px; /* whole pixels: fractional line heights drift apart over thousands of lines */
|
|
24
8
|
}
|
|
25
|
-
@media (prefers-color-scheme: dark) {
|
|
26
|
-
:root:not([data-theme="light"]) {
|
|
27
|
-
--ce-bg: #12161d;
|
|
28
|
-
--ce-gutter: #171c24;
|
|
29
|
-
--ce-gutter-text: #8f99a8;
|
|
30
|
-
--ce-text: #e3e7ed;
|
|
31
|
-
--ce-kw: #d39bff;
|
|
32
|
-
--ce-type: #5fd0e6;
|
|
33
|
-
--ce-fn: #82b6ff;
|
|
34
|
-
--ce-str: #8fd694;
|
|
35
|
-
--ce-num: #ffb46b;
|
|
36
|
-
--ce-com: #8591a3;
|
|
37
|
-
--ce-bind: #ff8cc0;
|
|
38
|
-
--ce-qid: #e8c66a;
|
|
39
|
-
--ce-tag: #82b6ff;
|
|
40
|
-
--ce-attr: #d39bff;
|
|
41
|
-
--ce-err: #ff7a70;
|
|
42
|
-
--ce-match: rgb(105 164 255 / 25%);
|
|
43
|
-
--ce-select: rgb(105 164 255 / 30%);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
:root[data-theme="dark"] {
|
|
47
|
-
--ce-bg: #12161d;
|
|
48
|
-
--ce-gutter: #171c24;
|
|
49
|
-
--ce-gutter-text: #8f99a8;
|
|
50
|
-
--ce-text: #e3e7ed;
|
|
51
|
-
--ce-kw: #d39bff;
|
|
52
|
-
--ce-type: #5fd0e6;
|
|
53
|
-
--ce-fn: #82b6ff;
|
|
54
|
-
--ce-str: #8fd694;
|
|
55
|
-
--ce-num: #ffb46b;
|
|
56
|
-
--ce-com: #8591a3;
|
|
57
|
-
--ce-bind: #ff8cc0;
|
|
58
|
-
--ce-qid: #e8c66a;
|
|
59
|
-
--ce-tag: #82b6ff;
|
|
60
|
-
--ce-attr: #d39bff;
|
|
61
|
-
--ce-err: #ff7a70;
|
|
62
|
-
--ce-match: rgb(105 164 255 / 25%);
|
|
63
|
-
--ce-select: rgb(105 164 255 / 30%);
|
|
64
|
-
}
|
|
65
9
|
@media (pointer: coarse) {
|
|
66
10
|
:root { --ce-font: 16px; --ce-lh: 24px; } /* the same as other fields: no zoom on focus in iOS */
|
|
67
11
|
}
|
|
68
12
|
|
|
69
13
|
.ce {
|
|
70
14
|
position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr);
|
|
71
|
-
min-width: 0; max-width: 100%; border: 1px solid var(--border); border-radius: 6px; background: var(--ce-bg);
|
|
15
|
+
min-width: 0; max-width: 100%; border: 1px solid var(--field-border, var(--border)); border-radius: var(--radius-sm, 6px); background: var(--ce-bg);
|
|
72
16
|
}
|
|
73
|
-
.ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0
|
|
17
|
+
.ce:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring, transparent); }
|
|
74
18
|
.ce-gutter {
|
|
75
19
|
--ce-digits: 2;
|
|
76
20
|
width: calc(var(--ce-digits) * 1ch + 1.1rem); overflow: hidden; position: relative;
|
|
77
|
-
background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: 6px 0 0 0;
|
|
21
|
+
background: var(--ce-gutter); border-right: 1px solid var(--border); border-radius: var(--radius-sm, 6px) 0 0 0;
|
|
78
22
|
color: var(--ce-gutter-text); user-select: none;
|
|
79
23
|
}
|
|
80
24
|
.ce-gutter, .ce .ce-code, .ce textarea.ce-input {
|
|
@@ -82,6 +26,11 @@
|
|
|
82
26
|
font-variant-ligatures: none; letter-spacing: 0; font-kerning: none;
|
|
83
27
|
}
|
|
84
28
|
.ce-nums { position: absolute; top: 0; left: 0; right: 0; white-space: pre; text-align: right; padding: 0.5rem 0.45rem 0 0; will-change: transform; }
|
|
29
|
+
/* the caret's line (sprint 108): a band behind the text and its number in the gutter, while the editor has the focus */
|
|
30
|
+
.ce-cur, .ce-cur-num { position: absolute; left: 0; right: 0; top: 0; height: var(--ce-lh); will-change: transform; display: none; }
|
|
31
|
+
.ce-cur { background: var(--ce-line); }
|
|
32
|
+
.ce-cur-num { padding-right: 0.45rem; text-align: right; color: var(--ce-gutter-active); background: var(--ce-gutter); font-weight: 650; }
|
|
33
|
+
.ce:focus-within :is(.ce-cur, .ce-cur-num):not([hidden]) { display: block; }
|
|
85
34
|
.ce-main { position: relative; min-width: 0; }
|
|
86
35
|
.ce .ce-hl { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
|
87
36
|
.ce .ce-code {
|
|
@@ -99,29 +48,33 @@
|
|
|
99
48
|
.ce textarea.ce-input:focus-visible { outline: none; }
|
|
100
49
|
.ce textarea.sql-editor { min-height: 18rem; }
|
|
101
50
|
|
|
102
|
-
/* tokens */
|
|
103
|
-
.ce-code .t-kw { color: var(--ce-kw); font-weight: 600; }
|
|
104
|
-
.ce-code .t-type { color: var(--ce-type); }
|
|
105
|
-
.ce-code .t-fn { color: var(--ce-fn); }
|
|
106
|
-
.ce-code .t-str { color: var(--ce-str); }
|
|
107
|
-
.ce-code .t-key { color: var(--ce-fn); }
|
|
108
|
-
.ce-code .t-num { color: var(--ce-num); }
|
|
109
|
-
.ce-code .t-com { color: var(--ce-com); font-style: italic; }
|
|
110
|
-
.ce-code .t-bind, .ce-code .t-sub { color: var(--ce-bind); font-weight: 600; }
|
|
111
|
-
.ce-code .t-qid { color: var(--ce-qid); }
|
|
112
|
-
.ce-code .t-dol { color: var(--ce-str); font-weight: 600; }
|
|
113
|
-
.ce-code .t-tag { color: var(--ce-tag); }
|
|
114
|
-
.ce-code .t-attr { color: var(--ce-attr); }
|
|
115
|
-
.ce-code .t-ent { color: var(--ce-num); }
|
|
116
|
-
.ce-code .t-err { color: var(--ce-err); text-decoration: wavy underline; }
|
|
117
|
-
.ce-code
|
|
118
|
-
.ce-code .t-
|
|
51
|
+
/* tokens: in the editor and in read-only code (<pre data-code-view>, sprint 108) */
|
|
52
|
+
:is(.ce-code, .ce-view) .t-kw { color: var(--ce-kw); font-weight: 600; }
|
|
53
|
+
:is(.ce-code, .ce-view) .t-type { color: var(--ce-type); }
|
|
54
|
+
:is(.ce-code, .ce-view) .t-fn { color: var(--ce-fn); }
|
|
55
|
+
:is(.ce-code, .ce-view) .t-str { color: var(--ce-str); }
|
|
56
|
+
:is(.ce-code, .ce-view) .t-key { color: var(--ce-fn); }
|
|
57
|
+
:is(.ce-code, .ce-view) .t-num { color: var(--ce-num); }
|
|
58
|
+
:is(.ce-code, .ce-view) .t-com { color: var(--ce-com); font-style: italic; }
|
|
59
|
+
:is(.ce-code, .ce-view) .t-bind, :is(.ce-code, .ce-view) .t-sub { color: var(--ce-bind); font-weight: 600; }
|
|
60
|
+
:is(.ce-code, .ce-view) .t-qid { color: var(--ce-qid); }
|
|
61
|
+
:is(.ce-code, .ce-view) .t-dol { color: var(--ce-str); font-weight: 600; }
|
|
62
|
+
:is(.ce-code, .ce-view) .t-tag { color: var(--ce-tag); }
|
|
63
|
+
:is(.ce-code, .ce-view) .t-attr { color: var(--ce-attr); }
|
|
64
|
+
:is(.ce-code, .ce-view) .t-ent { color: var(--ce-num); }
|
|
65
|
+
:is(.ce-code, .ce-view) .t-err { color: var(--ce-err); text-decoration: wavy underline; }
|
|
66
|
+
:is(.ce-code, .ce-view) .t-op { color: var(--ce-op); }
|
|
67
|
+
:is(.ce-code, .ce-view) .t-pun, :is(.ce-code, .ce-view) .t-br, :is(.ce-code, .ce-view) .t-id { color: var(--ce-text); }
|
|
68
|
+
.ce-code .t-match { background: var(--ce-match); border-radius: 2px; outline: 1px solid var(--ce-match-ring); }
|
|
69
|
+
|
|
70
|
+
pre.ce-view { background: var(--ce-bg); color: var(--ce-text); border-color: var(--border); }
|
|
71
|
+
pre.ce-view::selection, pre.ce-view ::selection { background: var(--ce-select); }
|
|
119
72
|
|
|
120
73
|
/* status bar: language, hint, caret position, check */
|
|
121
74
|
.ce-bar {
|
|
122
75
|
grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem 0.75rem;
|
|
123
76
|
padding: 0.2rem 0.5rem; border-top: 1px solid var(--border); background: var(--ce-gutter);
|
|
124
|
-
border-radius: 0 0 6px 6px; font-size: 0.75rem; color: var(--muted); min-width: 0;
|
|
77
|
+
border-radius: 0 0 var(--radius-sm, 6px) var(--radius-sm, 6px); font-size: 0.75rem; color: var(--muted); min-width: 0;
|
|
125
78
|
}
|
|
126
79
|
.ce-lang { font-weight: 650; letter-spacing: 0.02em; }
|
|
127
80
|
.ce-hint { flex: 1 1 12rem; min-width: 0; }
|
|
@@ -138,7 +91,7 @@
|
|
|
138
91
|
position: absolute; z-index: 30; left: 0; top: 0; margin: 0; padding: 0.25rem 0; list-style: none;
|
|
139
92
|
width: max-content; min-width: 14rem; max-width: min(34rem, calc(100% - 0.5rem)); max-height: 15rem; overflow: auto;
|
|
140
93
|
background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
|
|
141
|
-
box-shadow: 0 8px 24px rgb(16 24 40 / 18%); font-size: 0.82rem;
|
|
94
|
+
box-shadow: var(--shadow-lg, 0 8px 24px rgb(16 24 40 / 18%)); font-size: 0.82rem;
|
|
142
95
|
}
|
|
143
96
|
.ce-opt { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.2rem 0.6rem 0.2rem 0.4rem; cursor: pointer; white-space: nowrap; }
|
|
144
97
|
.ce-opt::before {
|
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)); }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// pgkiln builder: the SQL Workshop's SQL Monitor (sprint 102, src/builder/sql-monitor.ts).
|
|
2
|
+
//
|
|
3
|
+
// Progressive enhancement of the Sessions view: the live part ([data-sm-live]: tiles, sessions, wait events,
|
|
4
|
+
// progress) is fetched again (?fragment=1) every 5, 15 or 60 seconds and swapped in place, with a Pause button.
|
|
5
|
+
// The interval and the pause are remembered in localStorage (pgkiln.sqlMonitor). No refresh while the tab is
|
|
6
|
+
// hidden or a session's confirmation is open; a failed or redirected fetch (signed out) stops it. Without
|
|
7
|
+
// JavaScript the page's Refresh link reloads it.
|
|
8
|
+
(() => {
|
|
9
|
+
'use strict';
|
|
10
|
+
const live = document.querySelector('[data-sm-live]');
|
|
11
|
+
const controls = document.querySelector('[data-sm-controls]');
|
|
12
|
+
if (!live || !controls) return;
|
|
13
|
+
const select = controls.querySelector('[data-sm-interval]');
|
|
14
|
+
const pause = controls.querySelector('[data-sm-pause]');
|
|
15
|
+
const KEY = 'pgkiln.sqlMonitor';
|
|
16
|
+
const load = () => { try { return JSON.parse(localStorage.getItem(KEY) || 'null') || {}; } catch { return {}; } };
|
|
17
|
+
const save = (v) => { try { localStorage.setItem(KEY, JSON.stringify(v)); } catch { /* private mode */ } };
|
|
18
|
+
const pref = load();
|
|
19
|
+
const allowed = [...select.options].map((o) => Number(o.value));
|
|
20
|
+
let seconds = allowed.includes(Number(pref.seconds)) ? Number(pref.seconds) : allowed[0];
|
|
21
|
+
let paused = pref.paused === true;
|
|
22
|
+
let timer = 0;
|
|
23
|
+
let busy = false;
|
|
24
|
+
select.value = String(seconds);
|
|
25
|
+
controls.hidden = false;
|
|
26
|
+
|
|
27
|
+
const show = () => {
|
|
28
|
+
pause.setAttribute('aria-pressed', paused ? 'true' : 'false');
|
|
29
|
+
pause.textContent = paused ? 'Resume' : 'Pause';
|
|
30
|
+
live.classList.toggle('is-paused', paused);
|
|
31
|
+
};
|
|
32
|
+
const schedule = () => {
|
|
33
|
+
window.clearTimeout(timer);
|
|
34
|
+
if (!paused) timer = window.setTimeout(refresh, seconds * 1000);
|
|
35
|
+
};
|
|
36
|
+
async function refresh() {
|
|
37
|
+
if (busy || paused) return;
|
|
38
|
+
if (document.hidden) return schedule();
|
|
39
|
+
busy = true;
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(live.dataset.smLive, { credentials: 'same-origin', headers: { accept: 'text/html' }, cache: 'no-store' });
|
|
42
|
+
if (!res.ok || res.redirected) throw new Error(String(res.status));
|
|
43
|
+
const text = await res.text();
|
|
44
|
+
// keep focus on a button that survives the swap (by its label)
|
|
45
|
+
const focused = document.activeElement && live.contains(document.activeElement) ? document.activeElement.getAttribute('aria-label') : null;
|
|
46
|
+
live.innerHTML = text;
|
|
47
|
+
if (focused) live.querySelector(`[aria-label="${CSS.escape(focused)}"]`)?.focus();
|
|
48
|
+
busy = false;
|
|
49
|
+
schedule();
|
|
50
|
+
} catch {
|
|
51
|
+
busy = false;
|
|
52
|
+
paused = true;
|
|
53
|
+
show();
|
|
54
|
+
const note = live.querySelector('[data-sm-updated]');
|
|
55
|
+
if (note) note.textContent = 'Refresh stopped: the monitor could not be reached (signed out?). Reload the page.';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
select.addEventListener('change', () => {
|
|
59
|
+
seconds = Number(select.value);
|
|
60
|
+
save({ seconds, paused });
|
|
61
|
+
schedule();
|
|
62
|
+
});
|
|
63
|
+
pause.addEventListener('click', () => {
|
|
64
|
+
paused = !paused;
|
|
65
|
+
save({ seconds, paused });
|
|
66
|
+
show();
|
|
67
|
+
if (paused) window.clearTimeout(timer);
|
|
68
|
+
else refresh();
|
|
69
|
+
});
|
|
70
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden && !paused) refresh(); });
|
|
71
|
+
show();
|
|
72
|
+
schedule();
|
|
73
|
+
})();
|
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; }
|