normalized-schema-generator 0.1.0__py3-none-any.whl
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.
- normalized_schema_generator/__init__.py +6 -0
- normalized_schema_generator/widget.css +125 -0
- normalized_schema_generator/widget.js +635 -0
- normalized_schema_generator/widget.py +573 -0
- normalized_schema_generator-0.1.0.dist-info/METADATA +85 -0
- normalized_schema_generator-0.1.0.dist-info/RECORD +8 -0
- normalized_schema_generator-0.1.0.dist-info/WHEEL +4 -0
- normalized_schema_generator-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
export function render({ model, el }) {
|
|
2
|
+
const root = document.createElement("div");
|
|
3
|
+
root.className = "nsw";
|
|
4
|
+
el.appendChild(root);
|
|
5
|
+
|
|
6
|
+
/* ── helpers ──────────────────────────────────────────── */
|
|
7
|
+
function h(tag, attrs, ...children) {
|
|
8
|
+
const el = document.createElement(tag);
|
|
9
|
+
if (attrs) {
|
|
10
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
11
|
+
if (v === undefined || v === null) continue;
|
|
12
|
+
if (k === "className") el.className = v;
|
|
13
|
+
else if (k === "textContent") el.textContent = v;
|
|
14
|
+
else if (k === "innerHTML") el.innerHTML = v;
|
|
15
|
+
else if (k.startsWith("on")) el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
16
|
+
else el.setAttribute(k, v);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const child of children.flat()) {
|
|
20
|
+
if (child == null) continue;
|
|
21
|
+
if (typeof child === "string") el.appendChild(document.createTextNode(child));
|
|
22
|
+
else el.appendChild(child);
|
|
23
|
+
}
|
|
24
|
+
return el;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dtypeBadge(dtype) {
|
|
28
|
+
const known = { int: "INT", double: "FLOAT", string: "STR", timestamp: "DATE", boolean: "BOOL" };
|
|
29
|
+
const cls = known[dtype] ? dtype : "other";
|
|
30
|
+
return h("span", { className: "nsw-col-dtype " + cls, textContent: known[dtype] || dtype.toUpperCase().slice(0, 8) });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toggleSwitch(checked, onChange) {
|
|
34
|
+
const lbl = h("label", { className: "nsw-toggle-cb" });
|
|
35
|
+
const inp = h("input", { type: "checkbox" });
|
|
36
|
+
inp.checked = checked;
|
|
37
|
+
inp.addEventListener("change", () => onChange(inp.checked));
|
|
38
|
+
lbl.appendChild(inp);
|
|
39
|
+
lbl.appendChild(h("span", { className: "slider" }));
|
|
40
|
+
return lbl;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function selectEl(options, value, onChange, placeholder, disabled) {
|
|
44
|
+
const sel = h("select", { className: "nsw-select" });
|
|
45
|
+
if (disabled) sel.disabled = true;
|
|
46
|
+
if (placeholder) {
|
|
47
|
+
const opt = h("option", { value: "", textContent: placeholder });
|
|
48
|
+
opt.disabled = true;
|
|
49
|
+
if (!value) opt.selected = true;
|
|
50
|
+
sel.appendChild(opt);
|
|
51
|
+
}
|
|
52
|
+
for (const o of options) {
|
|
53
|
+
const opt = h("option", { value: o.value, textContent: o.label });
|
|
54
|
+
if (o.value === value) opt.selected = true;
|
|
55
|
+
sel.appendChild(opt);
|
|
56
|
+
}
|
|
57
|
+
sel.addEventListener("change", () => onChange(sel.value));
|
|
58
|
+
return sel;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── persistent state (survives rebuilds) ────────────── */
|
|
62
|
+
const S = {
|
|
63
|
+
eventsRef: { visible: false, editIndex: -1, targetTable: "", sourceCol: "", targetCol: "", keyType: "Integer", exportName: "", targetCols: [], loading: false },
|
|
64
|
+
casesRef: { visible: false, editIndex: -1, targetTable: "", sourceCol: "", targetCol: "", keyType: "Integer", exportName: "", targetCols: [], loading: false },
|
|
65
|
+
pendingRefFor: null,
|
|
66
|
+
outputOpen: true,
|
|
67
|
+
copied: false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function resetRefForm(form) {
|
|
71
|
+
form.visible = false;
|
|
72
|
+
form.editIndex = -1;
|
|
73
|
+
form.targetTable = "";
|
|
74
|
+
form.sourceCol = "";
|
|
75
|
+
form.targetCol = "";
|
|
76
|
+
form.keyType = "Integer";
|
|
77
|
+
form.exportName = "";
|
|
78
|
+
form.targetCols = [];
|
|
79
|
+
form.loading = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── rebuild UI ──────────────────────────────────────── */
|
|
83
|
+
function rebuild() {
|
|
84
|
+
root.innerHTML = "";
|
|
85
|
+
root.appendChild(buildHeader());
|
|
86
|
+
root.appendChild(buildLakehouseCard());
|
|
87
|
+
root.appendChild(buildEventsCard());
|
|
88
|
+
root.appendChild(buildCasesCard());
|
|
89
|
+
root.appendChild(buildOutputCard());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── header ──────────────────────────────────────────── */
|
|
93
|
+
function buildHeader() {
|
|
94
|
+
return h("div", { className: "nsw-header" },
|
|
95
|
+
h("h2", { textContent: "Normalized Schema Generator" }),
|
|
96
|
+
h("p", { textContent: "Configure your process mining star-schema and generate the dataSource JSON." })
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Section A: Lakehouse & source type ──────────────── */
|
|
101
|
+
function buildLakehouseCard() {
|
|
102
|
+
const card = h("div", { className: "nsw-card" });
|
|
103
|
+
const lakehouses = model.get("lakehouses") || [];
|
|
104
|
+
const selId = model.get("selected_lakehouse_id") || "";
|
|
105
|
+
const fileType = model.get("data_source_file_type");
|
|
106
|
+
|
|
107
|
+
// Lakehouse selector
|
|
108
|
+
const status = model.get("loading_status") || "";
|
|
109
|
+
const lhOpts = lakehouses.map(l => ({ value: l.id, label: l.displayName }));
|
|
110
|
+
const lhRow = h("div", { className: "nsw-row" },
|
|
111
|
+
h("span", { className: "nsw-label", textContent: "Lakehouse" }),
|
|
112
|
+
selectEl(lhOpts, selId, (val) => {
|
|
113
|
+
const lh = lakehouses.find(l => l.id === val);
|
|
114
|
+
model.set("selected_lakehouse_id", val);
|
|
115
|
+
model.set("selected_lakehouse_name", lh ? lh.displayName : "");
|
|
116
|
+
model.save_changes();
|
|
117
|
+
}, "Select a lakehouse...")
|
|
118
|
+
);
|
|
119
|
+
if (status && status.includes("tables")) {
|
|
120
|
+
lhRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: status }));
|
|
121
|
+
}
|
|
122
|
+
card.appendChild(lhRow);
|
|
123
|
+
|
|
124
|
+
// Data source type toggle
|
|
125
|
+
const toggle = h("div", { className: "nsw-toggle" });
|
|
126
|
+
const btnDelta = h("button", {
|
|
127
|
+
textContent: "Delta Tables",
|
|
128
|
+
className: fileType === 2 ? "active" : "",
|
|
129
|
+
onClick: () => { model.set("data_source_file_type", 2); model.save_changes(); }
|
|
130
|
+
});
|
|
131
|
+
const btnFiles = h("button", {
|
|
132
|
+
textContent: "Files",
|
|
133
|
+
className: fileType < 2 ? "active" : "",
|
|
134
|
+
onClick: () => { model.set("data_source_file_type", 0); model.save_changes(); }
|
|
135
|
+
});
|
|
136
|
+
toggle.appendChild(btnDelta);
|
|
137
|
+
toggle.appendChild(btnFiles);
|
|
138
|
+
|
|
139
|
+
const row2 = h("div", { className: "nsw-row" },
|
|
140
|
+
h("span", { className: "nsw-label", textContent: "Source type" }),
|
|
141
|
+
toggle
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// File format sub-selector (only for Files mode)
|
|
145
|
+
if (fileType < 2) {
|
|
146
|
+
const fmtOpts = [{ value: "0", label: "CSV" }, { value: "1", label: "Parquet" }];
|
|
147
|
+
row2.appendChild(h("span", { className: "nsw-label", textContent: "Format", style: "margin-left:12px" }));
|
|
148
|
+
row2.appendChild(selectEl(fmtOpts, String(fileType), (v) => {
|
|
149
|
+
model.set("data_source_file_type", parseInt(v));
|
|
150
|
+
model.save_changes();
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
card.appendChild(row2);
|
|
154
|
+
|
|
155
|
+
return card;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ── Column checklist ────────────────────────────────── */
|
|
159
|
+
function buildColumnList(columns, onChange) {
|
|
160
|
+
if (!columns || columns.length === 0) return h("div", { className: "nsw-empty", textContent: "No columns loaded." });
|
|
161
|
+
const wrap = h("div", { className: "nsw-col-list" });
|
|
162
|
+
columns.forEach((col, i) => {
|
|
163
|
+
wrap.appendChild(h("div", { className: "nsw-col-row" },
|
|
164
|
+
toggleSwitch(col.included, (val) => {
|
|
165
|
+
const updated = JSON.parse(JSON.stringify(columns));
|
|
166
|
+
updated[i].included = val;
|
|
167
|
+
onChange(updated);
|
|
168
|
+
}),
|
|
169
|
+
h("span", { className: "nsw-col-name", textContent: col.name }),
|
|
170
|
+
dtypeBadge(col.dtype)
|
|
171
|
+
));
|
|
172
|
+
});
|
|
173
|
+
return wrap;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ── Join list ───────────────────────────────────────── */
|
|
177
|
+
function buildJoinList(joins, onEdit, onRemove) {
|
|
178
|
+
if (!joins || joins.length === 0) return h("div", { className: "nsw-empty", textContent: "No references configured." });
|
|
179
|
+
const wrap = h("div", null);
|
|
180
|
+
joins.forEach((j, i) => {
|
|
181
|
+
const targetColNames = (j.target_columns || []).filter(c => c.included).map(c => c.name).join(", ");
|
|
182
|
+
wrap.appendChild(h("div", { className: "nsw-join-item" },
|
|
183
|
+
h("span", { className: "join-type", textContent: j.join_key_type }),
|
|
184
|
+
h("span", { className: "join-desc", textContent: j.source_column + " → " + j.target_table + "." + j.target_column }),
|
|
185
|
+
targetColNames ? h("span", { className: "join-desc", textContent: "cols: " + targetColNames, style: "color:#666" }) : null,
|
|
186
|
+
h("button", { className: "nsw-btn sm", textContent: "Edit", onClick: () => onEdit(i) }),
|
|
187
|
+
h("button", { className: "nsw-btn danger sm", textContent: "Remove", onClick: () => onRemove(i) })
|
|
188
|
+
));
|
|
189
|
+
});
|
|
190
|
+
return wrap;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── Add Reference form ──────────────────────────────── */
|
|
194
|
+
function buildRefForm(datasetType) {
|
|
195
|
+
const form = datasetType === "events" ? S.eventsRef : S.casesRef;
|
|
196
|
+
if (!form.visible) return null;
|
|
197
|
+
|
|
198
|
+
const tables = model.get("available_tables") || [];
|
|
199
|
+
const existingJoins = model.get(datasetType + "_joins") || [];
|
|
200
|
+
const currentTable = model.get(datasetType + "_table_name") || "";
|
|
201
|
+
const currentCols = model.get(datasetType + "_columns") || [];
|
|
202
|
+
const usedTargets = existingJoins.map(j => j.target_table);
|
|
203
|
+
|
|
204
|
+
// Exclude current table and already-referenced tables (but allow the edited one)
|
|
205
|
+
const editingTarget = form.editIndex >= 0 && existingJoins[form.editIndex] ? existingJoins[form.editIndex].target_table : null;
|
|
206
|
+
let availTargets = tables.filter(t => t.name !== currentTable && (!usedTargets.includes(t.name) || t.name === editingTarget));
|
|
207
|
+
const targetOpts = availTargets.map(t => ({ value: t.name, label: t.name }));
|
|
208
|
+
const isEditing = form.editIndex >= 0;
|
|
209
|
+
|
|
210
|
+
const wrap = h("div", { className: "nsw-ref-form" });
|
|
211
|
+
const tgtRow = h("div", { className: "nsw-row" },
|
|
212
|
+
h("span", { className: "nsw-label", textContent: "Target table" }),
|
|
213
|
+
isEditing
|
|
214
|
+
? h("span", { className: "nsw-col-name", textContent: form.targetTable, style: "padding:5px 0" })
|
|
215
|
+
: selectEl(targetOpts, form.targetTable, (val) => {
|
|
216
|
+
form.targetTable = val;
|
|
217
|
+
form.targetCol = "";
|
|
218
|
+
form.sourceCol = "";
|
|
219
|
+
form.targetCols = [];
|
|
220
|
+
form.loading = true;
|
|
221
|
+
S.pendingRefFor = datasetType;
|
|
222
|
+
model.set("fetch_columns_request", Date.now() + ":" + val);
|
|
223
|
+
model.save_changes();
|
|
224
|
+
rebuild();
|
|
225
|
+
}, "Select target table...")
|
|
226
|
+
);
|
|
227
|
+
if (form.loading) {
|
|
228
|
+
tgtRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "Loading columns..." }));
|
|
229
|
+
}
|
|
230
|
+
wrap.appendChild(tgtRow);
|
|
231
|
+
|
|
232
|
+
if (!form.loading && form.targetCols.length > 0) {
|
|
233
|
+
// FK + PK side-by-side
|
|
234
|
+
const srcColOpts = currentCols.map(c => ({ value: c.name, label: c.name + " (" + c.dtype + ")" }));
|
|
235
|
+
const tgtColOpts = form.targetCols.map(c => ({ value: c.name, label: c.name + " (" + c.dtype + ")" }));
|
|
236
|
+
wrap.appendChild(h("div", { className: "nsw-pair-row" },
|
|
237
|
+
h("div", { className: "nsw-pair" },
|
|
238
|
+
h("span", { className: "nsw-label sm", textContent: "FK column" }),
|
|
239
|
+
selectEl(srcColOpts, form.sourceCol, (v) => { form.sourceCol = v; rebuild(); }, "Source FK...")
|
|
240
|
+
),
|
|
241
|
+
h("div", { className: "nsw-pair" },
|
|
242
|
+
h("span", { className: "nsw-label sm", textContent: "PK column" }),
|
|
243
|
+
selectEl(tgtColOpts, form.targetCol, (v) => { form.targetCol = v; rebuild(); }, "Target PK...")
|
|
244
|
+
)
|
|
245
|
+
));
|
|
246
|
+
|
|
247
|
+
// Key type + Export name side-by-side
|
|
248
|
+
const keyOpts = [{ value: "Integer", label: "Integer" }, { value: "String", label: "String" }];
|
|
249
|
+
const expInp = h("input", {
|
|
250
|
+
type: "text",
|
|
251
|
+
className: "nsw-input",
|
|
252
|
+
placeholder: "Optional export name",
|
|
253
|
+
style: "min-width:140px;flex:1",
|
|
254
|
+
});
|
|
255
|
+
expInp.value = form.exportName;
|
|
256
|
+
expInp.addEventListener("input", (e) => { form.exportName = e.target.value; });
|
|
257
|
+
wrap.appendChild(h("div", { className: "nsw-pair-row" },
|
|
258
|
+
h("div", { className: "nsw-pair" },
|
|
259
|
+
h("span", { className: "nsw-label sm", textContent: "Key type" }),
|
|
260
|
+
selectEl(keyOpts, form.keyType, (v) => { form.keyType = v; rebuild(); })
|
|
261
|
+
),
|
|
262
|
+
h("div", { className: "nsw-pair" },
|
|
263
|
+
h("span", { className: "nsw-label sm", textContent: "Export name" }),
|
|
264
|
+
expInp
|
|
265
|
+
)
|
|
266
|
+
));
|
|
267
|
+
|
|
268
|
+
// Target columns to include
|
|
269
|
+
wrap.appendChild(h("div", { className: "nsw-joins-title", textContent: "Columns to include from target:" }));
|
|
270
|
+
const colList = h("div", { className: "nsw-col-list" });
|
|
271
|
+
form.targetCols.forEach((col, i) => {
|
|
272
|
+
colList.appendChild(h("div", { className: "nsw-col-row" },
|
|
273
|
+
toggleSwitch(col.included, (val) => { form.targetCols[i].included = val; rebuild(); }),
|
|
274
|
+
h("span", { className: "nsw-col-name", textContent: col.name }),
|
|
275
|
+
dtypeBadge(col.dtype)
|
|
276
|
+
));
|
|
277
|
+
});
|
|
278
|
+
wrap.appendChild(colList);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Buttons
|
|
282
|
+
const canConfirm = form.targetTable && form.sourceCol && form.targetCol && !form.loading;
|
|
283
|
+
const actions = h("div", { className: "form-actions" },
|
|
284
|
+
h("button", {
|
|
285
|
+
className: "nsw-btn primary",
|
|
286
|
+
textContent: isEditing ? "Save" : "Confirm",
|
|
287
|
+
disabled: canConfirm ? undefined : "disabled",
|
|
288
|
+
onClick: () => {
|
|
289
|
+
if (!canConfirm) return;
|
|
290
|
+
const joins = JSON.parse(JSON.stringify(model.get(datasetType + "_joins") || []));
|
|
291
|
+
const entry = {
|
|
292
|
+
target_table: form.targetTable,
|
|
293
|
+
source_column: form.sourceCol,
|
|
294
|
+
target_column: form.targetCol,
|
|
295
|
+
join_key_type: form.keyType,
|
|
296
|
+
export_name: form.exportName || "",
|
|
297
|
+
target_columns: form.targetCols.map(c => ({ name: c.name, dtype: c.dtype, included: c.included })),
|
|
298
|
+
};
|
|
299
|
+
if (isEditing && form.editIndex < joins.length) {
|
|
300
|
+
joins[form.editIndex] = entry;
|
|
301
|
+
} else {
|
|
302
|
+
joins.push(entry);
|
|
303
|
+
}
|
|
304
|
+
model.set(datasetType + "_joins", joins);
|
|
305
|
+
model.save_changes();
|
|
306
|
+
resetRefForm(form);
|
|
307
|
+
rebuild();
|
|
308
|
+
}
|
|
309
|
+
}),
|
|
310
|
+
h("button", {
|
|
311
|
+
className: "nsw-btn",
|
|
312
|
+
textContent: "Cancel",
|
|
313
|
+
onClick: () => { resetRefForm(form); rebuild(); }
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
wrap.appendChild(actions);
|
|
317
|
+
return wrap;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Section B: Events table ─────────────────────────── */
|
|
321
|
+
function buildEventsCard() {
|
|
322
|
+
const card = h("div", { className: "nsw-card events" });
|
|
323
|
+
card.appendChild(h("div", { className: "nsw-card-title" },
|
|
324
|
+
h("span", { textContent: "Events Table" }),
|
|
325
|
+
h("span", { className: "badge", textContent: "Required" })
|
|
326
|
+
));
|
|
327
|
+
|
|
328
|
+
const tables = model.get("available_tables") || [];
|
|
329
|
+
const selTable = model.get("events_table_name") || "";
|
|
330
|
+
const columns = model.get("events_columns") || [];
|
|
331
|
+
const joins = model.get("events_joins") || [];
|
|
332
|
+
const selLH = model.get("selected_lakehouse_id") || "";
|
|
333
|
+
|
|
334
|
+
// Table selector
|
|
335
|
+
const loadingStatus = model.get("loading_status") || "";
|
|
336
|
+
const tOpts = tables.map(t => ({ value: t.name, label: t.name }));
|
|
337
|
+
const tblRow = h("div", { className: "nsw-row" },
|
|
338
|
+
h("span", { className: "nsw-label", textContent: "Table" }),
|
|
339
|
+
selectEl(tOpts, selTable, (val) => {
|
|
340
|
+
model.set("events_table_name", val);
|
|
341
|
+
model.set("events_joins", []);
|
|
342
|
+
model.save_changes();
|
|
343
|
+
resetRefForm(S.eventsRef);
|
|
344
|
+
}, selLH ? "Select events table..." : "Select a lakehouse first", !selLH)
|
|
345
|
+
);
|
|
346
|
+
if (selTable && columns.length === 0 && loadingStatus.includes("columns")) {
|
|
347
|
+
tblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "Loading columns..." }));
|
|
348
|
+
}
|
|
349
|
+
card.appendChild(tblRow);
|
|
350
|
+
|
|
351
|
+
// Columns
|
|
352
|
+
if (selTable && columns.length > 0) {
|
|
353
|
+
card.appendChild(h("div", { className: "nsw-joins-title", textContent: "Columns" }));
|
|
354
|
+
card.appendChild(buildColumnList(columns, (updated) => {
|
|
355
|
+
model.set("events_columns", updated);
|
|
356
|
+
model.save_changes();
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// References
|
|
361
|
+
if (selTable) {
|
|
362
|
+
card.appendChild(h("hr", { className: "nsw-divider" }));
|
|
363
|
+
card.appendChild(h("div", { className: "nsw-joins-title", textContent: "References (Joins)" }));
|
|
364
|
+
card.appendChild(buildJoinList(joins, (idx) => {
|
|
365
|
+
const j = joins[idx];
|
|
366
|
+
S.eventsRef.visible = true;
|
|
367
|
+
S.eventsRef.editIndex = idx;
|
|
368
|
+
S.eventsRef.targetTable = j.target_table;
|
|
369
|
+
S.eventsRef.sourceCol = j.source_column;
|
|
370
|
+
S.eventsRef.targetCol = j.target_column;
|
|
371
|
+
S.eventsRef.keyType = j.join_key_type;
|
|
372
|
+
S.eventsRef.exportName = j.export_name || "";
|
|
373
|
+
S.eventsRef.targetCols = (j.target_columns || []).map(c => ({ name: c.name, dtype: c.dtype, included: c.included }));
|
|
374
|
+
S.eventsRef.loading = false;
|
|
375
|
+
rebuild();
|
|
376
|
+
}, (idx) => {
|
|
377
|
+
const updated = JSON.parse(JSON.stringify(joins));
|
|
378
|
+
updated.splice(idx, 1);
|
|
379
|
+
model.set("events_joins", updated);
|
|
380
|
+
model.save_changes();
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
// Add Reference button / form
|
|
384
|
+
const refForm = buildRefForm("events");
|
|
385
|
+
if (refForm) {
|
|
386
|
+
card.appendChild(refForm);
|
|
387
|
+
} else {
|
|
388
|
+
card.appendChild(h("button", {
|
|
389
|
+
className: "nsw-btn outline",
|
|
390
|
+
textContent: "+ Add Reference",
|
|
391
|
+
style: "margin-top:6px",
|
|
392
|
+
onClick: () => { S.eventsRef.visible = true; rebuild(); }
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return card;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* ── Cases: Events→Cases relationship form ─────────── */
|
|
401
|
+
function buildCasesRelationship() {
|
|
402
|
+
const evtCols = model.get("events_columns") || [];
|
|
403
|
+
const casesCols = model.get("cases_columns") || [];
|
|
404
|
+
const rel = model.get("cases_relationship") || {};
|
|
405
|
+
const evtTable = model.get("events_table_name") || "";
|
|
406
|
+
|
|
407
|
+
if (!evtTable) {
|
|
408
|
+
return h("div", { className: "nsw-rel-form" },
|
|
409
|
+
h("div", { className: "rel-title", textContent: "Link to Events Table" }),
|
|
410
|
+
h("div", { className: "nsw-empty", textContent: "Select an events table first to configure the relationship." })
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const wrap = h("div", { className: "nsw-rel-form" });
|
|
415
|
+
wrap.appendChild(h("div", { className: "rel-title", textContent: "Link to Events Table (required)" }));
|
|
416
|
+
|
|
417
|
+
// FK + PK side-by-side
|
|
418
|
+
const fkOpts = evtCols.map(c => ({ value: c.name, label: c.name + " (" + c.dtype + ")" }));
|
|
419
|
+
const pkOpts = casesCols.map(c => ({ value: c.name, label: c.name + " (" + c.dtype + ")" }));
|
|
420
|
+
wrap.appendChild(h("div", { className: "nsw-pair-row" },
|
|
421
|
+
h("div", { className: "nsw-pair" },
|
|
422
|
+
h("span", { className: "nsw-label sm", textContent: "Events FK" }),
|
|
423
|
+
selectEl(fkOpts, rel.fk_column || "", (v) => {
|
|
424
|
+
const updated = Object.assign({}, rel, { fk_column: v });
|
|
425
|
+
model.set("cases_relationship", updated);
|
|
426
|
+
model.save_changes();
|
|
427
|
+
}, "Column on " + evtTable + "...")
|
|
428
|
+
),
|
|
429
|
+
h("div", { className: "nsw-pair" },
|
|
430
|
+
h("span", { className: "nsw-label sm", textContent: "Cases PK" }),
|
|
431
|
+
selectEl(pkOpts, rel.pk_column || "", (v) => {
|
|
432
|
+
const updated = Object.assign({}, rel, { pk_column: v });
|
|
433
|
+
model.set("cases_relationship", updated);
|
|
434
|
+
model.save_changes();
|
|
435
|
+
}, "Column on cases...")
|
|
436
|
+
)
|
|
437
|
+
));
|
|
438
|
+
|
|
439
|
+
// Key type + status on one row
|
|
440
|
+
const keyOpts = [{ value: "Integer", label: "Integer" }, { value: "String", label: "String" }];
|
|
441
|
+
const configured = rel.fk_column && rel.pk_column;
|
|
442
|
+
wrap.appendChild(h("div", { className: "nsw-row" },
|
|
443
|
+
h("span", { className: "nsw-label sm", textContent: "Key type" }),
|
|
444
|
+
selectEl(keyOpts, rel.join_key_type || "Integer", (v) => {
|
|
445
|
+
const updated = Object.assign({}, rel, { join_key_type: v });
|
|
446
|
+
model.set("cases_relationship", updated);
|
|
447
|
+
model.save_changes();
|
|
448
|
+
}),
|
|
449
|
+
h("span", { className: "nsw-rel-status " + (configured ? "configured" : "missing"),
|
|
450
|
+
textContent: configured
|
|
451
|
+
? evtTable + "." + rel.fk_column + " → cases." + rel.pk_column
|
|
452
|
+
: "Select FK and PK columns"
|
|
453
|
+
})
|
|
454
|
+
));
|
|
455
|
+
|
|
456
|
+
return wrap;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* ── Section C: Cases table ──────────────────────────── */
|
|
460
|
+
function buildCasesCard() {
|
|
461
|
+
const enabled = model.get("cases_enabled");
|
|
462
|
+
|
|
463
|
+
if (!enabled) {
|
|
464
|
+
return h("div", { className: "nsw-cases-placeholder" },
|
|
465
|
+
h("button", {
|
|
466
|
+
className: "nsw-btn outline",
|
|
467
|
+
textContent: "+ Add Cases Table (optional)",
|
|
468
|
+
onClick: () => { model.set("cases_enabled", true); model.save_changes(); }
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const card = h("div", { className: "nsw-card cases" });
|
|
474
|
+
card.appendChild(h("div", { className: "nsw-card-title" },
|
|
475
|
+
h("span", { textContent: "Cases Table" }),
|
|
476
|
+
h("span", { className: "badge", textContent: "Optional" }),
|
|
477
|
+
h("span", { style: "flex:1" }),
|
|
478
|
+
h("button", {
|
|
479
|
+
className: "nsw-btn danger",
|
|
480
|
+
textContent: "Remove Cases Table",
|
|
481
|
+
onClick: () => {
|
|
482
|
+
model.set("cases_enabled", false);
|
|
483
|
+
model.set("cases_table_name", "");
|
|
484
|
+
model.set("cases_columns", []);
|
|
485
|
+
model.set("cases_joins", []);
|
|
486
|
+
model.set("cases_relationship", {});
|
|
487
|
+
model.save_changes();
|
|
488
|
+
resetRefForm(S.casesRef);
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
));
|
|
492
|
+
|
|
493
|
+
const tables = model.get("available_tables") || [];
|
|
494
|
+
const selTable = model.get("cases_table_name") || "";
|
|
495
|
+
const columns = model.get("cases_columns") || [];
|
|
496
|
+
const joins = model.get("cases_joins") || [];
|
|
497
|
+
const selLH = model.get("selected_lakehouse_id") || "";
|
|
498
|
+
|
|
499
|
+
const casesLoadingStatus = model.get("loading_status") || "";
|
|
500
|
+
const tOpts = tables.map(t => ({ value: t.name, label: t.name }));
|
|
501
|
+
const casesTblRow = h("div", { className: "nsw-row" },
|
|
502
|
+
h("span", { className: "nsw-label", textContent: "Table" }),
|
|
503
|
+
selectEl(tOpts, selTable, (val) => {
|
|
504
|
+
model.set("cases_table_name", val);
|
|
505
|
+
model.set("cases_joins", []);
|
|
506
|
+
model.set("cases_relationship", {});
|
|
507
|
+
model.save_changes();
|
|
508
|
+
resetRefForm(S.casesRef);
|
|
509
|
+
}, selLH ? "Select cases table..." : "Select a lakehouse first", !selLH)
|
|
510
|
+
);
|
|
511
|
+
if (selTable && columns.length === 0 && casesLoadingStatus.includes("columns")) {
|
|
512
|
+
casesTblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "Loading columns..." }));
|
|
513
|
+
}
|
|
514
|
+
card.appendChild(casesTblRow);
|
|
515
|
+
|
|
516
|
+
// Events→Cases relationship (shown as soon as cases table is selected)
|
|
517
|
+
if (selTable && columns.length > 0) {
|
|
518
|
+
card.appendChild(buildCasesRelationship());
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (selTable && columns.length > 0) {
|
|
522
|
+
card.appendChild(h("div", { className: "nsw-joins-title", textContent: "Columns" }));
|
|
523
|
+
card.appendChild(buildColumnList(columns, (updated) => {
|
|
524
|
+
model.set("cases_columns", updated);
|
|
525
|
+
model.save_changes();
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (selTable) {
|
|
530
|
+
card.appendChild(h("hr", { className: "nsw-divider" }));
|
|
531
|
+
card.appendChild(h("div", { className: "nsw-joins-title", textContent: "References (Join lookups only)" }));
|
|
532
|
+
card.appendChild(buildJoinList(joins, (idx) => {
|
|
533
|
+
const j = joins[idx];
|
|
534
|
+
S.casesRef.visible = true;
|
|
535
|
+
S.casesRef.editIndex = idx;
|
|
536
|
+
S.casesRef.targetTable = j.target_table;
|
|
537
|
+
S.casesRef.sourceCol = j.source_column;
|
|
538
|
+
S.casesRef.targetCol = j.target_column;
|
|
539
|
+
S.casesRef.keyType = j.join_key_type;
|
|
540
|
+
S.casesRef.exportName = j.export_name || "";
|
|
541
|
+
S.casesRef.targetCols = (j.target_columns || []).map(c => ({ name: c.name, dtype: c.dtype, included: c.included }));
|
|
542
|
+
S.casesRef.loading = false;
|
|
543
|
+
rebuild();
|
|
544
|
+
}, (idx) => {
|
|
545
|
+
const updated = JSON.parse(JSON.stringify(joins));
|
|
546
|
+
updated.splice(idx, 1);
|
|
547
|
+
model.set("cases_joins", updated);
|
|
548
|
+
model.save_changes();
|
|
549
|
+
}));
|
|
550
|
+
|
|
551
|
+
const refForm = buildRefForm("cases");
|
|
552
|
+
if (refForm) {
|
|
553
|
+
card.appendChild(refForm);
|
|
554
|
+
} else {
|
|
555
|
+
card.appendChild(h("button", {
|
|
556
|
+
className: "nsw-btn outline",
|
|
557
|
+
textContent: "+ Add Reference",
|
|
558
|
+
style: "margin-top:6px",
|
|
559
|
+
onClick: () => { S.casesRef.visible = true; rebuild(); }
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return card;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* ── Section D: JSON output ──────────────────────────── */
|
|
568
|
+
function buildOutputCard() {
|
|
569
|
+
const card = h("div", { className: "nsw-card output" });
|
|
570
|
+
const json = model.get("config_json") || "";
|
|
571
|
+
|
|
572
|
+
const toggleBtn = h("button", { className: "nsw-collapse-btn", onClick: () => { S.outputOpen = !S.outputOpen; rebuild(); } },
|
|
573
|
+
h("span", { className: "arrow " + (S.outputOpen ? "open" : ""), textContent: "▶" }),
|
|
574
|
+
h("span", { textContent: "Generated JSON Config" })
|
|
575
|
+
);
|
|
576
|
+
card.appendChild(toggleBtn);
|
|
577
|
+
|
|
578
|
+
if (S.outputOpen) {
|
|
579
|
+
if (!json) {
|
|
580
|
+
card.appendChild(h("div", { className: "nsw-empty", textContent: "Select a lakehouse and events table to generate the config." }));
|
|
581
|
+
} else {
|
|
582
|
+
const wrap = h("div", { className: "nsw-json-wrap" });
|
|
583
|
+
wrap.appendChild(h("div", { className: "nsw-json", textContent: json }));
|
|
584
|
+
if (S.copied) {
|
|
585
|
+
wrap.appendChild(h("span", { className: "nsw-copy-btn nsw-copied", textContent: "Copied!" }));
|
|
586
|
+
} else {
|
|
587
|
+
wrap.appendChild(h("button", {
|
|
588
|
+
className: "nsw-btn outline nsw-copy-btn",
|
|
589
|
+
textContent: "Copy",
|
|
590
|
+
onClick: () => {
|
|
591
|
+
navigator.clipboard.writeText(json).then(() => {
|
|
592
|
+
S.copied = true;
|
|
593
|
+
rebuild();
|
|
594
|
+
setTimeout(() => { S.copied = false; rebuild(); }, 1500);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
card.appendChild(wrap);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Error messages
|
|
604
|
+
const err = model.get("error_message") || "";
|
|
605
|
+
if (err) card.appendChild(h("div", { className: "nsw-error", textContent: err }));
|
|
606
|
+
|
|
607
|
+
return card;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* ── model change listeners ──────────────────────────── */
|
|
611
|
+
const watchedTraits = [
|
|
612
|
+
"lakehouses", "selected_lakehouse_id", "data_source_file_type",
|
|
613
|
+
"available_tables", "events_table_name", "events_columns", "events_joins",
|
|
614
|
+
"cases_enabled", "cases_table_name", "cases_columns", "cases_joins", "cases_relationship",
|
|
615
|
+
"config_json", "loading_status", "error_message"
|
|
616
|
+
];
|
|
617
|
+
for (const t of watchedTraits) {
|
|
618
|
+
model.on("change:" + t, rebuild);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Handle fetch_columns_response
|
|
622
|
+
model.on("change:fetch_columns_response", () => {
|
|
623
|
+
const resp = model.get("fetch_columns_response") || [];
|
|
624
|
+
const target = S.pendingRefFor;
|
|
625
|
+
if (target) {
|
|
626
|
+
const form = target === "events" ? S.eventsRef : S.casesRef;
|
|
627
|
+
form.targetCols = resp.map(c => ({ name: c.name, dtype: c.dtype, included: true }));
|
|
628
|
+
form.loading = false;
|
|
629
|
+
S.pendingRefFor = null;
|
|
630
|
+
}
|
|
631
|
+
rebuild();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
rebuild();
|
|
635
|
+
}
|