@jxsuite/studio 0.37.1 → 1.0.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/dist/studio.js +77654 -76269
- package/dist/studio.js.map +128 -119
- package/package.json +44 -44
- package/src/account-status.ts +39 -0
- package/src/browse/browse.ts +10 -14
- package/src/canvas/iframe-host.ts +1 -1
- package/src/editor/context-menu.ts +1 -1
- package/src/editor/repeater-scope.ts +8 -13
- package/src/files/files.ts +3 -0
- package/src/format/format-host.ts +63 -1
- package/src/new-project/add-repo-modal.ts +183 -0
- package/src/new-project/new-project-modal.ts +22 -3
- package/src/page-params.ts +34 -8
- package/src/panels/ai-chat/chat-markdown.ts +2 -2
- package/src/panels/ai-panel.ts +61 -4
- package/src/panels/data-grid.ts +619 -0
- package/src/panels/signals-panel.ts +102 -437
- package/src/panels/statusbar.ts +1 -1
- package/src/panels/welcome-screen.ts +50 -0
- package/src/platform-errors.ts +30 -0
- package/src/platforms/cloud.ts +172 -89
- package/src/platforms/devserver.ts +172 -0
- package/src/services/context-resolver.ts +73 -0
- package/src/services/data-service.ts +155 -0
- package/src/services/monaco-setup.ts +75 -26
- package/src/settings/contributed-section.ts +406 -0
- package/src/settings/extension-sections.ts +145 -0
- package/src/settings/schema-field-ui.ts +4 -2
- package/src/settings/settings-modal.ts +101 -42
- package/src/site-context.ts +10 -1
- package/src/studio.ts +24 -0
- package/src/tabs/transact.ts +1 -1
- package/src/types.ts +120 -1
- package/src/ui/form-controls.ts +322 -0
- package/src/ui/progress-modal.ts +2 -2
- package/src/ui/schema-form.ts +524 -0
- package/src/utils/studio-utils.ts +4 -3
- package/src/settings/content-types-editor.ts +0 -599
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Data grid — the owner console over the platform's data surface (plan Part 4a).
|
|
4
|
+
*
|
|
5
|
+
* Integration: the settings modal's contributed sections stay fully generic — extension-sections
|
|
6
|
+
* passes this module's {@link dataSectionActions} into the ContributedSectionOptions.actions slot
|
|
7
|
+
* for the data-domain sections ("connections"/"data") whenever the platform implements the
|
|
8
|
+
* protocol's data routes. Those actions surface Test Connection, Push Schema (dry-run plan
|
|
9
|
+
* confirmation before apply), and Open Data Grid; the grid itself is a full-surface modal
|
|
10
|
+
* (settings-modal pattern) gated on `platform.dataRows`.
|
|
11
|
+
*
|
|
12
|
+
* The grid is v1-minimal by design: connection + table picker (declared tables plus system tables
|
|
13
|
+
* discovered from backend introspection), 50-row pagination, inline cell edit committed via
|
|
14
|
+
* dataUpdateRow keyed on the introspected primary key, an add-row footer, and two-step per-row
|
|
15
|
+
* delete. No filter/sort UI.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { html, nothing } from "lit-html";
|
|
19
|
+
import { live } from "lit-html/directives/live.js";
|
|
20
|
+
import { openModal } from "../ui/layers";
|
|
21
|
+
import {
|
|
22
|
+
dataSurfaceAvailable,
|
|
23
|
+
deleteRow,
|
|
24
|
+
fetchConnections,
|
|
25
|
+
fetchRows,
|
|
26
|
+
insertRow,
|
|
27
|
+
pushSchema,
|
|
28
|
+
testConnection,
|
|
29
|
+
updateRow,
|
|
30
|
+
} from "../services/data-service";
|
|
31
|
+
import type { TemplateResult } from "lit-html";
|
|
32
|
+
import type {
|
|
33
|
+
DataColumnMeta,
|
|
34
|
+
DataConnectionInfo,
|
|
35
|
+
DataConnectionTestResult,
|
|
36
|
+
DataPushResult,
|
|
37
|
+
} from "../types";
|
|
38
|
+
import type { SectionActionsContext } from "../settings/contributed-section";
|
|
39
|
+
|
|
40
|
+
export const DATA_GRID_PAGE_SIZE = 50;
|
|
41
|
+
|
|
42
|
+
// ─── Grid state ───────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
interface GridState {
|
|
45
|
+
connections: DataConnectionInfo[];
|
|
46
|
+
connection: string | null;
|
|
47
|
+
table: string | null;
|
|
48
|
+
columns: DataColumnMeta[];
|
|
49
|
+
rows: Record<string, unknown>[];
|
|
50
|
+
total: number;
|
|
51
|
+
offset: number;
|
|
52
|
+
loading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
/** Pk value armed for the two-step delete confirm. */
|
|
55
|
+
pendingDelete: string | null;
|
|
56
|
+
/** Add-row footer draft, keyed by column name. */
|
|
57
|
+
draft: Record<string, string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const blankGrid = (): GridState => ({
|
|
61
|
+
columns: [],
|
|
62
|
+
connection: null,
|
|
63
|
+
connections: [],
|
|
64
|
+
draft: {},
|
|
65
|
+
error: null,
|
|
66
|
+
loading: false,
|
|
67
|
+
offset: 0,
|
|
68
|
+
pendingDelete: null,
|
|
69
|
+
rows: [],
|
|
70
|
+
table: null,
|
|
71
|
+
total: 0,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let grid: GridState = blankGrid();
|
|
75
|
+
let gridHandle: ReturnType<typeof openModal> | null = null;
|
|
76
|
+
|
|
77
|
+
/** Reset all module UI state and close any open surfaces (test hook / project switch). */
|
|
78
|
+
export function resetDataGridState(): void {
|
|
79
|
+
grid = blankGrid();
|
|
80
|
+
gridHandle?.close();
|
|
81
|
+
gridHandle = null;
|
|
82
|
+
actionsState = { pushing: false, testResult: null, testing: null };
|
|
83
|
+
closePushDialog();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when the platform serves the data grid. */
|
|
87
|
+
export function isDataGridAvailable(): boolean {
|
|
88
|
+
return dataSurfaceAvailable();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Close the grid modal. */
|
|
92
|
+
export function closeDataGrid(): void {
|
|
93
|
+
gridHandle?.close();
|
|
94
|
+
gridHandle = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Open the data grid modal, loading connections and the first page.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ connection?: string; table?: string }} [preselect]
|
|
101
|
+
*/
|
|
102
|
+
export async function openDataGrid(preselect: { connection?: string; table?: string } = {}) {
|
|
103
|
+
if (!isDataGridAvailable() || gridHandle) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
grid = blankGrid();
|
|
107
|
+
grid.loading = true;
|
|
108
|
+
renderGrid();
|
|
109
|
+
const res = await fetchConnections().catch(() => null);
|
|
110
|
+
grid.connections = res?.connections ?? [];
|
|
111
|
+
grid.connection =
|
|
112
|
+
(preselect.connection &&
|
|
113
|
+
grid.connections.some((c) => c.name === preselect.connection) &&
|
|
114
|
+
preselect.connection) ||
|
|
115
|
+
grid.connections.find((c) => c.isDefault)?.name ||
|
|
116
|
+
grid.connections[0]?.name ||
|
|
117
|
+
null;
|
|
118
|
+
const tables = currentTables();
|
|
119
|
+
grid.table =
|
|
120
|
+
preselect.table && tables.includes(preselect.table) ? preselect.table : (tables[0] ?? null);
|
|
121
|
+
await loadRows();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Table names of the selected connection (declared + backend-introspected system tables). */
|
|
125
|
+
function currentTables(): string[] {
|
|
126
|
+
return grid.connections.find((c) => c.name === grid.connection)?.tables ?? [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The primary-key column name (backend convention: "id" when nothing is flagged). */
|
|
130
|
+
function pkColumn(): string {
|
|
131
|
+
return grid.columns.find((c) => c.pk)?.name ?? "id";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Load the current page. */
|
|
135
|
+
async function loadRows(): Promise<void> {
|
|
136
|
+
if (!grid.table) {
|
|
137
|
+
grid.rows = [];
|
|
138
|
+
grid.columns = [];
|
|
139
|
+
grid.total = 0;
|
|
140
|
+
grid.loading = false;
|
|
141
|
+
renderGrid();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
grid.loading = true;
|
|
145
|
+
renderGrid();
|
|
146
|
+
try {
|
|
147
|
+
const result = await fetchRows({
|
|
148
|
+
limit: DATA_GRID_PAGE_SIZE,
|
|
149
|
+
offset: grid.offset,
|
|
150
|
+
table: grid.table,
|
|
151
|
+
...(grid.connection === null ? {} : { connection: grid.connection }),
|
|
152
|
+
});
|
|
153
|
+
grid.rows = result.rows;
|
|
154
|
+
grid.columns = result.columns;
|
|
155
|
+
grid.total = result.total;
|
|
156
|
+
grid.error = null;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
grid.rows = [];
|
|
159
|
+
grid.columns = [];
|
|
160
|
+
grid.total = 0;
|
|
161
|
+
grid.error = error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|
|
163
|
+
grid.loading = false;
|
|
164
|
+
grid.pendingDelete = null;
|
|
165
|
+
renderGrid();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Commit one edited cell via dataUpdateRow, keyed on the row's pk. */
|
|
169
|
+
async function commitCell(
|
|
170
|
+
row: Record<string, unknown>,
|
|
171
|
+
column: DataColumnMeta,
|
|
172
|
+
raw: string,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const current = row[column.name];
|
|
175
|
+
const currentText = current == null ? "" : String(current);
|
|
176
|
+
if (raw === currentText || !grid.table) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const updated = await updateRow({
|
|
181
|
+
pk: row[pkColumn()] as string | number,
|
|
182
|
+
set: { [column.name]: raw === "" ? null : raw },
|
|
183
|
+
table: grid.table,
|
|
184
|
+
...(grid.connection === null ? {} : { connection: grid.connection }),
|
|
185
|
+
});
|
|
186
|
+
const index = grid.rows.indexOf(row);
|
|
187
|
+
if (index !== -1) {
|
|
188
|
+
grid.rows[index] = updated.row;
|
|
189
|
+
}
|
|
190
|
+
grid.error = null;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
grid.error = error instanceof Error ? error.message : String(error);
|
|
193
|
+
}
|
|
194
|
+
renderGrid();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Two-step delete: first click arms the row, second click deletes. */
|
|
198
|
+
async function requestDelete(pk: string): Promise<void> {
|
|
199
|
+
if (grid.pendingDelete !== pk) {
|
|
200
|
+
grid.pendingDelete = pk;
|
|
201
|
+
renderGrid();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!grid.table) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
await deleteRow({
|
|
209
|
+
pk,
|
|
210
|
+
table: grid.table,
|
|
211
|
+
...(grid.connection === null ? {} : { connection: grid.connection }),
|
|
212
|
+
});
|
|
213
|
+
await loadRows();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
grid.error = error instanceof Error ? error.message : String(error);
|
|
216
|
+
grid.pendingDelete = null;
|
|
217
|
+
renderGrid();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Insert the add-row footer draft. */
|
|
222
|
+
async function commitDraft(): Promise<void> {
|
|
223
|
+
if (!grid.table) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const values: Record<string, unknown> = {};
|
|
227
|
+
for (const [name, raw] of Object.entries(grid.draft)) {
|
|
228
|
+
if (raw !== "") {
|
|
229
|
+
values[name] = raw;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await insertRow({
|
|
234
|
+
table: grid.table,
|
|
235
|
+
values,
|
|
236
|
+
...(grid.connection === null ? {} : { connection: grid.connection }),
|
|
237
|
+
});
|
|
238
|
+
grid.draft = {};
|
|
239
|
+
await loadRows();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
grid.error = error instanceof Error ? error.message : String(error);
|
|
242
|
+
renderGrid();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Columns editable in the grid (system-managed columns are display-only). */
|
|
247
|
+
function editableColumn(column: DataColumnMeta): boolean {
|
|
248
|
+
return !column.pk && column.name !== "created_at" && column.name !== "updated_at";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function gridBody(): TemplateResult {
|
|
252
|
+
if (grid.loading) {
|
|
253
|
+
return html`<div class="data-grid-empty">Loading…</div>`;
|
|
254
|
+
}
|
|
255
|
+
if (grid.error) {
|
|
256
|
+
return html`<div class="data-grid-error">${grid.error}</div>`;
|
|
257
|
+
}
|
|
258
|
+
if (!grid.table) {
|
|
259
|
+
return html`<div class="data-grid-empty">
|
|
260
|
+
No tables on this connection — push a schema first.
|
|
261
|
+
</div>`;
|
|
262
|
+
}
|
|
263
|
+
const pk = pkColumn();
|
|
264
|
+
return html`
|
|
265
|
+
<table class="data-grid-table">
|
|
266
|
+
<thead>
|
|
267
|
+
<tr>
|
|
268
|
+
${grid.columns.map(
|
|
269
|
+
(c) =>
|
|
270
|
+
html`<th title=${c.type}>
|
|
271
|
+
${c.name}${c.pk ? html` <span class="data-grid-pk">pk</span>` : nothing}
|
|
272
|
+
</th>`,
|
|
273
|
+
)}
|
|
274
|
+
<th></th>
|
|
275
|
+
</tr>
|
|
276
|
+
</thead>
|
|
277
|
+
<tbody>
|
|
278
|
+
${grid.rows.map((row) => {
|
|
279
|
+
const pkValue = String(row[pk] ?? "");
|
|
280
|
+
return html`<tr>
|
|
281
|
+
${grid.columns.map((c) =>
|
|
282
|
+
editableColumn(c)
|
|
283
|
+
? html`<td>
|
|
284
|
+
<input
|
|
285
|
+
class="data-grid-cell"
|
|
286
|
+
.value=${live(row[c.name] == null ? "" : String(row[c.name]))}
|
|
287
|
+
@change=${(e: Event) =>
|
|
288
|
+
void commitCell(row, c, (e.target as HTMLInputElement).value)}
|
|
289
|
+
/>
|
|
290
|
+
</td>`
|
|
291
|
+
: html`<td class="data-grid-readonly">
|
|
292
|
+
${row[c.name] == null ? "" : String(row[c.name])}
|
|
293
|
+
</td>`,
|
|
294
|
+
)}
|
|
295
|
+
<td>
|
|
296
|
+
<sp-action-button
|
|
297
|
+
quiet
|
|
298
|
+
size="s"
|
|
299
|
+
class="data-grid-delete"
|
|
300
|
+
@click=${() => void requestDelete(pkValue)}
|
|
301
|
+
>
|
|
302
|
+
${grid.pendingDelete === pkValue ? "Confirm?" : "Delete"}
|
|
303
|
+
</sp-action-button>
|
|
304
|
+
</td>
|
|
305
|
+
</tr>`;
|
|
306
|
+
})}
|
|
307
|
+
</tbody>
|
|
308
|
+
<tfoot>
|
|
309
|
+
<tr class="data-grid-add-row">
|
|
310
|
+
${grid.columns.map((c) =>
|
|
311
|
+
editableColumn(c)
|
|
312
|
+
? html`<td>
|
|
313
|
+
<input
|
|
314
|
+
class="data-grid-draft"
|
|
315
|
+
placeholder=${c.name}
|
|
316
|
+
.value=${live(grid.draft[c.name] ?? "")}
|
|
317
|
+
@input=${(e: Event) => {
|
|
318
|
+
grid.draft[c.name] = (e.target as HTMLInputElement).value;
|
|
319
|
+
}}
|
|
320
|
+
/>
|
|
321
|
+
</td>`
|
|
322
|
+
: html`<td></td>`,
|
|
323
|
+
)}
|
|
324
|
+
<td>
|
|
325
|
+
<sp-action-button size="s" class="data-grid-add" @click=${() => void commitDraft()}>
|
|
326
|
+
Add
|
|
327
|
+
</sp-action-button>
|
|
328
|
+
</td>
|
|
329
|
+
</tr>
|
|
330
|
+
</tfoot>
|
|
331
|
+
</table>
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function renderGrid(): void {
|
|
336
|
+
const first = grid.total === 0 ? 0 : grid.offset + 1;
|
|
337
|
+
const last = grid.offset + grid.rows.length;
|
|
338
|
+
const tpl = html`
|
|
339
|
+
<sp-underlay open @close=${closeDataGrid}></sp-underlay>
|
|
340
|
+
<div class="settings-modal data-grid-modal">
|
|
341
|
+
<div class="settings-modal-header">
|
|
342
|
+
<h2 class="settings-modal-title">Data</h2>
|
|
343
|
+
<sp-picker
|
|
344
|
+
size="s"
|
|
345
|
+
class="data-grid-connection"
|
|
346
|
+
value=${grid.connection ?? ""}
|
|
347
|
+
@change=${(e: Event) => {
|
|
348
|
+
grid.connection = (e.target as HTMLInputElement).value || null;
|
|
349
|
+
grid.offset = 0;
|
|
350
|
+
grid.table = currentTables()[0] ?? null;
|
|
351
|
+
void loadRows();
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
${grid.connections.map(
|
|
355
|
+
(c) => html`<sp-menu-item value=${c.name}>${c.name}</sp-menu-item>`,
|
|
356
|
+
)}
|
|
357
|
+
</sp-picker>
|
|
358
|
+
<sp-picker
|
|
359
|
+
size="s"
|
|
360
|
+
class="data-grid-tables"
|
|
361
|
+
value=${grid.table ?? ""}
|
|
362
|
+
@change=${(e: Event) => {
|
|
363
|
+
grid.table = (e.target as HTMLInputElement).value || null;
|
|
364
|
+
grid.offset = 0;
|
|
365
|
+
grid.draft = {};
|
|
366
|
+
void loadRows();
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
${currentTables().map(
|
|
370
|
+
(table) => html`<sp-menu-item value=${table}>${table}</sp-menu-item>`,
|
|
371
|
+
)}
|
|
372
|
+
</sp-picker>
|
|
373
|
+
<sp-action-button quiet size="s" title="Close" @click=${closeDataGrid}>
|
|
374
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
375
|
+
</sp-action-button>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="settings-modal-body data-grid-body">${gridBody()}</div>
|
|
378
|
+
<div class="data-grid-footer">
|
|
379
|
+
<sp-action-button
|
|
380
|
+
size="s"
|
|
381
|
+
class="data-grid-prev"
|
|
382
|
+
?disabled=${grid.offset === 0}
|
|
383
|
+
@click=${() => {
|
|
384
|
+
grid.offset = Math.max(0, grid.offset - DATA_GRID_PAGE_SIZE);
|
|
385
|
+
void loadRows();
|
|
386
|
+
}}
|
|
387
|
+
>Prev</sp-action-button
|
|
388
|
+
>
|
|
389
|
+
<span class="data-grid-range">${first}–${last} of ${grid.total}</span>
|
|
390
|
+
<sp-action-button
|
|
391
|
+
size="s"
|
|
392
|
+
class="data-grid-next"
|
|
393
|
+
?disabled=${grid.offset + DATA_GRID_PAGE_SIZE >= grid.total}
|
|
394
|
+
@click=${() => {
|
|
395
|
+
grid.offset += DATA_GRID_PAGE_SIZE;
|
|
396
|
+
void loadRows();
|
|
397
|
+
}}
|
|
398
|
+
>Next</sp-action-button
|
|
399
|
+
>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
`;
|
|
403
|
+
if (gridHandle) {
|
|
404
|
+
gridHandle.update(tpl);
|
|
405
|
+
} else {
|
|
406
|
+
gridHandle = openModal(tpl);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Push dialog (dry-run plan confirmation before apply) ─────────────────────
|
|
411
|
+
|
|
412
|
+
interface PushDialogState {
|
|
413
|
+
handle: ReturnType<typeof openModal>;
|
|
414
|
+
connection: string | undefined;
|
|
415
|
+
phase: "loading" | "confirm" | "applying" | "done";
|
|
416
|
+
plan: DataPushResult | null;
|
|
417
|
+
result: DataPushResult | null;
|
|
418
|
+
onDone: () => void;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let pushDialog: PushDialogState | null = null;
|
|
422
|
+
|
|
423
|
+
/** Close the push dialog (also part of resetDataGridState). */
|
|
424
|
+
function closePushDialog(): void {
|
|
425
|
+
pushDialog?.handle.close();
|
|
426
|
+
pushDialog = null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Start a push: dry-run first, then a confirmation dialog gates the apply. */
|
|
430
|
+
export async function startPush(connection: string | undefined, onDone: () => void) {
|
|
431
|
+
if (pushDialog) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const handle = openModal(html``);
|
|
435
|
+
pushDialog = { connection, handle, onDone, phase: "loading", plan: null, result: null };
|
|
436
|
+
renderPushDialog();
|
|
437
|
+
const plan = await pushSchema({
|
|
438
|
+
dryRun: true,
|
|
439
|
+
...(connection === undefined ? {} : { connection }),
|
|
440
|
+
});
|
|
441
|
+
if (!pushDialog) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
pushDialog.plan = plan;
|
|
445
|
+
pushDialog.phase = "confirm";
|
|
446
|
+
renderPushDialog();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function applyPush(): Promise<void> {
|
|
450
|
+
if (!pushDialog) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
pushDialog.phase = "applying";
|
|
454
|
+
renderPushDialog();
|
|
455
|
+
const result = await pushSchema(
|
|
456
|
+
pushDialog.connection === undefined ? {} : { connection: pushDialog.connection },
|
|
457
|
+
);
|
|
458
|
+
if (!pushDialog) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
pushDialog.result = result;
|
|
462
|
+
pushDialog.phase = "done";
|
|
463
|
+
renderPushDialog();
|
|
464
|
+
pushDialog.onDone();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function pushDialogBody(state: PushDialogState): TemplateResult {
|
|
468
|
+
if (state.phase === "loading" || state.phase === "applying") {
|
|
469
|
+
return html`<div class="push-dialog-status">
|
|
470
|
+
${state.phase === "loading" ? "Compiling plan…" : "Applying…"}
|
|
471
|
+
</div>`;
|
|
472
|
+
}
|
|
473
|
+
const shown = state.phase === "done" ? state.result : state.plan;
|
|
474
|
+
const steps = shown?.plan ?? [];
|
|
475
|
+
const warnings = shown?.warnings ?? [];
|
|
476
|
+
const errors = shown?.errors ?? [];
|
|
477
|
+
return html`
|
|
478
|
+
<div class="push-dialog-plan">
|
|
479
|
+
${state.phase === "done"
|
|
480
|
+
? html`<div class="push-dialog-status">
|
|
481
|
+
${shown?.applied ? "Schema applied." : "Push failed."}
|
|
482
|
+
</div>`
|
|
483
|
+
: steps.length === 0 && errors.length === 0
|
|
484
|
+
? html`<div class="push-dialog-status">Nothing to push — the schema is up to date.</div>`
|
|
485
|
+
: nothing}
|
|
486
|
+
<ul class="push-dialog-steps">
|
|
487
|
+
${steps.map(
|
|
488
|
+
(step) => html`<li class="push-step push-step-${step.kind}">${step.summary}</li>`,
|
|
489
|
+
)}
|
|
490
|
+
</ul>
|
|
491
|
+
${warnings.map((w) => html`<div class="push-dialog-warning">${w}</div>`)}
|
|
492
|
+
${errors.map((e) => html`<div class="push-dialog-error">${e}</div>`)}
|
|
493
|
+
</div>
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function renderPushDialog(): void {
|
|
498
|
+
if (!pushDialog) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const state = pushDialog;
|
|
502
|
+
const confirmable = state.phase === "confirm" && (state.plan?.plan.length ?? 0) > 0;
|
|
503
|
+
const tpl = html`
|
|
504
|
+
<sp-underlay open @close=${closePushDialog}></sp-underlay>
|
|
505
|
+
<div class="settings-modal push-dialog">
|
|
506
|
+
<div class="settings-modal-header">
|
|
507
|
+
<h2 class="settings-modal-title">
|
|
508
|
+
Push Schema${state.connection ? html` — ${state.connection}` : nothing}
|
|
509
|
+
</h2>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="settings-modal-body">${pushDialogBody(state)}</div>
|
|
512
|
+
<div class="push-dialog-actions">
|
|
513
|
+
<sp-action-button size="s" class="push-cancel" @click=${closePushDialog}>
|
|
514
|
+
${state.phase === "done" ? "Close" : "Cancel"}
|
|
515
|
+
</sp-action-button>
|
|
516
|
+
${confirmable
|
|
517
|
+
? html`<sp-action-button
|
|
518
|
+
size="s"
|
|
519
|
+
emphasized
|
|
520
|
+
class="push-apply"
|
|
521
|
+
@click=${() => void applyPush()}
|
|
522
|
+
>Apply</sp-action-button
|
|
523
|
+
>`
|
|
524
|
+
: nothing}
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
`;
|
|
528
|
+
state.handle.update(tpl);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── Contributed-section actions (Test / Push / Open grid) ────────────────────
|
|
532
|
+
|
|
533
|
+
interface ActionsState {
|
|
534
|
+
/** Connection currently being tested, when any. */
|
|
535
|
+
testing: string | null;
|
|
536
|
+
testResult: (DataConnectionTestResult & { connection: string }) | null;
|
|
537
|
+
pushing: boolean;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let actionsState: ActionsState = { pushing: false, testResult: null, testing: null };
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The actions renderer for a data-domain contributed section, or null when the section is not
|
|
544
|
+
* data-domain or the platform lacks the data routes. Section keys "connections"/"data" are the
|
|
545
|
+
* connector's host wire contract (the same literals the backend's data routes serve) — the generic
|
|
546
|
+
* contributed-section renderer itself stays extension-agnostic.
|
|
547
|
+
*
|
|
548
|
+
* @param {string} sectionKey
|
|
549
|
+
* @returns {((ctx: SectionActionsContext) => TemplateResult) | null}
|
|
550
|
+
*/
|
|
551
|
+
export function dataSectionActions(
|
|
552
|
+
sectionKey: string,
|
|
553
|
+
): ((ctx: SectionActionsContext) => TemplateResult) | null {
|
|
554
|
+
if ((sectionKey !== "connections" && sectionKey !== "data") || !dataSurfaceAvailable()) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
return (ctx) => renderSectionActions(sectionKey, ctx);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function runTest(connection: string, rerender: () => void): Promise<void> {
|
|
561
|
+
actionsState.testing = connection;
|
|
562
|
+
actionsState.testResult = null;
|
|
563
|
+
rerender();
|
|
564
|
+
const result = await testConnection(connection);
|
|
565
|
+
actionsState.testing = null;
|
|
566
|
+
actionsState.testResult = { ...result, connection };
|
|
567
|
+
rerender();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function renderSectionActions(sectionKey: string, ctx: SectionActionsContext): TemplateResult {
|
|
571
|
+
const { selected, rerender } = ctx;
|
|
572
|
+
const { testResult } = actionsState;
|
|
573
|
+
// On the connections section a selected entry scopes both Test and Push to that connection.
|
|
574
|
+
const pushTarget = sectionKey === "connections" && selected ? selected : undefined;
|
|
575
|
+
return html`
|
|
576
|
+
<div class="data-section-actions">
|
|
577
|
+
${sectionKey === "connections"
|
|
578
|
+
? html`<sp-action-button
|
|
579
|
+
size="s"
|
|
580
|
+
class="data-action-test"
|
|
581
|
+
?disabled=${!selected || actionsState.testing !== null}
|
|
582
|
+
@click=${() => {
|
|
583
|
+
if (selected) {
|
|
584
|
+
void runTest(selected, rerender);
|
|
585
|
+
}
|
|
586
|
+
}}
|
|
587
|
+
>
|
|
588
|
+
${actionsState.testing ? "Testing…" : "Test Connection"}
|
|
589
|
+
</sp-action-button>`
|
|
590
|
+
: nothing}
|
|
591
|
+
<sp-action-button
|
|
592
|
+
size="s"
|
|
593
|
+
class="data-action-push"
|
|
594
|
+
@click=${() => void startPush(pushTarget, rerender)}
|
|
595
|
+
>
|
|
596
|
+
Push Schema
|
|
597
|
+
</sp-action-button>
|
|
598
|
+
<sp-action-button
|
|
599
|
+
size="s"
|
|
600
|
+
class="data-action-grid"
|
|
601
|
+
@click=${() =>
|
|
602
|
+
void openDataGrid(
|
|
603
|
+
sectionKey === "connections" && selected ? { connection: selected } : {},
|
|
604
|
+
)}
|
|
605
|
+
>
|
|
606
|
+
Open Data Grid
|
|
607
|
+
</sp-action-button>
|
|
608
|
+
${testResult
|
|
609
|
+
? html`<span
|
|
610
|
+
class="data-test-result ${testResult.ok ? "ok" : "failed"}"
|
|
611
|
+
title=${testResult.error ?? ""}
|
|
612
|
+
>
|
|
613
|
+
${testResult.connection}:
|
|
614
|
+
${testResult.ok ? "connected" : (testResult.error ?? "failed")}
|
|
615
|
+
</span>`
|
|
616
|
+
: nothing}
|
|
617
|
+
</div>
|
|
618
|
+
`;
|
|
619
|
+
}
|