pgkiln 0.81.0 → 0.83.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/147_condition_types.sql +80 -0
- package/db/migrations/148_validation_types.sql +44 -0
- package/db/migrations/149_dynamic_action_events.sql +45 -0
- package/db/migrations/150_process_types.sql +23 -0
- package/db/migrations/151_item_types.sql +16 -0
- package/db/migrations/152_workflow_diagram_region.sql +10 -0
- package/db/migrations/153_component_events.sql +21 -0
- package/docs/apex-feature-parity.md +11 -9
- package/docs/guide/02-concepts.md +3 -3
- package/docs/guide/03-builder.md +8 -1
- package/docs/guide/04-pages-and-regions.md +19 -4
- package/docs/guide/05-items.md +5 -0
- package/docs/guide/06-processing.md +104 -17
- package/docs/guide/07-dynamic-actions.md +43 -1
- package/docs/guide/11-from-apex.md +8 -0
- package/docs/guide/12-development.md +9 -1
- package/examples/hr/hr_79_condition_types.sql +17 -0
- package/package.json +1 -1
- package/public/app.css +14 -0
- package/public/app.js +242 -13
- package/public/builder.css +25 -2
- package/public/data-modeler.js +8 -5
- package/public/sql-monitor.js +5 -2
- package/src/appfiles.ts +4 -4
- package/src/builder/advisor.ts +43 -7
- package/src/builder/ai-builder.ts +50 -46
- package/src/builder/api.ts +49 -49
- package/src/builder/autorest.ts +92 -87
- package/src/builder/changelog.ts +39 -37
- package/src/builder/code-editor.ts +14 -10
- package/src/builder/components.ts +54 -23
- package/src/builder/conditions.ts +64 -0
- package/src/builder/data-modeler.ts +89 -84
- package/src/builder/data-security.ts +39 -37
- package/src/builder/dataload.ts +69 -65
- package/src/builder/datapump.ts +108 -97
- package/src/builder/ddl-tools.ts +38 -34
- package/src/builder/designer.ts +9 -1
- package/src/builder/document-view.ts +11 -11
- package/src/builder/i18n/de.ts +1662 -0
- package/src/builder/i18n/en.ts +1662 -0
- package/src/builder/i18n/es.ts +1662 -0
- package/src/builder/i18n/fr.ts +1662 -0
- package/src/builder/i18n/it.ts +1662 -0
- package/src/builder/i18n/ja.ts +1662 -0
- package/src/builder/i18n/ko.ts +1662 -0
- package/src/builder/i18n/nl.ts +1662 -0
- package/src/builder/i18n/pt.ts +1662 -0
- package/src/builder/i18n/zh-cn.ts +1662 -0
- package/src/builder/i18n/zh-tw.ts +1662 -0
- package/src/builder/i18n.ts +18 -0
- package/src/builder/object-actions.ts +18 -16
- package/src/builder/object-browser.ts +2 -2
- package/src/builder/querybuilder.ts +42 -36
- package/src/builder/quicksql.ts +16 -18
- package/src/builder/region-help.ts +3 -1
- package/src/builder/region-settings.ts +225 -198
- package/src/builder/report-settings.ts +64 -60
- package/src/builder/rest-modules.ts +145 -143
- package/src/builder/sampledata.ts +59 -52
- package/src/builder/scheduling.ts +180 -159
- package/src/builder/scripts.ts +71 -65
- package/src/builder/sql-charts.ts +36 -30
- package/src/builder/sql-monitor.ts +95 -89
- package/src/builder/sql-utilities.ts +47 -40
- package/src/builder/top-sql.ts +21 -17
- package/src/builder/unload.ts +48 -44
- package/src/builder/worksheet.ts +3 -3
- package/src/cron-schedule.ts +47 -17
- package/src/i18n/ar.ts +5 -0
- package/src/i18n/cs.ts +5 -0
- package/src/i18n/da.ts +5 -0
- package/src/i18n/de.ts +5 -0
- package/src/i18n/el.ts +5 -0
- package/src/i18n/es.ts +5 -0
- package/src/i18n/fi.ts +5 -0
- package/src/i18n/fr.ts +5 -0
- package/src/i18n/he.ts +5 -0
- package/src/i18n/it.ts +5 -0
- package/src/i18n/ja.ts +5 -0
- package/src/i18n/ko.ts +5 -0
- package/src/i18n/nb.ts +5 -0
- package/src/i18n/pl.ts +5 -0
- package/src/i18n/pt.ts +5 -0
- package/src/i18n/ru.ts +5 -0
- package/src/i18n/sv.ts +5 -0
- package/src/i18n/tr.ts +5 -0
- package/src/i18n/uk.ts +5 -0
- package/src/i18n/zh.ts +5 -0
- package/src/i18n.ts +10 -0
- package/src/metadata.ts +74 -9
- package/src/runtime/authz.ts +9 -5
- package/src/runtime/calendar.ts +3 -2
- package/src/runtime/context.ts +6 -0
- package/src/runtime/da-styles.ts +37 -0
- package/src/runtime/documents.ts +3 -2
- package/src/runtime/engine.ts +95 -13
- package/src/runtime/grid.ts +1 -0
- package/src/runtime/items.ts +33 -0
- package/src/runtime/logic.ts +79 -8
- package/src/runtime/maps.ts +56 -7
- package/src/runtime/process-types.ts +139 -0
- package/src/runtime/processes.ts +2 -0
- package/src/runtime/regions.ts +4 -0
- package/src/runtime/render.ts +19 -3
- package/src/runtime/routes.ts +6 -2
- package/src/runtime/tree.ts +4 -3
- package/src/runtime/workflow-diagram.ts +45 -0
- package/src/workflow.ts +10 -2
package/src/builder/top-sql.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { owner } from '../db.ts';
|
|
|
3
3
|
import { html, raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import { appHeader, back, BASE, csrf, developer, flash, region, send, shell, type Req } from './ui.ts';
|
|
6
|
+
import { tr, type BuilderTranslate } from './i18n.ts';
|
|
6
7
|
|
|
7
8
|
// App Builder → Activity → Top SQL: the statements the application's
|
|
8
9
|
// database role ran, from pg_stat_statements (APEX: Top SQL in the
|
|
@@ -27,12 +28,14 @@ export async function statementsState(): Promise<{ ok: true } | { ok: false; rea
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Why the statistics can't be shown, and how to turn them on. */
|
|
30
|
-
export function statementsNote(reason: 'not_installed' | 'not_loaded' | 'error', message?: string) {
|
|
31
|
+
export function statementsNote(tx: BuilderTranslate, reason: 'not_installed' | 'not_loaded' | 'error', message?: string) {
|
|
32
|
+
const ext = html`<code>pg_stat_statements</code>`;
|
|
33
|
+
const setting = html`<code>shared_preload_libraries = 'pg_stat_statements'</code>`;
|
|
31
34
|
return html`<div class="alert alert-error" role="alert">${reason === 'not_installed'
|
|
32
|
-
?
|
|
35
|
+
? tx.rich('topsql.not_installed', { ext, setting, alter: html`<code>alter system set shared_preload_libraries = 'pg_stat_statements'</code>`, create: html`<code>create extension pg_stat_statements;</code>` })
|
|
33
36
|
: reason === 'not_loaded'
|
|
34
|
-
?
|
|
35
|
-
:
|
|
37
|
+
? tx.rich('topsql.not_loaded', { ext, setting, compose: html`<code>docker-compose.yml</code>` })
|
|
38
|
+
: tx('topsql.read_error', { message: message ?? '' })}</div>`;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/** The role's statements, slowest in total first (or by mean time, calls or rows). */
|
|
@@ -66,28 +69,29 @@ export async function topSqlRoutes(app: FastifyInstance) {
|
|
|
66
69
|
if (!s) return;
|
|
67
70
|
const a = await appOf(req.params.id);
|
|
68
71
|
if (!a) return reply.code(404).send('Not found');
|
|
72
|
+
const tx = tr(s);
|
|
69
73
|
const sort = SORTS[req.query.sort ?? ''] ? req.query.sort! : 'total';
|
|
70
74
|
const top = await topSql(a.db_role, sort);
|
|
71
75
|
const sortLink = (key: string, label: string) =>
|
|
72
76
|
html`<a href="${BASE}/apps/${a.id}/top-sql?sort=${key}"${sort === key ? raw(' aria-current="true"') : ''}>${label}</a>`;
|
|
73
77
|
const body = top.ok
|
|
74
78
|
? top.rows.length
|
|
75
|
-
? html`<div class="seg u-mb1"
|
|
76
|
-
<div class="table-wrap"><table class="report report-reflow top-sql"><thead><tr><th class="num"
|
|
79
|
+
? html`<div class="seg u-mb1">${tx.rich('topsql.sort_by', { total: sortLink('total', tx('topsql.sort.total')), mean: sortLink('mean', tx('topsql.sort.mean')), calls: sortLink('calls', tx('topsql.sort.calls')), rows: sortLink('rows', tx('topsql.sort.rows')) })}</div>
|
|
80
|
+
<div class="table-wrap"><table class="report report-reflow top-sql"><thead><tr><th class="num">${tx('sm.col.calls')}</th><th class="num">${tx('sm.col.total_ms')}</th><th class="num">${tx('sm.col.mean_ms')}</th><th class="num">${tx('sm.col.rows')}</th><th class="num">${tx('topsql.share')}</th><th>${tx('sm.col.statement')}</th></tr></thead>
|
|
77
81
|
<tbody>${top.rows.map((r) => html`<tr>
|
|
78
|
-
<td class="num" data-label="
|
|
79
|
-
<td class="num" data-label="
|
|
80
|
-
<td data-label="
|
|
82
|
+
<td class="num" data-label="${tx('sm.col.calls')}">${r.calls}</td><td class="num" data-label="${tx('sm.col.total_ms')}">${r.total_ms}</td><td class="num" data-label="${tx('sm.col.mean_ms')}">${r.mean_ms}</td>
|
|
83
|
+
<td class="num" data-label="${tx('sm.col.rows')}">${r.rows}</td><td class="num" data-label="${tx('topsql.share')}">${r.share ?? 0}%</td>
|
|
84
|
+
<td data-label="${tx('sm.col.statement')}"><code class="sql-text">${r.query}</code></td></tr>`)}</tbody></table></div>
|
|
81
85
|
<form method="post" action="${BASE}/apps/${a.id}/top-sql/reset" class="u-mt1">${csrf(s)}
|
|
82
|
-
<button class="btn" data-confirm="
|
|
83
|
-
: html`<p class="muted"
|
|
86
|
+
<button class="btn" data-confirm="${tx('topsql.reset_confirm', { role: a.db_role })}">${icon('history')} ${tx('topsql.reset')}</button></form>`
|
|
87
|
+
: html`<p class="muted">${tx.rich('topsql.none_yet', { role: html`<code>${a.db_role}</code>` })}</p>`
|
|
84
88
|
: top.reason === 'no_role'
|
|
85
|
-
? html`<div class="alert alert-error" role="alert"
|
|
86
|
-
: statementsNote(top.reason, top.message);
|
|
89
|
+
? html`<div class="alert alert-error" role="alert">${tx('topsql.no_role')}</div>`
|
|
90
|
+
: statementsNote(tx, top.reason, top.message);
|
|
87
91
|
const main = html`${appHeader(a, 'activity', s)}
|
|
88
|
-
${region('
|
|
92
|
+
${region(tx('sql.tool.topsql'), html`<p class="muted u-mt0">${tx.rich('topsql.intro', { role: html`<code>${a.db_role ?? tx('topsql.owner')}</code>`, ext: html`<code>pg_stat_statements</code>`, p1: html`<code>$1</code>`, p2: html`<code>$2</code>` })} <a href="${BASE}/apps/${a.id}/activity">${tx('topsql.back_activity')}</a></p>
|
|
89
93
|
${body}`)}`;
|
|
90
|
-
return send(reply, s, shell(s,
|
|
94
|
+
return send(reply, s, shell(s, `${tx('sql.tool.topsql')} · ${a.name}`, [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('topsql.activity'), `${BASE}/apps/${a.id}/activity`], [tx('sql.tool.topsql')]], main));
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
app.post(`${BASE}/apps/:id/top-sql/reset`, async (req: Req, reply) => {
|
|
@@ -96,9 +100,9 @@ export async function topSqlRoutes(app: FastifyInstance) {
|
|
|
96
100
|
const a = await appOf(req.params.id);
|
|
97
101
|
if (!a) return reply.code(404).send('Not found');
|
|
98
102
|
try {
|
|
99
|
-
if (!a.db_role) throw new Error('
|
|
103
|
+
if (!a.db_role) throw new Error(tr(s)('topsql.no_db_role'));
|
|
100
104
|
await owner.query(`select pg_stat_statements_reset((select oid from pg_roles where rolname = $1), (select oid from pg_database where datname = current_database()), 0)`, [a.db_role]);
|
|
101
|
-
flash(s, '
|
|
105
|
+
flash(s, tr(s)('topsql.reset_done'));
|
|
102
106
|
} catch (e) {
|
|
103
107
|
flash(s, (e as Error).message, 'error');
|
|
104
108
|
}
|
package/src/builder/unload.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '../unload.ts';
|
|
23
23
|
import { BASE, csrf, developer, region, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
|
|
24
24
|
import { sessionSchema, workshopDb } from './workspaces.ts';
|
|
25
|
+
import { tr, type BuilderTranslate } from './i18n.ts';
|
|
25
26
|
|
|
26
27
|
// SQL Workshop → Unload Data (APEX: Data Workshop → Unload Data): a table or
|
|
27
28
|
// view (chosen columns, an optional WHERE and ORDER BY) or a query, to CSV,
|
|
@@ -88,86 +89,89 @@ const list = (v: unknown) => (Array.isArray(v) ? v : v === undefined || v === nu
|
|
|
88
89
|
export async function unloadRoutes(app: FastifyInstance) {
|
|
89
90
|
const page = (s: Session, reply: FastifyReply, main: Raw, code = 200) => {
|
|
90
91
|
reply.code(code);
|
|
91
|
-
|
|
92
|
+
const tx = tr(s);
|
|
93
|
+
return send(reply, s, shell(s, tx('sql.tool.unload'), [[tx('rail.sql_workshop'), `${BASE}/sql`], [tx('sql.tool.unload')]], html`<h1 class="u-mb1">${tx('rail.sql_workshop')}</h1>${workshopTabs('unload', s)}${main}`, 'sql'));
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
const formatFields = (b: Body) => {
|
|
96
|
+
const formatFields = (tx: BuilderTranslate, b: Body) => {
|
|
95
97
|
const fmt = (UNLOAD_FORMATS as string[]).includes(b.format ?? '') ? b.format : 'csv';
|
|
96
98
|
const posted = b.format !== undefined; // unticked checkboxes post nothing
|
|
97
99
|
const on = (name: string) => (!posted || b[name] === '1' ? raw(' checked') : '');
|
|
98
100
|
const opt = (map: Record<string, string>, field: string, labels: Record<string, string>) =>
|
|
99
101
|
Object.keys(map).map((k) => html`<option value="${k}"${(b[field] ?? Object.keys(map)[0]) === k ? raw(' selected') : ''}>${labels[k]}</option>`);
|
|
100
102
|
return html`
|
|
101
|
-
<fieldset class="prop-group u-mt125"><legend
|
|
102
|
-
<div class="unload-formats" role="radiogroup" aria-label="
|
|
103
|
-
(f) => html`<label class="check"><input type="radio" name="format" value="${f}"${fmt === f ? raw(' checked') : ''}> ${{ csv: 'CSV', json: 'JSON', xlsx: '
|
|
103
|
+
<fieldset class="prop-group u-mt125"><legend>${tx('cl.format')}</legend>
|
|
104
|
+
<div class="unload-formats" role="radiogroup" aria-label="${tx('cl.format')}">${UNLOAD_FORMATS.map(
|
|
105
|
+
(f) => html`<label class="check"><input type="radio" name="format" value="${f}"${fmt === f ? raw(' checked') : ''}> ${{ csv: 'CSV', json: 'JSON', xlsx: tx('ul.fmt.xlsx'), xml: 'XML', sql: tx('ul.fmt.sql') }[f]}</label>`,
|
|
104
106
|
)}</div>
|
|
105
107
|
</fieldset>
|
|
106
108
|
<fieldset class="prop-group"><legend>CSV</legend><div class="form-grid">
|
|
107
|
-
<div class="field"><label class="label" for="f_delimiter"
|
|
108
|
-
<select id="f_delimiter" name="delimiter">${opt(CSV_DELIMITERS, 'delimiter', { comma: '
|
|
109
|
-
<div class="field"><label class="label" for="f_enclosure"
|
|
110
|
-
<select id="f_enclosure" name="enclosure">${opt(CSV_ENCLOSURES, 'enclosure', { double: '
|
|
111
|
-
<small class="help"
|
|
109
|
+
<div class="field"><label class="label" for="f_delimiter">${tx('ul.separator')}</label>
|
|
110
|
+
<select id="f_delimiter" name="delimiter">${opt(CSV_DELIMITERS, 'delimiter', { comma: tx('ul.sep.comma'), semicolon: tx('ul.sep.semicolon'), tab: tx('ul.sep.tab'), pipe: tx('ul.sep.pipe') })}</select></div>
|
|
111
|
+
<div class="field"><label class="label" for="f_enclosure">${tx('ul.enclosed')}</label>
|
|
112
|
+
<select id="f_enclosure" name="enclosure">${opt(CSV_ENCLOSURES, 'enclosure', { double: tx('ul.enc.double'), single: tx('ul.enc.single') })}</select>
|
|
113
|
+
<small class="help">${tx('ul.enclosed_help')}</small></div>
|
|
112
114
|
<div class="field"><span class="label" aria-hidden="true"></span>
|
|
113
|
-
<label class="check"><input type="checkbox" name="header" value="1"${on('header')}>
|
|
114
|
-
<label class="check"><input type="checkbox" name="bom" value="1"${on('bom')}>
|
|
115
|
+
<label class="check"><input type="checkbox" name="header" value="1"${on('header')}> ${tx('ul.header')}</label>
|
|
116
|
+
<label class="check"><input type="checkbox" name="bom" value="1"${on('bom')}> ${tx('ul.bom')}</label></div>
|
|
115
117
|
</div></fieldset>
|
|
116
118
|
<fieldset class="prop-group"><legend>XML</legend><div class="form-grid">
|
|
117
|
-
<div class="field"><label class="label" for="f_root_tag"
|
|
118
|
-
<div class="field"><label class="label" for="f_row_tag"
|
|
119
|
-
<small class="help"
|
|
119
|
+
<div class="field"><label class="label" for="f_root_tag">${tx('ul.root')}</label><input id="f_root_tag" name="root_tag" value="${b.root_tag ?? DEFAULT_UNLOAD.rootTag}" maxlength="64"></div>
|
|
120
|
+
<div class="field"><label class="label" for="f_row_tag">${tx('ul.row')}</label><input id="f_row_tag" name="row_tag" value="${b.row_tag ?? DEFAULT_UNLOAD.rowTag}" maxlength="64">
|
|
121
|
+
<small class="help">${tx('ul.row_help')}</small></div>
|
|
120
122
|
</div></fieldset>`;
|
|
121
123
|
};
|
|
122
124
|
|
|
123
|
-
const sourceTabs = (active: 'table' | 'query') =>
|
|
124
|
-
html`<nav class="ide-tabs u-mb1" aria-label="
|
|
125
|
-
<a class="ide-tab" href="${BASE}/sql/unload"${active === 'table' ? raw(' aria-current="page"') : ''}>${icon('table')}<span
|
|
126
|
-
<a class="ide-tab" href="${BASE}/sql/unload?source=query"${active === 'query' ? raw(' aria-current="page"') : ''}>${icon('code')}<span
|
|
125
|
+
const sourceTabs = (tx: BuilderTranslate, active: 'table' | 'query') =>
|
|
126
|
+
html`<nav class="ide-tabs u-mb1" aria-label="${tx('ul.from')}">
|
|
127
|
+
<a class="ide-tab" href="${BASE}/sql/unload"${active === 'table' ? raw(' aria-current="page"') : ''}>${icon('table')}<span>${tx('ul.table_or_view')}</span></a>
|
|
128
|
+
<a class="ide-tab" href="${BASE}/sql/unload?source=query"${active === 'query' ? raw(' aria-current="page"') : ''}>${icon('code')}<span>${tx('sm.col.query')}</span></a></nav>`;
|
|
127
129
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
+
const introOf = (tx: BuilderTranslate) => html`<p class="muted">${tx('ul.intro', { n: DOWNLOAD_MAX_ROWS.toLocaleString('en') })}
|
|
131
|
+
${tx.rich('ul.intro2', { load: html`<a href="${BASE}/sql/load">${tx('sql.tool.load')}</a>` })}</p>`;
|
|
130
132
|
|
|
131
133
|
const form = async (s: Session, b: Body, error?: string) => {
|
|
134
|
+
const tx = tr(s);
|
|
135
|
+
const intro = introOf(tx);
|
|
132
136
|
const alert = error ? html`<div class="alert alert-error" role="alert">${error}</div>` : '';
|
|
133
137
|
if (b.source === 'query')
|
|
134
|
-
return html`${intro}${sourceTabs('query')}${region(
|
|
135
|
-
'
|
|
138
|
+
return html`${intro}${sourceTabs(tx, 'query')}${region(
|
|
139
|
+
tx('ul.unload_query'),
|
|
136
140
|
html`${alert}<form method="post" action="${BASE}/sql/unload">${csrf(s)}<input type="hidden" name="source" value="query">
|
|
137
|
-
<div class="field"><label class="label" for="f_query"
|
|
141
|
+
<div class="field"><label class="label" for="f_query">${tx('sm.col.query')}</label>
|
|
138
142
|
<textarea id="f_query" name="query" class="code sql-editor" rows="10" spellcheck="false" data-code="plpgsql" required>${b.query ?? ''}</textarea>
|
|
139
|
-
<small class="help"
|
|
140
|
-
${formatFields(b)}
|
|
141
|
-
<div class="buttons"><button class="btn btn-hot">${icon('download')}
|
|
143
|
+
<small class="help">${tx('ul.one_select')}</small></div>
|
|
144
|
+
${formatFields(tx, b)}
|
|
145
|
+
<div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('scr.download')}</button></div></form>`,
|
|
142
146
|
)}`;
|
|
143
147
|
const rels = await relations();
|
|
144
148
|
const rel = rels.find((r) => r.qname === b.table);
|
|
145
149
|
let schema = '';
|
|
146
150
|
const chooser = html`<form method="get" action="${BASE}/sql/unload" class="unload-pick">
|
|
147
|
-
<label class="sr-only" for="f_table"
|
|
148
|
-
<select id="f_table" name="table" required><option value=""
|
|
151
|
+
<label class="sr-only" for="f_table">${tx('ul.table_or_view')}</label>
|
|
152
|
+
<select id="f_table" name="table" required><option value="">${tx('ul.choose_table')}</option>${rels.map((r) => {
|
|
149
153
|
const group = r.schema !== schema ? html`${schema ? raw('</optgroup>') : ''}<optgroup label="${(schema = r.schema)}">` : '';
|
|
150
|
-
return html`${group}<option value="${r.qname}"${r === rel ? raw(' selected') : ''}>${r.name}${r.kind === 'view' ?
|
|
154
|
+
return html`${group}<option value="${r.qname}"${r === rel ? raw(' selected') : ''}>${r.name}${r.kind === 'view' ? ` ${tx('ul.view')}` : ''}</option>`;
|
|
151
155
|
})}${schema ? raw('</optgroup>') : ''}</select>
|
|
152
|
-
<button class="btn"
|
|
153
|
-
if (!rel) return html`${intro}${sourceTabs('table')}${region('
|
|
156
|
+
<button class="btn">${tx('ul.choose')}</button></form>`;
|
|
157
|
+
if (!rel) return html`${intro}${sourceTabs(tx, 'table')}${region(tx('ul.unload_table'), html`${alert}${chooser}`)}`;
|
|
154
158
|
const cols = await columns(rel.qname);
|
|
155
159
|
const chosen = new Set(b.format !== undefined ? list(b.columns) : cols.map((c) => c.name));
|
|
156
|
-
return html`${intro}${sourceTabs('table')}${region(
|
|
157
|
-
|
|
160
|
+
return html`${intro}${sourceTabs(tx, 'table')}${region(
|
|
161
|
+
tx('ul.unload_of', { name: rel.qname }),
|
|
158
162
|
html`${alert}${chooser}
|
|
159
163
|
<form method="post" action="${BASE}/sql/unload" class="u-mt1">${csrf(s)}<input type="hidden" name="source" value="table"><input type="hidden" name="table" value="${rel.qname}">
|
|
160
|
-
<fieldset class="prop-group"><legend
|
|
164
|
+
<fieldset class="prop-group"><legend>${tx('ul.columns')}</legend><div class="qb-pick">${cols.map(
|
|
161
165
|
(c) => html`<label class="check"><input type="checkbox" name="columns" value="${c.name}"${chosen.has(c.name) ? raw(' checked') : ''}> ${c.name} <span class="muted small">${c.type}</span></label>`,
|
|
162
166
|
)}</div></fieldset>
|
|
163
167
|
<div class="form-grid">
|
|
164
|
-
<div class="field"><label class="label" for="f_where"
|
|
165
|
-
<small class="help"
|
|
166
|
-
<div class="field"><label class="label" for="f_order"
|
|
167
|
-
<small class="help"
|
|
168
|
+
<div class="field"><label class="label" for="f_where">${tx('ul.where')}</label><input id="f_where" name="where" value="${b.where ?? ''}" placeholder="${tx('ul.eg', { x: 'deptno = 10' })}" class="code" spellcheck="false">
|
|
169
|
+
<small class="help">${tx('ul.where_help')}</small></div>
|
|
170
|
+
<div class="field"><label class="label" for="f_order">${tx('ul.order')}</label><input id="f_order" name="order" value="${b.order ?? ''}" placeholder="${tx('ul.eg', { x: '1, created desc' })}" class="code" spellcheck="false">
|
|
171
|
+
<small class="help">${tx('ul.order_help')}</small></div>
|
|
168
172
|
</div>
|
|
169
|
-
${formatFields(b)}
|
|
170
|
-
<div class="buttons"><button class="btn btn-hot">${icon('download')}
|
|
173
|
+
${formatFields(tx, b)}
|
|
174
|
+
<div class="buttons"><button class="btn btn-hot">${icon('download')} ${tx('scr.download')}</button></div></form>`,
|
|
171
175
|
)}`;
|
|
172
176
|
};
|
|
173
177
|
|
|
@@ -192,10 +196,10 @@ export async function unloadRoutes(app: FastifyInstance) {
|
|
|
192
196
|
name = 'query';
|
|
193
197
|
} else {
|
|
194
198
|
const rel = (await relations()).find((r) => r.qname === b.table);
|
|
195
|
-
if (!rel) return refuse('
|
|
199
|
+
if (!rel) return refuse(tr(s)('ul.err.table'));
|
|
196
200
|
const known = new Set((await columns(rel.qname)).map((c) => c.name));
|
|
197
201
|
const cols = list(b.columns).filter((c) => known.has(c));
|
|
198
|
-
if (!cols.length) return refuse('
|
|
202
|
+
if (!cols.length) return refuse(tr(s)('ul.err.column'));
|
|
199
203
|
const where = (b.where ?? '').trim();
|
|
200
204
|
const order = (b.order ?? '').trim();
|
|
201
205
|
// the WHERE and ORDER BY text is the developer's SQL: the whole statement must still be one SELECT
|
|
@@ -226,7 +230,7 @@ export async function unloadRoutes(app: FastifyInstance) {
|
|
|
226
230
|
} catch (e) {
|
|
227
231
|
await c.query('rollback').catch(() => {});
|
|
228
232
|
c.release(true);
|
|
229
|
-
return refuse(
|
|
233
|
+
return refuse(tr(s)('ul.err.failed', { error: (e as Error).message }));
|
|
230
234
|
}
|
|
231
235
|
await logActivity({ username: s.username, event: 'sql_unload', ip: clientIp(req), detail: `${opts.format}: ${sql.length > 1900 ? `${sql.slice(0, 1900)}…` : sql}` });
|
|
232
236
|
const out = new PassThrough();
|
package/src/builder/worksheet.ts
CHANGED
|
@@ -121,9 +121,9 @@ async function navigatorTree(db: Awaited<ReturnType<typeof workshopDb>>, schema:
|
|
|
121
121
|
return html`<ul class="ws-tree" id="ws-tree">${objects.map((o) => {
|
|
122
122
|
const list = cols.get(o.ref);
|
|
123
123
|
const obLink = `${BASE}/sql/objects?${new URLSearchParams(o.type === 'policy' ? { schema, k: 'table', o: o.ref, tab: 'rls' } : { schema, k: o.type, o: o.ref })}`;
|
|
124
|
-
if (!list) return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}"><a href="${obLink}" title="${label(o.type)}">${icon(iconOf(o.type))}<span>${o.name}</span></a>${objectMenu(schema, o)}</li>`;
|
|
124
|
+
if (!list) return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}"><a href="${obLink}" title="${label(o.type)}">${icon(iconOf(o.type))}<span>${o.name}</span></a>${objectMenu(schema, o, tx)}</li>`;
|
|
125
125
|
const st = objectStatements(schema, o.name, list, o.type);
|
|
126
|
-
return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}">${objectMenu(schema, o)}<details>
|
|
126
|
+
return html`<li class="ws-obj" data-name="${o.name.toLowerCase()}">${objectMenu(schema, o, tx)}<details>
|
|
127
127
|
<summary draggable="true" data-ws-sql="${st.select}" title="${tx('ws.nav.drag', { type: tx.lang === 'en' ? label(o.type).replace(/s$/, '') : label(o.type) })}">${icon(iconOf(o.type))}<span>${o.name}</span>${o.rls ? html`<span class="tag">RLS</span>` : ''}</summary>
|
|
128
128
|
<ul class="ws-cols">${list.map((c) => html`<li title="${c.type}${c.notnull ? ' not null' : ''}">${c.pk ? icon('key') : icon('minus')}<span>${c.name}</span><small class="muted">${c.type}</small></li>`)}</ul>
|
|
129
129
|
<div class="ws-obj-actions">
|
|
@@ -525,7 +525,7 @@ async function worksheet(req: Req, reply: FastifyReply) {
|
|
|
525
525
|
else {
|
|
526
526
|
const d = drawChart('', lastQuery.columns, lastQuery.data ?? [], spec);
|
|
527
527
|
chartCss = d.css;
|
|
528
|
-
chartPane = html`${chartControls(lastQuery.columns, spec)}
|
|
528
|
+
chartPane = html`${chartControls(lastQuery.columns, spec, 'ch', tx)}
|
|
529
529
|
<div class="ch-actions"><button class="btn btn-sm" name="mode" value="chart">${icon('refresh')} ${tx('ws.chart.again')}</button>
|
|
530
530
|
<input type="hidden" name="chart_sql" value="${lastQuery.sql}">
|
|
531
531
|
<label class="sr-only" for="ws-chart-name">${tx('ws.chart.name')}</label><input id="ws-chart-name" name="chart_name" placeholder="${tx('ws.chart.name')}" maxlength="120">
|
package/src/cron-schedule.ts
CHANGED
|
@@ -231,12 +231,12 @@ const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
231
231
|
const sorted = (s: Set<number>) => [...s].sort((a, b) => a - b);
|
|
232
232
|
const isAll = (s: Set<number>, min: number, max: number) => s.size === max - min + 1;
|
|
233
233
|
/** a list of numbers with runs as ranges: 1-5, 7 */
|
|
234
|
-
function compact(values: number[], name: (n: number) => string = String) {
|
|
234
|
+
function compact(values: number[], name: (n: number) => string = String, t: CronWords = EN_WORDS) {
|
|
235
235
|
const out: string[] = [];
|
|
236
236
|
for (let i = 0; i < values.length; i++) {
|
|
237
237
|
let j = i;
|
|
238
238
|
while (j + 1 < values.length && values[j + 1] === values[j] + 1) j++;
|
|
239
|
-
out.push(j - i >= 2 ?
|
|
239
|
+
out.push(j - i >= 2 ? fillWords(t('cron.range'), { from: name(values[i]), to: name(values[j]) }) : j > i ? `${name(values[i])}, ${name(values[j])}` : name(values[i]));
|
|
240
240
|
i = j;
|
|
241
241
|
}
|
|
242
242
|
return out.join(', ');
|
|
@@ -250,33 +250,63 @@ function stepOf(s: Set<number>, min: number, max: number) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/** The schedule in words ("At 02:00, every day"). */
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
/**
|
|
254
|
+
* (sprint 162, CD-8) The phrases of a schedule in words, by key: English here; the builder passes its translator
|
|
255
|
+
* (the same keys, cron.*, in src/builder/i18n/*.ts) and the language for the day and month names.
|
|
256
|
+
*/
|
|
257
|
+
export const EN_WORDS_TEXT: Record<string, string> = {
|
|
258
|
+
'cron.reboot': 'When the database server starts', 'cron.every_second': 'Every second', 'cron.every_seconds': 'Every {n} seconds',
|
|
259
|
+
'cron.every_minute': 'Every minute', 'cron.every_minutes': 'Every {n} minutes', 'cron.every_hour': 'Every hour',
|
|
260
|
+
'cron.minute_of_hour': 'At minute {m} of every hour', 'cron.minutes_of_hour': 'At minutes {list} of every hour', 'cron.at': 'At {times}',
|
|
261
|
+
'cron.at_minute': 'At minute {m}', 'cron.at_minutes': 'At minutes {list}', 'cron.of_every_hours': '{mins} of every {n} hours',
|
|
262
|
+
'cron.past_hours': '{mins} past hours {list}', 'cron.day_and_last': 'on day {list} and the last day of the month',
|
|
263
|
+
'cron.last_day': 'on the last day of the month', 'cron.day_of_month': 'on day {list} of the month', 'cron.weekdays': 'on weekdays',
|
|
264
|
+
'cron.on_days': 'on {list}', 'cron.a_days': 'a {list}', 'cron.either': '{dom} or {dow}', 'cron.if': '{dom} if it is {dow}',
|
|
265
|
+
'cron.every_day': 'every day', 'cron.in_months': 'in {list}', 'cron.range': '{from} to {to}',
|
|
266
|
+
};
|
|
267
|
+
export type CronWords = (key: string) => string;
|
|
268
|
+
const EN_WORDS: CronWords = (k) => EN_WORDS_TEXT[k] ?? k;
|
|
269
|
+
const fillWords = (text: string, p: Record<string, string | number>) => text.replace(/\{(\w+)\}/g, (m, k: string) => (k in p ? String(p[k]) : m));
|
|
270
|
+
|
|
271
|
+
/** Day (0 = Sunday) and month (1 = January) names in a language (English: as before). */
|
|
272
|
+
const dayName = (lang: string, d: number) => (lang === 'en' ? DAY_NAMES[d] : new Intl.DateTimeFormat(lang, { weekday: 'long', timeZone: 'UTC' }).format(new Date(Date.UTC(2023, 0, 1 + d))));
|
|
273
|
+
const monthName = (lang: string, m: number) => (lang === 'en' ? MONTH_NAMES[m - 1] : new Intl.DateTimeFormat(lang, { month: 'long', timeZone: 'UTC' }).format(new Date(Date.UTC(2023, m - 1, 1))));
|
|
274
|
+
|
|
275
|
+
export function describeSchedule(s: Schedule, t: CronWords = EN_WORDS, lang = 'en'): string {
|
|
276
|
+
const w = (key: string, p: Record<string, string | number> = {}) => fillWords(t(key), p);
|
|
277
|
+
const list = (v: number[], name?: (n: number) => string) => compact(v, name, t);
|
|
278
|
+
if (s.kind === 'reboot') return w('cron.reboot');
|
|
279
|
+
if (s.kind === 'seconds') return s.seconds === 1 ? w('cron.every_second') : w('cron.every_seconds', { n: s.seconds });
|
|
256
280
|
const minutes = sorted(s.minutes);
|
|
257
281
|
const hours = sorted(s.hours);
|
|
258
282
|
let time: string;
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
// whether the time already says how often (then no "every day")
|
|
284
|
+
let often = false;
|
|
285
|
+
if (isAll(s.minutes, 0, 59) && isAll(s.hours, 0, 23)) {
|
|
286
|
+
time = w('cron.every_minute');
|
|
287
|
+
often = true;
|
|
288
|
+
} else if (isAll(s.hours, 0, 23)) {
|
|
261
289
|
const step = stepOf(s.minutes, 0, 59);
|
|
262
|
-
|
|
263
|
-
|
|
290
|
+
often = true;
|
|
291
|
+
time = step ? w('cron.every_minutes', { n: step }) : minutes.length === 1 ? (minutes[0] === 0 ? w('cron.every_hour') : w('cron.minute_of_hour', { m: minutes[0] })) : w('cron.minutes_of_hour', { list: list(minutes) });
|
|
292
|
+
} else if (hours.length * minutes.length <= 6) time = w('cron.at', { times: hours.flatMap((h) => minutes.map((m) => `${pad(h)}:${pad(m)}`)).join(', ') });
|
|
264
293
|
else {
|
|
265
294
|
const step = stepOf(s.hours, 0, 23);
|
|
266
|
-
const mins = isAll(s.minutes, 0, 59) ? '
|
|
267
|
-
time =
|
|
295
|
+
const mins = isAll(s.minutes, 0, 59) ? w('cron.every_minute') : minutes.length === 1 ? w('cron.at_minute', { m: minutes[0] }) : w('cron.at_minutes', { list: list(minutes) });
|
|
296
|
+
time = step ? w('cron.of_every_hours', { mins, n: step }) : w('cron.past_hours', { mins, list: list(hours) });
|
|
268
297
|
}
|
|
269
298
|
const parts = [time];
|
|
270
299
|
const domAll = isAll(s.days, 1, 31) && !s.lastDay;
|
|
271
300
|
const dowAll = isAll(s.weekdays, 0, 6);
|
|
272
|
-
const dom = s.lastDay ? (s.days.size ?
|
|
301
|
+
const dom = s.lastDay ? (s.days.size ? w('cron.day_and_last', { list: list(sorted(s.days)) }) : w('cron.last_day')) : w('cron.day_of_month', { list: list(sorted(s.days)) });
|
|
273
302
|
const weekdays = sorted(s.weekdays);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
303
|
+
const days = list(weekdays, (d) => dayName(lang, d));
|
|
304
|
+
const dow = weekdays.join() === '1,2,3,4,5' ? w('cron.weekdays') : w('cron.on_days', { list: days });
|
|
305
|
+
if (s.either) parts.push(w('cron.either', { dom, dow }));
|
|
306
|
+
else if (!domAll && !dowAll) parts.push(w('cron.if', { dom, dow: weekdays.join() === '1,2,3,4,5' ? w('cron.weekdays') : w('cron.a_days', { list: days }) }));
|
|
277
307
|
else if (!domAll) parts.push(dom);
|
|
278
308
|
else if (!dowAll) parts.push(dow);
|
|
279
|
-
else if (
|
|
280
|
-
if (!isAll(s.months, 1, 12)) parts.push(
|
|
309
|
+
else if (!often) parts.push(w('cron.every_day'));
|
|
310
|
+
if (!isAll(s.months, 1, 12)) parts.push(w('cron.in_months', { list: list(sorted(s.months), (m) => monthName(lang, m)) }));
|
|
281
311
|
return parts.join(', ');
|
|
282
312
|
}
|
package/src/i18n/ar.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const ar: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "بدأ في الخلفية (المهمة {id}).",
|
|
677
677
|
'process.background_type': "لا يمكن تشغيل العملية «{name}» ({type}) في الخلفية.",
|
|
678
678
|
'workflow.started': "بدأ مسار العمل {id}.",
|
|
679
|
+
'process.no_geocoder': 'لم يُعدّ أي محدِّد مواقع جغرافية لهذا التثبيت.',
|
|
680
|
+
'process.address_not_found': 'لم يُعثر على مكان لـ "{address}".',
|
|
681
|
+
'process.task_created': 'تم إنشاء المهمة {id}.',
|
|
682
|
+
'item.map_of': 'خريطة {position}',
|
|
683
|
+
'process.no_task': 'لا توجد مهمة للتعامل معها.',
|
|
679
684
|
'workflow.terminated': "أُنهي مسار العمل {id}.",
|
|
680
685
|
'workflow.retried': "يعمل مسار العمل {id} من جديد.",
|
|
681
686
|
'workflow.no_instance': "لم يُحدد أي مسار عمل.",
|
package/src/i18n/cs.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const cs: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Spuštěno na pozadí (úloha {id}).",
|
|
677
677
|
'process.background_type': "Proces „{name}“ ({type}) nemůže běžet na pozadí.",
|
|
678
678
|
'workflow.started': "Pracovní postup {id} byl spuštěn.",
|
|
679
|
+
'process.no_geocoder': 'Tato instalace nemá nastavený geokodér.',
|
|
680
|
+
'process.address_not_found': 'Pro „{address}“ nebylo nalezeno žádné místo.',
|
|
681
|
+
'process.task_created': 'Úkol {id} byl vytvořen.',
|
|
682
|
+
'item.map_of': 'Mapa: {position}',
|
|
683
|
+
'process.no_task': 'Žádný úkol ke zpracování.',
|
|
679
684
|
'workflow.terminated': "Pracovní postup {id} byl ukončen.",
|
|
680
685
|
'workflow.retried': "Pracovní postup {id} běží znovu.",
|
|
681
686
|
'workflow.no_instance': "Není vybrán žádný pracovní postup.",
|
package/src/i18n/da.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const da: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Startet i baggrunden (job {id}).",
|
|
677
677
|
'process.background_type': "Processen “{name}” ({type}) kan ikke køre i baggrunden.",
|
|
678
678
|
'workflow.started': "Arbejdsgang {id} er startet.",
|
|
679
|
+
'process.no_geocoder': 'Denne installation har ingen geokoder.',
|
|
680
|
+
'process.address_not_found': 'Intet sted fundet for "{address}".',
|
|
681
|
+
'process.task_created': 'Opgave {id} er oprettet.',
|
|
682
|
+
'item.map_of': 'Kort over {position}',
|
|
683
|
+
'process.no_task': 'Ingen opgave at behandle.',
|
|
679
684
|
'workflow.terminated': "Arbejdsgang {id} er afsluttet.",
|
|
680
685
|
'workflow.retried': "Arbejdsgang {id} kører igen.",
|
|
681
686
|
'workflow.no_instance': "Ingen arbejdsgang er valgt.",
|
package/src/i18n/de.ts
CHANGED
|
@@ -686,6 +686,11 @@ export const de: Record<MessageKey, string> = {
|
|
|
686
686
|
'process.queued': 'Im Hintergrund gestartet (Job {id}).',
|
|
687
687
|
'process.background_type': 'Der Prozess "{name}" ({type}) kann nicht im Hintergrund laufen.',
|
|
688
688
|
'workflow.started': 'Workflow {id} gestartet.',
|
|
689
|
+
'process.no_geocoder': 'Für diese Installation ist kein Geocoder eingerichtet.',
|
|
690
|
+
'process.address_not_found': 'Kein Ort für „{address}“ gefunden.',
|
|
691
|
+
'process.task_created': 'Aufgabe {id} wurde erstellt.',
|
|
692
|
+
'item.map_of': 'Karte von {position}',
|
|
693
|
+
'process.no_task': 'Keine Aufgabe zum Bearbeiten.',
|
|
689
694
|
'workflow.terminated': 'Workflow {id} beendet.',
|
|
690
695
|
'workflow.retried': 'Workflow {id} läuft wieder.',
|
|
691
696
|
'workflow.no_instance': 'Es ist kein Workflow ausgewählt.',
|
package/src/i18n/el.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const el: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Ξεκίνησε στο παρασκήνιο (εργασία {id}).",
|
|
677
677
|
'process.background_type': "Η διαδικασία «{name}» ({type}) δεν μπορεί να εκτελεστεί στο παρασκήνιο.",
|
|
678
678
|
'workflow.started': "Η ροή εργασίας {id} ξεκίνησε.",
|
|
679
|
+
'process.no_geocoder': 'Δεν έχει ρυθμιστεί γεωκωδικοποιητής για αυτή την εγκατάσταση.',
|
|
680
|
+
'process.address_not_found': 'Δεν βρέθηκε τοποθεσία για «{address}».',
|
|
681
|
+
'process.task_created': 'Η εργασία {id} δημιουργήθηκε.',
|
|
682
|
+
'item.map_of': 'Χάρτης του {position}',
|
|
683
|
+
'process.no_task': 'Δεν υπάρχει εργασία για ενέργεια.',
|
|
679
684
|
'workflow.terminated': "Η ροή εργασίας {id} τερματίστηκε.",
|
|
680
685
|
'workflow.retried': "Η ροή εργασίας {id} εκτελείται ξανά.",
|
|
681
686
|
'workflow.no_instance': "Δεν έχει επιλεγεί ροή εργασίας.",
|
package/src/i18n/es.ts
CHANGED
|
@@ -686,6 +686,11 @@ export const es: Record<MessageKey, string> = {
|
|
|
686
686
|
'process.queued': 'Iniciado en segundo plano (trabajo {id}).',
|
|
687
687
|
'process.background_type': 'El proceso "{name}" ({type}) no se puede ejecutar en segundo plano.',
|
|
688
688
|
'workflow.started': 'Flujo de trabajo {id} iniciado.',
|
|
689
|
+
'process.no_geocoder': 'Esta instalación no tiene un geocodificador configurado.',
|
|
690
|
+
'process.address_not_found': 'No se encontró ningún lugar para "{address}".',
|
|
691
|
+
'process.task_created': 'Tarea {id} creada.',
|
|
692
|
+
'item.map_of': 'Mapa de {position}',
|
|
693
|
+
'process.no_task': 'No hay ninguna tarea sobre la que actuar.',
|
|
689
694
|
'workflow.terminated': 'Flujo de trabajo {id} terminado.',
|
|
690
695
|
'workflow.retried': 'El flujo de trabajo {id} se está ejecutando de nuevo.',
|
|
691
696
|
'workflow.no_instance': 'No hay ningún flujo de trabajo seleccionado.',
|
package/src/i18n/fi.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const fi: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Käynnistetty taustalla (työ {id}).",
|
|
677
677
|
'process.background_type': "Prosessia ”{name}” ({type}) ei voi suorittaa taustalla.",
|
|
678
678
|
'workflow.started': "Työnkulku {id} käynnistetty.",
|
|
679
|
+
'process.no_geocoder': 'Tälle asennukselle ei ole määritetty geokoodaajaa.',
|
|
680
|
+
'process.address_not_found': 'Paikkaa ei löytynyt haulle "{address}".',
|
|
681
|
+
'process.task_created': 'Tehtävä {id} luotiin.',
|
|
682
|
+
'item.map_of': 'Kartta: {position}',
|
|
683
|
+
'process.no_task': 'Ei käsiteltävää tehtävää.',
|
|
679
684
|
'workflow.terminated': "Työnkulku {id} lopetettu.",
|
|
680
685
|
'workflow.retried': "Työnkulku {id} on taas käynnissä.",
|
|
681
686
|
'workflow.no_instance': "Työnkulkua ei ole valittu.",
|
package/src/i18n/fr.ts
CHANGED
|
@@ -686,6 +686,11 @@ export const fr: Record<MessageKey, string> = {
|
|
|
686
686
|
'process.queued': 'Démarré en arrière-plan (tâche {id}).',
|
|
687
687
|
'process.background_type': 'Le processus « {name} » ({type}) ne peut pas s’exécuter en arrière-plan.',
|
|
688
688
|
'workflow.started': 'Workflow {id} démarré.',
|
|
689
|
+
'process.no_geocoder': 'Aucun géocodeur n\'est configuré pour cette installation.',
|
|
690
|
+
'process.address_not_found': 'Aucun lieu trouvé pour « {address} ».',
|
|
691
|
+
'process.task_created': 'Tâche {id} créée.',
|
|
692
|
+
'item.map_of': 'Carte de {position}',
|
|
693
|
+
'process.no_task': 'Aucune tâche à traiter.',
|
|
689
694
|
'workflow.terminated': 'Workflow {id} arrêté.',
|
|
690
695
|
'workflow.retried': 'Workflow {id} relancé.',
|
|
691
696
|
'workflow.no_instance': 'Aucun workflow n’est sélectionné.',
|
package/src/i18n/he.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const he: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "הופעל ברקע (עבודה {id}).",
|
|
677
677
|
'process.background_type': "התהליך \"{name}\" ({type}) אינו יכול לפעול ברקע.",
|
|
678
678
|
'workflow.started': "תהליך העבודה {id} הופעל.",
|
|
679
|
+
'process.no_geocoder': 'לא הוגדר מקודד גאוגרפי בהתקנה זו.',
|
|
680
|
+
'process.address_not_found': 'לא נמצא מקום עבור "{address}".',
|
|
681
|
+
'process.task_created': 'המשימה {id} נוצרה.',
|
|
682
|
+
'item.map_of': 'מפה של {position}',
|
|
683
|
+
'process.no_task': 'אין משימה לטיפול.',
|
|
679
684
|
'workflow.terminated': "תהליך העבודה {id} הופסק.",
|
|
680
685
|
'workflow.retried': "תהליך העבודה {id} פועל שוב.",
|
|
681
686
|
'workflow.no_instance': "לא נבחר תהליך עבודה.",
|
package/src/i18n/it.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const it: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Avviato in background (job {id}).",
|
|
677
677
|
'process.background_type': "Il processo “{name}” ({type}) non può essere eseguito in background.",
|
|
678
678
|
'workflow.started': "Workflow {id} avviato.",
|
|
679
|
+
'process.no_geocoder': 'Nessun geocodificatore configurato per questa installazione.',
|
|
680
|
+
'process.address_not_found': 'Nessun luogo trovato per "{address}".',
|
|
681
|
+
'process.task_created': 'Attività {id} creata.',
|
|
682
|
+
'item.map_of': 'Mappa di {position}',
|
|
683
|
+
'process.no_task': 'Nessuna attività da gestire.',
|
|
679
684
|
'workflow.terminated': "Workflow {id} terminato.",
|
|
680
685
|
'workflow.retried': "Il workflow {id} è di nuovo in esecuzione.",
|
|
681
686
|
'workflow.no_instance': "Nessun workflow selezionato.",
|
package/src/i18n/ja.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const ja: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "バックグラウンドで開始しました (ジョブ {id})。",
|
|
677
677
|
'process.background_type': "プロセス「{name}」({type}) はバックグラウンドで実行できません。",
|
|
678
678
|
'workflow.started': "ワークフロー {id} を開始しました。",
|
|
679
|
+
'process.no_geocoder': 'このインストールにはジオコーダーが設定されていません。',
|
|
680
|
+
'process.address_not_found': '「{address}」の場所が見つかりません。',
|
|
681
|
+
'process.task_created': 'タスク {id} を作成しました。',
|
|
682
|
+
'item.map_of': '{position} の地図',
|
|
683
|
+
'process.no_task': '処理するタスクがありません。',
|
|
679
684
|
'workflow.terminated': "ワークフロー {id} を終了しました。",
|
|
680
685
|
'workflow.retried': "ワークフロー {id} を再実行しています。",
|
|
681
686
|
'workflow.no_instance': "ワークフローが選択されていません。",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const ko: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "백그라운드에서 시작했습니다(작업 {id}).",
|
|
677
677
|
'process.background_type': "프로세스 '{name}'({type})은(는) 백그라운드에서 실행할 수 없습니다.",
|
|
678
678
|
'workflow.started': "워크플로 {id}을(를) 시작했습니다.",
|
|
679
|
+
'process.no_geocoder': '이 설치에는 지오코더가 설정되어 있지 않습니다.',
|
|
680
|
+
'process.address_not_found': '"{address}"에 대한 장소를 찾을 수 없습니다.',
|
|
681
|
+
'process.task_created': '작업 {id}이(가) 생성되었습니다.',
|
|
682
|
+
'item.map_of': '{position} 지도',
|
|
683
|
+
'process.no_task': '처리할 작업이 없습니다.',
|
|
679
684
|
'workflow.terminated': "워크플로 {id}을(를) 종료했습니다.",
|
|
680
685
|
'workflow.retried': "워크플로 {id}이(가) 다시 실행 중입니다.",
|
|
681
686
|
'workflow.no_instance': "선택한 워크플로가 없습니다.",
|
package/src/i18n/nb.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const nb: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Startet i bakgrunnen (jobb {id}).",
|
|
677
677
|
'process.background_type': "Prosessen «{name}» ({type}) kan ikke kjøre i bakgrunnen.",
|
|
678
678
|
'workflow.started': "Arbeidsflyt {id} er startet.",
|
|
679
|
+
'process.no_geocoder': 'Denne installasjonen har ingen geokoder.',
|
|
680
|
+
'process.address_not_found': 'Fant ikke noe sted for «{address}».',
|
|
681
|
+
'process.task_created': 'Oppgave {id} er opprettet.',
|
|
682
|
+
'item.map_of': 'Kart over {position}',
|
|
683
|
+
'process.no_task': 'Ingen oppgave å behandle.',
|
|
679
684
|
'workflow.terminated': "Arbeidsflyt {id} er avsluttet.",
|
|
680
685
|
'workflow.retried': "Arbeidsflyt {id} kjører igjen.",
|
|
681
686
|
'workflow.no_instance': "Ingen arbeidsflyt er valgt.",
|
package/src/i18n/pl.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const pl: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Uruchomiono w tle (zadanie {id}).",
|
|
677
677
|
'process.background_type': "Proces „{name}” ({type}) nie może działać w tle.",
|
|
678
678
|
'workflow.started': "Przepływ pracy {id} uruchomiony.",
|
|
679
|
+
'process.no_geocoder': 'Ta instalacja nie ma skonfigurowanego geokodera.',
|
|
680
|
+
'process.address_not_found': 'Nie znaleziono miejsca dla „{address}”.',
|
|
681
|
+
'process.task_created': 'Utworzono zadanie {id}.',
|
|
682
|
+
'item.map_of': 'Mapa: {position}',
|
|
683
|
+
'process.no_task': 'Brak zadania do obsłużenia.',
|
|
679
684
|
'workflow.terminated': "Przepływ pracy {id} przerwany.",
|
|
680
685
|
'workflow.retried': "Przepływ pracy {id} działa ponownie.",
|
|
681
686
|
'workflow.no_instance': "Nie wybrano przepływu pracy.",
|
package/src/i18n/pt.ts
CHANGED
|
@@ -676,6 +676,11 @@ export const pt: Record<MessageKey, string> = {
|
|
|
676
676
|
'process.queued': "Iniciado em segundo plano (tarefa {id}).",
|
|
677
677
|
'process.background_type': "O processo “{name}” ({type}) não pode ser executado em segundo plano.",
|
|
678
678
|
'workflow.started': "Fluxo de trabalho {id} iniciado.",
|
|
679
|
+
'process.no_geocoder': 'Esta instalação não tem um geocodificador configurado.',
|
|
680
|
+
'process.address_not_found': 'Nenhum lugar encontrado para "{address}".',
|
|
681
|
+
'process.task_created': 'Tarefa {id} criada.',
|
|
682
|
+
'item.map_of': 'Mapa de {position}',
|
|
683
|
+
'process.no_task': 'Nenhuma tarefa para tratar.',
|
|
679
684
|
'workflow.terminated': "Fluxo de trabalho {id} terminado.",
|
|
680
685
|
'workflow.retried': "O fluxo de trabalho {id} está novamente em execução.",
|
|
681
686
|
'workflow.no_instance': "Nenhum fluxo de trabalho selecionado.",
|