pgkiln 0.82.0 → 0.84.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/db/migrations/153_component_events.sql +21 -0
- package/db/migrations/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +4 -3
- package/docs/guide/03-builder.md +10 -1
- package/docs/guide/04-pages-and-regions.md +44 -1
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +28 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +3 -1
- package/docs/guide/12-development.md +16 -1
- package/package.json +1 -1
- package/public/app.css +31 -0
- package/public/app.js +163 -4
- package/public/builder.css +25 -2
- package/public/data-modeler.js +8 -5
- package/public/sql-monitor.js +5 -2
- package/public/sw.js +5 -1
- package/src/app.ts +2 -0
- package/src/builder/ai-builder.ts +50 -46
- package/src/builder/api.ts +49 -49
- package/src/builder/autorest.ts +92 -87
- package/src/builder/changelog.ts +39 -37
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +13 -8
- package/src/builder/data-modeler.ts +89 -84
- package/src/builder/data-security.ts +39 -37
- package/src/builder/dataload.ts +69 -65
- package/src/builder/datapump.ts +108 -97
- package/src/builder/ddl-tools.ts +38 -34
- package/src/builder/document-view.ts +11 -11
- package/src/builder/i18n/de.ts +1635 -0
- package/src/builder/i18n/en.ts +1635 -0
- package/src/builder/i18n/es.ts +1635 -0
- package/src/builder/i18n/fr.ts +1635 -0
- package/src/builder/i18n/it.ts +1635 -0
- package/src/builder/i18n/ja.ts +1635 -0
- package/src/builder/i18n/ko.ts +1635 -0
- package/src/builder/i18n/nl.ts +1635 -0
- package/src/builder/i18n/pt.ts +1635 -0
- package/src/builder/i18n/zh-cn.ts +1635 -0
- package/src/builder/i18n/zh-tw.ts +1635 -0
- package/src/builder/i18n.ts +18 -0
- package/src/builder/notification-settings.ts +27 -0
- package/src/builder/object-actions.ts +18 -16
- package/src/builder/object-browser.ts +2 -2
- package/src/builder/querybuilder.ts +42 -36
- package/src/builder/quicksql.ts +16 -18
- package/src/builder/region-settings.ts +201 -199
- package/src/builder/report-settings.ts +64 -60
- package/src/builder/rest-modules.ts +145 -143
- package/src/builder/routes.ts +3 -0
- package/src/builder/sampledata.ts +59 -52
- package/src/builder/scheduling.ts +180 -159
- package/src/builder/scripts.ts +71 -65
- package/src/builder/sql-charts.ts +36 -30
- package/src/builder/sql-monitor.ts +95 -89
- package/src/builder/sql-utilities.ts +47 -40
- package/src/builder/top-sql.ts +21 -17
- package/src/builder/unload.ts +48 -44
- package/src/builder/worksheet.ts +3 -3
- package/src/cli/replace.ts +2 -0
- package/src/cron-schedule.ts +47 -17
- package/src/i18n/ar.ts +9 -0
- package/src/i18n/cs.ts +9 -0
- package/src/i18n/da.ts +9 -0
- package/src/i18n/de.ts +9 -0
- package/src/i18n/el.ts +9 -0
- package/src/i18n/es.ts +9 -0
- package/src/i18n/fi.ts +9 -0
- package/src/i18n/fr.ts +9 -0
- package/src/i18n/he.ts +9 -0
- package/src/i18n/it.ts +9 -0
- package/src/i18n/ja.ts +9 -0
- package/src/i18n/ko.ts +9 -0
- package/src/i18n/nb.ts +9 -0
- package/src/i18n/pl.ts +9 -0
- package/src/i18n/pt.ts +9 -0
- package/src/i18n/ru.ts +9 -0
- package/src/i18n/sv.ts +9 -0
- package/src/i18n/tr.ts +9 -0
- package/src/i18n/uk.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/i18n.ts +18 -0
- package/src/metadata.ts +11 -3
- package/src/runtime/calendar.ts +3 -2
- package/src/runtime/context.ts +2 -0
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/grid.ts +1 -0
- package/src/runtime/lists.ts +43 -11
- package/src/runtime/maps.ts +7 -4
- package/src/runtime/notifications.ts +237 -0
- package/src/runtime/processes.ts +45 -1
- package/src/runtime/regions.ts +11 -15
- package/src/runtime/render.ts +18 -4
- package/src/runtime/routes.ts +16 -4
- package/src/runtime/signin.ts +2 -1
- package/src/runtime/tree.ts +4 -3
- package/src/session.ts +13 -4
|
@@ -9,6 +9,7 @@ import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs,
|
|
|
9
9
|
import { isAdmin } from './locks.ts';
|
|
10
10
|
import { planView } from './plan-view.ts';
|
|
11
11
|
import { statementsNote, statementsState } from './top-sql.ts';
|
|
12
|
+
import { tr, type BuilderKey, type BuilderTranslate } from './i18n.ts';
|
|
12
13
|
|
|
13
14
|
// (sprint 102, Database Actions DA-14) SQL Workshop › SQL Monitor, for administrators: Database Actions' Real Time
|
|
14
15
|
// SQL Monitor and Performance Hub on PostgreSQL's statistics views.
|
|
@@ -37,7 +38,7 @@ async function readOnly<T>(fn: (q: (sql: string, params?: unknown[]) => Promise<
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const crumbs = (...more: [string, string?][]): [string, string?][] => [['
|
|
41
|
+
const crumbs = (tx: BuilderTranslate, ...more: [string, string?][]): [string, string?][] => [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.monitor'), more.length ? URL_BASE : undefined], ...more];
|
|
41
42
|
const num = (v: unknown) => (v == null ? '' : Number(v).toLocaleString('en-US', { maximumFractionDigits: 2 }));
|
|
42
43
|
const QUERYID_RE = /^-?\d{1,20}$/;
|
|
43
44
|
|
|
@@ -45,10 +46,10 @@ const QUERYID_RE = /^-?\d{1,20}$/;
|
|
|
45
46
|
const bar = (value: number, max: number) =>
|
|
46
47
|
html`<svg class="sm-bar" viewBox="0 0 100 8" preserveAspectRatio="none" aria-hidden="true" focusable="false"><rect class="sm-bar-track" width="100" height="8"></rect><rect class="sm-bar-fill" width="${max > 0 ? Math.max(1, Math.round((value / max) * 100)) : 0}" height="8"></rect></svg>`;
|
|
47
48
|
|
|
48
|
-
const viewTabs = (active: 'sessions' | 'top') =>
|
|
49
|
-
html`<nav class="seg u-mb1" aria-label="
|
|
50
|
-
<a href="${URL_BASE}"${active === 'sessions' ? raw(' aria-current="page"') : ''}
|
|
51
|
-
<a href="${URL_BASE}/top"${active === 'top' ? raw(' aria-current="page"') : ''}
|
|
49
|
+
const viewTabs = (tx: BuilderTranslate, active: 'sessions' | 'top') =>
|
|
50
|
+
html`<nav class="seg u-mb1" aria-label="${tx('sm.views')}">
|
|
51
|
+
<a href="${URL_BASE}"${active === 'sessions' ? raw(' aria-current="page"') : ''}>${tx('sm.sessions')}</a>
|
|
52
|
+
<a href="${URL_BASE}/top"${active === 'top' ? raw(' aria-current="page"') : ''}>${tx('sql.tool.topsql')}</a></nav>`;
|
|
52
53
|
|
|
53
54
|
// ------------------------------------------------------------------ sessions
|
|
54
55
|
|
|
@@ -104,57 +105,58 @@ async function liveData(showIdle: boolean) {
|
|
|
104
105
|
|
|
105
106
|
/** The part of the Sessions view that public/sql-monitor.js reloads in place. */
|
|
106
107
|
async function livePart(s: Session, showIdle: boolean): Promise<Raw> {
|
|
108
|
+
const tx = tr(s);
|
|
107
109
|
const d = await liveData(showIdle);
|
|
108
110
|
const t = d.totals as Record<string, number>;
|
|
109
111
|
const backTo = `${URL_BASE}${showIdle ? '?idle=1' : ''}`;
|
|
110
112
|
const action = (pid: unknown, kind: 'cancel' | 'terminate') => html`<form method="post" action="${URL_BASE}/session" class="sm-action">${csrf(s)}
|
|
111
113
|
<input type="hidden" name="pid" value="${String(pid)}"><input type="hidden" name="action" value="${kind}"><input type="hidden" name="idle" value="${showIdle ? '1' : ''}">
|
|
112
|
-
<button class="btn btn-sm${kind === 'terminate' ? ' btn-danger' : ''}" data-confirm="${kind === 'cancel' ?
|
|
113
|
-
aria-label="${kind === 'cancel' ? '
|
|
114
|
+
<button class="btn btn-sm${kind === 'terminate' ? ' btn-danger' : ''}" data-confirm="${kind === 'cancel' ? tx('sm.cancel_confirm', { pid: String(pid) }) : tx('sm.terminate_confirm', { pid: String(pid) })}"
|
|
115
|
+
aria-label="${kind === 'cancel' ? tx('sm.cancel_label', { pid: String(pid) }) : tx('sm.terminate_label', { pid: String(pid) })}">${kind === 'cancel' ? tx('sm.cancel') : tx('sm.terminate')}</button></form>`;
|
|
114
116
|
return html`<div class="stat-grid u-mb1">
|
|
115
|
-
${([['
|
|
117
|
+
${([[tx('sm.sessions'), `${t.sessions} / ${t.max}`], [tx('sm.active'), t.active], [tx('sm.idle_tx'), t.idle_tx], [tx('sm.blocked'), t.blocked], [tx('sm.longest'), t.longest]] as [string, unknown][])
|
|
116
118
|
.map(([l, v]) => html`<div class="stat"><b>${String(v)}</b><span>${l}</span></div>`)}
|
|
117
119
|
</div>
|
|
118
|
-
${region('
|
|
119
|
-
<a href="${showIdle ? URL_BASE : `${URL_BASE}?idle=1`}">${showIdle ? '
|
|
120
|
-
|
|
120
|
+
${region(tx('sm.sessions'), html`<p class="muted u-mt0">${showIdle ? tx('sm.sessions_intro_all') : tx('sm.sessions_intro')}
|
|
121
|
+
<a href="${showIdle ? URL_BASE : `${URL_BASE}?idle=1`}">${showIdle ? tx('sm.hide_idle') : tx('sm.show_idle')}</a>.
|
|
122
|
+
${tx.rich('sm.query_text_note', { role: html`<code>pg_read_all_stats</code>` })}</p>
|
|
121
123
|
<div class="table-wrap"><table class="report report-reflow sm-sessions">
|
|
122
|
-
<thead><tr><th class="num">PID</th><th
|
|
124
|
+
<thead><tr><th class="num">PID</th><th>${tx('sm.col.user')}</th><th>${tx('sm.col.application')}</th><th>${tx('sm.col.state')}</th><th>${tx('sm.col.wait')}</th><th class="num">${tx('sm.col.running')}</th><th>${tx('sm.col.blocked_by')}</th><th>${tx('sm.col.query')}</th><th>${tx('sm.col.actions')}</th></tr></thead>
|
|
123
125
|
<tbody>${d.sessions.length ? d.sessions.map((x) => html`<tr${(x.blocked_by as number[]).length ? raw(' class="is-blocked"') : ''}>
|
|
124
126
|
<td class="num" data-label="PID">${String(x.pid)}</td>
|
|
125
|
-
<td data-label="
|
|
126
|
-
<td data-label="
|
|
127
|
-
<td data-label="
|
|
128
|
-
<td data-label="
|
|
129
|
-
<td class="num" data-label="
|
|
130
|
-
<td data-label="
|
|
131
|
-
<td data-label="
|
|
132
|
-
? html`<a href="${URL_BASE}/statements/${String(x.queryid)}" title="
|
|
127
|
+
<td data-label="${tx('sm.col.user')}">${String(x.user ?? '')}</td>
|
|
128
|
+
<td data-label="${tx('sm.col.application')}" class="u-break">${String(x.app ?? '')}${x.client ? html` <span class="muted">${String(x.client)}</span>` : ''}</td>
|
|
129
|
+
<td data-label="${tx('sm.col.state')}">${x.idle_tx_s != null ? html`<span class="tag tag-warning">${String(x.state)} ${String(x.idle_tx_s)} s</span>` : String(x.state ?? '')}</td>
|
|
130
|
+
<td data-label="${tx('sm.col.wait')}">${x.wait_type ? `${x.wait_type}: ${x.wait ?? ''}` : ''}</td>
|
|
131
|
+
<td class="num" data-label="${tx('sm.col.running')}">${x.running_s != null ? String(x.running_s) : ''}</td>
|
|
132
|
+
<td data-label="${tx('sm.col.blocked_by')}">${(x.blocked_by as number[]).length ? html`<span class="tag tag-warning">${(x.blocked_by as number[]).join(', ')}</span>` : ''}</td>
|
|
133
|
+
<td data-label="${tx('sm.col.query')}" class="u-break">${x.query ? x.queryid && x.queryid !== '0'
|
|
134
|
+
? html`<a href="${URL_BASE}/statements/${String(x.queryid)}" title="${tx('sm.statement_link')}"><code>${String(x.query).slice(0, 500)}</code></a>`
|
|
133
135
|
: html`<code>${String(x.query).slice(0, 500)}</code>` : ''}</td>
|
|
134
|
-
<td data-label="
|
|
135
|
-
: html`<tr><td colspan="9" class="empty"
|
|
136
|
-
${region('
|
|
137
|
-
${d.waits.length ? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
138
|
-
<tbody>${d.waits.map((w) => html`<tr><td data-label="
|
|
139
|
-
: html`<p class="muted"
|
|
140
|
-
${d.progress.length ? region('
|
|
141
|
-
<tbody>${d.progress.map((p) => html`<tr><td class="num" data-label="PID">${String(p.pid)}</td><td data-label="
|
|
142
|
-
<td data-label="
|
|
143
|
-
<td data-label="
|
|
144
|
-
<p class="muted small sm-updated" data-sm-updated
|
|
136
|
+
<td data-label="${tx('sm.col.actions')}"><div class="sm-actions">${x.state === 'active' ? action(x.pid, 'cancel') : ''}${action(x.pid, 'terminate')}</div></td></tr>`)
|
|
137
|
+
: html`<tr><td colspan="9" class="empty">${showIdle ? tx('sm.no_other') : tx('sm.no_busy')}</td></tr>`}</tbody></table></div>`)}
|
|
138
|
+
${region(tx('sm.waits'), html`<p class="muted u-mt0">${tx.rich('sm.waits_intro', { cpu: html`<em>${tx('sm.cpu_running')}</em>` })}</p>
|
|
139
|
+
${d.waits.length ? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('sm.col.type')}</th><th>${tx('sm.col.event')}</th><th class="num">${tx('sm.sessions')}</th></tr></thead>
|
|
140
|
+
<tbody>${d.waits.map((w) => html`<tr><td data-label="${tx('sm.col.type')}">${String(w.type)}</td><td data-label="${tx('sm.col.event')}">${String(w.event)}</td><td class="num" data-label="${tx('sm.sessions')}">${String(w.sessions)}</td></tr>`)}</tbody></table></div>`
|
|
141
|
+
: html`<p class="muted">${tx('sm.no_other_active')}</p>`}`)}
|
|
142
|
+
${d.progress.length ? region(tx('sm.in_progress'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th class="num">PID</th><th>${tx('sm.col.command')}</th><th>${tx('sm.col.object')}</th><th>${tx('sm.col.phase')}</th><th>${tx('sm.col.done')}</th></tr></thead>
|
|
143
|
+
<tbody>${d.progress.map((p) => html`<tr><td class="num" data-label="PID">${String(p.pid)}</td><td data-label="${tx('sm.col.command')}">${String(p.command)}</td>
|
|
144
|
+
<td data-label="${tx('sm.col.object')}" class="u-break">${String(p.relation ?? '')}</td><td data-label="${tx('sm.col.phase')}">${String(p.phase ?? '')}</td>
|
|
145
|
+
<td data-label="${tx('sm.col.done')}">${p.pct != null ? html`<progress max="100" value="${String(p.pct)}" aria-label="${String(p.command)} ${String(p.pid)}"></progress> ${String(p.pct)}%` : ''}</td></tr>`)}</tbody></table></div>`) : ''}
|
|
146
|
+
<p class="muted small sm-updated" data-sm-updated>${tx('sm.updated', { time: new Date().toISOString().slice(11, 19) })} · <a href="${backTo}">${tx('sm.refresh')}</a></p>`;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
// ------------------------------------------------------------------ Top SQL
|
|
148
150
|
|
|
149
|
-
const SORTS: Record<string, { col: string; label:
|
|
150
|
-
role: { col: 'r.rolname', label: '
|
|
151
|
-
calls: { col: 's.calls', label: '
|
|
152
|
-
total: { col: 's.total_exec_time', label: '
|
|
153
|
-
mean: { col: 's.mean_exec_time', label: '
|
|
154
|
-
rows: { col: 's.rows', label: '
|
|
155
|
-
hit: { col: 's.shared_blks_hit', label: '
|
|
156
|
-
read: { col: 's.shared_blks_read', label: '
|
|
157
|
-
query: { col: 's.query', label: '
|
|
151
|
+
const SORTS: Record<string, { col: string; label: BuilderKey; asc?: boolean }> = {
|
|
152
|
+
role: { col: 'r.rolname', label: 'sm.col.role', asc: true },
|
|
153
|
+
calls: { col: 's.calls', label: 'sm.col.calls' },
|
|
154
|
+
total: { col: 's.total_exec_time', label: 'sm.col.total_ms' },
|
|
155
|
+
mean: { col: 's.mean_exec_time', label: 'sm.col.mean_ms' },
|
|
156
|
+
rows: { col: 's.rows', label: 'sm.col.rows' },
|
|
157
|
+
hit: { col: 's.shared_blks_hit', label: 'sm.col.hit' },
|
|
158
|
+
read: { col: 's.shared_blks_read', label: 'sm.col.read' },
|
|
159
|
+
query: { col: 's.query', label: 'sm.col.statement', asc: true },
|
|
158
160
|
};
|
|
159
161
|
|
|
160
162
|
export interface TopRow { queryid: string | null; role: string | null; calls: number; total_ms: number; mean_ms: number; rows: number; hit: number; read: number; query: string }
|
|
@@ -176,7 +178,7 @@ export async function topStatements(sort: string, desc: boolean, filter: string,
|
|
|
176
178
|
|
|
177
179
|
const EXPLAINABLE = /^(select|with|insert|update|delete|merge|values|table)\b/i;
|
|
178
180
|
|
|
179
|
-
export type MonitorPlan = { ok: true; plan: Plan; text: string; generic: boolean } | { ok: false; error: string };
|
|
181
|
+
export type MonitorPlan = { ok: true; plan: Plan; text: string; generic: boolean } | { ok: false; error: string; key?: BuilderKey };
|
|
180
182
|
|
|
181
183
|
/**
|
|
182
184
|
* EXPLAIN (never ANALYZE) of a statement someone else ran: one statement only, of a kind that has a plan, in a
|
|
@@ -185,14 +187,14 @@ export type MonitorPlan = { ok: true; plan: Plan; text: string; generic: boolean
|
|
|
185
187
|
*/
|
|
186
188
|
export async function explainForMonitor(sql: string, schema: string | null): Promise<MonitorPlan> {
|
|
187
189
|
const stmts = splitScript(sql);
|
|
188
|
-
if (stmts.length !== 1 || stmts[0].psql) return { ok: false, error: 'Only one SQL statement can be explained.' };
|
|
190
|
+
if (stmts.length !== 1 || stmts[0].psql) return { ok: false, error: 'Only one SQL statement can be explained.', key: 'sm.err.one' };
|
|
189
191
|
const text = stmts[0].sql;
|
|
190
|
-
if (!EXPLAINABLE.test(text)) return { ok: false, error: 'Only SELECT, INSERT, UPDATE, DELETE, MERGE and VALUES statements have a plan.' };
|
|
192
|
+
if (!EXPLAINABLE.test(text)) return { ok: false, error: 'Only SELECT, INSERT, UPDATE, DELETE, MERGE and VALUES statements have a plan.', key: 'sm.err.kind' };
|
|
191
193
|
const generic = /\$\d+/.test(text);
|
|
192
194
|
const c = await owner.pool.connect();
|
|
193
195
|
try {
|
|
194
196
|
if (generic && Number((await c.query(`select current_setting('server_version_num') as v`)).rows[0].v) < 160000)
|
|
195
|
-
return { ok: false, error: 'The statement has parameters ($1, …): PostgreSQL 16 or later can explain it with a generic plan. Open it in the worksheet and fill in values.' };
|
|
197
|
+
return { ok: false, error: 'The statement has parameters ($1, …): PostgreSQL 16 or later can explain it with a generic plan. Open it in the worksheet and fill in values.', key: 'sm.err.generic' };
|
|
196
198
|
await c.query('begin transaction read only');
|
|
197
199
|
await c.query(`set local statement_timeout = '5s'`);
|
|
198
200
|
await c.query(`set local lock_timeout = '1s'`);
|
|
@@ -200,7 +202,7 @@ export async function explainForMonitor(sql: string, schema: string | null): Pro
|
|
|
200
202
|
// the extended protocol (Parse/Bind): the server accepts one statement only, whatever the text hides; a generic
|
|
201
203
|
// plan's parameters are bound to nulls, which it doesn't look at
|
|
202
204
|
const params = Math.max(0, ...[...text.matchAll(/\$(\d+)/g)].map((m) => Number(m[1])));
|
|
203
|
-
if (params > 100) return { ok: false, error: 'The statement has too many parameters to explain.' };
|
|
205
|
+
if (params > 100) return { ok: false, error: 'The statement has too many parameters to explain.', key: 'sm.err.params' };
|
|
204
206
|
const out = await c.query({ text: `explain (format json, verbose, costs${generic ? ', generic_plan' : ''}) ${text}`, values: Array(params).fill(null), queryMode: 'extended' } as never);
|
|
205
207
|
const json = (out as { rows: Row[] }).rows[0]?.['QUERY PLAN'];
|
|
206
208
|
const plan = parsePlan(typeof json === 'string' ? JSON.parse(json) : json, false);
|
|
@@ -246,14 +248,15 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
246
248
|
const showIdle = req.query?.idle === '1';
|
|
247
249
|
const live = await livePart(s, showIdle);
|
|
248
250
|
if (req.query?.fragment === '1') return reply.header('cache-control', 'no-store').type('text/html').send(String(live));
|
|
249
|
-
const
|
|
251
|
+
const tx = tr(s);
|
|
252
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('monitor', s)}${viewTabs(tx, 'sessions')}
|
|
250
253
|
<div class="sm-toolbar" data-sm-controls hidden>
|
|
251
|
-
<label for="sm-interval"
|
|
252
|
-
<select id="sm-interval" data-sm-interval>${REFRESH_SECONDS.map((n) => html`<option value="${n}">${n}
|
|
253
|
-
<button type="button" class="btn btn-sm" data-sm-pause aria-pressed="false"
|
|
254
|
+
<label for="sm-interval">${tx('sm.refresh_every')}</label>
|
|
255
|
+
<select id="sm-interval" data-sm-interval>${REFRESH_SECONDS.map((n) => html`<option value="${n}">${tx('sm.seconds', { n })}</option>`)}</select>
|
|
256
|
+
<button type="button" class="btn btn-sm" data-sm-pause aria-pressed="false">${tx('sm.pause')}</button>
|
|
254
257
|
</div>
|
|
255
258
|
<div class="sm-live" data-sm-live="${URL_BASE}?${showIdle ? 'idle=1&' : ''}fragment=1" aria-live="off">${live}</div>`;
|
|
256
|
-
return send(reply, s, shell(s, '
|
|
259
|
+
return send(reply, s, shell(s, tx('sql.tool.monitor'), crumbs(tx), main, 'sql'));
|
|
257
260
|
});
|
|
258
261
|
|
|
259
262
|
app.post(`${URL_BASE}/session`, async (req: Req, reply) => {
|
|
@@ -265,15 +268,16 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
265
268
|
// only a client session of this database, and not the connection doing it
|
|
266
269
|
const target = await owner.one<{ pid: number }>(
|
|
267
270
|
`select pid from pg_stat_activity where pid = $1 and datname = current_database() and backend_type = 'client backend' and pid <> pg_backend_pid()`, [Number(b.pid)]);
|
|
271
|
+
const tx = tr(s);
|
|
268
272
|
if (!target) {
|
|
269
|
-
flash(s,
|
|
273
|
+
flash(s, tx('sm.not_client', { pid: String(b.pid) }), 'error');
|
|
270
274
|
return back(reply, s, to);
|
|
271
275
|
}
|
|
272
276
|
try {
|
|
273
277
|
const fn = b.action === 'cancel' ? 'pg_cancel_backend' : 'pg_terminate_backend';
|
|
274
278
|
const r = await owner.one<{ ok: boolean }>(`select ${fn}($1) as ok`, [target.pid]);
|
|
275
|
-
if (r?.ok) flash(s, b.action === 'cancel' ?
|
|
276
|
-
else flash(s,
|
|
279
|
+
if (r?.ok) flash(s, b.action === 'cancel' ? tx('sm.cancelled', { pid: target.pid }) : tx('sm.terminated', { pid: target.pid }));
|
|
280
|
+
else flash(s, tx('sm.not_signalled', { pid: target.pid }), 'error');
|
|
277
281
|
} catch (e) {
|
|
278
282
|
flash(s, (e as Error).message, 'error');
|
|
279
283
|
}
|
|
@@ -287,9 +291,10 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
287
291
|
const sort = SORTS[q.sort ?? ''] ? q.sort! : 'total';
|
|
288
292
|
const desc = q.dir ? q.dir !== 'asc' : !SORTS[sort].asc;
|
|
289
293
|
const filter = String(q.q ?? '').slice(0, 200);
|
|
294
|
+
const tx = tr(s);
|
|
290
295
|
const state = await statementsState();
|
|
291
296
|
let body: Raw;
|
|
292
|
-
if (!state.ok) body = statementsNote(state.reason, state.message);
|
|
297
|
+
if (!state.ok) body = statementsNote(tx, state.reason, state.message);
|
|
293
298
|
else {
|
|
294
299
|
const [rows, info] = await Promise.all([
|
|
295
300
|
topStatements(sort, desc, filter),
|
|
@@ -301,32 +306,32 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
301
306
|
const on = key === sort;
|
|
302
307
|
const nextDesc = on ? !desc : !SORTS[key].asc;
|
|
303
308
|
const params = new URLSearchParams({ sort: key, dir: nextDesc ? 'desc' : 'asc', ...(filter ? { q: filter } : {}) });
|
|
304
|
-
return html`<a href="${URL_BASE}/top?${params}" title="
|
|
309
|
+
return html`<a href="${URL_BASE}/top?${params}" title="${tx('sm.sort_by', { column: tx(SORTS[key].label) })}">${tx(SORTS[key].label)}${on ? (desc ? ' ▼' : ' ▲') : ''}</a>`;
|
|
305
310
|
};
|
|
306
311
|
const th = (key: string, numeric = true) => html`<th${numeric ? raw(' class="num"') : ''}${key === sort ? raw(` aria-sort="${desc ? 'descending' : 'ascending'}"`) : ''}>${link(key)}</th>`;
|
|
307
312
|
const i = info[0] as Row | undefined;
|
|
308
313
|
body = html`<form method="get" action="${URL_BASE}/top" class="sm-filter u-mb1" role="search">
|
|
309
314
|
<input type="hidden" name="sort" value="${sort}"><input type="hidden" name="dir" value="${desc ? 'desc' : 'asc'}">
|
|
310
|
-
<label class="sr-only" for="sm-q"
|
|
311
|
-
<button class="btn btn-sm">${icon('search')}
|
|
315
|
+
<label class="sr-only" for="sm-q">${tx('sm.contains')}</label><input id="sm-q" name="q" type="search" value="${filter}" maxlength="200" placeholder="${tx('sm.contains')}…">
|
|
316
|
+
<button class="btn btn-sm">${icon('search')} ${tx('common.search')}</button></form>
|
|
312
317
|
<div class="table-wrap"><table class="report report-reflow sm-top"><thead><tr>${th('role', false)}${th('calls')}${th('total')}${th('mean')}${th('rows')}${th('hit')}${th('read')}${th('query', false)}</tr></thead>
|
|
313
318
|
<tbody>${rows.length ? rows.map((r) => html`<tr>
|
|
314
|
-
<td data-label="
|
|
315
|
-
<td class="num" data-label="
|
|
316
|
-
<td class="num sm-barcell" data-label="
|
|
317
|
-
<td class="num sm-barcell" data-label="
|
|
318
|
-
<td class="num" data-label="
|
|
319
|
-
<td class="num" data-label="
|
|
320
|
-
<td class="num" data-label="
|
|
321
|
-
<td data-label="
|
|
322
|
-
: html`<tr><td colspan="8" class="empty">${filter ? '
|
|
323
|
-
<p class="muted small">${i?.reset ?
|
|
319
|
+
<td data-label="${tx('sm.col.role')}">${r.role ?? ''}</td>
|
|
320
|
+
<td class="num" data-label="${tx('sm.col.calls')}">${num(r.calls)}</td>
|
|
321
|
+
<td class="num sm-barcell" data-label="${tx('sm.col.total_ms')}">${bar(r.total_ms, maxTotal)}<span>${num(r.total_ms)}</span></td>
|
|
322
|
+
<td class="num sm-barcell" data-label="${tx('sm.col.mean_ms')}">${bar(r.mean_ms, maxMean)}<span>${num(r.mean_ms)}</span></td>
|
|
323
|
+
<td class="num" data-label="${tx('sm.col.rows')}">${num(r.rows)}</td>
|
|
324
|
+
<td class="num" data-label="${tx('sm.col.hit')}">${num(r.hit)}</td>
|
|
325
|
+
<td class="num" data-label="${tx('sm.col.read')}">${num(r.read)}</td>
|
|
326
|
+
<td data-label="${tx('sm.col.statement')}" class="u-break">${r.queryid ? html`<a href="${URL_BASE}/statements/${r.queryid}"><code class="sql-text">${r.query}</code></a>` : html`<code class="sql-text">${r.query}</code>`}</td></tr>`)
|
|
327
|
+
: html`<tr><td colspan="8" class="empty">${filter ? tx('sm.no_match') : tx('sm.none_yet')}</td></tr>`}</tbody></table></div>
|
|
328
|
+
<p class="muted small">${i?.reset ? `${tx('sm.since', { time: String(i.reset) })} ` : ''}${tx.rich('sm.at_most', { p1: html`<code>$1</code>`, p2: html`<code>$2</code>` })}</p>
|
|
324
329
|
<form method="post" action="${URL_BASE}/reset">${csrf(s)}
|
|
325
|
-
<button class="btn" data-confirm="
|
|
330
|
+
<button class="btn" data-confirm="${tx('sm.reset_confirm')}">${icon('history')} ${tx('sm.reset')}</button></form>`;
|
|
326
331
|
}
|
|
327
|
-
const main = html`<h1 class="u-mb1"
|
|
328
|
-
${region('
|
|
329
|
-
return send(reply, s, shell(s, '
|
|
332
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('monitor', s)}${viewTabs(tx, 'top')}
|
|
333
|
+
${region(tx('sql.tool.topsql'), html`<p class="muted u-mt0">${tx.rich('sm.top_intro', { ext: html`<code>pg_stat_statements</code>` })}</p>${body}`)}`;
|
|
334
|
+
return send(reply, s, shell(s, tx('sql.tool.topsql'), crumbs(tx, [tx('sql.tool.topsql')]), main, 'sql'));
|
|
330
335
|
});
|
|
331
336
|
|
|
332
337
|
app.post(`${URL_BASE}/reset`, async (req: Req, reply) => {
|
|
@@ -334,7 +339,7 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
334
339
|
if (!s) return;
|
|
335
340
|
try {
|
|
336
341
|
await owner.query('select pg_stat_statements_reset()');
|
|
337
|
-
flash(s, '
|
|
342
|
+
flash(s, tr(s)('sm.reset_done'));
|
|
338
343
|
} catch (e) {
|
|
339
344
|
flash(s, (e as Error).message, 'error');
|
|
340
345
|
}
|
|
@@ -346,10 +351,11 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
346
351
|
if (!s) return;
|
|
347
352
|
const queryid = String(req.params.queryid);
|
|
348
353
|
if (!QUERYID_RE.test(queryid)) return reply.code(404).send('Not found');
|
|
354
|
+
const tx = tr(s);
|
|
349
355
|
const state = await statementsState();
|
|
350
356
|
if (!state.ok) {
|
|
351
|
-
const main = html`<h1 class="u-mb1"
|
|
352
|
-
return send(reply, s, shell(s, '
|
|
357
|
+
const main = html`<h1 class="u-mb1">${tx('sm.statement')}</h1>${statementsNote(tx, state.reason, state.message)}`;
|
|
358
|
+
return send(reply, s, shell(s, tx('sm.statement'), crumbs(tx, [tx('sm.statement')]), main, 'sql'));
|
|
353
359
|
}
|
|
354
360
|
let rows: Row[];
|
|
355
361
|
try {
|
|
@@ -366,22 +372,22 @@ export async function sqlMonitorRoutes(app: FastifyInstance) {
|
|
|
366
372
|
const calls = sum('calls');
|
|
367
373
|
const total = sum('total_ms');
|
|
368
374
|
const tile = (label: string, v: string) => html`<div class="stat"><b>${v}</b><span>${label}</span></div>`;
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
${region('
|
|
375
|
+
const BY_ROLE: Record<string, BuilderKey> = { calls: 'sm.col.calls', total_ms: 'sm.col.total_ms', mean_ms: 'sm.col.mean_ms', stddev_ms: 'sm.col.stddev_ms', min_ms: 'sm.col.min_ms', max_ms: 'sm.col.max_ms', rows: 'sm.col.rows', hit: 'sm.col.hit_short', read: 'sm.col.read_short', temp_read: 'sm.col.temp_read', temp_written: 'sm.col.temp_written' };
|
|
376
|
+
const main = html`<h1 class="u-mb1">${tx('sm.statement')}</h1>
|
|
377
|
+
<div class="stat-grid u-mb1">${tile(tx('sm.col.calls'), num(calls))}${tile(tx('sm.col.total_ms'), num(total))}${tile(tx('sm.col.mean_ms'), num(calls ? total / calls : 0))}${tile(tx('sm.col.rows'), num(sum('rows')))}
|
|
378
|
+
${tile(tx('sm.col.hit'), num(sum('hit')))}${tile(tx('sm.col.read'), num(sum('read')))}${tile(tx('sm.temp_blocks_written'), num(sum('temp_written')))}</div>
|
|
379
|
+
${region(tx('sm.text'), html`<pre class="source u-break sm-sql" data-code-view="sql" tabindex="0" aria-label="${tx('sm.sql_text')}">${sql}</pre>
|
|
380
|
+
<div class="buttons"><form method="post" action="${URL_BASE}/statements/${queryid}/worksheet">${csrf(s)}<button class="btn">${icon('code')} ${tx('sm.open_worksheet')}</button></form>
|
|
381
|
+
<a class="btn" href="${URL_BASE}/top">${tx('sm.back_top')}</a></div>`)}
|
|
382
|
+
${region(tx('sm.by_role'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('sm.col.role')}</th>${Object.values(BY_ROLE).map((k) => html`<th class="num">${tx(k)}</th>`)}</tr></thead>
|
|
383
|
+
<tbody>${rows.map((r) => html`<tr><td data-label="${tx('sm.col.role')}">${String(r.role ?? '')}${r.toplevel === false ? html` <span class="tag">${tx('sm.nested')}</span>` : ''}</td>
|
|
384
|
+
${Object.entries(BY_ROLE).map(([k, label]) => html`<td class="num" data-label="${tx(label)}">${num(r[k])}</td>`)}</tr>`)}</tbody></table></div>`)}
|
|
385
|
+
${region(tx('sm.explain_plan'), html`<p class="muted u-mt0">${/\$\d+/.test(sql) ? tx('sm.explain_intro_params') : tx('sm.explain_intro')}</p>
|
|
380
386
|
<form method="get" action="${URL_BASE}/statements/${queryid}" class="sm-filter u-mb1"><input type="hidden" name="explain" value="1">
|
|
381
|
-
<label for="sm-schema"
|
|
382
|
-
<button class="btn">${icon('play')}
|
|
383
|
-
${explained ? explained.ok ? planView(explained.plan, explained.text) : html`<div class="alert alert-error" role="alert">${explained.error}</div>` : ''}`)}`;
|
|
384
|
-
return send(reply, s, shell(s, '
|
|
387
|
+
<label for="sm-schema">${tx('sm.schema')}</label><select id="sm-schema" name="schema"><option value="">${tx('sm.search_path')}</option>${schemas.map((n) => html`<option value="${n}"${n === schema ? raw(' selected') : ''}>${n}</option>`)}</select>
|
|
388
|
+
<button class="btn">${icon('play')} ${tx('sm.explain')}</button></form>
|
|
389
|
+
${explained ? explained.ok ? planView(explained.plan, explained.text) : html`<div class="alert alert-error" role="alert">${explained.key ? tx(explained.key) : explained.error}</div>` : ''}`)}`;
|
|
390
|
+
return send(reply, s, shell(s, tx('sm.statement'), crumbs(tx, [tx('sm.statement')]), main, 'sql'));
|
|
385
391
|
});
|
|
386
392
|
|
|
387
393
|
app.post(`${URL_BASE}/statements/:queryid/worksheet`, async (req: Req, reply) => {
|
|
@@ -5,6 +5,8 @@ import { icon } from '../icons.ts';
|
|
|
5
5
|
import { BASE, developer, region, send, shell, workshopTabs, type Req } from './ui.ts';
|
|
6
6
|
import { isAdmin } from './locks.ts';
|
|
7
7
|
import { workshopDb, workshopRunsAs } from './workspaces.ts';
|
|
8
|
+
import { tr, type BuilderTranslate } from './i18n.ts';
|
|
9
|
+
import { SQL_TOOLS, toolDescription, toolLabel } from './sql-tools.ts';
|
|
8
10
|
|
|
9
11
|
// SQL Workshop → Utilities (sprint 62; APEX: SQL Workshop → Utilities): one
|
|
10
12
|
// page with the workshop's tools (Load and Unload Data, Sample Data, DDL,
|
|
@@ -134,30 +136,30 @@ async function readOnly<T>(db: Db, fn: (q: (sql: string, params?: unknown[]) =>
|
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
const crumbs = (last: string): [string, string?][] => [['
|
|
139
|
+
const crumbs = (tx: BuilderTranslate, last: string): [string, string?][] => [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.utilities'), `${BASE}/sql/utilities`], [last]];
|
|
140
|
+
/** (sprint 162) an Object Report's title, text and column labels in the builder's language (su.r.<key>.*, su.col.<column>) */
|
|
141
|
+
const reportTitle = (tx: BuilderTranslate, key: string, r: Report) => tx.dyn(`su.r.${key.replace(/-/g, '_')}`, r.title);
|
|
142
|
+
const reportAbout = (tx: BuilderTranslate, key: string, r: Report) => tx.dyn(`su.r.${key.replace(/-/g, '_')}.about`, r.about);
|
|
143
|
+
const colLabel = (tx: BuilderTranslate, col: string, label: string) => tx.dyn(`su.col.${col}`, label);
|
|
138
144
|
|
|
139
145
|
export async function sqlUtilityRoutes(app: FastifyInstance) {
|
|
140
146
|
app.get(`${BASE}/sql/utilities`, async (req: Req, reply) => {
|
|
141
147
|
const s = await developer(req, reply);
|
|
142
148
|
if (!s) return;
|
|
143
149
|
const admin = await isAdmin(s.username);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
150
|
+
const tx = tr(s);
|
|
151
|
+
// (sprint 162) the tools' names and descriptions from the SQL Workshop's tool list, in the builder's language
|
|
152
|
+
const tile = (key: string, ic: string) => {
|
|
153
|
+
const t = SQL_TOOLS.find((x) => x.key === key)!;
|
|
154
|
+
if (t.admin && !admin) return '';
|
|
155
|
+
return html`<a class="ab-tile sw-tile" href="${t.href}"><span class="ab-tile-icon sw-tile-icon">${icon(ic)}</span><strong>${toolLabel(tx, t)}</strong><span>${toolDescription(tx, t)}</span></a>`;
|
|
156
|
+
};
|
|
157
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('utilities', s)}
|
|
147
158
|
<div class="ab-tiles ab-tiles-wrap">
|
|
148
|
-
${tile('
|
|
149
|
-
${tile('
|
|
150
|
-
${tile('/sql/sample-data', 'layers', 'Sample Data', 'Generate realistic rows for your tables.')}
|
|
151
|
-
${tile('/sql/query', 'filter', 'Query Builder', 'Build a query by drawing joins between tables.')}
|
|
152
|
-
${tile('/sql/ddl', 'file', 'Generate DDL', 'The SQL that creates a schema\'s objects.')}
|
|
153
|
-
${tile('/sql/compare', 'layers', 'Schema Comparison', 'The differences between two schemas, and the SQL to align them.')}
|
|
154
|
-
${tile('/sql/utilities/reports', 'list', 'Object Reports', 'Tables without a primary key, foreign keys without an index, row-level security, grants, sizes.')}
|
|
155
|
-
${tile('/sql/utilities/about', 'help', 'About Database', 'The PostgreSQL version, settings, extensions and sizes.')}
|
|
156
|
-
${admin ? tile('/sql/data-pump', 'archive', 'Schema Export / Import', 'A schema to a file with pg_dump, a file into a schema with pg_restore (administrators).') : ''}
|
|
157
|
-
${admin ? tile('/sql/scheduling', 'clock', 'Scheduling', 'pg_cron jobs: SQL on a schedule, next runs and run history (administrators).') : ''}
|
|
158
|
-
${admin ? tile('/sql/monitor', 'activity', 'SQL Monitor', 'Sessions, running and blocked queries, wait events and Top SQL (administrators).') : ''}
|
|
159
|
+
${tile('load', 'upload')}${tile('unload', 'download')}${tile('sample', 'layers')}${tile('query', 'filter')}${tile('ddl', 'file')}
|
|
160
|
+
${tile('compare', 'layers')}${tile('reports', 'list')}${tile('about', 'help')}${tile('datapump', 'archive')}${tile('scheduling', 'clock')}${tile('monitor', 'activity')}
|
|
159
161
|
</div>`;
|
|
160
|
-
return send(reply, s, shell(s, '
|
|
162
|
+
return send(reply, s, shell(s, tx('su.title'), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.utilities')]], main, 'sql'));
|
|
161
163
|
});
|
|
162
164
|
|
|
163
165
|
app.get(`${BASE}/sql/utilities/reports`, async (req: Req, reply) => {
|
|
@@ -174,20 +176,23 @@ export async function sqlUtilityRoutes(app: FastifyInstance) {
|
|
|
174
176
|
} catch (e) {
|
|
175
177
|
error = (e as Error).message;
|
|
176
178
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
+
const tx = tr(s);
|
|
180
|
+
const reports = tx('sql.tool.reports');
|
|
181
|
+
const nav = html`<nav aria-label="${reports}"><ul class="ab-links ab-tasks">${Object.entries(OBJECT_REPORTS).map(([k, x]) =>
|
|
182
|
+
html`<li><a href="${BASE}/sql/utilities/reports?r=${k}"${k === key ? raw(' aria-current="page"') : ''}>${reportTitle(tx, k, x)}${icon('chevron')}</a></li>`)}</ul></nav>`;
|
|
179
183
|
const table = html`<div class="table-wrap"><table class="report report-reflow">
|
|
180
|
-
<thead><tr><th
|
|
184
|
+
<thead><tr><th>${tx('sm.col.object')}</th>${r.columns.map(([k, l, c]) => html`<th${c ? raw(` class="${c}"`) : ''}>${colLabel(tx, k, l)}</th>`)}</tr></thead>
|
|
181
185
|
<tbody>${rows.length
|
|
182
|
-
? rows.map((row) => html`<tr><td data-label="
|
|
183
|
-
${r.columns.map(([k, l, c]) => html`<td data-label="${l}"${c ? raw(` class="${c}"`) : raw(' class="u-break"')}>${k === 'suggestion' ? html`<code>${String(row[k] ?? '')}</code>` : String(row[k] ?? '')}</td>`)}</tr>`)
|
|
184
|
-
: html`<tr><td colspan="${r.columns.length + 1}" class="empty">${error ? '
|
|
185
|
-
const
|
|
186
|
+
? rows.map((row) => html`<tr><td data-label="${tx('sm.col.object')}" class="u-break"><a href="${BASE}/sql/objects?o=${encodeURIComponent(String(row.object))}">${String(row.object)}</a></td>
|
|
187
|
+
${r.columns.map(([k, l, c]) => html`<td data-label="${colLabel(tx, k, l)}"${c ? raw(` class="${c}"`) : raw(' class="u-break"')}>${k === 'suggestion' ? html`<code>${String(row[k] ?? '')}</code>` : String(row[k] ?? '')}</td>`)}</tr>`)
|
|
188
|
+
: html`<tr><td colspan="${r.columns.length + 1}" class="empty">${error ? tx('su.failed') : tx('su.nothing')}</td></tr>`}</tbody></table></div>`;
|
|
189
|
+
const title = reportTitle(tx, key, r);
|
|
190
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('utilities', s)}
|
|
186
191
|
<div class="ab-home"><div class="ab-main">
|
|
187
|
-
${region(
|
|
192
|
+
${region(title, html`<p class="muted u-mt0">${reportAbout(tx, key, r)} ${tx('su.over', { role: workshopRunsAs(db) })}</p>
|
|
188
193
|
${error ? html`<div class="alert alert-error" role="alert">${error}</div>` : ''}${table}`)}
|
|
189
|
-
</div><aside class="ab-side" aria-label="
|
|
190
|
-
return send(reply, s, shell(s,
|
|
194
|
+
</div><aside class="ab-side" aria-label="${reports}"><section><h2>${reports}</h2>${nav}</section></aside></div>`;
|
|
195
|
+
return send(reply, s, shell(s, title, crumbs(tx, reports), main, 'sql'));
|
|
191
196
|
});
|
|
192
197
|
|
|
193
198
|
app.get(`${BASE}/sql/utilities/about`, async (req: Req, reply) => {
|
|
@@ -214,26 +219,28 @@ export async function sqlUtilityRoutes(app: FastifyInstance) {
|
|
|
214
219
|
where ${USABLE} group by 1, 2 order by 1`),
|
|
215
220
|
}));
|
|
216
221
|
const sv = info.server as Record<string, string>;
|
|
217
|
-
const
|
|
222
|
+
const tx = tr(s);
|
|
223
|
+
const L = { schema: tx('sm.schema'), owner: tx('su.col.owner'), tables: tx('su.tables'), size: tx('scr.col.size'), setting: tx('su.setting'), value: tx('su.value'), ext: tx('su.extension'), version: tx('su.version') };
|
|
224
|
+
const main = html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('utilities', s)}
|
|
218
225
|
<div class="stat-grid u-mb1">
|
|
219
|
-
${[['PostgreSQL', sv.server_version], ['
|
|
226
|
+
${[['PostgreSQL', sv.server_version], [tx('su.database'), sv.database], [L.size, sv.size], [tx('su.running_since'), new Date(sv.started).toISOString().slice(0, 16).replace('T', ' ') + ' UTC']]
|
|
220
227
|
.map(([l, v]) => html`<div class="stat"><b class="u-break">${v}</b><span>${l}</span></div>`)}
|
|
221
228
|
</div>
|
|
222
|
-
<p class="muted">${sv.version}.
|
|
229
|
+
<p class="muted">${sv.version}. ${tx.rich('su.runs_as', { user: html`<code>${sv.user}</code>`, login: html`<code>${sv.login}</code>` })}</p>
|
|
223
230
|
<div class="columns">
|
|
224
|
-
${region('
|
|
225
|
-
<tbody>${info.settings.map((x) => html`<tr><td data-label="
|
|
226
|
-
<td data-label="
|
|
227
|
-
${region('
|
|
228
|
-
<tbody>${info.extensions.map((x) => html`<tr><td data-label="
|
|
229
|
-
<td data-label="
|
|
231
|
+
${region(tx('su.settings'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${L.setting}</th><th>${L.value}</th></tr></thead>
|
|
232
|
+
<tbody>${info.settings.map((x) => html`<tr><td data-label="${L.setting}" title="${String(x.short_desc ?? '')}"><code>${String(x.name)}</code></td>
|
|
233
|
+
<td data-label="${L.value}" class="u-break">${String(x.setting ?? '')}${x.unit ? ` ${x.unit}` : ''}</td></tr>`)}</tbody></table></div>`)}
|
|
234
|
+
${region(tx('su.extensions'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${L.ext}</th><th>${L.version}</th><th>${L.schema}</th></tr></thead>
|
|
235
|
+
<tbody>${info.extensions.map((x) => html`<tr><td data-label="${L.ext}" class="u-break"><strong>${String(x.name)}</strong>${x.comment ? html` <span class="muted">${String(x.comment)}</span>` : ''}</td>
|
|
236
|
+
<td data-label="${L.version}">${String(x.version)}</td><td data-label="${L.schema}">${String(x.schema)}</td></tr>`)}</tbody></table></div>`)}
|
|
230
237
|
</div>
|
|
231
238
|
<div class="u-spacer"></div>
|
|
232
|
-
${region('
|
|
233
|
-
<tbody>${info.schemas.length ? info.schemas.map((x) => html`<tr><td data-label="
|
|
234
|
-
<td class="num" data-label="
|
|
235
|
-
: html`<tr><td colspan="4" class="empty"
|
|
236
|
-
return send(reply, s, shell(s, '
|
|
239
|
+
${region(tx('cl.schemas'), html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${L.schema}</th><th>${L.owner}</th><th class="num">${L.tables}</th><th class="num">${L.size}</th></tr></thead>
|
|
240
|
+
<tbody>${info.schemas.length ? info.schemas.map((x) => html`<tr><td data-label="${L.schema}">${String(x.schema)}</td><td data-label="${L.owner}"><code>${String(x.owner)}</code></td>
|
|
241
|
+
<td class="num" data-label="${L.tables}">${String(x.tables)}</td><td class="num" data-label="${L.size}">${String(x.size)}</td></tr>`)
|
|
242
|
+
: html`<tr><td colspan="4" class="empty">${tx('su.no_schemas')}</td></tr>`}</tbody></table></div>`)}`;
|
|
243
|
+
return send(reply, s, shell(s, tx('sql.tool.about'), crumbs(tx, tx('sql.tool.about')), main, 'sql'));
|
|
237
244
|
});
|
|
238
245
|
|
|
239
246
|
// (sprint 102) the Database Monitor became SQL Workshop › SQL Monitor (sql-monitor.ts)
|