pgkiln 0.53.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 +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +62 -8
- package/docs/guide/12-development.md +50 -3
- package/examples/gallery/README.md +37 -6
- package/examples/gallery/gallery.json +23 -1
- package/examples/gallery/samples/sample-calendar.sql +172 -0
- package/examples/gallery/samples/sample-cards.sql +157 -0
- package/examples/gallery/samples/sample-charts.sql +265 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
- package/examples/gallery/samples/sample-grids.sql +151 -0
- package/examples/gallery/samples/sample-reporting.sql +151 -0
- package/examples/gallery/samples/ux-patterns.sql +295 -0
- package/examples/gallery/utilities/announcements.json +24 -0
- package/examples/gallery/utilities/lookup-lists.json +28 -0
- package/examples/gallery/utilities/release-notes.json +41 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/builder.css +39 -1
- 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/gallery.ts +94 -47
- 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/src/runtime/charts.ts +4 -1
- package/src/runtime/regions.ts +8 -5
|
@@ -94,7 +94,7 @@ async function page(req: Req, appId: string, result?: { kind: Kind; run: RunResu
|
|
|
94
94
|
const scripts = (await owner.query('select id, name, kind, seq, script from meta.supporting_script where app_id = $1 order by kind, seq, name', [a.id])).rows;
|
|
95
95
|
const imported = req.query?.imported === '1';
|
|
96
96
|
const byKind = (k: Kind) => scripts.filter((x) => x.kind === k);
|
|
97
|
-
return { a, body: (s: Session) => html`${appHeader(a, 'shared')}
|
|
97
|
+
return { a, body: (s: Session) => html`${appHeader(a, 'shared', s)}
|
|
98
98
|
${imported && scripts.length ? html`<div class="alert alert-warning" role="status">${icon('alert')} This application came with supporting objects. They were <b>not</b> run: read them below and run them only if you trust them.</div>` : ''}
|
|
99
99
|
${result ? region(`Result: ${result.kind} scripts`, resultsHtml(result.kind, result.run)) : ''}
|
|
100
100
|
${region('Supporting objects', html`<p class="muted u-mt0">Scripts that install, upgrade or remove what the application needs in the database. They travel with the export and run only from here, when you choose to:
|
|
@@ -209,7 +209,7 @@ async function page(req: Req, reply: FastifyReply) {
|
|
|
209
209
|
if (errors.length) tab = 'columns';
|
|
210
210
|
}
|
|
211
211
|
const title = editing ? `Table ${editing.schema}.${editing.name}` : 'Create table';
|
|
212
|
-
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects')}
|
|
212
|
+
const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}
|
|
213
213
|
${form(s, def, { editing, schemas, refTables, ddl, errors, output, tab, ref: editing ? ref : '' })}`;
|
|
214
214
|
return send(reply, s, shell(s, title, [['SQL Workshop', `${BASE}/sql`], ['Object Browser', `${BASE}/sql/objects`], [title]], main, 'sql'));
|
|
215
215
|
}
|
package/src/builder/team.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '../team.ts';
|
|
9
9
|
import { TEAM_LANGUAGES, TEAM_SAMPLES } from '../team-samples.ts';
|
|
10
10
|
import { ago } from './home.ts';
|
|
11
|
+
import { tr } from './i18n.ts';
|
|
11
12
|
import { back, BASE, csrf, designerUrl, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
12
13
|
import { appAllowed, appWorkspace, currentWorkspace, workspaceAdmin } from './workspaces.ts';
|
|
13
14
|
|
|
@@ -975,12 +976,13 @@ export async function teamHomeBlock(s: Session) {
|
|
|
975
976
|
const ws = currentWorkspace(s);
|
|
976
977
|
if (ws < 0) return '';
|
|
977
978
|
const c = await teamCounts(ws, s.username);
|
|
978
|
-
|
|
979
|
+
const tx = tr(s);
|
|
980
|
+
return html`<section class="wh-team" aria-labelledby="wh-team-h"><h2 id="wh-team-h">${tx('rail.team')}</h2>
|
|
979
981
|
<ul class="ab-links ab-tasks">
|
|
980
|
-
<li><a href="${TEAM}">${icon('kanban')}
|
|
981
|
-
<li><a href="${TEAM}?assignee=me">${icon('user')}
|
|
982
|
-
${c.mentions ? html`<li><a href="${TEAM}#mentions">${icon('at')}
|
|
983
|
-
${c.set_up ? '' : html`<li><a href="${TEAM}/setup">${icon('settings')}
|
|
982
|
+
<li><a href="${TEAM}">${icon('kanban')} ${tx('team.open')} <b class="wh-team-n">${c.open}</b></a></li>
|
|
983
|
+
<li><a href="${TEAM}?assignee=me">${icon('user')} ${tx('team.mine')} <b class="wh-team-n">${c.mine}</b></a></li>
|
|
984
|
+
${c.mentions ? html`<li><a href="${TEAM}#mentions">${icon('at')} ${tx('team.mentions')} <b class="wh-team-n">${c.mentions}</b></a></li>` : ''}
|
|
985
|
+
${c.set_up ? '' : html`<li><a href="${TEAM}/setup">${icon('settings')} ${tx('team.setup')}</a></li>`}
|
|
984
986
|
</ul></section>`;
|
|
985
987
|
}
|
|
986
988
|
|
|
@@ -114,7 +114,7 @@ export async function themeRollerRoutes(app: FastifyInstance) {
|
|
|
114
114
|
};
|
|
115
115
|
const swatch = (hex: string | undefined) =>
|
|
116
116
|
!hex ? html`<span class="muted">base</span>` : ITEM_REF.test(hex) ? html`<code>${hex}</code>` : html`<input type="color" value="${hex}" disabled aria-label="${hex}" class="swatch"> <code>${hex}</code>`;
|
|
117
|
-
const main = html`${appHeader(a, 'settings')}
|
|
117
|
+
const main = html`${appHeader(a, 'settings', s)}
|
|
118
118
|
<div class="ide-body">
|
|
119
119
|
${region('Theme Roller: style variants', html`
|
|
120
120
|
<p class="muted u-mt0">Several saved styles for this application (APEX: theme styles). The base style
|
package/src/builder/top-sql.ts
CHANGED
|
@@ -84,7 +84,7 @@ export async function topSqlRoutes(app: FastifyInstance) {
|
|
|
84
84
|
: top.reason === 'no_role'
|
|
85
85
|
? html`<div class="alert alert-error" role="alert">This application runs as the owner role, so its statements can't be told apart. Give it a database role in Settings.</div>`
|
|
86
86
|
: statementsNote(top.reason, top.message);
|
|
87
|
-
const main = html`${appHeader(a, 'activity')}
|
|
87
|
+
const main = html`${appHeader(a, 'activity', s)}
|
|
88
88
|
${region('Top SQL', html`<p class="muted u-mt0">Statements run as the role <code>${a.db_role ?? '(owner)'}</code> since the statistics were last reset, from <code>pg_stat_statements</code>. Literals and binds show as <code>$1</code>, <code>$2</code>…; applications that share a role share this list. <a href="${BASE}/apps/${a.id}/activity">Back to Activity</a></p>
|
|
89
89
|
${body}`)}`;
|
|
90
90
|
return send(reply, s, shell(s, `Top SQL · ${a.name}`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Activity', `${BASE}/apps/${a.id}/activity`], ['Top SQL']], main));
|
package/src/builder/ui.ts
CHANGED
|
@@ -7,9 +7,11 @@ import { html, raw, type Raw } from '../html.ts';
|
|
|
7
7
|
import { icon } from '../icons.ts';
|
|
8
8
|
import { documentShell } from '../layout.ts';
|
|
9
9
|
import { getSession, saveState, takeFlash, type Session } from '../session.ts';
|
|
10
|
-
import { SQL_TOOLS, TOOL_GROUPS } from './sql-tools.ts';
|
|
10
|
+
import { SQL_TOOLS, TOOL_GROUPS, toolLabel } from './sql-tools.ts';
|
|
11
11
|
import { ensureDevbar } from '../runtime/devbar.ts';
|
|
12
12
|
import { noteChange } from './app-updates.ts';
|
|
13
|
+
import { tr } from './i18n.ts';
|
|
14
|
+
import { languageMenu, loadLanguage } from './language.ts';
|
|
13
15
|
|
|
14
16
|
// Shared building blocks of the builder UI.
|
|
15
17
|
|
|
@@ -101,94 +103,97 @@ export function shell(s: Session, title: string, crumbs: [string, string?][], ma
|
|
|
101
103
|
const ok = takeFlash(s, '__FLASH');
|
|
102
104
|
const err = takeFlash(s, '__ERROR');
|
|
103
105
|
const theme = builderTheme(s);
|
|
106
|
+
// (sprint 121) the developer's builder language (i18n.ts); known English breadcrumb labels are translated here
|
|
107
|
+
const tx = tr(s);
|
|
104
108
|
// the application these pages belong to (for the Search entry), from the breadcrumb
|
|
105
109
|
const appId = crumbs.map(([, href]) => /^\/builder\/apps\/(\d+)$/.exec(href ?? '')?.[1]).find(Boolean);
|
|
106
110
|
const rail = (key: Section | 'search' | 'help', href: string, ic: string, label: string, extra: Raw | '' = '') =>
|
|
107
111
|
html`<li><a href="${href}" class="rail-link" title="${label}"${section === key ? raw(' aria-current="page"') : ''}${extra}>${bicon(ic)}<span class="rail-label">${label}</span></a></li>`;
|
|
108
|
-
const back = [...crumbs].reverse().find(([, href], i) => i > 0 && href);
|
|
109
112
|
// (sprint 74) the default-password warning: on the Workspace home (sprint 109), the App Builder home and the Developers page only, until
|
|
110
113
|
// dismissed (for this sign-in); a dot on the avatar stays until the password is changed
|
|
111
114
|
const weak = !!s.state.__WEAK;
|
|
112
115
|
const weakBanner = weak && !s.state.__WEAK_HIDDEN && crumbs.length === 1 && (crumbs[0][0] === 'Home' || crumbs[0][0] === 'App Builder' || crumbs[0][0] === 'Developers');
|
|
116
|
+
const trail = crumbs.map(([label, href]): [string, string?] => [tx.label(label), href]);
|
|
117
|
+
const back = [...trail].reverse().find(([, href], i) => i > 0 && href);
|
|
113
118
|
const themeButton = (t: BuilderTheme, ic: string, label: string) =>
|
|
114
119
|
html`<button name="theme" value="${t}" aria-pressed="${theme === t ? 'true' : 'false'}">${bicon(ic)}<span>${label}</span></button>`;
|
|
115
120
|
return documentShell(
|
|
116
|
-
`${title} ·
|
|
117
|
-
html`<a class="skip-link" href="#main"
|
|
118
|
-
<nav class="ide-rail" aria-label="
|
|
119
|
-
<a class="rail-logo" href="${BASE}/home" title="
|
|
121
|
+
`${tx.label(title)} · ${tx('shell.builder')}`,
|
|
122
|
+
html`<a class="skip-link" href="#main">${tx('shell.skip')}</a>
|
|
123
|
+
<nav class="ide-rail" aria-label="${tx('shell.builder')}">
|
|
124
|
+
<a class="rail-logo" href="${BASE}/home" title="${tx('shell.logo')}">${brandMark()}<span class="sr-only">${tx('shell.logo_home')}</span></a>
|
|
120
125
|
<ul class="rail-list">
|
|
121
|
-
${rail('home', `${BASE}/home`, 'home', '
|
|
122
|
-
${rail('apps', `${BASE}/apps`, 'builder', '
|
|
123
|
-
${s.workspace?.sql_workshop === false ? '' : rail('sql', `${BASE}/sql`, 'database', '
|
|
124
|
-
${rail('gallery', `${BASE}/gallery`, 'package', '
|
|
125
|
-
${rail('reporter', `${BASE}/reporter`, 'pie-chart', '
|
|
126
|
-
${rail('team', `${BASE}/team`, 'kanban', '
|
|
127
|
-
${appId ? rail('search', `${BASE}/apps/${appId}/search`, 'search', '
|
|
126
|
+
${rail('home', `${BASE}/home`, 'home', tx('rail.home'))}
|
|
127
|
+
${rail('apps', `${BASE}/apps`, 'builder', tx('rail.app_builder'))}
|
|
128
|
+
${s.workspace?.sql_workshop === false ? '' : rail('sql', `${BASE}/sql`, 'database', tx('rail.sql_workshop'))}
|
|
129
|
+
${rail('gallery', `${BASE}/gallery`, 'package', tx('rail.gallery'))}
|
|
130
|
+
${rail('reporter', `${BASE}/reporter`, 'pie-chart', tx('rail.reporter'))}
|
|
131
|
+
${rail('team', `${BASE}/team`, 'kanban', tx('rail.team'))}
|
|
132
|
+
${appId ? rail('search', `${BASE}/apps/${appId}/search`, 'search', tx('rail.search_app')) : ''}
|
|
128
133
|
</ul>
|
|
129
134
|
<ul class="rail-list rail-bottom">
|
|
130
135
|
<li><details class="menu rail-user rail-admin">
|
|
131
|
-
<summary class="rail-link${section === 'users' || section === 'developers' ? ' rail-open' : ''}" title="
|
|
136
|
+
<summary class="rail-link${section === 'users' || section === 'developers' ? ' rail-open' : ''}" title="${tx('rail.admin')}"${section === 'users' || section === 'developers' ? raw(' aria-current="page"') : ''}>${icon('settings')}<span class="rail-label">${tx('rail.admin')}</span></summary>
|
|
132
137
|
<div class="menu-panel">
|
|
133
138
|
<div class="menu-section menu-links">
|
|
134
|
-
<strong
|
|
135
|
-
<a href="${BASE}/utilities">${icon('grid')}
|
|
139
|
+
<strong>${tx('rail.admin')}</strong>
|
|
140
|
+
<a href="${BASE}/utilities">${icon('grid')} ${tx('admin.utilities')}</a>
|
|
136
141
|
</div>
|
|
137
142
|
${s.workspace?.admin ? html`<div class="menu-section menu-links">
|
|
138
|
-
<span class="small muted"
|
|
139
|
-
<a href="${BASE}/workspaces/${s.workspace.id}#service">${icon('bell')}
|
|
140
|
-
<a href="${BASE}/workspaces/${s.workspace.id}#preferences">${icon('sliders-horizontal')}
|
|
141
|
-
<a href="${BASE}/workspaces/${s.workspace.id}#links">${icon('external-link')}
|
|
142
|
-
<a href="${BASE}/workspaces/${s.workspace.id}/utilization">${icon('chart')}
|
|
143
|
-
<a href="${BASE}/mail-queue">${icon('inbox')}
|
|
144
|
-
<a href="${BASE}/network">${icon('globe')}
|
|
145
|
-
<a href="${BASE}/workspaces/${s.workspace.id}">${icon('layers')}
|
|
143
|
+
<span class="small muted">${tx('admin.manage_service', { name: s.workspace.name })}</span>
|
|
144
|
+
<a href="${BASE}/workspaces/${s.workspace.id}#service">${icon('bell')} ${tx('admin.ws_message')}</a>
|
|
145
|
+
<a href="${BASE}/workspaces/${s.workspace.id}#preferences">${icon('sliders-horizontal')} ${tx('admin.ws_prefs')}</a>
|
|
146
|
+
<a href="${BASE}/workspaces/${s.workspace.id}#links">${icon('external-link')} ${tx('admin.ext_links')}</a>
|
|
147
|
+
<a href="${BASE}/workspaces/${s.workspace.id}/utilization">${icon('chart')} ${tx('admin.utilization')}</a>
|
|
148
|
+
<a href="${BASE}/mail-queue">${icon('inbox')} ${tx('admin.mail_queue')}</a>
|
|
149
|
+
<a href="${BASE}/network">${icon('globe')} ${tx('admin.network')}</a>
|
|
150
|
+
<a href="${BASE}/workspaces/${s.workspace.id}">${icon('layers')} ${tx('admin.ws_devs')}</a>
|
|
146
151
|
</div>` : ''}
|
|
147
152
|
<div class="menu-section menu-links">
|
|
148
|
-
<a href="${BASE}/users">${icon('user')}
|
|
149
|
-
<a href="${BASE}/developers">${icon('users')}
|
|
150
|
-
<a href="${BASE}/dashboard">${icon('activity')}
|
|
151
|
-
${s.instanceAdmin ? html`<a href="${BASE}/db-users">${icon('key')}
|
|
152
|
-
<a href="${BASE}/developers">${icon('key')}
|
|
153
|
+
<a href="${BASE}/users">${icon('user')} ${tx('admin.users')}</a>
|
|
154
|
+
<a href="${BASE}/developers">${icon('users')} ${tx('admin.developers')}</a>
|
|
155
|
+
<a href="${BASE}/dashboard">${icon('activity')} ${tx('admin.monitor')}</a>
|
|
156
|
+
${s.instanceAdmin ? html`<a href="${BASE}/db-users">${icon('key')} ${tx('admin.db_users')}</a>` : ''}
|
|
157
|
+
<a href="${BASE}/developers">${icon('key')} ${tx('admin.change_my_password')}</a>
|
|
153
158
|
</div>
|
|
154
159
|
</div></details></li>
|
|
155
160
|
${s.workspace?.links?.length ? html`<li><details class="menu rail-user rail-admin rail-extensions">
|
|
156
|
-
<summary class="rail-link" title="
|
|
157
|
-
<div class="menu-panel"><div class="menu-section menu-links"><strong
|
|
161
|
+
<summary class="rail-link" title="${tx('rail.extensions')}">${icon('puzzle')}<span class="rail-label">${tx('rail.extensions')}</span></summary>
|
|
162
|
+
<div class="menu-panel"><div class="menu-section menu-links"><strong>${tx('rail.extensions')}</strong>
|
|
158
163
|
${s.workspace.links.map((l) => html`<a href="${l.url}" target="_blank" rel="noopener noreferrer"${l.description ? html` title="${l.description}"` : ''}>${icon('external-link')} ${l.label}</a>`)}
|
|
159
164
|
</div></div></details></li>` : ''}
|
|
160
|
-
${rail('help', 'https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md', 'help', '
|
|
165
|
+
${rail('help', 'https://github.com/pgkiln/pgkiln/blob/main/docs/guide/03-builder.md', 'help', tx('rail.help'), raw(' target="_blank" rel="noopener"'))}
|
|
161
166
|
<li><details class="menu rail-user">
|
|
162
|
-
<summary class="rail-avatar${weak ? ' has-dot' : ''}" title="${weak ?
|
|
167
|
+
<summary class="rail-avatar${weak ? ' has-dot' : ''}" title="${weak ? tx('user.weak_title', { user: s.username }) : s.username}"><span aria-hidden="true">${initials(s.username ?? '')}</span><span class="sr-only">${tx(weak ? 'user.account_weak' : 'user.account', { user: s.username })}</span></summary>
|
|
163
168
|
<div class="menu-panel">
|
|
164
|
-
<div class="menu-section"><strong>${s.username}</strong><span class="muted small"
|
|
165
|
-
${weak ? html`<div class="menu-section"><a class="weak-note" href="${BASE}/developers">${icon('alert')}
|
|
169
|
+
<div class="menu-section"><strong>${s.username}</strong><span class="muted small">${tx('user.developer')}</span></div>
|
|
170
|
+
${weak ? html`<div class="menu-section"><a class="weak-note" href="${BASE}/developers">${icon('alert')} ${tx('user.weak_note')}</a></div>` : ''}
|
|
166
171
|
${workspaceMenu(s)}
|
|
167
|
-
<div class="menu-section"><span class="small muted" id="theme-label"
|
|
172
|
+
<div class="menu-section"><span class="small muted" id="theme-label">${tx('user.theme')}</span>
|
|
168
173
|
<form method="post" action="${BASE}/theme" class="segmented ide-theme" role="group" aria-labelledby="theme-label">${csrf(s)}
|
|
169
|
-
${themeButton('dark', 'moon', '
|
|
174
|
+
${themeButton('dark', 'moon', tx('user.theme_dark'))}${themeButton('light', 'sun', tx('user.theme_light'))}${themeButton('auto', 'monitor', tx('user.theme_auto'))}
|
|
170
175
|
</form></div>
|
|
171
176
|
<div class="menu-section menu-links">
|
|
172
|
-
<a href="${BASE}/developers">${icon('key')}
|
|
173
|
-
<form method="post" action="${BASE}/logout">${csrf(s)}<button>${icon('logout')}
|
|
177
|
+
<a href="${BASE}/developers">${icon('key')} ${tx('user.change_password')}</a>
|
|
178
|
+
<form method="post" action="${BASE}/logout">${csrf(s)}<button>${icon('logout')} ${tx('user.sign_out')}</button></form>
|
|
174
179
|
</div>
|
|
175
180
|
</div></details></li>
|
|
176
181
|
</ul>
|
|
177
182
|
</nav>
|
|
178
183
|
<div class="ide-main">
|
|
179
184
|
<header class="ide-bar">
|
|
180
|
-
${back ? html`<a class="ide-back" href="${back[1]}" title="
|
|
181
|
-
<nav class="ide-crumbs" aria-label="
|
|
182
|
-
html`<li>${href && i <
|
|
185
|
+
${back ? html`<a class="ide-back" href="${back[1]}" title="${tx('bar.back', { page: back[0] })}">${bicon('back')}<span class="sr-only">${tx('bar.back', { page: back[0] })}</span></a>` : ''}
|
|
186
|
+
<nav class="ide-crumbs" aria-label="${tx('bar.breadcrumb')}"><ol>${trail.map(([label, href], i) =>
|
|
187
|
+
html`<li>${href && i < trail.length - 1 ? html`<a href="${href}">${label}</a>` : html`<span aria-current="page">${label}</span>`}</li>`)}</ol></nav>
|
|
183
188
|
<form method="get" action="${BASE}/palette" class="ide-search" role="search" data-palette>
|
|
184
|
-
<label class="sr-only" for="ide-search-q"
|
|
185
|
-
<input id="ide-search-q" name="q" type="search" placeholder="
|
|
189
|
+
<label class="sr-only" for="ide-search-q">${tx('bar.search_label')}</label>
|
|
190
|
+
<input id="ide-search-q" name="q" type="search" placeholder="${tx('bar.search_placeholder')}" autocomplete="off" aria-keyshortcuts="Control+K"></form>
|
|
186
191
|
<div class="ide-tools">${opts.toolbar ?? ''}</div>
|
|
187
192
|
</header>
|
|
188
|
-
${s.workspace?.banner_label ? html`<div class="ide-banner ide-banner-${BANNER_COLORS.includes(s.workspace.banner_color ?? '') ? s.workspace.banner_color : 'blue'}" role="note" aria-label="
|
|
193
|
+
${s.workspace?.banner_label ? html`<div class="ide-banner ide-banner-${BANNER_COLORS.includes(s.workspace.banner_color ?? '') ? s.workspace.banner_color : 'blue'}" role="note" aria-label="${tx('bar.environment')}">${s.workspace.banner_label}</div>` : ''}
|
|
189
194
|
${weakBanner || ok || err ? html`<div class="ide-alerts">
|
|
190
|
-
${weakBanner ? html`<div class="alert alert-error alert-dismissible" role="alert"><span
|
|
191
|
-
<form method="post" action="${BASE}/weak/dismiss">${csrf(s)}<button class="link-button" aria-label="
|
|
195
|
+
${weakBanner ? html`<div class="alert alert-error alert-dismissible" role="alert"><span>${tx.rich('weak.banner', { link: html`<a href="${BASE}/developers">${tx('weak.change_now')}</a>` })}</span>
|
|
196
|
+
<form method="post" action="${BASE}/weak/dismiss">${csrf(s)}<button class="link-button" aria-label="${tx('weak.dismiss')}" title="${tx('weak.dismiss')}">${icon('close')}</button></form></div>` : ''}
|
|
192
197
|
${ok ? html`<div class="alert alert-success" role="status">${ok}</div>` : ''}
|
|
193
198
|
${err ? html`<div class="alert alert-error" role="alert">${err}</div>` : ''}
|
|
194
199
|
</div>` : ''}
|
|
@@ -196,17 +201,17 @@ export function shell(s: Session, title: string, crumbs: [string, string?][], ma
|
|
|
196
201
|
${main}
|
|
197
202
|
</main>
|
|
198
203
|
<footer class="ide-status">
|
|
199
|
-
<span title="
|
|
200
|
-
${s.workspace ? html`<span title="
|
|
201
|
-
<span title="
|
|
202
|
-
|
|
204
|
+
<span title="${tx('status.signed_in_as')}">${icon('user')}${s.username}</span>
|
|
205
|
+
${s.workspace ? html`<span title="${tx('status.workspace')}">${icon('layers')}${s.workspace.name}</span>` : ''}
|
|
206
|
+
<span title="${tx('status.database')}">${icon('database')}${DATABASE}</span>
|
|
207
|
+
${languageMenu(s)}
|
|
203
208
|
<span class="ide-status-version">pgkiln ${VERSION}</span>
|
|
204
209
|
</footer>
|
|
205
210
|
</div>`,
|
|
206
211
|
`builder-body ide${opts.full ? ' ide-full' : ''}`,
|
|
207
212
|
{},
|
|
208
213
|
builderHead(),
|
|
209
|
-
{ theme },
|
|
214
|
+
{ theme, lang: tx.lang },
|
|
210
215
|
);
|
|
211
216
|
}
|
|
212
217
|
|
|
@@ -232,50 +237,52 @@ export function select(name: string, label: string, value: unknown, options: (st
|
|
|
232
237
|
export const region = (title: string, body: Raw | Raw[], extra: Raw | '' = '', id = '') =>
|
|
233
238
|
html`<section class="region region-standard"${id ? raw(` id="${id.replace(/[^a-z0-9-]/g, '')}"`) : ''}><header class="region-header"><h2>${title}</h2>${extra}</header><div class="region-body">${body}</div></section>`;
|
|
234
239
|
|
|
235
|
-
/** Title and tabs of an application's builder pages (Pages, Shared Components, …). */
|
|
236
|
-
export const appHeader = (a: any, active: 'pages' | 'shared' | 'settings' | 'activity' | 'api' | 'search' | 'advisor') => {
|
|
240
|
+
/** Title and tabs of an application's builder pages (Pages, Shared Components, …), in the session's builder language. */
|
|
241
|
+
export const appHeader = (a: any, active: 'pages' | 'shared' | 'settings' | 'activity' | 'api' | 'search' | 'advisor', s?: Pick<Session, 'state'>) => {
|
|
242
|
+
const tx = tr(s);
|
|
237
243
|
const tab = (key: typeof active, href: string, ic: string, label: string) =>
|
|
238
244
|
html`<a class="ide-tab" href="${href}"${active === key ? raw(' aria-current="page"') : ''}>${bicon(ic)}<span>${label}</span></a>`;
|
|
239
245
|
return html`
|
|
240
246
|
<div class="ide-head">
|
|
241
247
|
<div class="ide-head-title"><span class="ide-app-icon" aria-hidden="true">${String(a.name ?? '?').slice(0, 1).toUpperCase()}</span>
|
|
242
|
-
<div><h1>${a.name}</h1><span class="muted small"
|
|
248
|
+
<div><h1>${a.name}</h1><span class="muted small">${tx('app.head.id', { id: a.id })} · /a/${a.alias}</span> ${availabilityBadge(a)}</div>
|
|
243
249
|
<div class="buttons ide-head-actions">
|
|
244
|
-
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/working-copies">${icon('layers')}
|
|
245
|
-
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/backups">${icon('history')}
|
|
246
|
-
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/availability">${icon('power')}
|
|
247
|
-
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/export">${icon('download')}
|
|
248
|
-
<a class="btn btn-sm btn-run" href="/a/${a.alias}" target="_blank" rel="noopener">${icon('play')}
|
|
250
|
+
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/working-copies">${icon('layers')} ${tx('app.head.copies')}</a>
|
|
251
|
+
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/backups">${icon('history')} ${tx('app.head.backups')}</a>
|
|
252
|
+
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/availability">${icon('power')} ${tx('app.head.availability')}</a>
|
|
253
|
+
<a class="btn btn-sm" href="${BASE}/apps/${a.id}/export">${icon('download')} ${tx('app.head.export')}</a>
|
|
254
|
+
<a class="btn btn-sm btn-run" href="/a/${a.alias}" target="_blank" rel="noopener">${icon('play')} ${tx('common.run')}</a>
|
|
249
255
|
</div></div>
|
|
250
|
-
<nav class="ide-tabs" aria-label="
|
|
251
|
-
${tab('pages', `${BASE}/apps/${a.id}`, 'pages', '
|
|
252
|
-
${tab('shared', `${BASE}/apps/${a.id}/shared`, 'shapes', '
|
|
253
|
-
${tab('activity', `${BASE}/apps/${a.id}/activity`, 'activity', '
|
|
254
|
-
${tab('api', `${BASE}/apps/${a.id}/api`, 'code', '
|
|
255
|
-
${tab('settings', `${BASE}/apps/${a.id}/settings`, 'settings', '
|
|
256
|
-
${tab('search', `${BASE}/apps/${a.id}/search`, 'search', '
|
|
257
|
-
${tab('advisor', `${BASE}/apps/${a.id}/advisor`, 'check', '
|
|
256
|
+
<nav class="ide-tabs" aria-label="${tx('app.head.tabs')}">
|
|
257
|
+
${tab('pages', `${BASE}/apps/${a.id}`, 'pages', tx('app.tab.pages'))}
|
|
258
|
+
${tab('shared', `${BASE}/apps/${a.id}/shared`, 'shapes', tx('app.tab.shared'))}
|
|
259
|
+
${tab('activity', `${BASE}/apps/${a.id}/activity`, 'activity', tx('app.tab.activity'))}
|
|
260
|
+
${tab('api', `${BASE}/apps/${a.id}/api`, 'code', tx('app.tab.api'))}
|
|
261
|
+
${tab('settings', `${BASE}/apps/${a.id}/settings`, 'settings', tx('app.tab.settings'))}
|
|
262
|
+
${tab('search', `${BASE}/apps/${a.id}/search`, 'search', tx('common.search'))}
|
|
263
|
+
${tab('advisor', `${BASE}/apps/${a.id}/advisor`, 'check', tx('app.tab.advisor'))}
|
|
258
264
|
</nav>
|
|
259
265
|
</div>`;
|
|
260
266
|
};
|
|
261
267
|
|
|
262
|
-
/** Tabs of the SQL Workshop. */
|
|
268
|
+
/** Tabs of the SQL Workshop, in the session's builder language (tool names: sql.tool.<key> in i18n/en.ts). */
|
|
263
269
|
export type WorkshopTab = 'home' | 'sql' | 'scripts' | 'quicksql' | 'query' | 'objects' | 'load' | 'unload' | 'sample' | 'ai' | 'ddl' | 'compare' | 'utilities' | 'charts' | 'modeler' | 'rest' | 'scheduling' | 'datapump' | 'monitor' | 'changes';
|
|
264
|
-
export const workshopTabs = (active: WorkshopTab) => {
|
|
270
|
+
export const workshopTabs = (active: WorkshopTab, s?: Pick<Session, 'state'>) => {
|
|
271
|
+
const tx = tr(s);
|
|
265
272
|
const tab = (key: string, href: string, ic: string, label: string) =>
|
|
266
273
|
html`<a class="ide-tab" href="${href}"${active === key ? raw(' aria-current="page"') : ''}>${icon(ic)}<span>${label}</span></a>`;
|
|
267
274
|
// (sprint 75) the main tools as tabs, the others in a More menu (the open one is a tab too), so the strip never
|
|
268
275
|
// scrolls; (sprint 96) the More menu under the Launchpad's headings, from SQL_TOOLS
|
|
269
276
|
const main = SQL_TOOLS.filter((t) => t.main);
|
|
270
277
|
const open = SQL_TOOLS.find((t) => !t.main && t.tab === active);
|
|
271
|
-
return html`<nav class="ide-tabs sw-tabs u-mb1" aria-label="
|
|
272
|
-
${tab('home', `${BASE}/sql`, 'home', '
|
|
273
|
-
${open ? tab(open.tab!, open.href, open.icon, open
|
|
274
|
-
<details class="menu sw-more"><summary class="ide-tab">${icon('chevron-down')}<span
|
|
278
|
+
return html`<nav class="ide-tabs sw-tabs u-mb1" aria-label="${tx('rail.sql_workshop')}">
|
|
279
|
+
${tab('home', `${BASE}/sql`, 'home', tx('rail.home'))}${main.map((t) => tab(t.tab!, t.href, t.icon, toolLabel(tx, t)))}
|
|
280
|
+
${open ? tab(open.tab!, open.href, open.icon, toolLabel(tx, open)) : ''}
|
|
281
|
+
<details class="menu sw-more"><summary class="ide-tab">${icon('chevron-down')}<span>${tx('sw.more')}</span></summary>
|
|
275
282
|
<div class="menu-panel align-right">${TOOL_GROUPS.map(([g, heading]) => {
|
|
276
283
|
// administrators' tools are on the Launchpad and the Utilities page, not in every developer's menu
|
|
277
284
|
const list = SQL_TOOLS.filter((t) => t.group === g && !t.main && !t.admin);
|
|
278
|
-
return list.length ? html`<div class="menu-section menu-links"><span class="small muted">${heading}</span>${list.map((t) => html`<a href="${t.href}"${t.tab && t.tab === active ? raw(' aria-current="page"') : ''}>${icon(t.icon)} ${t
|
|
285
|
+
return list.length ? html`<div class="menu-section menu-links"><span class="small muted">${tx.dyn(`sql.group.${g}`, heading)}</span>${list.map((t) => html`<a href="${t.href}"${t.tab && t.tab === active ? raw(' aria-current="page"') : ''}>${icon(t.icon)} ${toolLabel(tx, t)}</a>`)}</div>` : '';
|
|
279
286
|
})}</div></details></nav>`;
|
|
280
287
|
};
|
|
281
288
|
|
|
@@ -304,9 +311,11 @@ export async function developer(req: Req, reply: FastifyReply) {
|
|
|
304
311
|
return null;
|
|
305
312
|
}
|
|
306
313
|
if (req.method === 'POST' && req.body?.__csrf !== s.csrf_token) {
|
|
307
|
-
reply.code(403).send('
|
|
314
|
+
reply.code(403).send(tr(s)('error.csrf'));
|
|
308
315
|
return null;
|
|
309
316
|
}
|
|
317
|
+
// (sprint 121) a session from before the developer's language was kept in it: the stored choice (language.ts)
|
|
318
|
+
if (s.state.__BLANG === undefined) await loadLanguage(s);
|
|
310
319
|
// (079) the developer toolbar on the applications' pages in this browser
|
|
311
320
|
await ensureDevbar(req, reply, s);
|
|
312
321
|
// workspaces: an application outside the developer's workspaces doesn't exist for them
|
|
@@ -318,8 +327,9 @@ export async function developer(req: Req, reply: FastifyReply) {
|
|
|
318
327
|
}
|
|
319
328
|
// (093) a workspace preference may turn the SQL Workshop off
|
|
320
329
|
if (s.workspace?.sql_workshop === false && /^\/builder\/sql(?:[/?]|$)/.test(req.url)) {
|
|
330
|
+
const tx = tr(s);
|
|
321
331
|
await send(reply.code(403), s, shell(s, 'SQL Workshop', [['App Builder', BASE], ['SQL Workshop']],
|
|
322
|
-
html`<h1
|
|
332
|
+
html`<h1>${tx('rail.sql_workshop')}</h1><div class="alert alert-info" role="status">${tx(s.workspaces && s.workspaces.length > 1 ? 'error.sql_off_switch' : 'error.sql_off', { name: s.workspace.name })}</div>`, 'sql'));
|
|
323
333
|
return null;
|
|
324
334
|
}
|
|
325
335
|
// a page or application locked by another developer can't be changed (locks.ts)
|
package/src/builder/unload.ts
CHANGED
|
@@ -88,7 +88,7 @@ const list = (v: unknown) => (Array.isArray(v) ? v : v === undefined || v === nu
|
|
|
88
88
|
export async function unloadRoutes(app: FastifyInstance) {
|
|
89
89
|
const page = (s: Session, reply: FastifyReply, main: Raw, code = 200) => {
|
|
90
90
|
reply.code(code);
|
|
91
|
-
return send(reply, s, shell(s, 'Unload Data', [['SQL Workshop', `${BASE}/sql`], ['Unload Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('unload')}${main}`, 'sql'));
|
|
91
|
+
return send(reply, s, shell(s, 'Unload Data', [['SQL Workshop', `${BASE}/sql`], ['Unload Data']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('unload', s)}${main}`, 'sql'));
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const formatFields = (b: Body) => {
|
package/src/builder/wizards.ts
CHANGED
|
@@ -246,7 +246,7 @@ export async function wizardRoutes(app: FastifyInstance) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
const kindName = WIZARD_KINDS.find((k) => k[0] === kind)![1];
|
|
249
|
-
const main = html`${appHeader(a, 'pages')}
|
|
249
|
+
const main = html`${appHeader(a, 'pages', s)}
|
|
250
250
|
${region(`${kindName} on ${rel.t}`, html`
|
|
251
251
|
<p class="muted u-mt0">${WIZARD_KINDS.find((k) => k[0] === kind)![2]} The proposals come from the table's columns, key and foreign keys; change them below. <a href="${BASE}/apps/${a.id}">Back to choose another type or table</a>.</p>
|
|
252
252
|
${needsPk && !pk ? html`<p class="alert alert-error" role="status">${rel.t} has no single-column primary key, which this page type needs.</p>` : ''}
|
|
@@ -179,7 +179,7 @@ export async function workflowMigrateRoutes(app: FastifyInstance) {
|
|
|
179
179
|
: html`<p class="wf-problem">Map a step for every instance (or leave the others out) and preview again: nothing is migrated while one can't move.</p>`}`)
|
|
180
180
|
: ''}
|
|
181
181
|
${target ? region(`Version ${to}`, html`<div class="wf-wrap">${workflowDiagram(target.steps, { id: 'target' })}</div>`) : ''}`;
|
|
182
|
-
const main = html`${appHeader(a, 'shared')}<div class="ide-body">${body}</div>`;
|
|
182
|
+
const main = html`${appHeader(a, 'shared', s)}<div class="ide-body">${body}</div>`;
|
|
183
183
|
return send(reply, s, shell(s, `Migrate ${d.name}`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Shared Components', `${BASE}/apps/${a.id}/shared`],
|
|
184
184
|
[d.name, `${BASE}/apps/${a.id}/shared?c=workflow_definition-${d.id}`], ['Migrate instances']], main));
|
|
185
185
|
});
|
|
@@ -86,7 +86,7 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
86
86
|
<div class="buttons"><button class="btn btn-hot">${icon('plus')} Create working copy</button></div>
|
|
87
87
|
</form>`)}`;
|
|
88
88
|
}
|
|
89
|
-
const main = html`${appHeader(a, 'pages')}<div class="ide-body">${body}</div>`;
|
|
89
|
+
const main = html`${appHeader(a, 'pages', s)}<div class="ide-body">${body}</div>`;
|
|
90
90
|
return send(reply, s, shell(s, `${a.name} working copies`, [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Working copies']], main));
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -148,7 +148,7 @@ export async function workingCopyRoutes(app: FastifyInstance) {
|
|
|
148
148
|
</div>
|
|
149
149
|
</form>`
|
|
150
150
|
: html`<p class="u-mt0">The working copy and the main application are the same: nothing to merge.</p>`;
|
|
151
|
-
const page = html`${appHeader(a, 'pages')}<div class="ide-body">
|
|
151
|
+
const page = html`${appHeader(a, 'pages', s)}<div class="ide-body">
|
|
152
152
|
${region(`Compare with ${main.name}`, html`<p class="muted u-mt0">Working copy <b>${l.copy.name}</b> of
|
|
153
153
|
<a href="${BASE}/apps/${main.id}">${main.name}</a>, compared per component with the main application as it was
|
|
154
154
|
when the copy was made${l.copy.refreshed_at || l.copy.merged_at ? ', refreshed or merged' : ''}.</p>${body}`)}
|
package/src/builder/worksheet.ts
CHANGED
|
@@ -557,7 +557,7 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
557
557
|
html`<button class="btn btn-sm${hot ? ' btn-hot' : ''}" name="mode" value="${m}" title="${label} (${key})" data-ws-key="${key}">${icon(ic)}<span>${label}</span></button>`;
|
|
558
558
|
const main = html`${chartCss ? html`<style nonce="${req.cspNonce}">${raw(chartCss.replace(/</g, ''))}</style>` : ''}<div class="ws-page">
|
|
559
559
|
<h1 class="sr-only">SQL Worksheet</h1>
|
|
560
|
-
${workshopTabs('sql')}
|
|
560
|
+
${workshopTabs('sql', s)}
|
|
561
561
|
<form method="post" action="${BASE}/sql/worksheet" class="ws" id="ws" data-ran="${ran ? `${ran.start},${ran.end}` : ''}" data-error-line="${firstError?.errorLine ?? ''}"
|
|
562
562
|
data-source="${source}" data-script-id="${script?.id ?? ''}" data-script-name="${script?.name ?? ''}" data-tab="${tabKey}">${csrf(s)}
|
|
563
563
|
<button class="ws-default" name="mode" value="nav" tabindex="-1">Refresh the navigator</button>
|