pgkiln 0.83.0 → 0.85.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/154_notifications.sql +186 -0
- package/docs/apex-feature-parity.md +3 -2
- package/docs/guide/03-builder.md +16 -5
- package/docs/guide/04-pages-and-regions.md +42 -0
- package/docs/guide/06-processing.md +22 -1
- package/docs/guide/07-dynamic-actions.md +1 -0
- package/docs/guide/09-reference.md +15 -0
- package/docs/guide/11-from-apex.md +1 -0
- package/docs/guide/12-development.md +13 -1
- package/package.json +1 -1
- package/public/app.css +29 -0
- package/public/app.js +78 -0
- package/public/sw.js +5 -1
- package/src/app.ts +11 -0
- package/src/builder/ai-agents.ts +86 -85
- package/src/builder/ai-builder.ts +30 -30
- package/src/builder/ai.ts +93 -92
- package/src/builder/api.ts +47 -47
- package/src/builder/app-groups.ts +33 -32
- package/src/builder/appfromfile.ts +57 -58
- package/src/builder/appsheets.ts +33 -32
- package/src/builder/appwizard.ts +65 -65
- package/src/builder/arrange.ts +23 -22
- package/src/builder/assistant.ts +39 -38
- package/src/builder/automations.ts +32 -31
- package/src/builder/availability.ts +24 -24
- package/src/builder/backups.ts +30 -31
- package/src/builder/blueprints.ts +62 -61
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +10 -6
- package/src/builder/create-page.ts +52 -50
- package/src/builder/cross-app.ts +37 -35
- package/src/builder/dataload.ts +2 -2
- package/src/builder/datasets.ts +107 -106
- package/src/builder/dependencies.ts +20 -19
- package/src/builder/designer.ts +36 -36
- package/src/builder/diagnostics.ts +58 -57
- package/src/builder/features.ts +39 -35
- package/src/builder/feedback.ts +23 -20
- package/src/builder/find-workspace.ts +28 -24
- package/src/builder/form-designer.ts +36 -35
- package/src/builder/gallery.ts +25 -29
- package/src/builder/globalization.ts +50 -49
- package/src/builder/i18n/de.ts +3461 -0
- package/src/builder/i18n/en.ts +3461 -0
- package/src/builder/i18n/es.ts +3461 -0
- package/src/builder/i18n/fr.ts +3461 -0
- package/src/builder/i18n/it.ts +3461 -0
- package/src/builder/i18n/ja.ts +3461 -0
- package/src/builder/i18n/ko.ts +3461 -0
- package/src/builder/i18n/nl.ts +3461 -0
- package/src/builder/i18n/pt.ts +3461 -0
- package/src/builder/i18n/zh-cn.ts +3461 -0
- package/src/builder/i18n/zh-tw.ts +3461 -0
- package/src/builder/i18n.ts +17 -3
- package/src/builder/json-sources.ts +63 -62
- package/src/builder/layouts.ts +12 -11
- package/src/builder/locks.ts +29 -28
- package/src/builder/login-page.ts +26 -25
- package/src/builder/mail.ts +148 -147
- package/src/builder/newapp.ts +15 -12
- package/src/builder/notification-settings.ts +27 -0
- package/src/builder/plugins.ts +47 -44
- package/src/builder/process-jobs.ts +11 -10
- package/src/builder/pwa.ts +22 -21
- package/src/builder/remote-databases.ts +55 -56
- package/src/builder/report-settings.ts +1 -1
- package/src/builder/reporter.ts +43 -42
- package/src/builder/rest-catalogs.ts +87 -86
- package/src/builder/rest.ts +9 -8
- package/src/builder/routes.ts +30 -27
- package/src/builder/search.ts +14 -13
- package/src/builder/static-files.ts +31 -32
- package/src/builder/subscriptions.ts +47 -45
- package/src/builder/supporting.ts +23 -18
- package/src/builder/team.ts +179 -179
- package/src/builder/telemetry.ts +59 -62
- package/src/builder/templates.ts +66 -65
- package/src/builder/themeroller.ts +60 -63
- package/src/builder/ui-defaults.ts +21 -20
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +25 -24
- package/src/builder/utilities.ts +48 -47
- package/src/builder/vector-providers.ts +67 -66
- package/src/builder/websources.ts +43 -42
- package/src/builder/wizards.ts +95 -89
- package/src/builder/workflow-migrate.ts +30 -31
- package/src/builder/workflows.ts +40 -39
- package/src/builder/workingcopies.ts +41 -49
- package/src/builder/workspace-backups.ts +21 -20
- package/src/builder/workspace-mail.ts +17 -16
- package/src/builder/workspace-resources.ts +104 -112
- package/src/builder/workspace-transfer.ts +34 -26
- package/src/cli/replace.ts +2 -0
- 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 +9 -3
- package/src/runtime/engine.ts +2 -1
- package/src/runtime/feedback.ts +1 -1
- package/src/runtime/lists.ts +43 -11
- 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 +12 -3
- package/src/runtime/routes.ts +12 -4
- package/src/runtime/signin.ts +2 -1
- package/src/session.ts +13 -4
package/src/builder/datasets.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { logActivity, clientIp, type Session } from '../session.ts';
|
|
|
15
15
|
import { checkNewApp, createAppError } from './newapp.ts';
|
|
16
16
|
import { back, BASE, csrf, developer, flash, input, region, send, shell, type Body, type Req } from './ui.ts';
|
|
17
17
|
import { currentWorkspace, usableSchemas, workshopDb } from './workspaces.ts';
|
|
18
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
18
19
|
|
|
19
20
|
// (sprint 110, migration 120) App Builder → Data Reporter (/builder/reporter; APEX 26.1: Data Reporter as a
|
|
20
21
|
// workspace tool): the workspace's datasets (tables and views of one schema, joins proposed from foreign keys,
|
|
@@ -27,7 +28,7 @@ import { currentWorkspace, usableSchemas, workshopDb } from './workspaces.ts';
|
|
|
27
28
|
const root = `${BASE}/reporter`;
|
|
28
29
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
29
30
|
const PREVIEW_ROWS = 10;
|
|
30
|
-
const crumbs = (extra: [string, string?][] = []): [string, string?][] => [['
|
|
31
|
+
const crumbs = (extra: [string, string?][] = []): [string, string?][] => [[tx('dts.app_builder'), BASE], ['Data Reporter', root], ...extra];
|
|
31
32
|
const when = (d: string | Date | null) => (d ? new Date(d).toISOString().slice(0, 16).replace('T', ' ') : '');
|
|
32
33
|
|
|
33
34
|
const page = (s: Session, reply: FastifyReply, title: string, extra: [string, string?][], main: Raw) =>
|
|
@@ -106,7 +107,7 @@ export function datasetFromForm(d: Dataset, b: Body, cat: Awaited<ReturnType<typ
|
|
|
106
107
|
|
|
107
108
|
/** The first rows of a dataset, read only, through the SQL Workshop's connection (a timeout; errors as text). */
|
|
108
109
|
async function preview(db: Db, d: Dataset): Promise<{ fields: string[]; rows: unknown[][] } | { error: string }> {
|
|
109
|
-
if (!d.columns.length) return { error: '
|
|
110
|
+
if (!d.columns.length) return { error: tx('dts.the_dataset_offers_no_columns') };
|
|
110
111
|
try {
|
|
111
112
|
return await db.tx(async (c) => {
|
|
112
113
|
await c.query('set transaction read only');
|
|
@@ -129,30 +130,30 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
129
130
|
const ws = currentWorkspace(s);
|
|
130
131
|
const sets = await workspaceDatasets(owner, ws);
|
|
131
132
|
const apps = await reportingApps(ws);
|
|
132
|
-
const main = html`<div class="title-row"><h1
|
|
133
|
-
<div class="buttons"><a class="btn btn-hot" href="${root}/datasets/new">${icon('plus')}
|
|
134
|
-
<p class="muted u-mt0"
|
|
135
|
-
${region('
|
|
136
|
-
<thead><tr><th scope="col"
|
|
133
|
+
const main = html`<div class="title-row"><h1>${tx('dts.data_reporter')}</h1>
|
|
134
|
+
<div class="buttons"><a class="btn btn-hot" href="${root}/datasets/new">${icon('plus')} ${tx('dts.new_dataset')}</a><a class="btn" href="${root}/import">${icon('upload')} ${tx('dts.import')}</a>${sets.length ? html`<a class="btn" href="${root}/export">${icon('download')} ${tx('dts.export_all')}</a>` : ''}</div></div>
|
|
135
|
+
<p class="muted u-mt0">${tx('dts.intro', { workspace: s.workspace?.name ?? tx('dts.this_workspace') })}</p>
|
|
136
|
+
${region(tx('dts.datasets'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
137
|
+
<thead><tr><th scope="col">${tx('dts.dataset')}</th><th scope="col">${tx('dts.schema')}</th><th scope="col">${tx('dts.tables_and_views')}</th><th scope="col" class="num">${tx('dts.columns')}</th><th scope="col">${tx('dts.updated_utc')}</th><th scope="col">${tx('dts.actions')}</th></tr></thead>
|
|
137
138
|
<tbody>${sets.length ? sets.map((d) => html`<tr>
|
|
138
|
-
<td data-label="
|
|
139
|
-
<td data-label="
|
|
140
|
-
<td data-label="
|
|
141
|
-
<td class="num" data-label="
|
|
142
|
-
<td data-label="
|
|
143
|
-
<td data-label=""><a class="btn" href="${root}/apps/new?dataset=${encodeURIComponent(d.name)}">${icon('plus')}
|
|
144
|
-
</tr>`) : html`<tr><td colspan="6" class="empty"
|
|
145
|
-
${region('
|
|
146
|
-
<thead><tr><th scope="col"
|
|
139
|
+
<td data-label="${tx('dts.dataset')}"><a href="${root}/datasets/${d.id}">${d.label}</a> <code>${d.name}</code>${d.description ? html`<br><span class="muted small">${d.description.slice(0, 160)}</span>` : ''}</td>
|
|
140
|
+
<td data-label="${tx('dts.schema')}"><code>${d.schema_name}</code></td>
|
|
141
|
+
<td data-label="${tx('dts.tables_and_views')}">${d.tables.map((t) => t.table).join(', ')}</td>
|
|
142
|
+
<td class="num" data-label="${tx('dts.columns')}">${d.columns.length}</td>
|
|
143
|
+
<td data-label="${tx('dts.updated_utc')}">${when(d.updated_at)}${d.updated_by ? html` <span class="muted">${d.updated_by}</span>` : ''}</td>
|
|
144
|
+
<td data-label=""><a class="btn" href="${root}/apps/new?dataset=${encodeURIComponent(d.name)}">${icon('plus')} ${tx('dts.create_reporting_application')}</a></td>
|
|
145
|
+
</tr>`) : html`<tr><td colspan="6" class="empty">${tx('dts.no_datasets_yet_create_one')}</td></tr>`}</tbody></table></div>`)}
|
|
146
|
+
${region(tx('dts.reporting_applications'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
147
|
+
<thead><tr><th scope="col">${tx('dts.application')}</th><th scope="col">${tx('dts.datasets')}</th><th scope="col" class="num">${tx('dts.users')}</th><th scope="col" class="num">${tx('dts.published_reports')}</th><th scope="col">${tx('dts.actions')}</th></tr></thead>
|
|
147
148
|
<tbody>${apps.length ? apps.map((a) => html`<tr>
|
|
148
|
-
<td data-label="
|
|
149
|
-
<td data-label="
|
|
150
|
-
<td class="num" data-label="
|
|
151
|
-
<td class="num" data-label="
|
|
152
|
-
<td data-label=""><a class="btn" href="/a/${a.alias}/">${icon('play')}
|
|
153
|
-
</tr>`) : html`<tr><td colspan="5" class="empty"
|
|
154
|
-
<p class="muted small"
|
|
155
|
-
return page(s, reply, '
|
|
149
|
+
<td data-label="${tx('dts.application')}"><a href="${BASE}/apps/${a.id}">${a.name}</a> <code>${a.alias}</code></td>
|
|
150
|
+
<td data-label="${tx('dts.datasets')}">${a.datasets.join(', ')}</td>
|
|
151
|
+
<td class="num" data-label="${tx('dts.users')}">${a.users}</td>
|
|
152
|
+
<td class="num" data-label="${tx('dts.published_reports')}">${a.reports}</td>
|
|
153
|
+
<td data-label=""><a class="btn" href="/a/${a.alias}/">${icon('play')} ${tx('dts.run')}</a> <a class="btn" href="${BASE}/apps/${a.id}/shared">${icon('users')} ${tx('dts.users_and_roles')}</a></td>
|
|
154
|
+
</tr>`) : html`<tr><td colspan="5" class="empty">${tx('dts.no_reporting_applications_yet_choose')}</td></tr>`}</tbody></table></div>
|
|
155
|
+
<p class="muted small">${tx.rich('dts.roles_help', { viewer: html`<b>viewer</b>`, editor: html`<b>editor</b>`, administrator: html`<b>administrator</b>` })}</p>`)}`;
|
|
156
|
+
return page(s, reply, tx('dts.data_reporter'), [], main);
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
// ---------------------------------------------------------------- a new dataset: schema, then tables and views
|
|
@@ -163,23 +164,23 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
163
164
|
const schemas = await usableSchemas(db);
|
|
164
165
|
const schema = await usableSchema(db, String(req.query.schema ?? ''));
|
|
165
166
|
const cat = schema ? await readSchemaCatalog(db, schema) : null;
|
|
166
|
-
const main = html`<h1
|
|
167
|
-
${region('
|
|
168
|
-
<label class="label" for="f_schema"
|
|
167
|
+
const main = html`<h1>${tx('dts.new_dataset')}</h1>
|
|
168
|
+
${region(tx('dts.1_schema'), html`<form method="get" action="${root}/datasets/new" class="search u-mwnone dataset-schema">
|
|
169
|
+
<label class="label" for="f_schema">${tx('dts.schema')}</label>
|
|
169
170
|
<select id="f_schema" name="schema">${schemas.map((n) => html`<option${n === schema ? raw(' selected') : ''}>${n}</option>`)}</select>
|
|
170
|
-
<button class="btn"
|
|
171
|
-
${cat ? region('
|
|
171
|
+
<button class="btn">${tx('dts.show_its_tables_and_views')}</button></form>`)}
|
|
172
|
+
${cat ? region(tx('dts.2_tables_and_views'), html`<form method="post" action="${root}/datasets">${csrf(s)}<input type="hidden" name="schema" value="${schema}">
|
|
172
173
|
<div class="form-grid">
|
|
173
|
-
${input('name', '
|
|
174
|
-
${input('label', '
|
|
174
|
+
${input('name', tx('dts.name'), '', { required: true, placeholder: tx('dts.e_g_sales'), help: tx('dts.lower_case_letters_digits_and') })}
|
|
175
|
+
${input('label', tx('dts.label'), '', { placeholder: tx('dts.e_g_sales_2'), help: tx('dts.what_business_users_see') })}
|
|
175
176
|
</div>
|
|
176
|
-
<fieldset class="prop-group"><legend
|
|
177
|
+
<fieldset class="prop-group"><legend>${tx('dts.tables_and_views_the_first')}</legend>
|
|
177
178
|
${cat.objects.length ? html`<div class="dataset-checks">${cat.objects.map((o) => html`<label class="check"><input type="checkbox" name="tables" value="${o.name}"> ${o.name} <span class="muted">${o.kind}, ${o.columns.length} columns</span></label>`)}</div>`
|
|
178
|
-
: html`<p class="muted"
|
|
179
|
-
<small class="help"
|
|
179
|
+
: html`<p class="muted">${tx('dts.the_schema_has_no_tables')}</p>`}
|
|
180
|
+
<small class="help">${tx('dts.joins_are_proposed_from_foreign')}</small>
|
|
180
181
|
</fieldset>
|
|
181
|
-
<div class="buttons"><a class="btn" href="${root}"
|
|
182
|
-
return page(s, reply, '
|
|
182
|
+
<div class="buttons"><a class="btn" href="${root}">${tx('dts.cancel')}</a><button class="btn btn-hot">${icon('plus')} ${tx('dts.create_dataset')}</button></div></form>`) : ''}`;
|
|
183
|
+
return page(s, reply, tx('dts.new_dataset_title'), [[tx('dts.new_dataset')]], main);
|
|
183
184
|
});
|
|
184
185
|
|
|
185
186
|
app.post(`${root}/datasets`, async (req: Req, reply) => {
|
|
@@ -190,20 +191,20 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
190
191
|
const schema = await usableSchema(db, String(b.schema ?? ''));
|
|
191
192
|
const name = String(b.name ?? '').trim().toLowerCase();
|
|
192
193
|
const again = (msg: string) => (flash(s, msg, 'error'), back(reply, s, `${root}/datasets/new${schema ? `?schema=${encodeURIComponent(schema)}` : ''}`));
|
|
193
|
-
if (!schema) return again('
|
|
194
|
+
if (!schema) return again(tx('dts.choose_one_of_the_schemas'));
|
|
194
195
|
if (!DATASET_NAME.test(name)) return again(`"${name}" is not a valid dataset name: lower case letters, digits and _, starting with a letter (at most 40).`);
|
|
195
196
|
const raw = (req.body as unknown as Record<string, unknown>)?.tables;
|
|
196
197
|
const chosen = (Array.isArray(raw) ? raw : raw ? [raw] : []).map(String);
|
|
197
|
-
if (!chosen.length) return again('
|
|
198
|
+
if (!chosen.length) return again(tx('dts.choose_at_least_one_table'));
|
|
198
199
|
const cat = await readSchemaCatalog(db, schema);
|
|
199
200
|
const proposed = proposeDataset(name, String(b.label ?? '').trim(), schema, chosen, cat);
|
|
200
201
|
const { dataset, errors } = checkDataset(proposed.dataset, catalogOf(cat.objects));
|
|
201
202
|
if (errors.length || !dataset.tables.length) return again([...proposed.errors, ...errors].join(' ') || 'The dataset is not valid.');
|
|
202
203
|
const exists = await owner.one('select 1 from meta.dataset where workspace_id = $1 and name = $2', [currentWorkspace(s), name]);
|
|
203
|
-
if (exists) return again(
|
|
204
|
+
if (exists) return again(tx('dts.exists', { name }));
|
|
204
205
|
const id = await storeDataset(owner, currentWorkspace(s), dataset, s.username, { replace: false });
|
|
205
206
|
logActivity({ appId: null, username: s.username, event: 'dataset_create', ip: clientIp(req), detail: `${name} on ${schema}` });
|
|
206
|
-
flash(s, proposed.errors.length ?
|
|
207
|
+
flash(s, proposed.errors.length ? tx('dts.created_errors', { name, errors: proposed.errors.join(' ') }) : tx('dts.created', { name }), proposed.errors.length ? 'error' : 'ok');
|
|
207
208
|
return back(reply, s, `${root}/datasets/${id}`);
|
|
208
209
|
});
|
|
209
210
|
|
|
@@ -232,60 +233,60 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
232
233
|
const shown = req.query.preview === '1' ? await preview(db, d) : null;
|
|
233
234
|
const opt = (v: string, l: string, cur: string) => html`<option value="${v}"${v === cur ? raw(' selected') : ''}>${l}</option>`;
|
|
234
235
|
const main = html`<div class="title-row"><h1>${d.label} <code>${d.name}</code></h1>
|
|
235
|
-
<div class="buttons"><a class="btn" href="${root}/datasets/${row.id}/export">${icon('download')}
|
|
236
|
+
<div class="buttons"><a class="btn" href="${root}/datasets/${row.id}/export">${icon('download')} ${tx('dts.download')}</a><a class="btn btn-hot" href="${root}/apps/new?dataset=${encodeURIComponent(d.name)}">${icon('plus')} ${tx('dts.create_reporting_application')}</a></div></div>
|
|
236
237
|
<p class="muted u-mt0">On schema <code>${d.schema}</code>${used.length ? html` · used by ${used.map((a, i) => html`${i ? ', ' : ''}<a href="${BASE}/apps/${a.id}">${a.name}</a>`)}` : ''} · updated ${when(row.updated_at)} (UTC)${row.updated_by ? ` by ${row.updated_by}` : ''}</p>
|
|
237
|
-
${errors.length ? html`<div class="alert alert-error" role="alert"><b
|
|
238
|
+
${errors.length ? html`<div class="alert alert-error" role="alert"><b>${tx('dts.the_dataset_doesn_t_match')}</b><ul>${errors.map((e) => html`<li>${e}</li>`)}</ul></div>` : ''}
|
|
238
239
|
<form method="post" action="${root}/datasets/${row.id}">${csrf(s)}
|
|
239
|
-
${region('
|
|
240
|
-
${input('label', '
|
|
241
|
-
<div class="field" data-wide><label class="label" for="f_description"
|
|
240
|
+
${region(tx('dts.dataset'), html`<div class="form-grid">
|
|
241
|
+
${input('label', tx('dts.label'), d.label, { required: true })}
|
|
242
|
+
<div class="field" data-wide><label class="label" for="f_description">${tx('dts.description_shown_to_users')}</label><input id="f_description" name="description" maxlength="${DATASET_LIMITS.description}" value="${d.description ?? ''}"></div>
|
|
242
243
|
</div>`)}
|
|
243
|
-
${region('
|
|
244
|
-
<thead><tr><th scope="col"
|
|
244
|
+
${region(tx('dts.tables_and_views'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
245
|
+
<thead><tr><th scope="col">${tx('dts.alias')}</th><th scope="col">${tx('dts.table_or_view')}</th><th scope="col">${tx('dts.joins_to')}</th><th scope="col">${tx('dts.join')}</th><th scope="col">${tx('dts.on_header')}</th><th scope="col">${tx('dts.remove')}</th></tr></thead><tbody>
|
|
245
246
|
${d.tables.map((t, i) => html`<tr>
|
|
246
|
-
<td data-label="
|
|
247
|
-
<td data-label="
|
|
248
|
-
${i === 0 ? html`<td data-label="
|
|
249
|
-
<td data-label="
|
|
250
|
-
<td data-label="
|
|
251
|
-
<td data-label="
|
|
252
|
-
<td data-label="
|
|
247
|
+
<td data-label="${tx('dts.alias')}"><code>${t.alias}</code><input type="hidden" name="t${i}_alias" value="${t.alias}"></td>
|
|
248
|
+
<td data-label="${tx('dts.table_or_view')}"><code>${t.table}</code></td>
|
|
249
|
+
${i === 0 ? html`<td data-label="${tx('dts.joins_to')}" colspan="3"><span class="muted">${tx('dts.the_main_table')}</span></td>` : html`
|
|
250
|
+
<td data-label="${tx('dts.joins_to')}"><select name="t${i}_to" aria-label="${t.table} joins to">${d.tables.slice(0, i).map((x) => opt(x.alias, `${x.alias} ${x.table}`, t.join?.to ?? ''))}</select></td>
|
|
251
|
+
<td data-label="${tx('dts.join')}"><select name="t${i}_type" aria-label="${tx('dts.aria_join_type', { table: t.table })}">${opt('left', tx('dts.join_left'), t.join?.type ?? 'left')}${opt('inner', tx('dts.join_inner'), t.join?.type ?? 'left')}</select></td>
|
|
252
|
+
<td data-label="${tx('dts.on')}"><input name="t${i}_on" value="${t.join?.on.map((p) => `${p.column} = ${p.ref}`).join(', ') ?? ''}" aria-label="${tx('dts.aria_join_cols', { table: t.table })}" placeholder="deptno = deptno"></td>`}
|
|
253
|
+
<td data-label="${tx('dts.remove')}">${i === 0 ? '' : html`<input type="checkbox" name="t${i}_remove" value="true" aria-label="${tx('dts.aria_remove', { table: t.table })}">`}</td>
|
|
253
254
|
</tr>`)}</tbody></table></div>
|
|
254
255
|
${d.tables.length < DATASET_LIMITS.tables ? html`<div class="form-grid">
|
|
255
|
-
<div class="field"><label class="label" for="f_new_table"
|
|
256
|
-
<div class="field"><label class="label" for="f_new_to"
|
|
257
|
-
<div class="field"><label class="label" for="f_new_on"
|
|
256
|
+
<div class="field"><label class="label" for="f_new_table">${tx('dts.add_a_table_or_view')}</label><select id="f_new_table" name="new_table"><option value="">- none -</option>${cat.objects.map((o) => html`<option value="${o.name}">${o.name} (${o.kind})</option>`)}</select></div>
|
|
257
|
+
<div class="field"><label class="label" for="f_new_to">${tx('dts.joined_to')}</label><select id="f_new_to" name="new_to">${d.tables.map((x) => opt(x.alias, `${x.alias} ${x.table}`, ''))}</select></div>
|
|
258
|
+
<div class="field"><label class="label" for="f_new_on">${tx('dts.on')}</label><input id="f_new_on" name="new_on" placeholder="${tx('dts.from_a_foreign_key')}"><small class="help">${tx('dts.empty_proposed_from_a_foreign')}</small></div>
|
|
258
259
|
</div>` : ''}`)}
|
|
259
|
-
${region('
|
|
260
|
+
${region(tx('dts.columns'), html`<p class="muted u-mt0">${tx('dts.business_users_see_only_the')}</p>
|
|
260
261
|
<div class="table-wrap"><table class="report report-reflow dataset-columns">
|
|
261
|
-
<thead><tr><th scope="col"
|
|
262
|
+
<thead><tr><th scope="col">${tx('dts.offer')}</th><th scope="col">${tx('dts.column')}</th><th scope="col">${tx('dts.name')}</th><th scope="col">${tx('dts.label')}</th><th scope="col">${tx('dts.format_mask')}</th><th scope="col">${tx('dts.description')}</th></tr></thead><tbody>
|
|
262
263
|
${rows.map((r, j) => html`<tr>
|
|
263
|
-
<td data-label="
|
|
264
|
-
<td data-label="
|
|
265
|
-
<td data-label="
|
|
266
|
-
<td data-label="
|
|
267
|
-
<td data-label="
|
|
268
|
-
<td data-label="
|
|
264
|
+
<td data-label="${tx('dts.offer')}"><input type="hidden" name="c${j}_ref" value="${r.ref}"><input type="checkbox" name="c${j}_on" value="true"${r.c ? raw(' checked') : ''} aria-label="${tx('dts.aria_offer', { column: `${r.table}.${r.column}` })}"></td>
|
|
265
|
+
<td data-label="${tx('dts.column')}"><code>${r.alias}.${r.column}</code> <span class="muted">${r.type}</span></td>
|
|
266
|
+
<td data-label="${tx('dts.name')}"><input name="c${j}_name" maxlength="40" value="${r.c?.name ?? r.column.toLowerCase()}" aria-label="${tx('dts.aria_name', { column: `${r.table}.${r.column}` })}"></td>
|
|
267
|
+
<td data-label="${tx('dts.label')}"><input name="c${j}_label" maxlength="${DATASET_LIMITS.label}" value="${r.c?.label ?? ''}" placeholder="${heading(r.column)}" aria-label="${tx('dts.aria_label', { column: `${r.table}.${r.column}` })}"></td>
|
|
268
|
+
<td data-label="${tx('dts.format_mask')}"><input name="c${j}_format" maxlength="${DATASET_LIMITS.format}" value="${r.c?.format ?? ''}" placeholder="999G990D00" aria-label="${tx('dts.aria_format', { column: `${r.table}.${r.column}` })}"></td>
|
|
269
|
+
<td data-label="${tx('dts.description')}"><input name="c${j}_desc" maxlength="${DATASET_LIMITS.columnDescription}" value="${r.c?.description ?? ''}" aria-label="${tx('dts.aria_desc', { column: `${r.table}.${r.column}` })}"></td>
|
|
269
270
|
</tr>`)}</tbody></table></div>`)}
|
|
270
|
-
${region('
|
|
271
|
+
${region(tx('dts.row_filter'), html`<p class="muted u-mt0">${tx('dts.only_rows_that_match_every')}</p>
|
|
271
272
|
${Array.from({ length: DATASET_LIMITS.filters }, (_, k) => {
|
|
272
273
|
const f = d.filters[k];
|
|
273
274
|
return html`<div class="dataset-filter">
|
|
274
|
-
<select name="f${k}_col" aria-label="
|
|
275
|
-
<select name="f${k}_op" aria-label="
|
|
276
|
-
<input name="f${k}_value" maxlength="${DATASET_LIMITS.value}" value="${f?.value ?? ''}" aria-label="
|
|
275
|
+
<select name="f${k}_col" aria-label="${tx('dts.aria_filter_col', { n: k + 1 })}">${opt('', tx('dts.none_opt'), '')}${colOptions.map(([v, l]) => opt(v, l, f ? `${f.table}.${f.column}` : ''))}</select>
|
|
276
|
+
<select name="f${k}_op" aria-label="${tx('dts.aria_filter_op', { n: k + 1 })}">${Object.keys(OPERATORS).map((o) => opt(o, opLabel(english, o), f?.op ?? 'eq'))}</select>
|
|
277
|
+
<input name="f${k}_value" maxlength="${DATASET_LIMITS.value}" value="${f?.value ?? ''}" aria-label="${tx('dts.aria_filter_value', { n: k + 1 })}" placeholder="${tx('dts.value')}">
|
|
277
278
|
</div>`;
|
|
278
279
|
})}`)}
|
|
279
|
-
<div class="buttons"><button class="btn btn-hot">${icon('save')}
|
|
280
|
+
<div class="buttons"><button class="btn btn-hot">${icon('save')} ${tx('dts.save_dataset')}</button></div>
|
|
280
281
|
</form>
|
|
281
|
-
${region('
|
|
282
|
+
${region(tx('dts.query_and_preview'), html`<p class="muted u-mt0">${tx('dts.the_query_reports_run_on')}</p>
|
|
282
283
|
<pre class="code" data-code-view="sql">${datasetSql(d)}</pre>
|
|
283
284
|
<p><a class="btn" href="${root}/datasets/${row.id}?preview=1#preview">${icon('play')} Preview ${PREVIEW_ROWS} rows</a></p>
|
|
284
285
|
${shown ? 'error' in shown ? html`<div class="alert alert-error" role="alert" id="preview">${shown.error}</div>` : html`<div class="table-wrap" id="preview"><table class="report report-reflow"><thead><tr>${shown.fields.map((f) => html`<th scope="col">${d.columns.find((c) => c.name === f)?.label ?? f}</th>`)}</tr></thead>
|
|
285
|
-
<tbody>${shown.rows.length ? shown.rows.map((r) => html`<tr>${r.map((v, i) => html`<td data-label="${shown.fields[i]}">${cellText(v)}</td>`)}</tr>`) : html`<tr><td colspan="${shown.fields.length}" class="empty"
|
|
286
|
-
${region('
|
|
287
|
-
<p class="muted u-mt0">${used.length ?
|
|
288
|
-
<button class="btn btn-danger" data-confirm="
|
|
286
|
+
<tbody>${shown.rows.length ? shown.rows.map((r) => html`<tr>${r.map((v, i) => html`<td data-label="${shown.fields[i]}">${cellText(v)}</td>`)}</tr>`) : html`<tr><td colspan="${shown.fields.length}" class="empty">${tx('dts.no_rows')}</td></tr>`}</tbody></table></div>` : ''}`)}
|
|
287
|
+
${region(tx('dts.delete'), html`<form method="post" action="${root}/datasets/${row.id}/delete">${csrf(s)}
|
|
288
|
+
<p class="muted u-mt0">${used.length ? tx('dts.used_by', { apps: used.map((a) => a.name).join(', ') }) : tx('dts.no_reporting_application_uses_it')}</p>
|
|
289
|
+
<button class="btn btn-danger" data-confirm="${tx('dts.delete_confirm', { name: d.name })}">${icon('trash')} ${tx('dts.delete_dataset')}</button></form>`)}`;
|
|
289
290
|
return page(s, reply, d.label, [[d.label]], main);
|
|
290
291
|
});
|
|
291
292
|
|
|
@@ -297,7 +298,7 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
297
298
|
const d = datasetOf(row);
|
|
298
299
|
const db = await workshopDb(s);
|
|
299
300
|
if (!(await usableSchema(db, d.schema))) {
|
|
300
|
-
flash(s,
|
|
301
|
+
flash(s, tx('dts.schema_refused', { schema: d.schema }), 'error');
|
|
301
302
|
return back(reply, s, `${root}/datasets/${row.id}`);
|
|
302
303
|
}
|
|
303
304
|
const cat = await readSchemaCatalog(db, d.schema);
|
|
@@ -308,7 +309,7 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
308
309
|
}
|
|
309
310
|
await storeDataset(owner, row.workspace_id, dataset, s.username);
|
|
310
311
|
logActivity({ appId: null, username: s.username, event: 'dataset_save', ip: clientIp(req), detail: dataset.name });
|
|
311
|
-
flash(s, errors.length ? `Saved, but: ${errors.join(' ')}` : '
|
|
312
|
+
flash(s, errors.length ? `Saved, but: ${errors.join(' ')}` : tx('dts.dataset_saved'), errors.length ? 'error' : 'ok');
|
|
312
313
|
return back(reply, s, `${root}/datasets/${row.id}`);
|
|
313
314
|
});
|
|
314
315
|
|
|
@@ -319,7 +320,7 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
319
320
|
if (!row) return reply.code(404).send('Not found');
|
|
320
321
|
await owner.query('delete from meta.dataset where id = $1', [row.id]);
|
|
321
322
|
logActivity({ appId: null, username: s.username, event: 'dataset_delete', ip: clientIp(req), detail: row.name });
|
|
322
|
-
flash(s,
|
|
323
|
+
flash(s, tx('dts.deleted', { name: row.name }));
|
|
323
324
|
return back(reply, s, root);
|
|
324
325
|
});
|
|
325
326
|
|
|
@@ -345,16 +346,16 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
345
346
|
app.get(`${root}/import`, async (req: Req, reply) => {
|
|
346
347
|
const s = await developer(req, reply);
|
|
347
348
|
if (!s) return;
|
|
348
|
-
const main = html`<h1
|
|
349
|
-
${region('
|
|
350
|
-
<p class="muted u-mt0"
|
|
349
|
+
const main = html`<h1>${tx('dts.import_datasets')}</h1>
|
|
350
|
+
${region(tx('dts.a_dataset_file'), html`<form method="post" action="${root}/import" enctype="multipart/form-data">${csrf(s)}
|
|
351
|
+
<p class="muted u-mt0">${tx.code('dts.import_intro')}</p>
|
|
351
352
|
<div class="form-grid">
|
|
352
|
-
<div class="field"><label class="label" for="f_file"
|
|
353
|
-
<div class="field" data-wide><label class="label" for="f_text"
|
|
354
|
-
<div class="field"><label class="check"><input type="checkbox" name="replace" value="true">
|
|
353
|
+
<div class="field"><label class="label" for="f_file">${tx('dts.file')}</label><input id="f_file" type="file" name="file" accept=".json,application/json"></div>
|
|
354
|
+
<div class="field" data-wide><label class="label" for="f_text">${tx('dts.or_paste_it')}</label><textarea id="f_text" name="text" class="code" rows="5" spellcheck="false"></textarea></div>
|
|
355
|
+
<div class="field"><label class="check"><input type="checkbox" name="replace" value="true"> ${tx('dts.replace_datasets_with_the_same')}</label></div>
|
|
355
356
|
</div>
|
|
356
|
-
<div class="buttons"><a class="btn" href="${root}"
|
|
357
|
-
return page(s, reply, '
|
|
357
|
+
<div class="buttons"><a class="btn" href="${root}">${tx('dts.cancel')}</a><button class="btn btn-hot">${icon('upload')} ${tx('dts.import')}</button></div></form>`)}`;
|
|
358
|
+
return page(s, reply, tx('dts.import_title'), [[tx('dts.import')]], main);
|
|
358
359
|
});
|
|
359
360
|
|
|
360
361
|
app.post(`${root}/import`, async (req: Req, reply) => {
|
|
@@ -371,9 +372,9 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
371
372
|
const s = await developer(req, reply);
|
|
372
373
|
if (!s) return;
|
|
373
374
|
const fail = (msg: string) => (flash(s, msg, 'error'), back(reply, s, `${root}/import`));
|
|
374
|
-
if (tooLarge) return fail('
|
|
375
|
+
if (tooLarge) return fail(tx('dts.too_large'));
|
|
375
376
|
if (!text.trim()) text = String(req.body?.text ?? '');
|
|
376
|
-
if (!text.trim()) return fail('
|
|
377
|
+
if (!text.trim()) return fail(tx('dts.no_file'));
|
|
377
378
|
let list: any[];
|
|
378
379
|
try {
|
|
379
380
|
list = readDatasetFile(text);
|
|
@@ -391,10 +392,10 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
391
392
|
for (const loose of list) {
|
|
392
393
|
const schema = typeof loose?.schema === 'string' ? loose.schema : '';
|
|
393
394
|
const name = typeof loose?.name === 'string' ? loose.name : '?';
|
|
394
|
-
if (!schemas.includes(schema)) throw new Error(
|
|
395
|
+
if (!schemas.includes(schema)) throw new Error(tx('dts.no_schema', { name, schema }));
|
|
395
396
|
if (!cats.has(schema)) cats.set(schema, await readSchemaCatalog(db, schema));
|
|
396
397
|
const { dataset, errors } = checkDataset(loose, catalogOf(cats.get(schema)!.objects));
|
|
397
|
-
if (errors.length || !dataset.columns.length) throw new Error(
|
|
398
|
+
if (errors.length || !dataset.columns.length) throw new Error(tx('dts.import_errors', { name, errors: errors.join(' ') || tx('dts.no_columns') }));
|
|
398
399
|
const exists = (await c.query('select 1 from meta.dataset where workspace_id = $1 and name = $2', [ws, dataset.name])).rowCount;
|
|
399
400
|
if (exists && !replace) {
|
|
400
401
|
lines.push(`${dataset.name}: kept (same name)`);
|
|
@@ -405,7 +406,7 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
405
406
|
}
|
|
406
407
|
});
|
|
407
408
|
} catch (e) {
|
|
408
|
-
return fail(
|
|
409
|
+
return fail(tx('dts.nothing_imported', { error: (e as Error).message }));
|
|
409
410
|
}
|
|
410
411
|
logActivity({ appId: null, username: s.username, event: 'dataset_import', ip: clientIp(req), detail: lines.join('; ') });
|
|
411
412
|
flash(s, `Imported: ${lines.join('; ')}.`);
|
|
@@ -415,18 +416,18 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
415
416
|
// ---------------------------------------------------------------- create a reporting application
|
|
416
417
|
const appForm = async (s: Session, b: Body & { datasets?: string | string[] }, sets: DatasetRow[], error?: string) => {
|
|
417
418
|
const chosen = new Set(Array.isArray(b.datasets) ? b.datasets : b.datasets ? [b.datasets] : []);
|
|
418
|
-
return html`<h1
|
|
419
|
+
return html`<h1>${tx('dts.create_a_reporting_application')}</h1>
|
|
419
420
|
${error ? html`<div class="alert alert-error" role="alert">${error}</div>` : ''}
|
|
420
421
|
<form method="post" action="${root}/apps">${csrf(s)}
|
|
421
|
-
${region('
|
|
422
|
-
${input('name', '
|
|
423
|
-
${input('alias', '
|
|
424
|
-
${input('admin_user', '
|
|
425
|
-
${input('admin_password', '
|
|
422
|
+
${region(tx('dts.application'), html`<div class="form-grid">
|
|
423
|
+
${input('name', tx('dts.name'), b.name ?? '', { required: true })}
|
|
424
|
+
${input('alias', tx('dts.alias'), b.alias ?? '', { required: true, help: tx('dts.its_address_a_alias_its') })}
|
|
425
|
+
${input('admin_user', tx('dts.first_administrator'), b.admin_user ?? s.username ?? '', { required: true, help: tx('dts.an_account_of_the_user') })}
|
|
426
|
+
${input('admin_password', tx('dts.password'), '', { type: 'password', auto: 'new-password', help: tx('dts.only_for_a_new_account') })}
|
|
426
427
|
</div>`)}
|
|
427
|
-
${region('
|
|
428
|
-
<small class="help"
|
|
429
|
-
<div class="buttons"><a class="btn" href="${root}"
|
|
428
|
+
${region(tx('dts.datasets'), html`<div class="dataset-checks">${sets.map((d) => html`<label class="check"><input type="checkbox" name="datasets" value="${d.name}"${chosen.has(d.name) ? raw(' checked') : ''}> ${d.label} <code>${d.name}</code> <span class="muted">${d.schema_name}: ${d.tables.map((t) => t.table).join(', ')}</span></label>`)}</div>
|
|
429
|
+
<small class="help">${tx('dts.the_first_dataset_s_schema')}</small>`)}
|
|
430
|
+
<div class="buttons"><a class="btn" href="${root}">${tx('dts.cancel')}</a><button class="btn btn-hot">${icon('plus')} ${tx('dts.create')}</button></div></form>`;
|
|
430
431
|
};
|
|
431
432
|
|
|
432
433
|
app.get(`${root}/apps/new`, async (req: Req, reply) => {
|
|
@@ -434,12 +435,12 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
434
435
|
if (!s) return;
|
|
435
436
|
const sets = await workspaceDatasets(owner, currentWorkspace(s));
|
|
436
437
|
if (!sets.length) {
|
|
437
|
-
flash(s, '
|
|
438
|
+
flash(s, tx('dts.create_a_dataset_first'), 'error');
|
|
438
439
|
return back(reply, s, root);
|
|
439
440
|
}
|
|
440
441
|
const first = sets.find((d) => d.name === req.query.dataset) ?? sets[0];
|
|
441
442
|
const base = first.name.replace(/_/g, '-').slice(0, 40);
|
|
442
|
-
return page(s, reply, '
|
|
443
|
+
return page(s, reply, tx('dts.create_app_title'), [[tx('dts.create_reporting_application')]],
|
|
443
444
|
await appForm(s, { name: `${first.label} reports`, alias: `${base}-reports`, datasets: first.name }, sets));
|
|
444
445
|
});
|
|
445
446
|
|
|
@@ -452,10 +453,10 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
452
453
|
const chosen = names.map((n) => sets.find((d) => d.name === n)).filter((d): d is DatasetRow => !!d);
|
|
453
454
|
const failed = async (msg: string) => {
|
|
454
455
|
reply.code(422);
|
|
455
|
-
return page(s, reply, '
|
|
456
|
+
return page(s, reply, tx('dts.create_app_title'), [[tx('dts.create_reporting_application')]], await appForm(s, b, sets, msg));
|
|
456
457
|
};
|
|
457
|
-
if (!chosen.length) return failed('
|
|
458
|
-
if (chosen.length > MAX_REPORTING_DATASETS) return failed(
|
|
458
|
+
if (!chosen.length) return failed(tx('dts.choose_one'));
|
|
459
|
+
if (chosen.length > MAX_REPORTING_DATASETS) return failed(tx('dts.choose_max', { n: MAX_REPORTING_DATASETS }));
|
|
459
460
|
let created: { id: number; existingAccount: boolean };
|
|
460
461
|
let alias = '';
|
|
461
462
|
try {
|
|
@@ -466,7 +467,7 @@ export async function datasetRoutes(app: FastifyInstance) {
|
|
|
466
467
|
return failed(createAppError(e, alias || String(b.alias ?? '')));
|
|
467
468
|
}
|
|
468
469
|
logActivity({ appId: created.id, username: s.username, event: 'app_create', ip: clientIp(req), detail: `reporting application on ${chosen.map((d) => d.name).join(', ')}` });
|
|
469
|
-
flash(s,
|
|
470
|
+
flash(s, tx('dts.app_created', { name: b.name, path: `/a/${alias}/` }));
|
|
470
471
|
return back(reply, s, root);
|
|
471
472
|
});
|
|
472
473
|
}
|
|
@@ -8,6 +8,7 @@ import { devDbForApp } from '../workspace-db.ts';
|
|
|
8
8
|
import { EXPLAINABLE, sqlFields, type SqlShape } from './advisor.ts';
|
|
9
9
|
import { COMPONENTS } from './components.ts';
|
|
10
10
|
import { appEntries, type Entry } from './search.ts';
|
|
11
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
11
12
|
import { appHeader, BASE, developer, region, send, shell, type Req } from './ui.ts';
|
|
12
13
|
|
|
13
14
|
// (sprint 138, AP-5) App Builder → Utilities → Database object dependencies
|
|
@@ -164,7 +165,7 @@ export async function appDependencies(appId: number): Promise<Dependencies> {
|
|
|
164
165
|
continue;
|
|
165
166
|
}
|
|
166
167
|
const { statements, skipped } = bodyOf(f.sql, f.shape);
|
|
167
|
-
if (skipped.length) unscanned.push({ ...use, message:
|
|
168
|
+
if (skipped.length) unscanned.push({ ...use, message: tx('dep.not_parsed', { statements: skipped.join(', ') }) });
|
|
168
169
|
if (!statements.length) continue;
|
|
169
170
|
scanned++;
|
|
170
171
|
// the extended protocol takes exactly one statement: SQL that ends the function early (…; end; drop …) is refused
|
|
@@ -262,7 +263,7 @@ export const objectUrl = (o: Pick<DepObject, 'kind' | 'schema' | 'ref'>) =>
|
|
|
262
263
|
|
|
263
264
|
const useLink = (u: Use) =>
|
|
264
265
|
u.entry
|
|
265
|
-
? html`<li><a href="${u.entry.url}">${icon(u.entry.icon)}<span>${u.entry.kindLabel}: ${u.entry.label}</span></a>${u.entry.pageNo !== null && u.entry.kind !== 'page' ? html` <span class="tag"
|
|
266
|
+
? html`<li><a href="${u.entry.url}">${icon(u.entry.icon)}<span>${u.entry.kindLabel}: ${u.entry.label}</span></a>${u.entry.pageNo !== null && u.entry.kind !== 'page' ? html` <span class="tag">${tx('dep.page_tag', { n: u.entry.pageNo })}</span>` : ''} <span class="muted small">${u.field}</span></li>`
|
|
266
267
|
: html`<li><span class="muted small">${u.field}</span></li>`;
|
|
267
268
|
|
|
268
269
|
export async function dependencyRoutes(app: FastifyInstance) {
|
|
@@ -279,33 +280,33 @@ export async function dependencyRoutes(app: FastifyInstance) {
|
|
|
279
280
|
const grantsN = deps.objects.filter((o) => o.lacking.length).length;
|
|
280
281
|
const pages = (o: DepObject) => [...new Set(o.usedBy.map((u) => u.entry?.pageNo).filter((p): p is number => p !== null && p !== undefined))].sort((x, y) => x - y);
|
|
281
282
|
const rows: Raw = shown.length
|
|
282
|
-
? html`<div class="table-wrap"><table class="table dep-table report-reflow"><thead><tr><th scope="col"
|
|
283
|
+
? html`<div class="table-wrap"><table class="table dep-table report-reflow"><thead><tr><th scope="col">${tx('dep.col_object')}</th><th scope="col">${tx('dep.col_type')}</th><th scope="col">${tx('dep.col_status')}</th><th scope="col">${tx('dep.col_pages')}</th><th scope="col">${tx('dep.col_used_by')}</th></tr></thead>
|
|
283
284
|
<tbody>${shown.map((o) => {
|
|
284
285
|
const url = objectUrl(o);
|
|
285
286
|
const status = o.missing
|
|
286
|
-
? html`<span class="tag tag-error"
|
|
287
|
+
? html`<span class="tag tag-error">${tx('dep.missing')}</span>`
|
|
287
288
|
: o.lacking.length
|
|
288
|
-
? html`<span class="tag tag-error"
|
|
289
|
-
: html`<span class="tag tag-success"
|
|
289
|
+
? html`<span class="tag tag-error">${tx('dep.no_grant')}</span> <span class="small dep-missing">${tx('dep.lacks', { role: deps.role, privileges: o.lacking.join(', ') })}</span>`
|
|
290
|
+
: html`<span class="tag tag-success">${tx('dep.ok')}</span> <span class="muted small">${o.privileges.join(', ')}</span>`;
|
|
290
291
|
return html`<tr${o.missing || o.lacking.length ? raw(' class="dep-problem"') : ''}>
|
|
291
|
-
<td data-label="
|
|
292
|
-
<td data-label="
|
|
293
|
-
<td data-label="
|
|
294
|
-
<td data-label="
|
|
295
|
-
<td data-label="
|
|
292
|
+
<td data-label="${tx('dep.col_object')}">${icon(KIND_ICON[o.kind])} ${url ? html`<a href="${url}">${o.display}</a>` : html`<span class="dep-missing">${o.display}</span>`}</td>
|
|
293
|
+
<td data-label="${tx('dep.col_type')}">${o.kind === 'relation' ? 'table or view' : o.kind}</td>
|
|
294
|
+
<td data-label="${tx('dep.col_status')}">${status}</td>
|
|
295
|
+
<td data-label="${tx('dep.col_pages')}">${pages(o).join(', ')}</td>
|
|
296
|
+
<td data-label="${tx('dep.col_used_by')}"><details class="dep-uses"><summary>${tx(o.usedBy.length === 1 ? 'dep.n_component' : 'dep.n_components', { n: o.usedBy.length })}</summary><ul class="dep-list">${o.usedBy.map(useLink)}</ul></details></td>
|
|
296
297
|
</tr>`;
|
|
297
298
|
})}</tbody></table></div>`
|
|
298
|
-
: html`<p class="muted"
|
|
299
|
+
: html`<p class="muted">${tx(filter || onlyProblems ? 'dep.no_match' : 'dep.no_objects')}</p>`;
|
|
299
300
|
const main = html`${appHeader(a, 'utilities', s)}
|
|
300
|
-
${region('
|
|
301
|
-
<p><span class="tag">${deps.objects.length}
|
|
301
|
+
${region(tx('util.c_deps'), html`<p class="muted u-mt0">${tx.rich('dep.intro', { n: deps.scanned, role: html`<code>${deps.role ?? tx('dep.runtime_connection')}</code>` })}</p>
|
|
302
|
+
<p><span class="tag">${tx('dep.n_objects', { n: deps.objects.length })}</span> <span class="tag tag-error">${tx('dep.n_missing', { n: missingN })}</span> <span class="tag tag-warning">${tx('dep.n_without_grants', { n: grantsN })}</span></p>
|
|
302
303
|
<form method="get" class="util-filters" role="search">
|
|
303
|
-
<div class="field"><label class="label" for="dep-q"
|
|
304
|
-
<div class="field"><label class="label" for="dep-show"
|
|
305
|
-
<div class="buttons"><button class="btn">${icon('filter')}
|
|
304
|
+
<div class="field"><label class="label" for="dep-q">${tx('dep.filter')}</label><input id="dep-q" name="q" type="search" value="${filter}" placeholder="${tx('dep.filter_ph')}"></div>
|
|
305
|
+
<div class="field"><label class="label" for="dep-show">${tx('common.show')}</label><select id="dep-show" name="show"><option value="">${tx('dep.all_objects')}</option><option value="problems"${onlyProblems ? raw(' selected') : ''}>${tx('dep.only_problems')}</option></select></div>
|
|
306
|
+
<div class="buttons"><button class="btn">${icon('filter')} ${tx('dep.filter')}</button></div>
|
|
306
307
|
</form>
|
|
307
308
|
${rows}`)}
|
|
308
|
-
${deps.unscanned.length ? region('
|
|
309
|
-
return send(reply, s, shell(s,
|
|
309
|
+
${deps.unscanned.length ? region(tx('dep.not_scanned'), html`<ul class="hits findings">${deps.unscanned.map((u) => html`<li class="finding-info">${u.entry ? html`<a href="${u.entry.url}">${icon(u.entry.icon)}<span>${u.entry.kindLabel}: ${u.entry.label}</span></a>` : html`<span>${u.field}</span>`}<span></span><span class="tag tag-info">${tx('dep.note')}</span><span class="hit-snippet"><b>${u.field}:</b> ${u.message}</span></li>`)}</ul>`) : ''}`;
|
|
310
|
+
return send(reply, s, shell(s, tx('dep.title', { name: a.name }), [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('app.tab.utilities'), `${BASE}/apps/${a.id}/utilities`], [tx('dep.crumb')]], main));
|
|
310
311
|
});
|
|
311
312
|
}
|