normalized-schema-generator 0.1.0__tar.gz → 0.3.0__tar.gz
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-0.1.0 → normalized_schema_generator-0.3.0}/PKG-INFO +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/pyproject.toml +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/src/normalized_schema_generator/__init__.py +1 -1
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/src/normalized_schema_generator/widget.css +42 -5
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/src/normalized_schema_generator/widget.js +159 -12
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/src/normalized_schema_generator/widget.py +102 -9
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/.gitignore +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/LICENSE +0 -0
- {normalized_schema_generator-0.1.0 → normalized_schema_generator-0.3.0}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: normalized-schema-generator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: An anywidget for Microsoft Fabric notebooks that visually configures normalized schema JSON for Process Mining data import.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rosikm/normalized-schema-generator
|
|
6
6
|
Project-URL: Repository, https://github.com/rosikm/normalized-schema-generator
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "normalized-schema-generator"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "An anywidget for Microsoft Fabric notebooks that visually configures normalized schema JSON for Process Mining data import."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
/* Header */
|
|
7
7
|
.nsw-header { margin-bottom: 16px; }
|
|
8
|
-
.nsw-header h2 { font-size: 20px; font-weight: 600; margin: 0
|
|
9
|
-
.nsw-header p { font-size: 12px; color: var(--f-text2); margin: 0; }
|
|
8
|
+
.nsw-header h2 { font-size: 20px; font-weight: 600; margin: 0; color: var(--f-text); }
|
|
9
|
+
.nsw-header p { font-size: 12px; color: var(--f-text2); margin: 2px 0 0; }
|
|
10
|
+
.nsw-version { font-size: 11px; color: var(--f-text3); font-weight: 400; }
|
|
10
11
|
|
|
11
12
|
/* Cards */
|
|
12
13
|
.nsw-card { border: 1px solid var(--f-border); border-radius: var(--f-radius); padding: 16px; margin-bottom: 12px; background: var(--f-bg); box-shadow: var(--f-shadow4); }
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
/* JSON output */
|
|
97
98
|
.nsw-json-wrap { position: relative; }
|
|
98
99
|
.nsw-json { font-family: var(--f-mono); font-size: 11.5px; background: var(--f-surface); border: 1px solid var(--f-border); border-radius: var(--f-radius-sm); padding: 12px; max-height: 400px; overflow: auto; white-space: pre; line-height: 1.45; color: var(--f-text); }
|
|
99
|
-
.nsw-
|
|
100
|
+
.nsw-json-actions { position: absolute; top: 8px; right: 16px; display: flex; gap: 4px; align-items: center; }
|
|
100
101
|
.nsw-copied { font-size: 11px; color: var(--f-success); font-weight: 600; padding: 3px 10px; }
|
|
101
102
|
|
|
102
103
|
/* Collapse toggle */
|
|
@@ -120,6 +121,42 @@
|
|
|
120
121
|
/* Cases placeholder */
|
|
121
122
|
.nsw-cases-placeholder { display: flex; align-items: center; justify-content: center; padding: 16px; border: 1.5px dashed #E8AB02; border-radius: var(--f-radius); margin-bottom: 12px; }
|
|
122
123
|
|
|
124
|
+
/* Attribute mapping */
|
|
125
|
+
.nsw-card.attrs { border-left: 3px solid #8764B8; }
|
|
126
|
+
.nsw-attr-list { }
|
|
127
|
+
.nsw-attr-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid #f2f2f2; min-height: 34px; }
|
|
128
|
+
.nsw-attr-row:last-child { border-bottom: none; }
|
|
129
|
+
.nsw-attr-row:hover { background: var(--f-surface); }
|
|
130
|
+
.nsw-attr-name { font-family: var(--f-mono); font-size: 12px; flex: 1; min-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--f-text); }
|
|
131
|
+
|
|
132
|
+
/* Icon button group */
|
|
133
|
+
.nsw-icon-group { display: inline-flex; gap: 1px; background: var(--f-surface); border-radius: var(--f-radius-sm); padding: 2px; border: 1px solid var(--f-border); }
|
|
134
|
+
.nsw-icon-btn { width: 26px; height: 24px; border: none; background: transparent; cursor: pointer; border-radius: 3px; color: var(--f-text3); display: inline-flex; align-items: center; justify-content: center; padding: 0; transition: all 0.1s; }
|
|
135
|
+
.nsw-icon-btn:hover { color: var(--f-text); background: rgba(0,0,0,.06); }
|
|
136
|
+
.nsw-icon-btn.active { background: var(--f-brand); color: #fff; }
|
|
137
|
+
.nsw-icon-btn svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
138
|
+
.nsw-icon-btn[title]:hover::after { content: attr(title); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--f-text); color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 3px; white-space: nowrap; pointer-events: none; z-index: 10; }
|
|
139
|
+
.nsw-icon-btn { position: relative; }
|
|
140
|
+
|
|
141
|
+
/* Data type pills */
|
|
142
|
+
.nsw-dtype-group { display: inline-flex; gap: 1px; background: var(--f-surface); border-radius: var(--f-radius-sm); padding: 2px; border: 1px solid var(--f-border); }
|
|
143
|
+
.nsw-dtype-btn { height: 22px; padding: 0 6px; border: none; background: transparent; cursor: pointer; border-radius: 3px; font-family: var(--f-font); font-size: 10px; font-weight: 600; color: var(--f-text3); letter-spacing: 0.03em; transition: all 0.1s; }
|
|
144
|
+
.nsw-dtype-btn:hover { color: var(--f-text); background: rgba(0,0,0,.06); }
|
|
145
|
+
.nsw-dtype-btn.active { color: #fff; }
|
|
146
|
+
.nsw-dtype-btn.active.t-str { background: var(--f-brand); }
|
|
147
|
+
.nsw-dtype-btn.active.t-int { background: var(--f-success); }
|
|
148
|
+
.nsw-dtype-btn.active.t-flt { background: var(--f-warn); }
|
|
149
|
+
.nsw-dtype-btn.active.t-date { background: #7B1FA2; }
|
|
150
|
+
.nsw-dtype-btn.active.t-bool { background: var(--f-danger); }
|
|
151
|
+
|
|
152
|
+
/* First/Last mini toggle */
|
|
153
|
+
.nsw-fl-group { display: inline-flex; gap: 1px; background: var(--f-surface); border-radius: var(--f-radius-sm); padding: 2px; border: 1px solid var(--f-border); }
|
|
154
|
+
.nsw-fl-btn { height: 22px; padding: 0 8px; border: none; background: transparent; cursor: pointer; border-radius: 3px; font-family: var(--f-font); font-size: 10px; font-weight: 500; color: var(--f-text3); transition: all 0.1s; }
|
|
155
|
+
.nsw-fl-btn:hover { color: var(--f-text); background: rgba(0,0,0,.06); }
|
|
156
|
+
.nsw-fl-btn.active { background: var(--f-brand); color: #fff; }
|
|
157
|
+
.nsw-fl-group.disabled { opacity: 0.35; pointer-events: none; }
|
|
158
|
+
.nsw-fl-group.disabled .nsw-fl-btn { cursor: default; }
|
|
159
|
+
|
|
123
160
|
/* Scrollbar */
|
|
124
|
-
.nsw-col-list::-webkit-scrollbar, .nsw-json::-webkit-scrollbar { width: 6px; }
|
|
125
|
-
.nsw-col-list::-webkit-scrollbar-thumb, .nsw-json::-webkit-scrollbar-thumb { background: var(--f-border-strong); border-radius: 3px; }
|
|
161
|
+
.nsw-col-list::-webkit-scrollbar, .nsw-json::-webkit-scrollbar, .nsw-attr-list::-webkit-scrollbar { width: 6px; }
|
|
162
|
+
.nsw-col-list::-webkit-scrollbar-thumb, .nsw-json::-webkit-scrollbar-thumb, .nsw-attr-list::-webkit-scrollbar-thumb { background: var(--f-border-strong); border-radius: 3px; }
|
|
@@ -63,6 +63,7 @@ export function render({ model, el }) {
|
|
|
63
63
|
eventsRef: { visible: false, editIndex: -1, targetTable: "", sourceCol: "", targetCol: "", keyType: "Integer", exportName: "", targetCols: [], loading: false },
|
|
64
64
|
casesRef: { visible: false, editIndex: -1, targetTable: "", sourceCol: "", targetCol: "", keyType: "Integer", exportName: "", targetCols: [], loading: false },
|
|
65
65
|
pendingRefFor: null,
|
|
66
|
+
attrsOpen: false,
|
|
66
67
|
outputOpen: true,
|
|
67
68
|
copied: false,
|
|
68
69
|
};
|
|
@@ -86,13 +87,17 @@ export function render({ model, el }) {
|
|
|
86
87
|
root.appendChild(buildLakehouseCard());
|
|
87
88
|
root.appendChild(buildEventsCard());
|
|
88
89
|
root.appendChild(buildCasesCard());
|
|
90
|
+
root.appendChild(buildAttributesCard());
|
|
89
91
|
root.appendChild(buildOutputCard());
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/* ── header ──────────────────────────────────────────── */
|
|
93
95
|
function buildHeader() {
|
|
94
96
|
return h("div", { className: "nsw-header" },
|
|
95
|
-
h("
|
|
97
|
+
h("div", { style: "display:flex;align-items:baseline;gap:8px" },
|
|
98
|
+
h("h2", { textContent: "Normalized Schema Generator" }),
|
|
99
|
+
h("span", { className: "nsw-version", textContent: "v0.3.0" })
|
|
100
|
+
),
|
|
96
101
|
h("p", { textContent: "Configure your process mining star-schema and generate the dataSource JSON." })
|
|
97
102
|
);
|
|
98
103
|
}
|
|
@@ -106,6 +111,7 @@ export function render({ model, el }) {
|
|
|
106
111
|
|
|
107
112
|
// Lakehouse selector
|
|
108
113
|
const status = model.get("loading_status") || "";
|
|
114
|
+
const lhLoading = !!(status && (status.includes("tables") || status.includes("Reloading")));
|
|
109
115
|
const lhOpts = lakehouses.map(l => ({ value: l.id, label: l.displayName }));
|
|
110
116
|
const lhRow = h("div", { className: "nsw-row" },
|
|
111
117
|
h("span", { className: "nsw-label", textContent: "Lakehouse" }),
|
|
@@ -114,9 +120,9 @@ export function render({ model, el }) {
|
|
|
114
120
|
model.set("selected_lakehouse_id", val);
|
|
115
121
|
model.set("selected_lakehouse_name", lh ? lh.displayName : "");
|
|
116
122
|
model.save_changes();
|
|
117
|
-
}, "Select a lakehouse...")
|
|
123
|
+
}, "Select a lakehouse...", lhLoading)
|
|
118
124
|
);
|
|
119
|
-
if (
|
|
125
|
+
if (lhLoading) {
|
|
120
126
|
lhRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: status }));
|
|
121
127
|
}
|
|
122
128
|
card.appendChild(lhRow);
|
|
@@ -333,6 +339,8 @@ export function render({ model, el }) {
|
|
|
333
339
|
|
|
334
340
|
// Table selector
|
|
335
341
|
const loadingStatus = model.get("loading_status") || "";
|
|
342
|
+
const isLoadingTables = loadingStatus.includes("tables") || loadingStatus.includes("Reloading");
|
|
343
|
+
const isLoadingCols = selTable && columns.length === 0 && loadingStatus.includes("columns");
|
|
336
344
|
const tOpts = tables.map(t => ({ value: t.name, label: t.name }));
|
|
337
345
|
const tblRow = h("div", { className: "nsw-row" },
|
|
338
346
|
h("span", { className: "nsw-label", textContent: "Table" }),
|
|
@@ -341,9 +349,11 @@ export function render({ model, el }) {
|
|
|
341
349
|
model.set("events_joins", []);
|
|
342
350
|
model.save_changes();
|
|
343
351
|
resetRefForm(S.eventsRef);
|
|
344
|
-
}, selLH ? "Select events table..." : "Select a lakehouse first", !selLH)
|
|
352
|
+
}, isLoadingTables ? "Loading tables..." : (selLH ? "Select events table..." : "Select a lakehouse first"), !selLH || isLoadingTables || isLoadingCols)
|
|
345
353
|
);
|
|
346
|
-
if (
|
|
354
|
+
if (isLoadingTables) {
|
|
355
|
+
tblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "" }));
|
|
356
|
+
} else if (isLoadingCols) {
|
|
347
357
|
tblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "Loading columns..." }));
|
|
348
358
|
}
|
|
349
359
|
card.appendChild(tblRow);
|
|
@@ -497,6 +507,8 @@ export function render({ model, el }) {
|
|
|
497
507
|
const selLH = model.get("selected_lakehouse_id") || "";
|
|
498
508
|
|
|
499
509
|
const casesLoadingStatus = model.get("loading_status") || "";
|
|
510
|
+
const casesLoadingTables = casesLoadingStatus.includes("tables") || casesLoadingStatus.includes("Reloading");
|
|
511
|
+
const casesLoadingCols = selTable && columns.length === 0 && casesLoadingStatus.includes("columns");
|
|
500
512
|
const tOpts = tables.map(t => ({ value: t.name, label: t.name }));
|
|
501
513
|
const casesTblRow = h("div", { className: "nsw-row" },
|
|
502
514
|
h("span", { className: "nsw-label", textContent: "Table" }),
|
|
@@ -506,9 +518,11 @@ export function render({ model, el }) {
|
|
|
506
518
|
model.set("cases_relationship", {});
|
|
507
519
|
model.save_changes();
|
|
508
520
|
resetRefForm(S.casesRef);
|
|
509
|
-
}, selLH ? "Select cases table..." : "Select a lakehouse first", !selLH)
|
|
521
|
+
}, casesLoadingTables ? "Loading tables..." : (selLH ? "Select cases table..." : "Select a lakehouse first"), !selLH || casesLoadingTables || casesLoadingCols)
|
|
510
522
|
);
|
|
511
|
-
if (
|
|
523
|
+
if (casesLoadingTables) {
|
|
524
|
+
casesTblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "" }));
|
|
525
|
+
} else if (casesLoadingCols) {
|
|
512
526
|
casesTblRow.appendChild(h("span", { className: "nsw-loading-inline", textContent: "Loading columns..." }));
|
|
513
527
|
}
|
|
514
528
|
card.appendChild(casesTblRow);
|
|
@@ -564,7 +578,125 @@ export function render({ model, el }) {
|
|
|
564
578
|
return card;
|
|
565
579
|
}
|
|
566
580
|
|
|
567
|
-
/* ──
|
|
581
|
+
/* ── SVG icons for attribute types (16x16 viewBox) ──── */
|
|
582
|
+
const ICONS = {
|
|
583
|
+
caseid: '<svg viewBox="0 0 16 16"><path d="M10 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a2 2 0 0 1 2 2v.27c.6.34 1 .99 1 1.73a2 2 0 0 1-4 0c0-.74.4-1.39 1-1.73V9a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v.27c.6.34 1 .99 1 1.73a2 2 0 0 1-4 0c0-.74.4-1.39 1-1.73V9a2 2 0 0 1 2-2h1V5.73C5.4 5.39 5 4.74 5 4a2 2 0 0 1 2-2"/><circle cx="10" cy="4" r="1"/><circle cx="6" cy="11" r="1"/><circle cx="14" cy="11" r="1"/>',
|
|
584
|
+
activity: '<svg viewBox="0 0 16 16"><polygon points="5,2 14,8 5,14"/>',
|
|
585
|
+
start: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.5" fill="none"/><path d="M8 4v4l3 1.5"/>',
|
|
586
|
+
end: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.5" fill="none"/><path d="M5.5 8.5l2 2 3.5-4"/>',
|
|
587
|
+
resource: '<svg viewBox="0 0 16 16"><circle cx="8" cy="5" r="2.5" fill="none"/><path d="M3 14c0-2.8 2.2-5 5-5s5 2.2 5 5"/>',
|
|
588
|
+
evtattr: '<svg viewBox="0 0 16 16"><path d="M2 4h12M2 8h12M2 12h8"/>',
|
|
589
|
+
caseattr: '<svg viewBox="0 0 16 16"><path d="M2 3h5l1.5 2H14v8H2z"/>',
|
|
590
|
+
fin_evt: '<svg viewBox="0 0 16 16"><path d="M2 4h7M2 8h7M2 12h5"/><text x="13" y="10" text-anchor="middle" font-size="7" font-weight="700" fill="currentColor" stroke="none">$</text>',
|
|
591
|
+
fin_case: '<svg viewBox="0 0 16 16"><path d="M2 3h5l1.5 2H11v5H2z"/><text x="14" y="12" text-anchor="middle" font-size="7" font-weight="700" fill="currentColor" stroke="none">$</text>',
|
|
592
|
+
fin_res: '<svg viewBox="0 0 16 16"><circle cx="6" cy="5" r="2.5" fill="none"/><path d="M1 14c0-2.8 2.2-5 5-5s5 2.2 5 5"/><text x="14" y="7" text-anchor="middle" font-size="7" font-weight="700" fill="currentColor" stroke="none">$</text>',
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/* Attribute type definitions: key, icon, tooltip, ImportType, Level, FinanceImportType */
|
|
596
|
+
const ATTR_TYPES = [
|
|
597
|
+
{ key: "caseid", icon: ICONS.caseid, tip: "Case ID", it: "Case", lv: "Event", fi: "None" },
|
|
598
|
+
{ key: "activity", icon: ICONS.activity, tip: "Activity", it: "Activity", lv: "Event", fi: "None" },
|
|
599
|
+
{ key: "start", icon: ICONS.start, tip: "Start timestamp", it: "Start", lv: "Event", fi: "None" },
|
|
600
|
+
{ key: "end", icon: ICONS.end, tip: "End timestamp", it: "End", lv: "Event", fi: "None" },
|
|
601
|
+
{ key: "resource", icon: ICONS.resource, tip: "Resource", it: "Resource", lv: "Event", fi: "None" },
|
|
602
|
+
{ key: "evtattr", icon: ICONS.evtattr, tip: "Event-level attribute",it: "Other", lv: "Event", fi: "None" },
|
|
603
|
+
{ key: "caseattr", icon: ICONS.caseattr, tip: "Case-level attribute", it: "Other", lv: "Case", fi: "None" },
|
|
604
|
+
{ key: "fin_evt", icon: ICONS.fin_evt, tip: "Finance per event", it: "Other", lv: "Event", fi: "PerEvent" },
|
|
605
|
+
{ key: "fin_case", icon: ICONS.fin_case, tip: "Finance per case", it: "Other", lv: "Case", fi: "PerCase" },
|
|
606
|
+
{ key: "fin_res", icon: ICONS.fin_res, tip: "Finance per resource", it: "Other", lv: "Event", fi: "PerResource" },
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
function attrTypeKey(a) {
|
|
610
|
+
for (const t of ATTR_TYPES) {
|
|
611
|
+
if (t.it === a.ImportType && t.lv === a.Level && t.fi === a.FinanceImportType) return t.key;
|
|
612
|
+
}
|
|
613
|
+
return "evtattr";
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const DTYPES = [
|
|
617
|
+
{ key: "String", label: "STR", cls: "t-str" },
|
|
618
|
+
{ key: "Integer", label: "INT", cls: "t-int" },
|
|
619
|
+
{ key: "Float", label: "FLT", cls: "t-flt" },
|
|
620
|
+
{ key: "Date", label: "DATE", cls: "t-date" },
|
|
621
|
+
{ key: "Boolean", label: "BOOL", cls: "t-bool" },
|
|
622
|
+
];
|
|
623
|
+
|
|
624
|
+
/* ── Section D: Attribute Mapping ────────────────────── */
|
|
625
|
+
function buildAttributesCard() {
|
|
626
|
+
const attrs = model.get("attributes_config") || [];
|
|
627
|
+
if (attrs.length === 0) return h("div", null);
|
|
628
|
+
|
|
629
|
+
const card = h("div", { className: "nsw-card attrs" });
|
|
630
|
+
|
|
631
|
+
const toggleBtn = h("button", { className: "nsw-collapse-btn", onClick: () => { S.attrsOpen = !S.attrsOpen; rebuild(); } },
|
|
632
|
+
h("span", { className: "arrow " + (S.attrsOpen ? "open" : ""), textContent: "▶" }),
|
|
633
|
+
h("span", { textContent: "Attribute Mapping" }),
|
|
634
|
+
h("span", { style: "font-weight:400;color:var(--f-text3);font-size:12px;margin-left:4px", textContent: "(" + attrs.length + ")" })
|
|
635
|
+
);
|
|
636
|
+
card.appendChild(toggleBtn);
|
|
637
|
+
|
|
638
|
+
if (!S.attrsOpen) return card;
|
|
639
|
+
|
|
640
|
+
function updAttr(i, fields) {
|
|
641
|
+
const updated = JSON.parse(JSON.stringify(attrs));
|
|
642
|
+
Object.assign(updated[i], fields);
|
|
643
|
+
model.set("attributes_config", updated);
|
|
644
|
+
model.save_changes();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const list = h("div", { className: "nsw-attr-list" });
|
|
648
|
+
attrs.forEach((attr, i) => {
|
|
649
|
+
const curType = attrTypeKey(attr);
|
|
650
|
+
const isCaseLevel = attr.Level === "Case";
|
|
651
|
+
|
|
652
|
+
// Attribute type icon group
|
|
653
|
+
const typeGroup = h("div", { className: "nsw-icon-group" });
|
|
654
|
+
for (const t of ATTR_TYPES) {
|
|
655
|
+
const btn = h("button", {
|
|
656
|
+
className: "nsw-icon-btn" + (curType === t.key ? " active" : ""),
|
|
657
|
+
title: t.tip,
|
|
658
|
+
innerHTML: t.icon,
|
|
659
|
+
onClick: () => updAttr(i, { ImportType: t.it, Level: t.lv, FinanceImportType: t.fi })
|
|
660
|
+
});
|
|
661
|
+
typeGroup.appendChild(btn);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// First/Last toggle (always visible, disabled when not case-level)
|
|
665
|
+
const curFL = attr.CaseLevelAttributeHandling || "First";
|
|
666
|
+
const flGroup = h("div", { className: "nsw-fl-group" + (isCaseLevel ? "" : " disabled") });
|
|
667
|
+
for (const v of ["First", "Last"]) {
|
|
668
|
+
flGroup.appendChild(h("button", {
|
|
669
|
+
className: "nsw-fl-btn" + (isCaseLevel && curFL === v ? " active" : ""),
|
|
670
|
+
textContent: v,
|
|
671
|
+
onClick: isCaseLevel ? () => updAttr(i, { CaseLevelAttributeHandling: v }) : null
|
|
672
|
+
}));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Data type pills
|
|
676
|
+
const dtGroup = h("div", { className: "nsw-dtype-group" });
|
|
677
|
+
const curDt = attr.SourceDataType || "String";
|
|
678
|
+
for (const d of DTYPES) {
|
|
679
|
+
dtGroup.appendChild(h("button", {
|
|
680
|
+
className: "nsw-dtype-btn " + d.cls + (curDt === d.key ? " active" : ""),
|
|
681
|
+
textContent: d.label,
|
|
682
|
+
onClick: () => updAttr(i, { SourceDataType: d.key })
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const row = h("div", { className: "nsw-attr-row" },
|
|
687
|
+
h("span", { className: "nsw-attr-name", textContent: attr.Name }),
|
|
688
|
+
typeGroup,
|
|
689
|
+
flGroup,
|
|
690
|
+
dtGroup
|
|
691
|
+
);
|
|
692
|
+
list.appendChild(row);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
card.appendChild(list);
|
|
696
|
+
return card;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* ── Section E: JSON output ──────────────────────────── */
|
|
568
700
|
function buildOutputCard() {
|
|
569
701
|
const card = h("div", { className: "nsw-card output" });
|
|
570
702
|
const json = model.get("config_json") || "";
|
|
@@ -581,11 +713,12 @@ export function render({ model, el }) {
|
|
|
581
713
|
} else {
|
|
582
714
|
const wrap = h("div", { className: "nsw-json-wrap" });
|
|
583
715
|
wrap.appendChild(h("div", { className: "nsw-json", textContent: json }));
|
|
716
|
+
const btnGroup = h("div", { className: "nsw-json-actions" });
|
|
584
717
|
if (S.copied) {
|
|
585
|
-
|
|
718
|
+
btnGroup.appendChild(h("span", { className: "nsw-copied", textContent: "Copied!" }));
|
|
586
719
|
} else {
|
|
587
|
-
|
|
588
|
-
className: "nsw-btn outline
|
|
720
|
+
btnGroup.appendChild(h("button", {
|
|
721
|
+
className: "nsw-btn outline sm",
|
|
589
722
|
textContent: "Copy",
|
|
590
723
|
onClick: () => {
|
|
591
724
|
navigator.clipboard.writeText(json).then(() => {
|
|
@@ -596,6 +729,20 @@ export function render({ model, el }) {
|
|
|
596
729
|
}
|
|
597
730
|
}));
|
|
598
731
|
}
|
|
732
|
+
btnGroup.appendChild(h("button", {
|
|
733
|
+
className: "nsw-btn outline sm",
|
|
734
|
+
textContent: "Save",
|
|
735
|
+
onClick: () => {
|
|
736
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
737
|
+
const url = URL.createObjectURL(blob);
|
|
738
|
+
const a = document.createElement("a");
|
|
739
|
+
a.href = url;
|
|
740
|
+
a.download = "normalized_config.json";
|
|
741
|
+
a.click();
|
|
742
|
+
URL.revokeObjectURL(url);
|
|
743
|
+
}
|
|
744
|
+
}));
|
|
745
|
+
wrap.appendChild(btnGroup);
|
|
599
746
|
card.appendChild(wrap);
|
|
600
747
|
}
|
|
601
748
|
}
|
|
@@ -612,7 +759,7 @@ export function render({ model, el }) {
|
|
|
612
759
|
"lakehouses", "selected_lakehouse_id", "data_source_file_type",
|
|
613
760
|
"available_tables", "events_table_name", "events_columns", "events_joins",
|
|
614
761
|
"cases_enabled", "cases_table_name", "cases_columns", "cases_joins", "cases_relationship",
|
|
615
|
-
"config_json", "loading_status", "error_message"
|
|
762
|
+
"attributes_config", "config_json", "loading_status", "error_message"
|
|
616
763
|
];
|
|
617
764
|
for (const t of watchedTraits) {
|
|
618
765
|
model.on("change:" + t, rebuild);
|
|
@@ -265,6 +265,9 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
265
265
|
# Events→Cases FK relationship: {fk_column, pk_column, join_key_type}
|
|
266
266
|
cases_relationship = traitlets.Dict({}).tag(sync=True)
|
|
267
267
|
|
|
268
|
+
# Attribute mapping
|
|
269
|
+
attributes_config = traitlets.List([]).tag(sync=True)
|
|
270
|
+
|
|
268
271
|
# Communication & output
|
|
269
272
|
fetch_columns_request = traitlets.Unicode("").tag(sync=True)
|
|
270
273
|
fetch_columns_response = traitlets.List([]).tag(sync=True)
|
|
@@ -409,6 +412,11 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
409
412
|
def _on_data_change(self, change):
|
|
410
413
|
self._generate_config()
|
|
411
414
|
|
|
415
|
+
@traitlets.observe("attributes_config")
|
|
416
|
+
def _on_attributes_change(self, change):
|
|
417
|
+
# Only regenerate JSON, don't re-sync attributes (avoid loop)
|
|
418
|
+
self._generate_config(sync_attributes=False)
|
|
419
|
+
|
|
412
420
|
# ── JSON generation ──────────────────────────────────
|
|
413
421
|
|
|
414
422
|
def _find_table_path(self, table_name):
|
|
@@ -417,13 +425,36 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
417
425
|
return t["path"]
|
|
418
426
|
return ""
|
|
419
427
|
|
|
420
|
-
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _dataset_name(table_name):
|
|
430
|
+
"""Extract a clean dataset name: strip path and file extension."""
|
|
431
|
+
import posixpath
|
|
432
|
+
base = posixpath.basename(table_name)
|
|
433
|
+
# Remove known extensions (.csv, .parquet, .snappy.parquet)
|
|
434
|
+
for ext in (".snappy.parquet", ".parquet", ".csv"):
|
|
435
|
+
if base.lower().endswith(ext):
|
|
436
|
+
base = base[: -len(ext)]
|
|
437
|
+
break
|
|
438
|
+
return base
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _dtype_to_source_data_type(dtype):
|
|
442
|
+
return {
|
|
443
|
+
"int": "Integer",
|
|
444
|
+
"double": "Float",
|
|
445
|
+
"string": "String",
|
|
446
|
+
"timestamp": "Date",
|
|
447
|
+
"boolean": "Boolean",
|
|
448
|
+
}.get(dtype, "String")
|
|
449
|
+
|
|
450
|
+
def _generate_config(self, sync_attributes=True):
|
|
421
451
|
self.error_message = ""
|
|
422
452
|
if not self.selected_lakehouse_id or not self.events_table_name:
|
|
423
453
|
self.config_json = ""
|
|
424
454
|
return
|
|
425
455
|
|
|
426
456
|
try:
|
|
457
|
+
dn = self._dataset_name
|
|
427
458
|
datasets = []
|
|
428
459
|
join_dataset_names = set()
|
|
429
460
|
|
|
@@ -446,19 +477,18 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
446
477
|
if c.get("included") and c["name"] not in events_fk_cols
|
|
447
478
|
]
|
|
448
479
|
events_join_arr = []
|
|
449
|
-
# Add the Events→Cases join first
|
|
450
480
|
if has_cases_rel:
|
|
451
481
|
events_join_arr.append({
|
|
452
482
|
"SourceColumnName": cases_rel["fk_column"],
|
|
453
483
|
"TargetColumnName": cases_rel["pk_column"],
|
|
454
|
-
"TargetDatasetName": self.cases_table_name,
|
|
484
|
+
"TargetDatasetName": dn(self.cases_table_name),
|
|
455
485
|
"JoinKeyType": cases_rel.get("join_key_type", "Integer"),
|
|
456
486
|
})
|
|
457
487
|
for j in self.events_joins:
|
|
458
488
|
entry = {
|
|
459
489
|
"SourceColumnName": j["source_column"],
|
|
460
490
|
"TargetColumnName": j["target_column"],
|
|
461
|
-
"TargetDatasetName": j["target_table"],
|
|
491
|
+
"TargetDatasetName": dn(j["target_table"]),
|
|
462
492
|
"JoinKeyType": j["join_key_type"],
|
|
463
493
|
}
|
|
464
494
|
if j.get("export_name"):
|
|
@@ -468,12 +498,19 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
468
498
|
|
|
469
499
|
datasets.append({
|
|
470
500
|
"Kind": 0,
|
|
471
|
-
"Name": self.events_table_name,
|
|
501
|
+
"Name": dn(self.events_table_name),
|
|
472
502
|
"Path": self._find_table_path(self.events_table_name),
|
|
473
503
|
"Columns": events_columns,
|
|
474
504
|
"Join": events_join_arr if events_join_arr else None,
|
|
475
505
|
})
|
|
476
506
|
|
|
507
|
+
# Collect attribute names + dtypes + level for Attributes section
|
|
508
|
+
# {col_name: {dtype, level}}
|
|
509
|
+
attr_info = {}
|
|
510
|
+
for c in self.events_columns:
|
|
511
|
+
if c.get("included") and c["name"] not in events_fk_cols:
|
|
512
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Event"}
|
|
513
|
+
|
|
477
514
|
# ── Cases dataset (Kind=1) ──
|
|
478
515
|
if self.cases_enabled and self.cases_table_name:
|
|
479
516
|
cases_fk_cols = {j["source_column"] for j in self.cases_joins}
|
|
@@ -487,7 +524,7 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
487
524
|
entry = {
|
|
488
525
|
"SourceColumnName": j["source_column"],
|
|
489
526
|
"TargetColumnName": j["target_column"],
|
|
490
|
-
"TargetDatasetName": j["target_table"],
|
|
527
|
+
"TargetDatasetName": dn(j["target_table"]),
|
|
491
528
|
"JoinKeyType": j["join_key_type"],
|
|
492
529
|
}
|
|
493
530
|
if j.get("export_name"):
|
|
@@ -497,12 +534,16 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
497
534
|
|
|
498
535
|
datasets.append({
|
|
499
536
|
"Kind": 1,
|
|
500
|
-
"Name": self.cases_table_name,
|
|
537
|
+
"Name": dn(self.cases_table_name),
|
|
501
538
|
"Path": self._find_table_path(self.cases_table_name),
|
|
502
539
|
"Columns": cases_columns,
|
|
503
540
|
"Join": cases_join_arr if cases_join_arr else None,
|
|
504
541
|
})
|
|
505
542
|
|
|
543
|
+
for c in self.cases_columns:
|
|
544
|
+
if c.get("included") and c["name"] not in cases_fk_cols:
|
|
545
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Case"}
|
|
546
|
+
|
|
506
547
|
# ── Join datasets (Kind=2) ──
|
|
507
548
|
join_table_cols = {}
|
|
508
549
|
for j in list(self.events_joins) + list(self.cases_joins):
|
|
@@ -524,11 +565,63 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
524
565
|
]
|
|
525
566
|
datasets.append({
|
|
526
567
|
"Kind": 2,
|
|
527
|
-
"Name": tname,
|
|
568
|
+
"Name": dn(tname),
|
|
528
569
|
"Path": self._find_table_path(tname),
|
|
529
570
|
"Columns": join_cols,
|
|
530
571
|
"Join": None,
|
|
531
572
|
})
|
|
573
|
+
# Join target columns become Event-level attributes
|
|
574
|
+
for c in cols_map.values():
|
|
575
|
+
if c.get("included"):
|
|
576
|
+
attr_info[c["name"]] = {"dtype": c["dtype"], "level": "Event"}
|
|
577
|
+
|
|
578
|
+
# Join ExportNames also become attributes
|
|
579
|
+
for j in list(self.events_joins) + list(self.cases_joins):
|
|
580
|
+
if j.get("export_name"):
|
|
581
|
+
attr_info[j["export_name"]] = {
|
|
582
|
+
"dtype": "int", # FK columns are typically int
|
|
583
|
+
"level": "Event",
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
# ── Sync attributes_config with current columns ──
|
|
587
|
+
if sync_attributes:
|
|
588
|
+
existing = {a["Name"]: a for a in self.attributes_config}
|
|
589
|
+
new_attrs = []
|
|
590
|
+
for col_name, info in attr_info.items():
|
|
591
|
+
if col_name in existing:
|
|
592
|
+
new_attrs.append(existing[col_name])
|
|
593
|
+
else:
|
|
594
|
+
new_attrs.append({
|
|
595
|
+
"Name": col_name,
|
|
596
|
+
"SourceDataType": self._dtype_to_source_data_type(info["dtype"]),
|
|
597
|
+
"ImportType": "Other",
|
|
598
|
+
"Level": info["level"],
|
|
599
|
+
"FinanceImportType": "None",
|
|
600
|
+
"CaseLevelAttributeHandling": "None",
|
|
601
|
+
"IsDisplayAttribute": False,
|
|
602
|
+
"IsExcluded": False,
|
|
603
|
+
"DefaultValue": "",
|
|
604
|
+
})
|
|
605
|
+
if new_attrs != list(self.attributes_config):
|
|
606
|
+
self.attributes_config = new_attrs
|
|
607
|
+
|
|
608
|
+
# ── Build Attributes for JSON from attributes_config ──
|
|
609
|
+
attrs_by_name = {a["Name"]: a for a in self.attributes_config}
|
|
610
|
+
attributes = []
|
|
611
|
+
for col_name in attr_info:
|
|
612
|
+
a = attrs_by_name.get(col_name)
|
|
613
|
+
if a:
|
|
614
|
+
attributes.append({
|
|
615
|
+
"Name": a["Name"],
|
|
616
|
+
"SourceDataType": a.get("SourceDataType", "String"),
|
|
617
|
+
"ImportType": a.get("ImportType", "Other"),
|
|
618
|
+
"Level": a.get("Level", "Event"),
|
|
619
|
+
"FinanceImportType": a.get("FinanceImportType", "None"),
|
|
620
|
+
"CaseLevelAttributeHandling": a.get("CaseLevelAttributeHandling", "None"),
|
|
621
|
+
"IsDisplayAttribute": a.get("IsDisplayAttribute", False),
|
|
622
|
+
"IsExcluded": a.get("IsExcluded", False),
|
|
623
|
+
"DefaultValue": a.get("DefaultValue", ""),
|
|
624
|
+
})
|
|
532
625
|
|
|
533
626
|
# ── Assemble full config ──
|
|
534
627
|
config = {
|
|
@@ -548,7 +641,7 @@ class NormalizedSchemaWidget(anywidget.AnyWidget):
|
|
|
548
641
|
"datasets": datasets,
|
|
549
642
|
},
|
|
550
643
|
"miningMetadata": {
|
|
551
|
-
"ImportConfiguration": {"Attributes":
|
|
644
|
+
"ImportConfiguration": {"Attributes": attributes},
|
|
552
645
|
"Views": [],
|
|
553
646
|
"ProcessExtendedMetadata": {
|
|
554
647
|
"CalculatedMetrics": [],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|