pgkiln 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +1 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +4 -4
- package/docs/guide/03-builder.md +22 -1
- package/docs/guide/12-development.md +41 -0
- package/package.json +1 -1
- package/public/builder.css +18 -0
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
package/SECURITY.md
CHANGED
|
@@ -109,6 +109,7 @@ first version (MVP). Every finding below has a regression test in
|
|
|
109
109
|
| Mail queue, workspace mail and network access (sprint 118) | Administration → Mail queue: administrators see every workspace, a workspace's administrators only their current workspace's applications, other developers get 403; Retry and Purge (one mail or all failed) check the mail's workspace again on the server and need the CSRF token; bodies and attachments are never shown. A workspace's own mail service is managed by its administrators only; its secret is encrypted like a web credential's, write-only, never shown or exported (not in the workspace export); it has no API base URL or token URL, and its SMTP host must pass the installation's allow-list and the workspace's network access list and resolve to a public address (unless `PGKILN_REST_PRIVATE_HOSTS` lists it), resolved once and connected to by that address (no DNS rebinding). A workspace's network access list narrows `PGKILN_REST_ALLOWED_HOSTS` on the server for every outgoing web call of its applications (`meta.web_request`, REST data sources incl. write-back and the response cache, invoke API, token endpoints, object storage, the builder's Test, REST source catalogs), on every redirect hop; it can't widen the installation's list or the private-address checks. Refusals go to the debug log and `meta.network_refusal` (7 days). Also fixed: *REST source catalogs → read from a URL* fetched the URL before checking the developer's session (any visitor could make the server call an allowed host); it is now read only after the developer and CSRF checks. |
|
|
110
110
|
| Data security per table (sprint 119) | SQL Workshop → a table → Security writes `GRANT` / `REVOKE` and a masking view, so it needs a signed-in developer and the CSRF token, and works only on tables of the workspace's schemas (never pgkiln's own or the system's: 404). Table, column, view and role names reach SQL quoted; view names and application roles that could break out of their quotes are refused. The masking role is `nologin`, a member of the reader roles, so row level security still applies inside the view, and the generated SQL refuses to run when that role would bypass RLS. MCP `run_query` with an application runs as that application's database role (read-only) and refuses unknown applications. |
|
|
111
111
|
| App Gallery (sprint 120) | Installing a sample app runs shipped SQL as the owner, so only administrators may, with the CSRF token, into their current workspace; a request names only a key of `gallery.json` of its own kind (sample, starter, utility), never a path, and the file names in the catalogue must match fixed patterns. The script and the workspace placement run in one transaction: a failing script leaves nothing behind. Utility apps are blueprints: any developer installs one into their own workspace, only after the review. An installed sample is edited only from its own workspace. The search term is escaped. |
|
|
112
|
+
| Builder language (sprint 121) | Only the codes of the builder's languages are accepted: anything else on a sign-in link is ignored (also a repeated parameter), the status bar's menu refuses it (400) and checks the CSRF token. A sign-in link sets only an HttpOnly, SameSite=Lax cookie that the sign-in page reads; a developer's stored choice changes only through their own signed-in session. Translated texts are escaped like the English ones; placeholders are filled with escaped values unless the code passes markup on purpose (`tx.rich`). |
|
|
112
113
|
|
|
113
114
|
## Findings of the MVP review and their fixes
|
|
114
115
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------
|
|
2
|
+
-- 127: the builder in more languages (sprint 121, WS-7; APEX: the
|
|
3
|
+
-- builder's language links on the sign-in page and the workspace home).
|
|
4
|
+
--
|
|
5
|
+
-- meta.developer.language: the builder language each developer chose (the
|
|
6
|
+
-- language menu in the builder's status bar, or a language link on the
|
|
7
|
+
-- builder's sign-in page); null = English. The builder's own texts are
|
|
8
|
+
-- message files in src/builder/i18n/ (en, de, es, fr, nl, pt); the check
|
|
9
|
+
-- keeps the column to codes of that shape, the server accepts only the
|
|
10
|
+
-- languages it has. A developer account is installation data: not
|
|
11
|
+
-- exported with an application or a workspace.
|
|
12
|
+
-- ---------------------------------------------------------------------
|
|
13
|
+
alter table meta.developer
|
|
14
|
+
add column language text check (language ~ '^[a-z]{2}(-[A-Z]{2})?$');
|
|
15
|
+
comment on column meta.developer.language is
|
|
16
|
+
'The developer''s builder language (127): en, de, es, fr, nl or pt; null = English.';
|
|
@@ -23,8 +23,8 @@ Last reviewed: 2026-10-20 against **APEX 26.2** (sprint 120: the App Gallery's s
|
|
|
23
23
|
| Data and integration | 13 | 0 | 0 | 2 | REST source catalogs from OpenAPI documents; Generate DDL and schema comparison; `meta.data_export` and data export downloads (CSV, JSON, XML, HTML, Excel, PDF); e-mail through SMTP (password or OAuth2), Microsoft Graph, Amazon SES and HTTP APIs; REST APIs via PostgREST, REST data sources that write back and synchronise, web credentials with OAuth2 grants, CSV/XLSX/JSON/XML loading with saved definitions and unloading, SQL scripts and Quick SQL, report PDFs and document templates |
|
|
24
24
|
| Workflow, automation and AI | 6 | 0 | 0 | 0 | Scheduled automations with several actions and runs from SQL, approvals, a task list and workflows with parallel branches, versions and invoke-API steps; AI with Claude or OpenAI: *Generate text with AI*, an assistant region with tools, natural-language report filters and blueprints |
|
|
25
25
|
| Administration | 14 | 0 | 0 | 0 | An APEX-like sign-in with a workspace and a Workspace home; a mail queue dashboard, a workspace's own mail service and network access list (26.1 / 26.2); a workspace's components exported and imported as one file; workspace message, environment banner and utilization; isolated workspaces with their own database logins and roles, and workspace administrators; application availability and Run only, automatic backups, user feedback; a developer toolbar with a session state viewer and debug per session; Top SQL per app; debug messages per request; an install/upgrade log |
|
|
26
|
-
| APEX 26.2 workspace (WS-1 … WS-13) |
|
|
27
|
-
| **Total** | **
|
|
26
|
+
| APEX 26.2 workspace (WS-1 … WS-13) | 13 | 0 | 0 | 1 | Workspace home, Data Reporter as a workspace tool, Team Development, AI Agents, AI interactive reports, Workflow 26.2, the mail queue, workspace mail and network access, the 26.1 component details, the App Builder list's details, the Pillar navigation appearances and data security per table (rows, columns and masked values), the App Gallery's single-topic sample apps, UX Pattern Catalog and Utility Apps; the builder in six languages |
|
|
27
|
+
| **Total** | **161** | **0** | **0** | **4** | 165 APEX features compared: 98% available, none missing, 4 deliberately not planned |
|
|
28
28
|
|
|
29
29
|
(Counts are of the rows in the tables below. A review on 2026-10-06 added 18 rows for APEX features that were
|
|
30
30
|
not compared before, one on 2026-10-12 added two more (property help, application availability), and one on
|
|
@@ -221,7 +221,7 @@ the 26.2 comparison.)
|
|
|
221
221
|
|
|
222
222
|
| APEX | pgkiln | Notes |
|
|
223
223
|
|---|---|---|
|
|
224
|
-
| Developer accounts, workspace sign-in | ✅ | (sprint 61) The builder's sign-in page like APEX's: workspace (optional; it becomes the current one, a wrong one answers like a wrong password), username, password with a Show button, *Remember workspace and username*, a link to Administration. (sprint 68) **Find my workspace**: developers' e-mail addresses, and a mail with their workspaces through the builder's mail service (the same answer for every address, limited per address and IP).
|
|
224
|
+
| Developer accounts, workspace sign-in | ✅ | (sprint 61) The builder's sign-in page like APEX's: workspace (optional; it becomes the current one, a wrong one answers like a wrong password), username, password with a Show button, *Remember workspace and username*, a link to Administration. (sprint 68) **Find my workspace**: developers' e-mail addresses, and a mail with their workspaces through the builder's mail service (the same answer for every address, limited per address and IP). (sprint 121) The sign-in page's language links: Deutsch, English, Español, Français, Nederlands, Português (WS-7) |
|
|
225
225
|
| Workspace home (tool cards, top apps and users, summary, workspace and system message) | ✅ | (sprint 109) `/builder/home` opens after sign-in ([chapter 3](guide/03-builder.md#workspace-home)): App Builder, SQL Workshop, Data Reporter and App Gallery cards; the workspace message edited in place; a Summary of applications, tables, developers and reporting applications; top applications, users and developers over an hour, a day or a week; a system message with the server's version and, with the optional release check (`PGKILN_RELEASE_CHECK`, off by default), *pgkiln X is available*. The App Builder moved to `/builder/apps`; `/builder` opens either, per workspace. |
|
|
226
226
|
| Workspace message, environment banner, workspace utilization, preferences and extension links (Manage Service) | ✅ | (sprint 61) An **Administration** menu in the icon rail; administrators and workspace administrators set a **workspace message** (on the App Builder home) and an **environment banner** (a coloured label on every builder page of the workspace), and see **workspace utilization** (applications, pages, page views, users, static files, schema storage) ([chapter 3](guide/03-builder.md#workspace-message-and-environment-banner)). (sprint 62) **Workspace preferences**: the SQL Workshop and the applications' RESTful services (REST modules) on or off per workspace; **extension links** in an Extensions menu of the rail ([chapter 3](guide/03-builder.md#workspace-preferences)). **Missing:** the account login controls of APEX's preferences (pgkiln's password policy and sign-in throttling are installation-wide) |
|
|
227
227
|
| Instance administration, install/upgrade logs (26.1) | ✅ | Versioned migrations (`npm run db:migrate`), every run logged; Workspace utilities → **Installation** (administrators): server version, install and upgrade runs with errors, applied migrations, and warnings for migrations the database misses or the server doesn't know ([chapter 3](guide/03-builder.md)). Workspace utilities → **Instance settings**: session length and sign-in throttling (stored in the database, over the environment variables) and an overview of the server's configuration without secrets; a server whose database lacks migrations says so (503) or applies them (`MIGRATE_ON_START`). Other settings stay environment variables |
|
|
@@ -249,7 +249,7 @@ row above where there is one.
|
|
|
249
249
|
| WS-4 AI Agents with tools | ✅ | (sprint 111) AI Agents and AI Tools as shared components, a Navigate tool, Gemini and OpenAI-compatible services ([Workflow, automation and AI](#workflow-automation-and-ai)) |
|
|
250
250
|
| WS-5 AI interactive reports: chips, computed columns | ✅ | (sprint 112) *Ask in your own words* with chips, Undo and computed columns; a phrase parser that needs no AI service. Not yet: phrases mapped to facets and smart filters |
|
|
251
251
|
| WS-6 Workflow 26.2: outcomes, migrating running instances, drafts, diagram export | ✅ | (sprint 114) Custom task outcomes, task events, Send E-Mail and draft steps, migrating running instances, the diagram as SVG / PNG ([Workflow, automation and AI](#workflow-automation-and-ai)) |
|
|
252
|
-
| WS-7 The builder in more languages |
|
|
252
|
+
| WS-7 The builder in more languages | ✅ | (sprint 121) The builder in Deutsch, English, Español, Français, Nederlands and Português: links on the sign-in page, a language menu in the status bar, the choice stored per developer (migration 127). Translated: the builder's frame, sign-in page, Workspace home, App Builder home, an application's home and tabs, the Page Designer's toolbar, menus, tree and gallery, the SQL Workshop's home and tabs; other screens move over incrementally (English until then). Builder texts are keyed message files with a coverage check in CI |
|
|
253
253
|
| WS-8 App Gallery: more sample apps, Utility Apps | ✅ | (sprint 120) Four tabs with counts and a search that also works without JavaScript ([chapter 3](guide/03-builder.md#app-gallery)). *Sample Apps*: HR Demo, Tasks, six single-topic apps (Sample Charts, Sample Cards, Sample Calendar, Sample Interactive Grids, Sample Dynamic Actions, Sample Reporting) and the **UX Pattern Catalog**, each with its own schema, data, demo account and explanatory texts, installed by administrators from one SQL file in one transaction; *Utility Apps*: Lookup Lists, Release Notes and Announcements as blueprints any developer installs into the workspace. APEX's 27 sample apps include topics pgkiln shows in HR Demo instead (maps, approvals, workflows, REST); no remote catalogue |
|
|
254
254
|
| WS-9 Mail queue reporting, workspace SMTP, workspace network ACLs | ✅ | (sprint 118) Administration → Mail queue (per hour and per workspace / application, the slowest provider calls, failures with the provider's answer, Retry and Purge), a workspace's own mail service, and a workspace's network access list that narrows the installation's allow-list for every outgoing call ([Administration](#administration)) |
|
|
255
255
|
| WS-10 Universal Theme 26.2: Pillar appearances, navigation polish | ✅ | (sprint 117) Theme Roller → Navigation appearance Pillar Light / Pillar Dark per application or style, with a header strip colour for light and dark mode; the current page filled, its parents bold with an accent guide line; `u-color-*-subtle` utilities; a lighter processing indicator; RTL mirrored |
|
package/docs/guide/03-builder.md
CHANGED
|
@@ -237,7 +237,7 @@ Every builder page has the same frame, modelled on an IDE:
|
|
|
237
237
|
API, Settings, Search and Advisor, with Export and Run on the right.
|
|
238
238
|
|
|
239
239
|
- **The status bar** at the bottom: who is signed in, the current [workspace](#workspaces), the database, the
|
|
240
|
-
builder
|
|
240
|
+
[builder language](#choosing-the-builder-language) (a menu) and the pgkiln version.
|
|
241
241
|
- **An environment banner** (when the current workspace has one) across the top: a coloured band with a label such as
|
|
242
242
|
DEVELOPMENT or TEST, so nobody mistakes one environment for another.
|
|
243
243
|
|
|
@@ -257,6 +257,27 @@ application of a workspace you are not a member of) shows a **Page not found** p
|
|
|
257
257
|
theme with a link to the App Builder home; a server error shows **Something went wrong** (the error
|
|
258
258
|
is in the server log, not on the page). Scripts get the plain answers they always got.
|
|
259
259
|
|
|
260
|
+
### Choosing the builder language
|
|
261
|
+
|
|
262
|
+
(sprint 121, APEX: the builder's language links) The builder speaks **Deutsch, English, Español, Français, Nederlands
|
|
263
|
+
and Português** (Brazilian). English is the default.
|
|
264
|
+
|
|
265
|
+
- **On the sign-in page** the languages are links under the form: choosing one shows the sign-in page in that
|
|
266
|
+
language and, when you then sign in, stores it as your builder language. The choice is remembered on this device
|
|
267
|
+
(a cookie), so the sign-in page keeps it after you sign out.
|
|
268
|
+
- **When signed in**, the language in the status bar at the bottom is a menu: pick another language and the page
|
|
269
|
+
you are on comes back in it.
|
|
270
|
+
|
|
271
|
+
The language is stored **per developer** (in your developer account), so it follows you to every session and every
|
|
272
|
+
device; other developers keep their own. It only changes the builder: your applications have their own languages
|
|
273
|
+
(chapter 7).
|
|
274
|
+
|
|
275
|
+
What is translated: the builder's frame (the rail, the top bar and its search, your account menu, the status bar,
|
|
276
|
+
standard messages and error pages), the sign-in page, the Workspace home, the App Builder home, an application's
|
|
277
|
+
home page and its tabs, the Page Designer's toolbar, menus, component tree, panes and gallery, and the SQL Workshop's
|
|
278
|
+
home page and tabs. Other builder pages are still in English (they move over screen by screen); so are the
|
|
279
|
+
Page Designer's property names and their help, the links to this guide, and any SQL the builder generates.
|
|
280
|
+
|
|
260
281
|
## Workspace home
|
|
261
282
|
|
|
262
283
|
(sprint 109; APEX: the workspace home page) The page the builder opens after signing in, `/builder/home`, above the
|
|
@@ -263,6 +263,11 @@ src/
|
|
|
263
263
|
builder/
|
|
264
264
|
components.ts property spec of every component (drives the property editor)
|
|
265
265
|
ui.ts IDE shell (icon rail, toolbar, breadcrumb, status bar), builder theme, form helpers, CSRF check, app tabs
|
|
266
|
+
i18n.ts (sprint 121) the builder's own texts: BUILDER_LANGUAGES, tr(s) / translatorFor(lang) → tx(key, params),
|
|
267
|
+
tx.rich (markup in a sentence), tx.dyn (keys made at run time), tx.label (known breadcrumb labels)
|
|
268
|
+
i18n/ en.ts defines the builder's keys; de, es, fr, nl, pt are Records of the same keys
|
|
269
|
+
language.ts (migration 127) the status bar's language menu (POST /builder/language), the sign-in page's
|
|
270
|
+
language links (?lang=, a cookie), meta.developer.language
|
|
266
271
|
routes.ts sign-in, app home, settings, activity, developers, create (POST, via newapp.ts)/import (POST)
|
|
267
272
|
wizards.ts create page wizards: step 2 forms per page type (defaults from meta.wizard_defaults), POST → meta.generate_page
|
|
268
273
|
(the generators are PL/pgSQL in migration 047: catalog, defaults, form/cards/calendar/chart/map/facets/master-detail)
|
|
@@ -484,6 +489,8 @@ test/
|
|
|
484
489
|
logic.test.ts computations, branches, menu buttons and badges, new dynamic actions, build options, export
|
|
485
490
|
page-logic.test.ts download, chain (background jobs) and workflow processes, function/app branches, dialog_closed (HR page 28)
|
|
486
491
|
code-editor.test.ts code editor: completions scoped to the app's role, the check, marked fields
|
|
492
|
+
builder-i18n.test.ts (sprint 121) builder languages: the coverage check of src/builder against i18n/en.ts, the message
|
|
493
|
+
files, Deutsch on the sign-in page, the status bar menu, the choice per developer, English fallback
|
|
487
494
|
builder-home.test.ts App Builder home: search, sort, views, Recent, Create/Import pages, dashboard, utilities
|
|
488
495
|
app-list.test.ts (116) the App Builder list's Updated by, working copies and lock; what counts as a change
|
|
489
496
|
import-only.test.ts (116) pgkiln import --replace --only: a page or component file, refusals
|
|
@@ -531,6 +538,7 @@ test/
|
|
|
531
538
|
ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions
|
|
532
539
|
helpers.ts a cookie-keeping test browser
|
|
533
540
|
e2e/responsive.test.ts browser tests at phone/tablet/desktop widths (Playwright)
|
|
541
|
+
e2e/builder-i18n.test.ts (sprint 121) Deutsch on the sign-in page, the status bar's language menu, German screens at 390 px, CSP
|
|
534
542
|
e2e/gallery.test.ts (sprint 120) the gallery's sample apps: every page at 4 widths, CSP, axe; dynamic actions; the four tabs at 390 px
|
|
535
543
|
e2e/admin-mail-network.test.ts (sprint 118) Mail queue, Network access and a workspace's mail service at 390 px without JavaScript
|
|
536
544
|
e2e/pillar.test.ts (sprint 117) Pillar appearances: axe in light and dark, subtle colours, current vs parent entry, Arabic RTL, 390 px, preview
|
|
@@ -551,6 +559,39 @@ test/
|
|
|
551
559
|
e2e/ai.test.ts Generate text with AI on HR page 37 in a browser (mock Claude): dynamic actions without a submit, errors, no JavaScript
|
|
552
560
|
```
|
|
553
561
|
|
|
562
|
+
## Builder texts in more languages
|
|
563
|
+
|
|
564
|
+
(sprint 121) The builder's own texts are keyed messages, like the runtime's: `src/builder/i18n/en.ts` defines every
|
|
565
|
+
key (`<screen>.<what>`, e.g. `rail.app_builder`, `pd.menu.create`, `ab.count` with `{n}`), and `de.ts`, `es.ts`,
|
|
566
|
+
`fr.ts`, `nl.ts` and `pt.ts` are `Record<BuilderKey, string>` of the same keys. A page takes its translator from the
|
|
567
|
+
session and uses it for every text it shows:
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
const tx = tr(s); // the developer's language (meta.developer.language, in the session)
|
|
571
|
+
html`<h2>${tx('wh.summary')}</h2><p>${tx('ab.count', { n: 3 })}</p>`; // plain text: html`` escapes it
|
|
572
|
+
tx.rich('ab.none_yet', { create: html`<a href="…">${tx('ab.create_one')}</a>` }); // markup inside a sentence
|
|
573
|
+
tx.dyn(`gallery.region.${type}`, REGION_LABELS[type][0]); // a key made at run time, with a fallback
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
A missing text falls back to English, never to the raw key. Shared helpers that render chrome take the session as
|
|
577
|
+
an optional last argument (`appHeader(a, 'pages', s)`, `workshopTabs('sql', s)`); `shell()` translates known English
|
|
578
|
+
breadcrumb labels (`tx.label`) so pages that aren't moved yet still get a translated trail.
|
|
579
|
+
|
|
580
|
+
**Moving a screen over**: replace its literal texts with `tx('key')`, add each key with its English text to `en.ts`
|
|
581
|
+
(keep the English exactly as it was: tests and e2e tests assert it), then add the key to the five other files
|
|
582
|
+
(TypeScript refuses a file with a missing or an unknown key). A sentence with a link is one key with a `{link}`
|
|
583
|
+
placeholder and `tx.rich`, not pieces glued together; a count has a `_one` and a plural key; never lower-case a
|
|
584
|
+
translated label (German nouns). Flash messages are translated where they are set.
|
|
585
|
+
|
|
586
|
+
**The coverage check** (`test/builder-i18n.test.ts`, part of `npm test` and CI) scans `src/builder/**/*.ts` for
|
|
587
|
+
`tx('…')`, `tx.rich('…')` and `tr(s)('…')` and fails when a key has no English text (templated keys such as
|
|
588
|
+
`` `ago.${unit}` `` must match at least one English key); checks that every region type, item type, button action,
|
|
589
|
+
display position, button position, component kind and SQL Workshop tool has its run-time key; and that every
|
|
590
|
+
language file has exactly the English keys, the same `{placeholders}`, no empty texts and is mostly translated.
|
|
591
|
+
|
|
592
|
+
Stays English by design: the property editor's field names and help, links to the guide, generated SQL, and pages
|
|
593
|
+
not moved yet. The builder's language is separate from an application's languages (`src/i18n.ts`).
|
|
594
|
+
|
|
554
595
|
## Principles
|
|
555
596
|
|
|
556
597
|
- **User-facing texts go through the translator**: `ctx.locale.t('key')` for pgkiln's own texts (add the key to `en` and `nl` in `src/i18n.ts` and to `src/i18n/de.ts`, `fr.ts`, `es.ts`; TypeScript checks that every language has every key), `ctx.locale.tr(text)` for texts derived from application metadata.
|
package/package.json
CHANGED
package/public/builder.css
CHANGED
|
@@ -1545,3 +1545,21 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
|
1545
1545
|
.gal-tabs a:nth-child(3) { border-left: 1px solid var(--border); border-top: 0; border-radius: 0 0 0 var(--radius); }
|
|
1546
1546
|
.gal-tabs a:last-child { border-top: 0; border-radius: 0 0 var(--radius) 0; }
|
|
1547
1547
|
}
|
|
1548
|
+
|
|
1549
|
+
/* ---------------------------------------------------------------- (sprint 121) builder language: status bar menu, sign-in links */
|
|
1550
|
+
.ide-status .ide-lang { flex: none; }
|
|
1551
|
+
.ide-status .ide-lang > summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: inherit; padding: 2px 4px; border-radius: 4px; }
|
|
1552
|
+
.ide-status .ide-lang > summary:hover, .ide-status .ide-lang[open] > summary { color: var(--chrome-text); background: rgb(255 255 255 / 8%); }
|
|
1553
|
+
.ide-status .ide-lang > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
1554
|
+
/* the status bar clips (overflow: hidden): the menu opens fixed, above it */
|
|
1555
|
+
.ide-status .ide-lang .menu-panel { position: fixed; top: auto; bottom: 34px; left: auto; right: auto; min-width: 200px; font-size: 13px; white-space: normal; }
|
|
1556
|
+
.ide-lang-form button { width: 100%; }
|
|
1557
|
+
.ide-lang-form button[aria-current="true"] { font-weight: 600; }
|
|
1558
|
+
.ide-lang-form .icon, .ide-lang-pad { width: 1em; height: 1em; flex: none; display: inline-block; }
|
|
1559
|
+
.signin-langs ul { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.25rem 1rem; margin: 0; padding: 0.75rem 0 0; list-style: none; font-size: 0.85rem; }
|
|
1560
|
+
.signin-langs a { color: var(--muted); text-decoration: none; }
|
|
1561
|
+
.signin-langs a:hover { color: var(--accent); text-decoration: underline; }
|
|
1562
|
+
.signin-langs a[aria-current="true"] { color: var(--text); font-weight: 600; }
|
|
1563
|
+
@media (max-width: 640px) { .ide-status .ide-lang .menu-panel { left: 0.5rem; right: 0.5rem; min-width: 0; } }
|
|
1564
|
+
/* long compound words of other languages (Workspace-Dienstprogramme) wrap instead of widening a phone's page */
|
|
1565
|
+
.ab-tile > *, .stat span { min-width: 0; overflow-wrap: anywhere; hyphens: auto; }
|
package/public/worksheet.css
CHANGED
|
@@ -263,8 +263,9 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
|
|
|
263
263
|
.lp-chips li { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); padding-left: 10px; }
|
|
264
264
|
.lp-chips a { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; color: var(--text); padding: 4px 0; font-size: 13px; }
|
|
265
265
|
.lp-recent li { padding-right: 10px; border-style: dashed; }
|
|
266
|
-
.lp-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 10px; }
|
|
267
|
-
.lp-tabs [role=tab] { border: 0; background: none; color: var(--muted); padding: 7px 12px; font: inherit; cursor: pointer; }
|
|
266
|
+
.lp-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 10px; overflow-x: auto; scrollbar-width: none; }
|
|
267
|
+
.lp-tabs [role=tab] { border: 0; background: none; color: var(--muted); padding: 7px 12px; font: inherit; cursor: pointer; white-space: nowrap; }
|
|
268
|
+
@media (max-width: 480px) { .lp-tabs [role=tab] { padding: 7px 8px; } }
|
|
268
269
|
.lp-tabs [role=tab][aria-selected=true] { color: var(--text); box-shadow: inset 0 -2px 0 var(--hot); }
|
|
269
270
|
.js .lp-group > h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
270
271
|
.lp-list { list-style: none; margin: 0 0 16px; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 8px; }
|
package/src/builder/advisor.ts
CHANGED
|
@@ -453,7 +453,7 @@ export async function advisorRoutes(app: FastifyInstance) {
|
|
|
453
453
|
<span class="hit-snippet">${f.entry ? html`<b>${f.field}:</b> ` : ''}${f.message}</span>
|
|
454
454
|
</li>`)}</ul>`
|
|
455
455
|
: html`<div class="alert alert-success" role="status">No problems found.</div>`;
|
|
456
|
-
const main = html`${appHeader(a, 'advisor')}
|
|
456
|
+
const main = html`${appHeader(a, 'advisor', s)}
|
|
457
457
|
${region('Advisor', html`<p class="muted u-mt0">Checked ${checked} pieces of SQL as the role <code>${a.db_role ?? '(owner)'}</code> (planned with EXPLAIN and rolled back, nothing is run), and every reference to pages, items, lists of values, authorization schemes and report layouts.
|
|
458
458
|
${plpgsqlCheck ? 'PL/pgSQL functions were checked with plpgsql_check.' : html`Install the <code>plpgsql_check</code> extension to check PL/pgSQL functions too.`}</p>
|
|
459
459
|
<p><span class="tag tag-error">${count('error')} errors</span> <span class="tag tag-warning">${count('warning')} warnings</span> <span class="tag tag-info">${count('info')} notes</span>
|
package/src/builder/ai-agents.ts
CHANGED
|
@@ -223,7 +223,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
223
223
|
const agents = (a.ai_agents ?? []) as AiAgent[];
|
|
224
224
|
const tools = (a.ai_tools ?? []) as AiTool[];
|
|
225
225
|
const problems = definitionProblems(agents, tools);
|
|
226
|
-
const main = html`${appHeader(a, 'shared')}
|
|
226
|
+
const main = html`${appHeader(a, 'shared', s)}
|
|
227
227
|
<div class="title-row"><h1>AI Agents</h1></div>
|
|
228
228
|
<p class="muted u-mt0">An agent answers in an AI assistant region and acts only through the tools you give it: SQL queries (read-only, as the application's database role, so row level security applies), PL/pgSQL functions, REST data sources, JavaScript actions in the browser, and links to pages (pgkiln builds the URL and its checksum, only for pages the user may open). What users type and the tools' results go to the agent's AI service. <a href="${BASE}/apps/${a.id}/ai">AI usage</a> lists every tool call.</p>
|
|
229
229
|
${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
@@ -250,7 +250,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
250
250
|
const x = name ? ((a.ai_agents ?? []) as AiAgent[]).find((g) => g.name === name) : {};
|
|
251
251
|
if (!x) return reply.code(404).send('Not found');
|
|
252
252
|
const title = name || 'New agent';
|
|
253
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI agent', await agentForm(a, s, x, name, []))}`;
|
|
253
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI agent', await agentForm(a, s, x, name, []))}`;
|
|
254
254
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
255
255
|
});
|
|
256
256
|
|
|
@@ -273,7 +273,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
273
273
|
return [];
|
|
274
274
|
});
|
|
275
275
|
if (problems.length) {
|
|
276
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('AI agent', await agentForm(a, s, x, original, problems))}`;
|
|
276
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New agent'}</h1></div>${region('AI agent', await agentForm(a, s, x, original, problems))}`;
|
|
277
277
|
return send(reply.code(422), s, shell(s, original || 'New agent', crumbs(a, original || 'New agent'), main));
|
|
278
278
|
}
|
|
279
279
|
if (original && original !== x.name)
|
|
@@ -305,7 +305,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
305
305
|
const x = name ? ((a.ai_tools ?? []) as AiTool[]).find((t) => t.name === name) : {};
|
|
306
306
|
if (!x) return reply.code(404).send('Not found');
|
|
307
307
|
const title = name || 'New tool';
|
|
308
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI tool', await toolForm(a, s, x, name, []))}`;
|
|
308
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${title}</h1></div>${region('AI tool', await toolForm(a, s, x, name, []))}`;
|
|
309
309
|
return send(reply, s, shell(s, title, crumbs(a, title), main));
|
|
310
310
|
});
|
|
311
311
|
|
|
@@ -337,7 +337,7 @@ export async function aiAgentRoutes(app: FastifyInstance) {
|
|
|
337
337
|
return [];
|
|
338
338
|
});
|
|
339
339
|
if (problems.length) {
|
|
340
|
-
const main = html`${appHeader(a, 'shared')}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('AI tool', await toolForm(a, s, x, original, problems))}`;
|
|
340
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="title-row"><h1 class="ai-name">${original || 'New tool'}</h1></div>${region('AI tool', await toolForm(a, s, x, original, problems))}`;
|
|
341
341
|
return send(reply.code(422), s, shell(s, original || 'New tool', crumbs(a, original || 'New tool'), main));
|
|
342
342
|
}
|
|
343
343
|
flash(s, 'AI tool saved.');
|
|
@@ -142,7 +142,7 @@ const SQL_SCHEMA = {
|
|
|
142
142
|
|
|
143
143
|
async function workshopPage(s: Session, reply: FastifyReply, out: { question?: string; schema?: string; sql?: string; error?: string; result?: Raw }) {
|
|
144
144
|
const list = await schemas(s);
|
|
145
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai')}
|
|
145
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai', s)}
|
|
146
146
|
${await serviceBox(s)}
|
|
147
147
|
<p><a href="${BASE}/sql/ai/describe">Describe tables for AI ▸</a> <span class="muted">(better answers: say what tables and columns mean)</span></p>
|
|
148
148
|
${out.result ?? ''}
|
|
@@ -178,7 +178,7 @@ async function describePage(s: Session, reply: FastifyReply, schema: string, tab
|
|
|
178
178
|
const t = table ? tables.find((x) => x.table === table) : undefined;
|
|
179
179
|
const noted = new Set((await owner.query<{ t: string }>(`select distinct table_name as t from meta.ai_table_note where schema_name = $1`, [schema])).rows.map((r) => r.t));
|
|
180
180
|
const value = (col: string, current: string | null) => (draft ? (col === '' ? draft.table : draft.columns?.[col]) ?? current ?? '' : current ?? '');
|
|
181
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai')}
|
|
181
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ai', s)}
|
|
182
182
|
<p><a href="${BASE}/sql/ai">◂ SQL Workshop AI</a></p>
|
|
183
183
|
${message ?? ''}
|
|
184
184
|
${region('Describe tables for AI', html`
|
|
@@ -225,7 +225,7 @@ async function pagesPage(s: Session, reply: FastifyReply, a: any, description: s
|
|
|
225
225
|
const tables = (await wizardTables(a.db_role)).filter((t) => t.access).map((t) => t.t);
|
|
226
226
|
const svc = await builderService();
|
|
227
227
|
const kindOptions = WIZARD_KINDS.map(([k, label]): [string, string] => [k, label]);
|
|
228
|
-
const main = html`${appHeader(a, 'pages')}
|
|
228
|
+
const main = html`${appHeader(a, 'pages', s)}
|
|
229
229
|
${message ?? ''}
|
|
230
230
|
${region('Create pages with AI', html`
|
|
231
231
|
<p class="muted u-mt0">Describe the pages you want. The App Builder's AI service proposes pages of the create page wizards for tables and views the application's role (${a.db_role ?? 'the owner'}) can read; you check the proposals and create the ones you tick. Table and column names, types and <a href="${BASE}/sql/ai/describe">descriptions</a> are sent to the service, never rows.</p>
|
package/src/builder/api.ts
CHANGED
|
@@ -116,7 +116,7 @@ async function apiPage(s: Session, a: any, issued?: { token: string; username: s
|
|
|
116
116
|
].join('\n');
|
|
117
117
|
|
|
118
118
|
const check = (ok: boolean, text: Raw | string) => html`<li>${ok ? '✓' : '✗'} ${text}</li>`;
|
|
119
|
-
const main = html`${appHeader(a, 'api')}
|
|
119
|
+
const main = html`${appHeader(a, 'api', s)}
|
|
120
120
|
<div class="columns">
|
|
121
121
|
${region('Setup', html`
|
|
122
122
|
<p class="muted u-mt0">PostgREST runs next to pgkiln and serves the <code>${API_SCHEMA}</code> schema over HTTP. Requests carry a JWT whose <code>role</code> claim is this application’s API role; row level security uses the same <code>meta.app_user()</code> and <code>meta.has_role()</code> as the web pages.</p>
|
package/src/builder/autorest.ts
CHANGED
|
@@ -167,7 +167,7 @@ async function listPage(s: Session, q: Record<string, unknown>, problems: string
|
|
|
167
167
|
<div class="buttons"><button class="btn btn-hot"${usable.length ? '' : raw(' disabled')}>${icon('bolt')} Enable</button></div>
|
|
168
168
|
${usable.length ? '' : html`<p class="muted">No application of this workspace has a database role to run as.</p>`}
|
|
169
169
|
</form>`;
|
|
170
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}${restNav('autorest')}
|
|
170
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}${restNav('autorest')}
|
|
171
171
|
<p class="muted">RESTful Services: publish a table or view as REST endpoints (AutoREST). Rows come paged as JSON, with filters; a primary key gives single-row GET, PUT and DELETE. Requests run as the publishing application's database role with the caller as <code>meta.app_user()</code>, so its grants and row level security apply. For tailored endpoints with your own SQL or PL/pgSQL, create a <a href="${BASE}/sql/rest/modules">module</a>.</p>
|
|
172
172
|
${region('Published objects', table)}
|
|
173
173
|
${region('Enable a table or view', form)}`;
|
|
@@ -227,7 +227,7 @@ async function detailPage(s: Session, o: Row, problems: string[] = []) {
|
|
|
227
227
|
].filter(Boolean).join('\n');
|
|
228
228
|
const fetchEx = `const res = await fetch('${base}?limit=10'${o.auth === 'public' ? '' : `, {\n headers: { Authorization: \`Bearer \${token}\` },\n}`});\nconst { items, has_more } = await res.json();`;
|
|
229
229
|
const openapi = `${publicUrl()}/a/${o.app_alias}/autorest/openapi.json`;
|
|
230
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}
|
|
230
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}
|
|
231
231
|
<h2 class="u-mb1 wrap-anywhere">${o.schema_name}.${o.object_name} <span class="muted small">${info ? kindLabel(info.kind) : ''} · ${o.enabled ? 'enabled' : 'disabled'}</span></h2>
|
|
232
232
|
${errorList(problems)}
|
|
233
233
|
${!o.enabled ? html`<div class="alert alert-info" role="status">Disabled: the endpoints answer 404.</div>` : ''}
|
|
@@ -40,7 +40,7 @@ export async function availabilityRoutes(app: FastifyInstance) {
|
|
|
40
40
|
'Run only: the builder refuses changes to the application (pages, shared components, settings, restores) until it is set back here. pgkiln import --replace still deploys it.')}
|
|
41
41
|
</div>
|
|
42
42
|
<div class="buttons"><button class="btn btn-hot">Save</button></div></form>`);
|
|
43
|
-
const main = html`${appHeader(a, 'settings')}<div class="ide-body">${body}</div>`;
|
|
43
|
+
const main = html`${appHeader(a, 'settings', s)}<div class="ide-body">${body}</div>`;
|
|
44
44
|
return send(reply, s, shell(s, `${a.name} availability`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Availability']], main));
|
|
45
45
|
});
|
|
46
46
|
|
package/src/builder/backups.ts
CHANGED
|
@@ -66,7 +66,7 @@ export async function backupRoutes(app: FastifyInstance) {
|
|
|
66
66
|
<div class="form-grid">${input('keep', 'Backups to keep', a.backups_keep, { type: 'number', help: '0 to 500; 0 makes no backups at all (also not before an import).' })}</div>
|
|
67
67
|
<div class="buttons"><button class="btn">Save</button></div></form>`)}
|
|
68
68
|
</div>`;
|
|
69
|
-
const main = html`${appHeader(a, 'pages')}<div class="ide-body">${body}</div>`;
|
|
69
|
+
const main = html`${appHeader(a, 'pages', s)}<div class="ide-body">${body}</div>`;
|
|
70
70
|
return send(reply, s, shell(s, `${a.name} backups`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Backups']], main));
|
|
71
71
|
});
|
|
72
72
|
|
package/src/builder/changelog.ts
CHANGED
|
@@ -106,7 +106,7 @@ async function listPage(s: Session, q: Record<string, unknown>) {
|
|
|
106
106
|
${exp ? html`<input type="hidden" name="app" value="${exp.id}">` : ''}
|
|
107
107
|
<div class="buttons"><button class="btn btn-hot">${icon('download')} Download migration</button></div></form>`
|
|
108
108
|
: '';
|
|
109
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('changes')}
|
|
109
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('changes', s)}
|
|
110
110
|
<p class="muted">The change log (Liquibase, Flyway): DDL run from the SQL Worksheet, Table Properties and Schema Comparison is kept as change sets per schema, with who ran it and when. Download them as a migration for another database: Flyway versioned files, a Liquibase formatted SQL changelog or a plain script. Only statements that succeeded are kept; data changes (INSERT, UPDATE, DELETE) are not.</p>
|
|
111
111
|
${nav}
|
|
112
112
|
${schema ? region(`Change sets of ${schema}`, html`${exportPick}${table}${downloads}`) : region('Change sets', table)}`;
|
|
@@ -87,7 +87,7 @@ async function listPage(req: Req, reply: FastifyReply) {
|
|
|
87
87
|
<small class="help">Names separated by commas or spaces; <code>*</code> for all, <code>ord*</code> for those starting with ord. You can add more later.</small></div>
|
|
88
88
|
</div>
|
|
89
89
|
<div class="buttons"><button class="btn btn-hot">${icon('plus')} Create diagram</button></div></form>`;
|
|
90
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
|
|
90
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
|
|
91
91
|
<p class="muted u-mt0">Relational diagrams of a schema's tables and views: their columns, keys and the foreign keys between them. From a diagram: its DDL and a data dictionary.</p>
|
|
92
92
|
${region('Diagrams', list)}<div class="u-spacer"></div>${region('Create diagram', create)}`;
|
|
93
93
|
return send(reply, s, shell(s, 'Data Modeler', [['SQL Workshop', `${BASE}/sql`], ['Data Modeler']], main, 'sql'));
|
|
@@ -102,7 +102,7 @@ async function diagramPage(req: Req, reply: FastifyReply) {
|
|
|
102
102
|
const m = await model(s, d);
|
|
103
103
|
const crumbs: [string, string?][] = [['SQL Workshop', `${BASE}/sql`], ['Data Modeler', M], [d.name]];
|
|
104
104
|
if (!m) {
|
|
105
|
-
const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler')}<div class="alert alert-error" role="alert">The schema ${d.schema_name} doesn't exist any more, or this workspace can't use it.</div>
|
|
105
|
+
const main = html`<h1 class="u-mb1">${d.name}</h1>${workshopTabs('modeler', s)}<div class="alert alert-error" role="alert">The schema ${d.schema_name} doesn't exist any more, or this workspace can't use it.</div>
|
|
106
106
|
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="Delete the diagram ${d.name}?">${icon('trash')} Delete the diagram</button></form>`;
|
|
107
107
|
return send(reply, s, shell(s, d.name, crumbs, main, 'sql'));
|
|
108
108
|
}
|
|
@@ -151,7 +151,7 @@ async function diagramPage(req: Req, reply: FastifyReply) {
|
|
|
151
151
|
const settings = html`<details class="dm-settings"><summary>Rename or delete the diagram</summary>
|
|
152
152
|
<form method="post" action="${M}/${d.id}/rename" class="dm-search-row">${csrf(s)}<label class="sr-only" for="f_rename">Name</label><input id="f_rename" name="name" value="${d.name}" maxlength="120" required><button class="btn">${icon('edit')} Rename</button></form>
|
|
153
153
|
<form method="post" action="${M}/${d.id}/delete">${csrf(s)}<button class="btn btn-danger" data-confirm="Delete the diagram ${d.name}? The tables stay as they are.">${icon('trash')} Delete diagram</button></form></details>`;
|
|
154
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
|
|
154
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
|
|
155
155
|
<div class="dm-head"><h2 class="dm-name">${d.name}</h2><span class="muted small">Schema ${d.schema_name} · ${tables.length} objects · updated ${when(d.updated_at)} by ${d.updated_by ?? d.created_by}</span></div>
|
|
156
156
|
${missing.length ? html`<div class="alert alert-error" role="alert">Not in ${d.schema_name} any more: ${missing.join(', ')}. Saving the diagram leaves them out.</div>` : ''}
|
|
157
157
|
${toolbar}
|
|
@@ -353,7 +353,7 @@ export async function dataModelerRoutes(app: FastifyInstance) {
|
|
|
353
353
|
const script = diagramDdl(d.schema_name, new Set(m.tables.map((t) => t.name)), await readSchema(m.db.pool, d.schema_name));
|
|
354
354
|
if (req.query.download === '1')
|
|
355
355
|
return reply.header('content-disposition', `attachment; filename="${fileName(d.name)}.sql"`).type('application/sql; charset=utf-8').send(script);
|
|
356
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler')}
|
|
356
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('modeler', s)}
|
|
357
357
|
${region(`DDL of ${d.name}`, html`<p class="muted u-mt0">The CREATE statements of the diagram's ${m.tables.length} objects in schema ${d.schema_name}: tables, their keys, indexes, triggers, row level security, policies, comments and grants, and views. Review before running.</p>
|
|
358
358
|
<pre class="source" data-code-view="sql" tabindex="0" aria-label="DDL">${script}</pre>
|
|
359
359
|
<form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${script}">
|
|
@@ -171,7 +171,7 @@ async function page(req: Req, reply: FastifyReply) {
|
|
|
171
171
|
}
|
|
172
172
|
const title = `Security of ${cur.schema}.${cur.table}`;
|
|
173
173
|
const objUrl = `${BASE}/sql/objects?${new URLSearchParams({ schema: cur.schema, k: 'table', o: ref })}`;
|
|
174
|
-
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects')}${form(s, cur, def, { ref, sql, warnings, errors, output })}`;
|
|
174
|
+
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}${form(s, cur, def, { ref, sql, warnings, errors, output })}`;
|
|
175
175
|
return send(reply, s, shell(s, title, [['SQL Workshop', `${BASE}/sql`], ['Object Browser', `${BASE}/sql/objects`], [`${cur.schema}.${cur.table}`, objUrl], ['Security']], main, 'sql'));
|
|
176
176
|
}
|
|
177
177
|
|
package/src/builder/dataload.ts
CHANGED
|
@@ -139,7 +139,7 @@ export function resultHtml(r: LoadResult, failed: boolean) {
|
|
|
139
139
|
|
|
140
140
|
export async function dataLoadRoutes(app: FastifyInstance) {
|
|
141
141
|
const page = (s: Session, reply: FastifyReply, main: Raw) =>
|
|
142
|
-
send(reply, s, shell(s, 'Load Data', [['SQL Workshop', `${BASE}/sql`], ['Load Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('load')}${main}`, 'sql'));
|
|
142
|
+
send(reply, s, shell(s, 'Load Data', [['SQL Workshop', `${BASE}/sql`], ['Load Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('load', s)}${main}`, 'sql'));
|
|
143
143
|
|
|
144
144
|
// step 1: choose a file
|
|
145
145
|
app.get(`${BASE}/sql/load`, async (req: Req, reply) => {
|
package/src/builder/datapump.ts
CHANGED
|
@@ -36,7 +36,7 @@ async function admin(req: Req, reply: FastifyReply) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const page = (reply: FastifyReply, s: Session, title: string, trail: [string, string?][], body: Raw, code = 200) =>
|
|
39
|
-
send(reply.code(code), s, shell(s, title, crumbs(...trail), html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('datapump')}${body}`, 'sql'));
|
|
39
|
+
send(reply.code(code), s, shell(s, title, crumbs(...trail), html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('datapump', s)}${body}`, 'sql'));
|
|
40
40
|
|
|
41
41
|
export const size = (n: number | null) =>
|
|
42
42
|
n === null ? '' : n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MB` : `${(n / 1024 ** 3).toFixed(2)} GB`;
|
package/src/builder/ddl-tools.ts
CHANGED
|
@@ -65,7 +65,7 @@ export async function ddlToolRoutes(app: FastifyInstance) {
|
|
|
65
65
|
result = html`<div class="alert alert-error" role="alert">${(e as Error).message}</div>`;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ddl')}
|
|
68
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('ddl', s)}
|
|
69
69
|
<p class="muted">The DDL of a schema's objects, from the database catalog, in an order that runs: types, sequences, tables, functions, views, constraints, indexes, triggers, row level security, comments and grants (APEX: Generate DDL). Objects of extensions are left out.</p>
|
|
70
70
|
<form method="get" class="u-mb1">
|
|
71
71
|
<div class="form-grid">${schemaSelect('schema', 'Schema', schemas, schema)}</div>
|
|
@@ -114,7 +114,7 @@ export async function ddlToolRoutes(app: FastifyInstance) {
|
|
|
114
114
|
result = html`<div class="alert alert-error" role="alert">${(e as Error).message}</div>`;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('compare')}
|
|
117
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('compare', s)}
|
|
118
118
|
<p class="muted">Compares the objects of two schemas of this database by their DDL (APEX: Schema Comparison), e.g. a development copy with the schema it came from, and writes a script that makes the target like the source. The order of a table's columns is not compared.</p>
|
|
119
119
|
<form method="get" class="u-mb1">
|
|
120
120
|
<div class="form-grid">${schemaSelect('source', 'Source', schemas, source)}${schemaSelect('target', 'Target (to make like the source)', schemas, target)}</div>
|