normalized-schema-generator 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: normalized-schema-generator
3
- Version: 0.2.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.2.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"}
@@ -3,4 +3,4 @@
3
3
  from .widget import NormalizedSchemaWidget
4
4
 
5
5
  __all__ = ["NormalizedSchemaWidget"]
6
- __version__ = "0.2.0"
6
+ __version__ = "0.3.0"
@@ -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 0 2px; color: var(--f-text); }
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-copy-btn { position: absolute; top: 8px; right: 8px; }
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("h2", { textContent: "Normalized Schema Generator" }),
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 (status && status.includes("tables")) {
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 (selTable && columns.length === 0 && loadingStatus.includes("columns")) {
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 (selTable && columns.length === 0 && casesLoadingStatus.includes("columns")) {
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
- /* ── Section D: JSON output ──────────────────────────── */
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
- wrap.appendChild(h("span", { className: "nsw-copy-btn nsw-copied", textContent: "Copied!" }));
718
+ btnGroup.appendChild(h("span", { className: "nsw-copied", textContent: "Copied!" }));
586
719
  } else {
587
- wrap.appendChild(h("button", {
588
- className: "nsw-btn outline nsw-copy-btn",
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);