pgkiln 0.84.0 → 0.86.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/155_mail_available.sql +23 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/04-pages-and-regions.md +1 -1
- package/docs/guide/06-processing.md +7 -3
- package/docs/guide/07-dynamic-actions.md +3 -3
- package/docs/guide/09-reference.md +1 -0
- package/docs/guide/12-development.md +10 -1
- package/examples/gallery/README.md +23 -6
- package/examples/gallery/gallery.json +4 -0
- package/examples/gallery/samples/sample-approvals.sql +102 -11
- package/examples/gallery/samples/sample-calendar.sql +40 -5
- package/examples/gallery/samples/sample-dynamic-actions.sql +237 -9
- package/examples/gallery/samples/sample-grids.sql +35 -3
- package/examples/gallery/samples/sample-maps.sql +65 -7
- package/examples/gallery/samples/sample-processes.sql +466 -0
- package/examples/gallery/samples/sample-validations.sql +407 -0
- package/examples/gallery/samples/ux-patterns.sql +154 -14
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_80_notifications.sql +125 -0
- package/examples/hr/hr_81_vector_search_type.sql +33 -0
- package/package.json +1 -1
- package/public/app.css +8 -1
- package/public/app.js +28 -9
- package/src/app.ts +9 -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/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 +3453 -0
- package/src/builder/i18n/en.ts +3453 -0
- package/src/builder/i18n/es.ts +3453 -0
- package/src/builder/i18n/fr.ts +3453 -0
- package/src/builder/i18n/it.ts +3453 -0
- package/src/builder/i18n/ja.ts +3453 -0
- package/src/builder/i18n/ko.ts +3453 -0
- package/src/builder/i18n/nl.ts +3453 -0
- package/src/builder/i18n/pt.ts +3453 -0
- package/src/builder/i18n/zh-cn.ts +3453 -0
- package/src/builder/i18n/zh-tw.ts +3453 -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/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 +27 -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/runtime/engine.ts +6 -3
- package/src/runtime/lists.ts +2 -2
- package/src/runtime/push.ts +7 -1
- package/src/runtime/regions.ts +9 -4
|
@@ -13,6 +13,7 @@ import { sourceProblems } from '../websources.ts';
|
|
|
13
13
|
import { back, BASE, csrf, developer, flash, input, region, send, shell, type Body, type Req } from './ui.ts';
|
|
14
14
|
import { currentWorkspace } from './workspaces.ts';
|
|
15
15
|
import { scopeForWorkspace } from '../network-acl.ts';
|
|
16
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
16
17
|
|
|
17
18
|
// Workspace utilities → REST Source Catalogs (migration 095; APEX: Workspace
|
|
18
19
|
// Utilities → REST Source Catalogs): the current workspace's catalogs, a
|
|
@@ -25,7 +26,7 @@ import { scopeForWorkspace } from '../network-acl.ts';
|
|
|
25
26
|
const NOTES = '__CATALOG_NOTES';
|
|
26
27
|
const MAX_DOC_BYTES = 5 * 1024 * 1024;
|
|
27
28
|
const root = `${BASE}/rest-catalogs`;
|
|
28
|
-
const crumbs = (extra: [string, string?][] = []): [string, string?][] => [['
|
|
29
|
+
const crumbs = (extra: [string, string?][] = []): [string, string?][] => [[tx('rcat.app_builder'), BASE], [tx('rcat.workspace_utilities'), `${BASE}/utilities`], ['REST Source Catalogs', root], ...extra];
|
|
29
30
|
const when = (d: string | Date | null) => (d ? new Date(d).toISOString().slice(0, 16).replace('T', ' ') : '');
|
|
30
31
|
const pretty = (v: unknown) => JSON.stringify(v ?? null, null, 2);
|
|
31
32
|
|
|
@@ -41,22 +42,22 @@ function notesHtml(s: Session): Raw | '' {
|
|
|
41
42
|
delete s.state[NOTES];
|
|
42
43
|
const notes = JSON.parse(v) as string[];
|
|
43
44
|
return notes.length
|
|
44
|
-
? html`<div class="alert alert-info" role="status"><b
|
|
45
|
+
? html`<div class="alert alert-info" role="status"><b>${tx('rcat.what_the_document_had_that')}</b><ul>${notes.map((n) => html`<li>${n}</li>`)}</ul></div>`
|
|
45
46
|
: '';
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/** The import form: a file, pasted text or a URL; base URL and name optional. */
|
|
49
50
|
function importForm(s: Session, action: string, replacing: boolean): Raw {
|
|
50
51
|
return html`<form method="post" action="${action}" enctype="multipart/form-data">${csrf(s)}
|
|
51
|
-
<p class="muted u-mt0"
|
|
52
|
+
<p class="muted u-mt0">${tx.rich('rcat.doc_intro', { openapi: html`<b>${tx('rcat.openapi_3')}</b>`, swagger: html`<b>${tx('rcat.swagger_2_0')}</b>`, file: html`<code>openapi.json</code>` })}${replacing ? ` ${tx('rcat.doc_replacing')}` : ''}</p>
|
|
52
53
|
<div class="form-grid">
|
|
53
|
-
<div class="field"><label class="label" for="f_file"
|
|
54
|
-
${input('url', '
|
|
55
|
-
<div class="field" data-wide><label class="label" for="f_text"
|
|
56
|
-
${input('base_url', '
|
|
57
|
-
${replacing ? '' : input('name', '
|
|
54
|
+
<div class="field"><label class="label" for="f_file">${tx('rcat.file')}</label><input id="f_file" type="file" name="file" accept=".json,application/json"></div>
|
|
55
|
+
${input('url', tx('rcat.or_read_it_from_a'), '', { placeholder: tx('rcat.https_api_example_com_openapi'), help: tx('rcat.the_server_reads_it_the') })}
|
|
56
|
+
<div class="field" data-wide><label class="label" for="f_text">${tx('rcat.or_paste_it')}</label><textarea id="f_text" name="text" class="code" rows="4" spellcheck="false"></textarea></div>
|
|
57
|
+
${input('base_url', tx('rcat.base_url'), '', { placeholder: tx('rcat.from_the_document'), help: tx('rcat.instead_of_the_document_s') })}
|
|
58
|
+
${replacing ? '' : input('name', tx('rcat.name'), '', { placeholder: tx('rcat.the_document_s_title') })}
|
|
58
59
|
</div>
|
|
59
|
-
<div class="buttons"><button class="btn btn-hot">${icon('upload')} ${replacing ? '
|
|
60
|
+
<div class="buttons"><button class="btn btn-hot">${icon('upload')} ${replacing ? tx('rcat.replace_the_services') : tx('rcat.import')}</button></div></form>`;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/** The text of an uploaded, pasted or fetched document (and where it came from). */
|
|
@@ -66,14 +67,14 @@ async function documentText(req: Req): Promise<{ text: string; from?: string; ur
|
|
|
66
67
|
const parsed = await readMultipart(req, MAX_DOC_BYTES / 1024 / 1024);
|
|
67
68
|
req.body = parsed.body as Body;
|
|
68
69
|
const f = parsed.files.get('file');
|
|
69
|
-
if (f?.truncated) throw new Error('
|
|
70
|
+
if (f?.truncated) throw new Error(tx('rcat.the_file_is_larger_than'));
|
|
70
71
|
if (f) text = f.data.toString('utf8');
|
|
71
72
|
}
|
|
72
73
|
if (!text.trim()) text = String(req.body?.text ?? '');
|
|
73
74
|
if (text.trim()) return { text };
|
|
74
75
|
const url = String(req.body?.url ?? '').trim();
|
|
75
|
-
if (!url) throw new Error('
|
|
76
|
-
if (!/^https?:\/\//i.test(url)) throw new Error('
|
|
76
|
+
if (!url) throw new Error(tx('rcat.choose_a_file_paste_a'));
|
|
77
|
+
if (!/^https?:\/\//i.test(url)) throw new Error(tx('rcat.the_url_starts_with_http'));
|
|
77
78
|
return { text: '', url };
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -87,7 +88,7 @@ async function documentOf(doc: { text: string; from?: string; url?: string }, s:
|
|
|
87
88
|
headers: { accept: 'application/json' }, timeoutMs: 15_000, maxBytes: MAX_DOC_BYTES,
|
|
88
89
|
scope: await scopeForWorkspace(currentWorkspace(s), 'REST source catalog (read from a URL)'),
|
|
89
90
|
});
|
|
90
|
-
if (r.status !== 200) throw new Error(
|
|
91
|
+
if (r.status !== 200) throw new Error(tx('rcat.url_answered', { status: r.status }));
|
|
91
92
|
return { text: r.body.toString('utf8'), from: r.url };
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -123,17 +124,17 @@ function serviceForm(s: Session, action: string, v: Partial<CatalogService>, lab
|
|
|
123
124
|
html`<div class="field" data-wide><label class="label" for="f_${name}">${title}</label><textarea id="f_${name}" name="${name}" class="code" rows="${rows}" spellcheck="false">${value}</textarea><small class="help">${help}</small></div>`;
|
|
124
125
|
return html`<form method="post" action="${action}">${csrf(s)}
|
|
125
126
|
<div class="form-grid">
|
|
126
|
-
${input('name', '
|
|
127
|
-
<div class="field"><label class="label" for="f_method"
|
|
128
|
-
${input('path', '
|
|
129
|
-
${input('row_selector', '
|
|
130
|
-
<div class="field" data-wide><label class="label" for="f_description"
|
|
131
|
-
${area('params', '
|
|
132
|
-
${area('columns', '
|
|
133
|
-
${area('headers', '
|
|
134
|
-
${area('body', '
|
|
135
|
-
${input('key_columns', '
|
|
136
|
-
${area('operations', '
|
|
127
|
+
${input('name', tx('rcat.name'), v.name ?? '', { required: true, placeholder: 'GET_PETS', help: tx('rcat.the_data_source_s_name') })}
|
|
128
|
+
<div class="field"><label class="label" for="f_method">${tx('rcat.method')}</label><select id="f_method" name="method">${['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map((m) => html`<option${(v.method ?? 'GET') === m ? raw(' selected') : ''}>${m}</option>`)}</select></div>
|
|
129
|
+
${input('path', tx('rcat.path'), v.path ?? '', { required: true, placeholder: tx('rcat.pets_pet_id'), help: tx('rcat.after_the_catalog_s_base') })}
|
|
130
|
+
${input('row_selector', tx('rcat.row_selector'), v.row_selector ?? '', { placeholder: 'items', help: tx('rcat.where_the_rows_are_in') })}
|
|
131
|
+
<div class="field" data-wide><label class="label" for="f_description">${tx('rcat.description')}</label><input id="f_description" name="description" value="${v.description ?? ''}"></div>
|
|
132
|
+
${area('params', tx('rcat.parameters_json'), pretty(v.params ?? []), tx('rcat.params_help'))}
|
|
133
|
+
${area('columns', tx('rcat.columns_json'), pretty(v.columns ?? []), '[{"name": "id", "path": "id", "type": "integer"}] · types: text, number, integer, boolean, date, timestamp, json.', 6)}
|
|
134
|
+
${area('headers', tx('rcat.headers_json'), pretty(v.headers ?? {}), tx('rcat.headers_help'), 2)}
|
|
135
|
+
${area('body', tx('rcat.body_template'), v.body ?? '', '{"query": {q}} — empty: the body parameters as a JSON object.', 2)}
|
|
136
|
+
${input('key_columns', tx('rcat.key_columns'), (v.key_columns ?? []).join(', '), { placeholder: 'id' })}
|
|
137
|
+
${area('operations', tx('rcat.operations_json'), pretty(v.operations ?? {}), '{"update": {"method": "PUT", "path": "/{id}"}, "delete": {"method": "DELETE", "path": "/{id}"}} · write-back for forms and grids.', 3)}
|
|
137
138
|
</div>
|
|
138
139
|
<div class="buttons"><button class="btn btn-hot">${icon('save')} ${label}</button></div></form>`;
|
|
139
140
|
}
|
|
@@ -147,24 +148,24 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
147
148
|
`select c.id, c.name, c.description, c.base_url, c.server, c.source, c.api_version, c.updated_at,
|
|
148
149
|
(select count(*)::int from meta.rest_catalog_service x where x.catalog_id = c.id) as services
|
|
149
150
|
from meta.rest_catalog c where c.workspace_id = $1 order by lower(c.name)`, [currentWorkspace(s)])).rows;
|
|
150
|
-
const main = html`<div class="title-row"><h1
|
|
151
|
-
<p class="muted u-mt0"
|
|
151
|
+
const main = html`<div class="title-row"><h1>${tx('rcat.rest_source_catalogs')}</h1></div>
|
|
152
|
+
<p class="muted u-mt0">${tx('rcat.intro', { workspace: s.workspace?.name ?? tx('rcat.this_workspace') })}</p>
|
|
152
153
|
${notesHtml(s)}
|
|
153
|
-
${region('
|
|
154
|
-
<thead><tr><th
|
|
154
|
+
${region(tx('rcat.catalogs'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
155
|
+
<thead><tr><th>${tx('rcat.catalog')}</th><th>${tx('rcat.base_url')}</th><th class="num">${tx('rcat.services')}</th><th>${tx('rcat.from')}</th><th>${tx('rcat.updated_utc')}</th></tr></thead>
|
|
155
156
|
<tbody>${rows.length ? rows.map((c) => html`<tr>
|
|
156
|
-
<td data-label="
|
|
157
|
-
<td data-label="
|
|
158
|
-
<td class="num" data-label="
|
|
159
|
-
<td data-label="
|
|
160
|
-
<td data-label="
|
|
161
|
-
: html`<tr><td colspan="5" class="empty"
|
|
162
|
-
${region('
|
|
163
|
-
${region('
|
|
164
|
-
<div class="form-grid">${input('name', '
|
|
165
|
-
<div class="field" data-wide><label class="label" for="f_cdescription"
|
|
166
|
-
<div class="buttons"><button class="btn">${icon('plus')}
|
|
167
|
-
return send(reply, s, shell(s, '
|
|
157
|
+
<td data-label="${tx('rcat.catalog')}"><a href="${root}/${c.id}">${c.name}</a>${c.description ? html`<br><span class="muted small">${String(c.description).slice(0, 160)}</span>` : ''}</td>
|
|
158
|
+
<td data-label="${tx('rcat.base_url')}">${c.server ? html`<span class="tag">${icon('server')} ${c.server}</span> ` : ''}<code class="wrap-anywhere">${c.base_url ?? ''}</code></td>
|
|
159
|
+
<td class="num" data-label="${tx('rcat.services')}">${c.services}</td>
|
|
160
|
+
<td data-label="${tx('rcat.from')}">${c.source === 'openapi' ? html`OpenAPI${c.api_version ? html` <span class="muted">${c.api_version}</span>` : ''}` : 'by hand'}</td>
|
|
161
|
+
<td data-label="${tx('rcat.updated')}">${when(c.updated_at)}</td></tr>`)
|
|
162
|
+
: html`<tr><td colspan="5" class="empty">${tx('rcat.no_catalogs_yet_import_an')}</td></tr>`}</tbody></table></div>`)}
|
|
163
|
+
${region(tx('rcat.import_a_catalog'), importForm(s, `${root}/import`, false))}
|
|
164
|
+
${region(tx('rcat.create_a_catalog_by_hand'), html`<form method="post" action="${root}">${csrf(s)}
|
|
165
|
+
<div class="form-grid">${input('name', tx('rcat.name'), '', { required: true, placeholder: tx('rcat.e_g_weather_service') })}${input('base_url', tx('rcat.base_url'), '', { required: true, placeholder: tx('rcat.https_api_example_com_v1') })}
|
|
166
|
+
<div class="field" data-wide><label class="label" for="f_cdescription">${tx('rcat.description')}</label><input id="f_cdescription" name="description"></div></div>
|
|
167
|
+
<div class="buttons"><button class="btn">${icon('plus')} ${tx('rcat.create')}</button></div></form>`)}`;
|
|
168
|
+
return send(reply, s, shell(s, tx('rcat.rest_source_catalogs'), crumbs(), main));
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
app.post(root, async (req: Req, reply) => {
|
|
@@ -181,7 +182,7 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
181
182
|
}
|
|
182
183
|
try {
|
|
183
184
|
const id = await insertCatalog(owner, currentWorkspace(s), c, s.username);
|
|
184
|
-
flash(s, '
|
|
185
|
+
flash(s, tx('rcat.catalog_created_add_its_services'));
|
|
185
186
|
return back(reply, s, `${root}/${id}`);
|
|
186
187
|
} catch (e) {
|
|
187
188
|
flash(s, uniqueError(e), 'error');
|
|
@@ -203,14 +204,14 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
203
204
|
doc = await documentOf(doc, s);
|
|
204
205
|
const { catalog, notes } = readCatalogText(doc.text, { baseUrl: req.body?.base_url, from: doc.from, name: req.body?.name });
|
|
205
206
|
const problems = wholeCatalogProblems(catalog);
|
|
206
|
-
if (problems.length) throw new Error(
|
|
207
|
+
if (problems.length) throw new Error(tx('rcat.unusable', { problems: `${problems.slice(0, 8).join(' ')}${problems.length > 8 ? ` ${tx('rcat.and_more', { n: problems.length - 8 })}` : ''}` }));
|
|
207
208
|
const id = await owner.tx((c) => insertCatalog(c, currentWorkspace(s), catalog, s.username));
|
|
208
209
|
s.state[NOTES] = JSON.stringify(notes);
|
|
209
210
|
await logActivity({ appId: null, username: s.username, event: 'rest_catalog', ip: clientIp(req), detail: `imported catalog ${catalog.name} (${catalog.services.length} services)` });
|
|
210
|
-
flash(s,
|
|
211
|
+
flash(s, tx('rcat.imported', { name: catalog.name, n: catalog.services.length }));
|
|
211
212
|
return back(reply, s, `${root}/${id}`);
|
|
212
213
|
} catch (e) {
|
|
213
|
-
flash(s,
|
|
214
|
+
flash(s, tx('rcat.import_failed', { error: uniqueError(e) }), 'error');
|
|
214
215
|
return back(reply, s, root);
|
|
215
216
|
}
|
|
216
217
|
});
|
|
@@ -224,35 +225,35 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
224
225
|
const base = `${root}/${c.id}`;
|
|
225
226
|
const sel = req.query.service ? c.services.find((x) => String(x.id) === req.query.service) : null;
|
|
226
227
|
const main = html`<div class="title-row"><h1>${c.name}</h1>
|
|
227
|
-
<a class="btn" href="${base}/download">${icon('download')}
|
|
228
|
-
<p class="muted u-mt0">${c.server ? html
|
|
228
|
+
<a class="btn" href="${base}/download">${icon('download')} ${tx('rcat.download')}</a></div>
|
|
229
|
+
<p class="muted u-mt0">${c.server ? html`${tx.rich('rcat.on_server', { server: html`<a href="${BASE}/remote-servers">${c.server}</a>` })} · ` : ''}<code class="wrap-anywhere">${c.base_url ?? ''}</code>${c.source === 'openapi' ? html` · ${c.api_version ? tx('rcat.from_openapi_version', { version: c.api_version }) : tx('rcat.from_openapi')}` : ''} · ${tx('rcat.updated_when', { when: when(c.updated_at) })}</p>
|
|
229
230
|
${notesHtml(s)}
|
|
230
|
-
${region('
|
|
231
|
-
<thead><tr><th
|
|
231
|
+
${region(tx('rcat.services'), html`<div class="table-wrap"><table class="report report-reflow">
|
|
232
|
+
<thead><tr><th>${tx('rcat.service')}</th><th>${tx('rcat.method')}</th><th>${tx('rcat.path')}</th><th class="num">${tx('rcat.parameters')}</th><th class="num">${tx('rcat.columns')}</th><th>${tx('rcat.write_back')}</th></tr></thead>
|
|
232
233
|
<tbody>${c.services.length ? c.services.map((x) => html`<tr${sel?.id === x.id ? raw(' aria-current="true"') : ''}>
|
|
233
|
-
<td data-label="
|
|
234
|
-
<td data-label="
|
|
235
|
-
<td data-label="
|
|
236
|
-
<td class="num" data-label="
|
|
237
|
-
<td class="num" data-label="
|
|
238
|
-
<td data-label="
|
|
239
|
-
: html`<tr><td colspan="6" class="empty"
|
|
240
|
-
<p class="u-m0 u-mt1"><a class="btn" href="${base}?service=new#service">${icon('plus')}
|
|
234
|
+
<td data-label="${tx('rcat.service')}"><a href="${base}?service=${x.id}#service">${x.name}</a>${x.description ? html`<br><span class="muted small">${x.description.slice(0, 140)}</span>` : ''}</td>
|
|
235
|
+
<td data-label="${tx('rcat.method')}"><span class="tag">${x.method}</span></td>
|
|
236
|
+
<td data-label="${tx('rcat.path')}"><code class="wrap-anywhere">${x.path}</code></td>
|
|
237
|
+
<td class="num" data-label="${tx('rcat.parameters')}">${x.params.length}</td>
|
|
238
|
+
<td class="num" data-label="${tx('rcat.columns')}">${x.columns.length}</td>
|
|
239
|
+
<td data-label="${tx('rcat.write_back')}">${Object.keys(x.operations).join(', ') || html`<span class="muted">—</span>`}</td></tr>`)
|
|
240
|
+
: html`<tr><td colspan="6" class="empty">${tx('rcat.no_services_yet')}</td></tr>`}</tbody></table></div>
|
|
241
|
+
<p class="u-m0 u-mt1"><a class="btn" href="${base}?service=new#service">${icon('plus')} ${tx('rcat.add_a_service')}</a></p>`)}
|
|
241
242
|
${req.query.service === 'new'
|
|
242
|
-
? region('
|
|
243
|
+
? region(tx('rcat.new_service'), serviceForm(s, `${base}/services`, { method: 'GET', path: '/' }, tx('rcat.add')), '', 'service')
|
|
243
244
|
: sel
|
|
244
|
-
? region(
|
|
245
|
-
${serviceForm(s, `${base}/services/${sel.id}`, sel, '
|
|
246
|
-
<form method="post" action="${base}/services/${sel.id}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="
|
|
245
|
+
? region(tx('rcat.service_title', { name: sel.name }), html`<p class="muted u-mt0">${tx('rcat.url')} <code class="wrap-anywhere">${c.server ? `${c.server} + ${sel.path}` : serviceUrl(c.base_url ?? '', sel.path)}</code></p>
|
|
246
|
+
${serviceForm(s, `${base}/services/${sel.id}`, sel, tx('rcat.save'))}
|
|
247
|
+
<form method="post" action="${base}/services/${sel.id}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="${tx('rcat.delete_service_confirm', { name: sel.name })}">${tx('rcat.delete_service')}</button></form>`, '', 'service')
|
|
247
248
|
: ''}
|
|
248
|
-
${region('
|
|
249
|
-
<div class="form-grid">${input('name', '
|
|
250
|
-
${input('server', '
|
|
251
|
-
${input('api_version', '
|
|
252
|
-
<div class="field" data-wide><label class="label" for="f_cdescription"
|
|
253
|
-
<div class="buttons"><button class="btn btn-hot">${icon('save')}
|
|
254
|
-
${region('
|
|
255
|
-
<form method="post" action="${base}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="
|
|
249
|
+
${region(tx('rcat.catalog'), html`<form method="post" action="${base}">${csrf(s)}
|
|
250
|
+
<div class="form-grid">${input('name', tx('rcat.name'), c.name, { required: true })}${input('base_url', tx('rcat.base_url'), c.base_url ?? '', { help: tx('rcat.e_g_a_test_system') })}
|
|
251
|
+
${input('server', tx('rcat.remote_server'), c.server ?? '', { placeholder: tx('rcat.e_g_crm'), help: tx('rcat.the_static_id_of_a') })}
|
|
252
|
+
${input('api_version', tx('rcat.api_version'), c.api_version ?? '')}
|
|
253
|
+
<div class="field" data-wide><label class="label" for="f_cdescription">${tx('rcat.description')}</label><textarea id="f_cdescription" name="description" rows="2">${c.description ?? ''}</textarea></div></div>
|
|
254
|
+
<div class="buttons"><button class="btn btn-hot">${icon('save')} ${tx('rcat.save')}</button></div></form>`)}
|
|
255
|
+
${region(tx('rcat.replace_from_a_document'), importForm(s, `${base}/import`, true))}
|
|
256
|
+
<form method="post" action="${base}/delete" class="danger-zone">${csrf(s)}<button class="btn btn-danger" data-confirm="${tx('rcat.delete_catalog_confirm', { name: c.name })}">${tx('rcat.delete_catalog')}</button></form>`;
|
|
256
257
|
return send(reply, s, shell(s, c.name, crumbs([[c.name]]), main));
|
|
257
258
|
});
|
|
258
259
|
|
|
@@ -274,9 +275,9 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
274
275
|
await owner.query('update meta.rest_catalog set name = $2, base_url = $3, description = $4, api_version = $5, server = $6, updated_at = now() where id = $1',
|
|
275
276
|
[c.id, v.name, v.base_url || null, v.description, v.api_version, v.server]);
|
|
276
277
|
if (v.server && !(await owner.one('select 1 as ok from meta.remote_server where workspace_id = $1 and static_id = $2', [c.workspace_id, v.server])))
|
|
277
|
-
flash(s,
|
|
278
|
+
flash(s, tx('rcat.no_server', { server: v.server }), 'error');
|
|
278
279
|
else
|
|
279
|
-
flash(s, '
|
|
280
|
+
flash(s, tx('rcat.catalog_saved'));
|
|
280
281
|
} catch (e) {
|
|
281
282
|
flash(s, uniqueError(e), 'error');
|
|
282
283
|
}
|
|
@@ -305,9 +306,9 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
305
306
|
await owner.tx((db) => replaceCatalog(db, c.id, catalog));
|
|
306
307
|
s.state[NOTES] = JSON.stringify(notes);
|
|
307
308
|
await logActivity({ appId: null, username: s.username, event: 'rest_catalog', ip: clientIp(req), detail: `replaced catalog ${c.name} (${catalog.services.length} services)` });
|
|
308
|
-
flash(s,
|
|
309
|
+
flash(s, tx('rcat.replaced', { name: c.name, n: catalog.services.length }));
|
|
309
310
|
} catch (e) {
|
|
310
|
-
flash(s,
|
|
311
|
+
flash(s, tx('rcat.import_failed', { error: uniqueError(e) }), 'error');
|
|
311
312
|
}
|
|
312
313
|
return back(reply, s, `${root}/${c.id}`);
|
|
313
314
|
});
|
|
@@ -330,7 +331,7 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
330
331
|
if (!c) return reply.code(404).send('Not found');
|
|
331
332
|
await owner.query('delete from meta.rest_catalog where id = $1', [c.id]);
|
|
332
333
|
await logActivity({ appId: null, username: s.username, event: 'rest_catalog', ip: clientIp(req), detail: `deleted catalog ${c.name}` });
|
|
333
|
-
flash(s,
|
|
334
|
+
flash(s, tx('rcat.deleted', { name: c.name }));
|
|
334
335
|
return back(reply, s, root);
|
|
335
336
|
});
|
|
336
337
|
|
|
@@ -359,7 +360,7 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
359
360
|
`insert into meta.rest_catalog_service (catalog_id, name, description, path, method, params, headers, body, row_selector, columns, key_columns, operations, position)
|
|
360
361
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, (select coalesce(max(position), 0) + 10 from meta.rest_catalog_service where catalog_id = $1)) returning id`, [c.id, ...vals])).id;
|
|
361
362
|
await owner.query('update meta.rest_catalog set updated_at = now() where id = $1', [c.id]);
|
|
362
|
-
flash(s,
|
|
363
|
+
flash(s, tx('rcat.service_saved', { name: v.name }));
|
|
363
364
|
return back(reply, s, `${root}/${c.id}?service=${id}#service`);
|
|
364
365
|
} catch (e) {
|
|
365
366
|
flash(s, uniqueError(e), 'error');
|
|
@@ -376,7 +377,7 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
376
377
|
if (!c) return reply.code(404).send('Not found');
|
|
377
378
|
await owner.query('delete from meta.rest_catalog_service where id = $1 and catalog_id = $2', [req.params.sid, c.id]);
|
|
378
379
|
await owner.query('update meta.rest_catalog set updated_at = now() where id = $1', [c.id]);
|
|
379
|
-
flash(s, '
|
|
380
|
+
flash(s, tx('rcat.service_deleted'));
|
|
380
381
|
return back(reply, s, `${root}/${c.id}`);
|
|
381
382
|
});
|
|
382
383
|
|
|
@@ -392,12 +393,12 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
392
393
|
const svc = await owner.one<{ id: number; catalog_id: number }>(
|
|
393
394
|
`select x.id, x.catalog_id from meta.rest_catalog_service x join meta.rest_catalog c on c.id = x.catalog_id
|
|
394
395
|
where x.id = $1 and c.workspace_id = meta.app_workspace($2)`, [/^\d{1,9}$/.test(String(req.body?.service)) ? req.body!.service : null, appId]);
|
|
395
|
-
if (!svc) throw new Error('
|
|
396
|
+
if (!svc) throw new Error(tx('rcat.choose_a_service_of_one'));
|
|
396
397
|
const c = (await loadCatalog(svc.catalog_id))!;
|
|
397
398
|
const v = sourceFromService(c, c.services.find((x) => x.id === svc.id)!, { name: req.body?.name, credential: req.body?.credential });
|
|
398
|
-
if (!/^[A-Z][A-Z0-9_]{0,59}$/.test(v.name)) throw new Error('
|
|
399
|
+
if (!/^[A-Z][A-Z0-9_]{0,59}$/.test(v.name)) throw new Error(tx('rcat.the_name_is_upper_case'));
|
|
399
400
|
if (v.credential && !(await owner.one('select 1 as ok from meta.web_credential where app_id = $1 and name = $2 union all select 1 from meta.workspace_credential where workspace_id = meta.app_workspace($1) and name = $2', [appId, v.credential])))
|
|
400
|
-
throw new Error(
|
|
401
|
+
throw new Error(tx('rcat.no_credential', { name: v.credential }));
|
|
401
402
|
const problems = sourceProblems(v);
|
|
402
403
|
if (problems.length) throw new Error(problems.join(' '));
|
|
403
404
|
const r = await owner.one<{ id: number }>(
|
|
@@ -408,7 +409,7 @@ export async function restCatalogRoutes(app: FastifyInstance) {
|
|
|
408
409
|
return back(reply, s, `${BASE}/apps/${appId}/shared?c=rest_source-${r!.id}`);
|
|
409
410
|
} catch (e) {
|
|
410
411
|
const m = (e as Error).message;
|
|
411
|
-
flash(s, m.includes('rest_source_app_id_name_key') ? '
|
|
412
|
+
flash(s, m.includes('rest_source_app_id_name_key') ? tx('rcat.this_application_has_a_rest') : m, 'error');
|
|
412
413
|
return back(reply, s, retry);
|
|
413
414
|
}
|
|
414
415
|
});
|
|
@@ -430,18 +431,18 @@ export async function catalogPicker(appId: number, s: Session): Promise<Raw | ''
|
|
|
430
431
|
`select x.id, x.name, x.method, x.path, c.name as catalog from meta.rest_catalog_service x join meta.rest_catalog c on c.id = x.catalog_id
|
|
431
432
|
where c.workspace_id = meta.app_workspace($1) order by lower(c.name), x.position, x.name`, [appId])).rows;
|
|
432
433
|
if (!services.length)
|
|
433
|
-
return html`<p class="muted u-mt0 u-mb1">${icon('globe')}
|
|
434
|
+
return html`<p class="muted u-mt0 u-mb1">${icon('globe')} ${tx.rich('rcat.described_once', { link: html`<a href="${root}">${tx('rcat.rest_source_catalogs')}</a>` })}</p>`;
|
|
434
435
|
const credentials = (await owner.query('select name from meta.web_credential where app_id = $1 union select name from meta.workspace_credential where workspace_id = meta.app_workspace($1) order by name', [appId])).rows;
|
|
435
436
|
const groups = new Map<string, typeof services>();
|
|
436
437
|
for (const x of services) groups.set(x.catalog, [...(groups.get(x.catalog) ?? []), x]);
|
|
437
|
-
return region('
|
|
438
|
-
<p class="muted u-mt0"
|
|
438
|
+
return region(tx('rcat.from_a_rest_source_catalog'), html`<form method="post" action="${BASE}/apps/${appId}/rest-sources/from-catalog">${csrf(s)}
|
|
439
|
+
<p class="muted u-mt0">${tx.rich('rcat.copy_of', { link: html`<a href="${root}">${tx('rcat.catalogs_link')}</a>` })}</p>
|
|
439
440
|
<div class="form-grid">
|
|
440
|
-
<div class="field" data-wide><label class="label" for="f_service"
|
|
441
|
+
<div class="field" data-wide><label class="label" for="f_service">${tx('rcat.service')}</label><select id="f_service" name="service" required>
|
|
441
442
|
${[...groups].map(([cat, xs]) => html`<optgroup label="${cat}">${xs.map((x) => html`<option value="${x.id}">${x.name} · ${x.method} ${x.path}</option>`)}</optgroup>`)}</select></div>
|
|
442
|
-
${input('name', '
|
|
443
|
-
<div class="field"><label class="label" for="f_credential"
|
|
443
|
+
${input('name', tx('rcat.name'), '', { placeholder: tx('rcat.the_service_s_name') })}
|
|
444
|
+
<div class="field"><label class="label" for="f_credential">${tx('rcat.web_credential')}</label><select id="f_credential" name="credential"><option value="">none</option>${credentials.map((c) => html`<option>${c.name}</option>`)}</select></div>
|
|
444
445
|
</div>
|
|
445
|
-
<div class="buttons"><button class="btn btn-hot">${icon('plus')}
|
|
446
|
+
<div class="buttons"><button class="btn btn-hot">${icon('plus')} ${tx('rcat.create_from_the_catalog')}</button></div></form>`);
|
|
446
447
|
}
|
|
447
448
|
|
package/src/builder/rest.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html } from '../html.ts';
|
|
2
2
|
import { handlerProblems, type Handler } from '../runtime/rest.ts';
|
|
3
3
|
import { publicUrl } from '../sso.ts';
|
|
4
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
4
5
|
|
|
5
6
|
// Shared Components → REST modules: the endpoints, curl examples and the
|
|
6
7
|
// OpenAPI description of a module.
|
|
@@ -10,15 +11,15 @@ export function restExtras(a: { id: number; alias: string }, row: { name: string
|
|
|
10
11
|
const handlers = Array.isArray(row.handlers) ? row.handlers : [];
|
|
11
12
|
const problems = handlerProblems(handlers);
|
|
12
13
|
const example = handlers.find((h) => h.method === 'GET') ?? handlers[0];
|
|
13
|
-
return html`<fieldset class="prop-group u-mt125"><legend
|
|
14
|
+
return html`<fieldset class="prop-group u-mt125"><legend>${tx('rst.endpoints')}</legend>
|
|
14
15
|
${problems.length ? html`<div class="alert alert-error" role="alert"><ul class="u-m0">${problems.map((p) => html`<li>${p}</li>`)}</ul></div>` : ''}
|
|
15
|
-
${!row.enabled ? html`<p class="muted"
|
|
16
|
-
<div class="table-wrap"><table class="report report-reflow"><thead><tr><th
|
|
17
|
-
<tbody>${handlers.map((h) => html`<tr><td data-label="
|
|
18
|
-
<td data-label="
|
|
19
|
-
${example ? html`<p class="muted small"
|
|
16
|
+
${!row.enabled ? html`<p class="muted">${tx('rst.disabled')}</p>` : ''}
|
|
17
|
+
<div class="table-wrap"><table class="report report-reflow"><thead><tr><th>${tx('rst.method')}</th><th>${tx('rst.url')}</th><th>${tx('rst.type')}</th><th>${tx('rst.access')}</th></tr></thead>
|
|
18
|
+
<tbody>${handlers.map((h) => html`<tr><td data-label="${tx('rst.method')}"><code>${h.method}</code></td><td data-label="${tx('rst.url')}"><code>${base}/${h.path}</code></td>
|
|
19
|
+
<td data-label="${tx('rst.type')}">${h.type}</td><td data-label="${tx('rst.access')}">${h.auth === 'public' ? 'public' : h.roles?.length ? `token, role ${h.roles.join(' or ')}` : 'token'}</td></tr>`)}</tbody></table></div>
|
|
20
|
+
${example ? html`<p class="muted small">${tx.rich('rst.try_it', { link: html`<a href="/builder/apps/${a.id}/api">${tx('rst.rest_api')}</a>` })}</p>
|
|
20
21
|
<pre class="source">curl ${base}/${example.path.replace(/:([a-z_]+)/gi, '1')} -H "Authorization: Bearer $TOKEN"</pre>` : ''}
|
|
21
|
-
<p class="muted small"
|
|
22
|
-
<p class="wrap-anywhere"
|
|
22
|
+
<p class="muted small">${tx.rich('rst.edit_in', { link: html`<a href="/builder/sql/rest/modules">${tx('rst.modules_path')}</a>` })}</p>
|
|
23
|
+
<p class="wrap-anywhere">${tx('rst.openapi')} <a href="${base}/openapi.json" target="_blank" rel="noopener"><code>${base}/openapi.json</code></a></p>
|
|
23
24
|
</fieldset>`;
|
|
24
25
|
}
|
package/src/builder/routes.ts
CHANGED
|
@@ -24,13 +24,13 @@ import { securityRoutes } from './security.ts';
|
|
|
24
24
|
import { instanceRoutes } from './instance.ts';
|
|
25
25
|
import { saveTimeZoneSettings, timeZoneSettings } from './globalization.ts';
|
|
26
26
|
import { notificationSettings, saveNotificationSettings } from './notification-settings.ts';
|
|
27
|
-
import { WIZARD_KINDS, wizardRoutes, wizardTables } from './wizards.ts';
|
|
27
|
+
import { kindLabel, WIZARD_KINDS, wizardRoutes, wizardTables } from './wizards.ts';
|
|
28
28
|
import { createPageRoutes } from './create-page.ts';
|
|
29
29
|
import { featureRoutes } from './features.ts';
|
|
30
30
|
import { appFromFileRoutes } from './appfromfile.ts';
|
|
31
31
|
import { clearDevbar } from '../runtime/devbar.ts';
|
|
32
32
|
import { EMAIL } from './find-workspace.ts';
|
|
33
|
-
import { cookieLanguage, LANG_COOKIE, tr, translatorFor, validBuilderLanguage } from './i18n.ts';
|
|
33
|
+
import { scopedTx as tx, cookieLanguage, LANG_COOKIE, tr, translatorFor, validBuilderLanguage } from './i18n.ts';
|
|
34
34
|
import { checkSecondFactor, codesLeft, forgetBrowsers, hasFactor, isTrustedBrowser, leavesTooFewAdmins, passkeyByCredential, passkeySignin, passkeysOf, trustBrowser, trustDays, usePasskey, userHandle } from '../mfa.ts';
|
|
35
35
|
import { isIP } from 'node:net';
|
|
36
36
|
import { giveChallenge, takeChallenge } from './security.ts';
|
|
@@ -588,7 +588,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
588
588
|
if (!s) return;
|
|
589
589
|
try {
|
|
590
590
|
const ws = currentWorkspace(s);
|
|
591
|
-
if (ws < 1) throw new Error('
|
|
591
|
+
if (ws < 1) throw new Error(tx('rte.you_are_not_a_developer'));
|
|
592
592
|
const doc = JSON.parse(req.body?.doc ?? '');
|
|
593
593
|
const r = await owner.tx(async (c) => {
|
|
594
594
|
const made = (await c.query('select meta.import_app($1::jsonb, $2) as id', [JSON.stringify(doc), req.body?.alias?.trim() || null])).rows[0];
|
|
@@ -687,7 +687,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
687
687
|
<p class="muted u-mt0">${tx('app.from_table_text')}</p>
|
|
688
688
|
<form method="get" action="${BASE}/apps/${a.id}/wizard">
|
|
689
689
|
<div class="form-grid">
|
|
690
|
-
${select('kind', tx('app.page_type'), 'report_form', WIZARD_KINDS.map(([k, label]): [string, string] => [k, label]))}
|
|
690
|
+
${select('kind', tx('app.page_type'), 'report_form', WIZARD_KINDS.map(([k, label]): [string, string] => [k, kindLabel(k, label)]))}
|
|
691
691
|
${select('table', tx('app.table_or_view'), '', tables.map((t): [string, string] => [t.t, t.access ? t.t : tx('app.no_access', { table: t.t, role: a.db_role })]))}
|
|
692
692
|
</div>
|
|
693
693
|
<div class="buttons"><button class="btn btn-hot">${tx('common.next')}</button></div>
|
|
@@ -723,7 +723,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
723
723
|
<li><a href="${BASE}/apps/${a.id}/settings?tab=definition">${icon('trash')} ${tx('app.task.delete')}</a></li>
|
|
724
724
|
</ul></aside>
|
|
725
725
|
</div>`;
|
|
726
|
-
return send(reply, s, shell(s, a.name, [['
|
|
726
|
+
return send(reply, s, shell(s, a.name, [[tx('rte.app_builder'), BASE], [a.name]], main));
|
|
727
727
|
});
|
|
728
728
|
|
|
729
729
|
app.post(`${BASE}/apps/:id/pages`, async (req: Req, reply) => {
|
|
@@ -776,8 +776,8 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
776
776
|
// ---------------------------------------------------------------- settings
|
|
777
777
|
/** (sprint 74) The tabs of an application's Settings. */
|
|
778
778
|
const SETTINGS_TABS: [string, string][] = [
|
|
779
|
-
['definition', '
|
|
780
|
-
['globalization', '
|
|
779
|
+
['definition', tx('rte.definition')], ['security', tx('rte.security')], ['signin', tx('rte.sign_in')], ['ui', tx('rte.user_interface')],
|
|
780
|
+
['globalization', tx('rte.globalization')], ['errors', tx('rte.errors_and_sessions')], ['pwa', tx('rte.progressive_web_app')],
|
|
781
781
|
];
|
|
782
782
|
app.get(`${BASE}/apps/:id/settings`, async (req: Req, reply) => {
|
|
783
783
|
const s = await developer(req, reply);
|
|
@@ -910,17 +910,17 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
910
910
|
${panel('security')}${region(tx('st.checklist'), html`<ul class="checklist">
|
|
911
911
|
<li>${a.db_role ? '✓' : '✗'} ${tx('st.ck.role')} ${a.db_role ? html`(<code>${a.db_role}</code>)` : html`<b>${tx('st.ck.runtime')}</b>`}</li>
|
|
912
912
|
<li>${a.authentication !== 'none' ? '✓' : '•'} ${a.authentication !== 'none' ? tx('st.ck.signin') : tx('st.ck.public')}</li>
|
|
913
|
-
${a.authentication === 'header' ? html`<li>${headerProxiesConfigured() ? '✓' : '✗'} Sign-in: HTTP header <code>${a.header_name || DEFAULT_HEADER}</code> ${headerProxiesConfigured() ? 'from the proxies in PGKILN_AUTH_HEADER_PROXIES' : html`<b
|
|
914
|
-
${a.authentication === 'database' ? html`<li>${a.db_auth_roles?.length || a.db_auth_member_of ? '✓' : '✗'} Sign-in: database accounts (${[a.db_auth_roles?.length ? `roles ${a.db_auth_roles.join(', ')}` : '', a.db_auth_member_of ? `members of ${a.db_auth_member_of}` : ''].filter(Boolean).join('; ') || html`<b
|
|
915
|
-
${a.authentication === 'custom' ? html`<li>${a.custom_auth_function || a.custom_auth_code ? '✓' : '✗'} Sign-in: custom ${a.custom_auth_function ? html`function <code>${a.custom_auth_function}</code>` : a.custom_auth_code ? 'function body' : html`<b
|
|
916
|
-
${a.authentication === 'app_users' ? html`<li>Sign-in: ${[a.local_login ? 'password' : '', ...a.ldap_directories.map((d: string) => `LDAP ${d}`), ...a.sso_providers].filter(Boolean).join(', ') || html`<b
|
|
913
|
+
${a.authentication === 'header' ? html`<li>${headerProxiesConfigured() ? '✓' : '✗'} Sign-in: HTTP header <code>${a.header_name || DEFAULT_HEADER}</code> ${headerProxiesConfigured() ? 'from the proxies in PGKILN_AUTH_HEADER_PROXIES' : html`<b>${tx('rte.refused_pgkiln_auth_header_proxies')}</b>`}; access: ${a.access_control === 'any_user' ? 'any active account' : 'listed accounts only'}${a.header_auto_create ? ', new accounts created automatically' : ''}</li>` : ''}
|
|
914
|
+
${a.authentication === 'database' ? html`<li>${a.db_auth_roles?.length || a.db_auth_member_of ? '✓' : '✗'} Sign-in: database accounts (${[a.db_auth_roles?.length ? `roles ${a.db_auth_roles.join(', ')}` : '', a.db_auth_member_of ? `members of ${a.db_auth_member_of}` : ''].filter(Boolean).join('; ') || html`<b>${tx('rte.no_roles_allowed')}</b>`})</li>` : ''}
|
|
915
|
+
${a.authentication === 'custom' ? html`<li>${a.custom_auth_function || a.custom_auth_code ? '✓' : '✗'} Sign-in: custom ${a.custom_auth_function ? html`function <code>${a.custom_auth_function}</code>` : a.custom_auth_code ? 'function body' : html`<b>${tx('rte.no_check_configured_nobody_can')}</b>`}${a.custom_auth_post_code ? ', with post-authentication code' : ''}</li>` : ''}
|
|
916
|
+
${a.authentication === 'app_users' ? html`<li>Sign-in: ${[a.local_login ? 'password' : '', ...a.ldap_directories.map((d: string) => `LDAP ${d}`), ...a.sso_providers].filter(Boolean).join(', ') || html`<b>${tx('rte.no_method_enabled')}</b>`}; access: ${a.access_control === 'any_user' ? 'any active account' : 'listed accounts only'}</li>` : ''}
|
|
917
917
|
<li>${a.debug ? `✗ ${tx('st.ck.debug_on')}` : `✓ ${tx('st.ck.debug_off')}`}</li>
|
|
918
918
|
<li>${a.debug_level ? `✗ ${tx('st.ck.msgs_on', { level: a.debug_level })}` : `✓ ${tx('st.ck.msgs_off')}`}</li>
|
|
919
919
|
<li>${tx('st.ck.unprotected', { n: (await owner.one("select count(*)::int as n from meta.page where app_id = $1 and protection = 'unrestricted'", [a.id])).n })}</li>
|
|
920
920
|
<li>${tx('st.ck.public_pages', { n: (await owner.one('select count(*)::int as n from meta.page where app_id = $1 and not requires_auth', [a.id])).n })}</li>
|
|
921
921
|
</ul>`)}${end}
|
|
922
922
|
</div>`;
|
|
923
|
-
return send(reply, s, shell(s, tx('st.title', { app: a.name }), [['
|
|
923
|
+
return send(reply, s, shell(s, tx('st.title', { app: a.name }), [[tx('rte.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('rte.settings')]], main));
|
|
924
924
|
});
|
|
925
925
|
|
|
926
926
|
// (085) error handling function and session timeout warning
|
|
@@ -1044,25 +1044,25 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
1044
1044
|
]);
|
|
1045
1045
|
const main = html`${appHeader(a, 'activity', s)}
|
|
1046
1046
|
<div class="stat-grid">
|
|
1047
|
-
<div class="stat"><b>${stats.views}</b><span
|
|
1048
|
-
<div class="stat"><b>${stats.users}</b><span
|
|
1049
|
-
<div class="stat"><b>${stats.avg_ms} ms</b><span
|
|
1050
|
-
<div class="stat"><b>${stats.failures}</b><span
|
|
1051
|
-
<div class="stat"><b>${stats.problems}</b><span
|
|
1047
|
+
<div class="stat"><b>${stats.views}</b><span>${tx('rte.page_views_24h')}</span></div>
|
|
1048
|
+
<div class="stat"><b>${stats.users}</b><span>${tx('rte.distinct_users_24h')}</span></div>
|
|
1049
|
+
<div class="stat"><b>${stats.avg_ms} ms</b><span>${tx('rte.average_page_time')}</span></div>
|
|
1050
|
+
<div class="stat"><b>${stats.failures}</b><span>${tx('rte.failed_locked_sign_ins_24h')}</span></div>
|
|
1051
|
+
<div class="stat"><b>${stats.problems}</b><span>${tx('rte.errors_access_denied_24h')}</span></div>
|
|
1052
1052
|
</div>
|
|
1053
1053
|
<div class="columns">
|
|
1054
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/top-sql">${icon('database')}
|
|
1055
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/ai">${icon('bolt')}
|
|
1056
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/feedback">${icon('chat')}
|
|
1057
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/mail">${icon('send')}
|
|
1058
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/debug">${icon('list')}
|
|
1059
|
-
${region('
|
|
1054
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/top-sql">${icon('database')} ${tx('rte.top_sql')}</a> <span class="muted">${tx('rte.the_slowest_statements_of_this')}</span></p>
|
|
1055
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/ai">${icon('bolt')} ${tx('rte.ai_usage')}</a> <span class="muted">${tx('rte.the_ai_services_this_application')}</span></p>
|
|
1056
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/feedback">${icon('chat')} ${tx('rte.feedback')}</a> <span class="muted">${tx('rte.what_users_sent_from_the')}</span></p>
|
|
1057
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/mail">${icon('send')} ${tx('rte.mail')}</a> <span class="muted">${tx('rte.the_mail_this_application_queued')}</span></p>
|
|
1058
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/debug">${icon('list')} ${tx('rte.debug_messages')}</a> <span class="muted">${a.debug_level ? `on (level ${a.debug_level}): ` : 'off: '}timed steps of each request and messages from <code>meta.debug()</code></span></p>
|
|
1059
|
+
${region(tx('rte.page_views_by_page_7'), html`<div class="table-wrap"><table class="report"><thead><tr><th class="num">${tx('rte.page')}</th><th>${tx('rte.name')}</th><th class="num">${tx('rte.views')}</th><th class="num">${tx('rte.avg_ms')}</th><th class="num">${tx('rte.max_ms')}</th></tr></thead>
|
|
1060
1060
|
<tbody>${byPage.rows.map((r) => html`<tr><td class="num">${r.page_no}</td><td>${r.name}</td><td class="num">${r.views}</td><td class="num">${r.avg_ms}</td><td class="num">${r.max_ms}</td></tr>`)}</tbody></table></div>`)}
|
|
1061
|
-
${region('
|
|
1062
|
-
<div class="table-wrap"><table class="report"><thead><tr><th
|
|
1061
|
+
${region(tx('rte.recent_events'), html`<p class="muted u-mt0">${req.query.all === '1' ? html`Showing all events. <a href="?">${tx('rte.hide_page_views')}</a>` : html`Sign-ins, denials and errors. <a href="?all=1">${tx('rte.include_page_views')}</a>`}</p>
|
|
1062
|
+
<div class="table-wrap"><table class="report"><thead><tr><th>${tx('rte.when')}</th><th>${tx('rte.event')}</th><th>${tx('rte.user')}</th><th class="num">${tx('rte.page')}</th><th>IP</th><th>${tx('rte.detail')}</th></tr></thead>
|
|
1063
1063
|
<tbody>${events.rows.map((r) => html`<tr><td>${String(r.at).slice(0, 19)}</td><td><span class="ev ev-${r.event}">${r.event}</span></td><td>${r.username}</td><td class="num">${r.page_no}</td><td>${r.ip}</td><td title="${r.detail}">${r.detail}</td></tr>`)}</tbody></table></div>`)}
|
|
1064
1064
|
</div>`;
|
|
1065
|
-
return send(reply, s, shell(s, '
|
|
1065
|
+
return send(reply, s, shell(s, tx('rte.activity'), [[tx('rte.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('rte.activity')]], main));
|
|
1066
1066
|
});
|
|
1067
1067
|
|
|
1068
1068
|
// ---------------------------------------------------------------- developers
|
|
@@ -1111,7 +1111,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
1111
1111
|
</form>`
|
|
1112
1112
|
: html`<p class="muted">${tx('dev.admins_only')}</p>`}`)}
|
|
1113
1113
|
</div>`;
|
|
1114
|
-
return send(reply, s, shell(s, tx('dev.title'), [['
|
|
1114
|
+
return send(reply, s, shell(s, tx('dev.title'), [[tx('rte.developers')]], main, 'developers'));
|
|
1115
1115
|
});
|
|
1116
1116
|
|
|
1117
1117
|
app.post(`${BASE}/developers`, async (req: Req, reply) => {
|