juxscript 1.1.170 → 1.1.171
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.
|
@@ -12,6 +12,8 @@ export interface DataFrameOptions {
|
|
|
12
12
|
filterable?: boolean;
|
|
13
13
|
paginated?: boolean;
|
|
14
14
|
rowsPerPage?: number;
|
|
15
|
+
showStatus?: boolean;
|
|
16
|
+
icon?: string;
|
|
15
17
|
style?: string;
|
|
16
18
|
class?: string;
|
|
17
19
|
}
|
|
@@ -30,78 +32,33 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
30
32
|
private _storageKey;
|
|
31
33
|
private _pendingSource;
|
|
32
34
|
private _inlineUpload;
|
|
35
|
+
private _showStatus;
|
|
36
|
+
private _icon;
|
|
33
37
|
constructor(id: string, options?: DataFrameOptions);
|
|
34
38
|
protected getTriggerEvents(): readonly string[];
|
|
35
39
|
protected getCallbackEvents(): readonly string[];
|
|
36
|
-
/**
|
|
37
|
-
* Load from IndexedDB by storage key
|
|
38
|
-
*/
|
|
39
40
|
fromStorage(key: string): this;
|
|
40
|
-
/**
|
|
41
|
-
* Load from a FileUpload component — auto-wires change event
|
|
42
|
-
*/
|
|
43
41
|
fromUpload(upload: FileUpload): this;
|
|
44
|
-
/**
|
|
45
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
46
|
-
*/
|
|
47
42
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
* @param label - Button/label text (default: 'Upload File')
|
|
53
|
-
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
54
|
-
*/
|
|
55
|
-
withUpload(label?: string, accept?: string): this;
|
|
56
|
-
/**
|
|
57
|
-
* Apply a transform to the current DataFrame and update the table
|
|
58
|
-
*/
|
|
43
|
+
withUpload(label?: string, accept?: string, icon?: string): this;
|
|
44
|
+
showStatus(v: boolean): this;
|
|
45
|
+
statusIcon(v: string): this;
|
|
59
46
|
apply(fn: (df: DataFrame) => DataFrame): this;
|
|
60
|
-
/**
|
|
61
|
-
* Filter rows
|
|
62
|
-
*/
|
|
63
47
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
|
|
64
|
-
/**
|
|
65
|
-
* Select columns
|
|
66
|
-
*/
|
|
67
48
|
select(...cols: string[]): this;
|
|
68
|
-
/**
|
|
69
|
-
* Sort by column
|
|
70
|
-
*/
|
|
71
49
|
sort(col: string, descending?: boolean): this;
|
|
72
|
-
/**
|
|
73
|
-
* Show first N rows
|
|
74
|
-
*/
|
|
75
50
|
head(n?: number): this;
|
|
76
|
-
/**
|
|
77
|
-
* Show last N rows
|
|
78
|
-
*/
|
|
79
51
|
tail(n?: number): this;
|
|
80
|
-
/**
|
|
81
|
-
* Add a computed column
|
|
82
|
-
*/
|
|
83
52
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
|
|
84
|
-
/**
|
|
85
|
-
* Where clause
|
|
86
|
-
*/
|
|
87
53
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
|
|
88
|
-
/** Get the underlying DataFrame */
|
|
89
54
|
get df(): DataFrame | null;
|
|
90
|
-
/** Get the underlying TabularDriver */
|
|
91
55
|
get driver(): TabularDriver;
|
|
92
|
-
/** Get the internal Table component */
|
|
93
56
|
get table(): Table | null;
|
|
94
|
-
/** Get describe() stats */
|
|
95
57
|
describe(): Record<string, any> | null;
|
|
96
|
-
/** Export to CSV string */
|
|
97
58
|
toCSV(delimiter?: string): string;
|
|
98
|
-
/** Export to row objects */
|
|
99
59
|
toRows(): Record<string, any>[];
|
|
100
|
-
/** Get shape */
|
|
101
60
|
get shape(): [number, number];
|
|
102
|
-
/** Get column names */
|
|
103
61
|
get columns(): string[];
|
|
104
|
-
/** Save current DataFrame to IndexedDB */
|
|
105
62
|
save(key?: string): Promise<string | null>;
|
|
106
63
|
striped(v: boolean): this;
|
|
107
64
|
hoverable(v: boolean): this;
|
|
@@ -112,8 +69,8 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
112
69
|
private _updateStatus;
|
|
113
70
|
private _setDataFrame;
|
|
114
71
|
private _updateTable;
|
|
115
|
-
update(prop: string, value: any): void;
|
|
116
72
|
private _showFilterInput;
|
|
73
|
+
update(prop: string, value: any): void;
|
|
117
74
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
118
75
|
}
|
|
119
76
|
export declare function dataframe(id: string, options?: DataFrameOptions): DataFrameComponent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAMnC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,cAAc,GAAG,SAAS,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,aAAa,CAOnB;IACF,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,aAAa,
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,12 +25,16 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
25
25
|
this._storageKey = null;
|
|
26
26
|
this._pendingSource = null;
|
|
27
27
|
this._inlineUpload = null;
|
|
28
|
+
this._showStatus = true;
|
|
29
|
+
this._icon = '';
|
|
28
30
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
31
|
+
this._showStatus = options.showStatus ?? true;
|
|
32
|
+
this._icon = options.icon ?? '';
|
|
29
33
|
this._tableOptions = {
|
|
30
34
|
striped: options.striped ?? true,
|
|
31
35
|
hoverable: options.hoverable ?? true,
|
|
32
36
|
sortable: options.sortable ?? true,
|
|
33
|
-
filterable: options.filterable ??
|
|
37
|
+
filterable: options.filterable ?? false,
|
|
34
38
|
paginated: options.paginated ?? true,
|
|
35
39
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
36
40
|
};
|
|
@@ -40,18 +44,17 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
40
44
|
/* ═══════════════════════════════════════════════════
|
|
41
45
|
* DATA SOURCES
|
|
42
46
|
* ═══════════════════════════════════════════════════ */
|
|
43
|
-
/**
|
|
44
|
-
* Load from IndexedDB by storage key
|
|
45
|
-
*/
|
|
46
47
|
fromStorage(key) {
|
|
47
48
|
this._storageKey = key;
|
|
48
|
-
|
|
49
|
+
const loadFn = async () => {
|
|
49
50
|
this.state.loading = true;
|
|
51
|
+
this._updateStatus('⏳ Loading...', 'loading');
|
|
50
52
|
try {
|
|
51
|
-
|
|
53
|
+
const df = await this._driver.loadByName(key);
|
|
52
54
|
if (!df) {
|
|
53
55
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
54
56
|
this.state.loading = false;
|
|
57
|
+
this._updateStatus('No table found: ' + key, 'empty');
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
this._setDataFrame(df, 'storage: ' + key);
|
|
@@ -59,42 +62,44 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
59
62
|
catch (err) {
|
|
60
63
|
this._triggerCallback('error', err.message, null, this);
|
|
61
64
|
this.state.loading = false;
|
|
65
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
62
66
|
}
|
|
63
67
|
};
|
|
68
|
+
if (this._table) {
|
|
69
|
+
loadFn();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this._pendingSource = loadFn;
|
|
73
|
+
}
|
|
64
74
|
return this;
|
|
65
75
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Load from a FileUpload component — auto-wires change event
|
|
68
|
-
*/
|
|
69
76
|
fromUpload(upload) {
|
|
70
77
|
this._uploadRef = upload;
|
|
71
78
|
this._pendingSource = async () => {
|
|
72
|
-
// Wire upload's change event to parse incoming files
|
|
73
79
|
upload.bind('change', async (files) => {
|
|
74
80
|
if (!files || files.length === 0)
|
|
75
81
|
return;
|
|
76
82
|
const file = files[0];
|
|
77
83
|
this.state.loading = true;
|
|
84
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
78
85
|
try {
|
|
79
86
|
const df = await this._driver.streamFile(file);
|
|
80
|
-
// Auto-persist to IndexedDB
|
|
81
87
|
await this._driver.store(file.name, df, { source: file.name });
|
|
82
|
-
this._setDataFrame(df,
|
|
88
|
+
this._setDataFrame(df, file.name);
|
|
83
89
|
}
|
|
84
90
|
catch (err) {
|
|
85
91
|
this._triggerCallback('error', err.message, null, this);
|
|
86
92
|
this.state.loading = false;
|
|
93
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
87
94
|
}
|
|
88
95
|
});
|
|
89
96
|
};
|
|
90
97
|
return this;
|
|
91
98
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
94
|
-
*/
|
|
95
99
|
fromData(data) {
|
|
96
|
-
|
|
100
|
+
const loadFn = async () => {
|
|
97
101
|
this.state.loading = true;
|
|
102
|
+
this._updateStatus('⏳ Loading data...', 'loading');
|
|
98
103
|
try {
|
|
99
104
|
const df = new DataFrame(data);
|
|
100
105
|
this._setDataFrame(df, 'inline data');
|
|
@@ -102,27 +107,29 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
102
107
|
catch (err) {
|
|
103
108
|
this._triggerCallback('error', err.message, null, this);
|
|
104
109
|
this.state.loading = false;
|
|
110
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
105
111
|
}
|
|
106
112
|
};
|
|
113
|
+
if (this._table) {
|
|
114
|
+
loadFn();
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this._pendingSource = loadFn;
|
|
118
|
+
}
|
|
107
119
|
return this;
|
|
108
120
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* Auto-wires parsing, storage, and display — zero callbacks needed.
|
|
112
|
-
*
|
|
113
|
-
* @param label - Button/label text (default: 'Upload File')
|
|
114
|
-
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
115
|
-
*/
|
|
116
|
-
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls') {
|
|
117
|
-
this._inlineUpload = { label, accept };
|
|
121
|
+
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
|
|
122
|
+
this._inlineUpload = { label, accept, icon };
|
|
118
123
|
return this;
|
|
119
124
|
}
|
|
120
125
|
/* ═══════════════════════════════════════════════════
|
|
121
|
-
*
|
|
126
|
+
* UI TOGGLES
|
|
127
|
+
* ═══════════════════════════════════════════════════ */
|
|
128
|
+
showStatus(v) { this._showStatus = v; return this; }
|
|
129
|
+
statusIcon(v) { this._icon = v; return this; }
|
|
130
|
+
/* ═══════════════════════════════════════════════════
|
|
131
|
+
* TRANSFORM API
|
|
122
132
|
* ═══════════════════════════════════════════════════ */
|
|
123
|
-
/**
|
|
124
|
-
* Apply a transform to the current DataFrame and update the table
|
|
125
|
-
*/
|
|
126
133
|
apply(fn) {
|
|
127
134
|
if (!this._df)
|
|
128
135
|
return this;
|
|
@@ -131,78 +138,38 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
131
138
|
this._triggerCallback('transform', result, null, this);
|
|
132
139
|
return this;
|
|
133
140
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Filter rows
|
|
136
|
-
*/
|
|
137
141
|
filter(predicate) {
|
|
138
142
|
return this.apply(df => df.filter(predicate));
|
|
139
143
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Select columns
|
|
142
|
-
*/
|
|
143
144
|
select(...cols) {
|
|
144
145
|
return this.apply(df => df.select(...cols));
|
|
145
146
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Sort by column
|
|
148
|
-
*/
|
|
149
147
|
sort(col, descending) {
|
|
150
148
|
return this.apply(df => df.sort(col, descending));
|
|
151
149
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Show first N rows
|
|
154
|
-
*/
|
|
155
150
|
head(n = 5) {
|
|
156
151
|
return this.apply(df => df.head(n));
|
|
157
152
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Show last N rows
|
|
160
|
-
*/
|
|
161
153
|
tail(n = 5) {
|
|
162
154
|
return this.apply(df => df.tail(n));
|
|
163
155
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Add a computed column
|
|
166
|
-
*/
|
|
167
156
|
withColumn(name, fn) {
|
|
168
157
|
return this.apply(df => df.withColumn(name, fn));
|
|
169
158
|
}
|
|
170
|
-
/**
|
|
171
|
-
* Where clause
|
|
172
|
-
*/
|
|
173
159
|
where(col, op, value) {
|
|
174
160
|
return this.apply(df => df.where(col, op, value));
|
|
175
161
|
}
|
|
176
162
|
/* ═══════════════════════════════════════════════════
|
|
177
163
|
* ACCESSORS
|
|
178
164
|
* ═══════════════════════════════════════════════════ */
|
|
179
|
-
/** Get the underlying DataFrame */
|
|
180
165
|
get df() { return this._df; }
|
|
181
|
-
/** Get the underlying TabularDriver */
|
|
182
166
|
get driver() { return this._driver; }
|
|
183
|
-
/** Get the internal Table component */
|
|
184
167
|
get table() { return this._table; }
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
toCSV(delimiter) {
|
|
191
|
-
return this._df?.toCSV(delimiter) ?? '';
|
|
192
|
-
}
|
|
193
|
-
/** Export to row objects */
|
|
194
|
-
toRows() {
|
|
195
|
-
return this._df?.toRows() ?? [];
|
|
196
|
-
}
|
|
197
|
-
/** Get shape */
|
|
198
|
-
get shape() {
|
|
199
|
-
return this._df?.shape ?? [0, 0];
|
|
200
|
-
}
|
|
201
|
-
/** Get column names */
|
|
202
|
-
get columns() {
|
|
203
|
-
return this._df?.columns ?? [];
|
|
204
|
-
}
|
|
205
|
-
/** Save current DataFrame to IndexedDB */
|
|
168
|
+
describe() { return this._df?.describe() ?? null; }
|
|
169
|
+
toCSV(delimiter) { return this._df?.toCSV(delimiter) ?? ''; }
|
|
170
|
+
toRows() { return this._df?.toRows() ?? []; }
|
|
171
|
+
get shape() { return this._df?.shape ?? [0, 0]; }
|
|
172
|
+
get columns() { return this._df?.columns ?? []; }
|
|
206
173
|
async save(key) {
|
|
207
174
|
if (!this._df)
|
|
208
175
|
return null;
|
|
@@ -221,10 +188,25 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
221
188
|
/* ═══════════════════════════════════════════════════
|
|
222
189
|
* INTERNAL
|
|
223
190
|
* ═══════════════════════════════════════════════════ */
|
|
224
|
-
_updateStatus(text) {
|
|
191
|
+
_updateStatus(text, type = 'empty') {
|
|
225
192
|
const el = document.getElementById(`${this._id}-status`);
|
|
226
|
-
if (el)
|
|
227
|
-
|
|
193
|
+
if (!el)
|
|
194
|
+
return;
|
|
195
|
+
el.className = 'jux-dataframe-status';
|
|
196
|
+
if (type)
|
|
197
|
+
el.classList.add(`jux-dataframe-status-${type}`);
|
|
198
|
+
el.innerHTML = '';
|
|
199
|
+
if (this._icon && type === 'success') {
|
|
200
|
+
const iconEl = renderIcon(this._icon);
|
|
201
|
+
iconEl.style.width = '16px';
|
|
202
|
+
iconEl.style.height = '16px';
|
|
203
|
+
iconEl.style.marginRight = '6px';
|
|
204
|
+
iconEl.style.verticalAlign = 'middle';
|
|
205
|
+
el.appendChild(iconEl);
|
|
206
|
+
}
|
|
207
|
+
const span = document.createElement('span');
|
|
208
|
+
span.textContent = text;
|
|
209
|
+
el.appendChild(span);
|
|
228
210
|
}
|
|
229
211
|
_setDataFrame(df, sourceName) {
|
|
230
212
|
this._df = df;
|
|
@@ -238,27 +220,21 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
238
220
|
this._df = df.select(...cleanCols);
|
|
239
221
|
}
|
|
240
222
|
this._updateTable();
|
|
241
|
-
this._updateStatus(
|
|
242
|
-
this._triggerCallback('load', this._df, null, this);
|
|
243
|
-
// ✅ Enable filter now that data exists (render it into DOM)
|
|
223
|
+
this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
|
|
244
224
|
if (this._tableOptions.filterable) {
|
|
245
225
|
this._showFilterInput();
|
|
246
226
|
}
|
|
227
|
+
this._triggerCallback('load', this._df, null, this);
|
|
247
228
|
}
|
|
248
229
|
_updateTable() {
|
|
249
230
|
if (!this._table || !this._df)
|
|
250
231
|
return;
|
|
251
232
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
252
233
|
}
|
|
253
|
-
update(prop, value) { }
|
|
254
|
-
/* ═══════════════════════════════════════════════════
|
|
255
|
-
* RENDER
|
|
256
|
-
* ═══════════════════════════════════════════════════ */
|
|
257
234
|
_showFilterInput() {
|
|
258
235
|
const wrapper = document.getElementById(this._id);
|
|
259
236
|
if (!wrapper)
|
|
260
237
|
return;
|
|
261
|
-
// Only add once
|
|
262
238
|
if (wrapper.querySelector('.jux-dataframe-filter'))
|
|
263
239
|
return;
|
|
264
240
|
const filterContainer = document.createElement('div');
|
|
@@ -275,14 +251,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
275
251
|
iconWrap.appendChild(iconEl);
|
|
276
252
|
filterContainer.appendChild(iconWrap);
|
|
277
253
|
filterContainer.appendChild(input);
|
|
278
|
-
// ✅ FIX: Find the table element (we know it exists because render() created it)
|
|
279
254
|
const tableElement = wrapper.querySelector('.jux-table-wrapper table');
|
|
280
255
|
if (tableElement && tableElement.parentElement) {
|
|
281
|
-
// Insert filter BEFORE the table element
|
|
282
256
|
tableElement.parentElement.insertBefore(filterContainer, tableElement);
|
|
283
257
|
}
|
|
284
258
|
else {
|
|
285
|
-
// Fallback: append to wrapper (shouldn't happen, but safe)
|
|
286
259
|
wrapper.appendChild(filterContainer);
|
|
287
260
|
}
|
|
288
261
|
input.addEventListener('input', () => {
|
|
@@ -299,6 +272,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
299
272
|
this._table?.rows(filtered.toRows());
|
|
300
273
|
});
|
|
301
274
|
}
|
|
275
|
+
update(prop, value) { }
|
|
276
|
+
/* ═══════════════════════════════════════════════════
|
|
277
|
+
* RENDER
|
|
278
|
+
* ═══════════════════════════════════════════════════ */
|
|
302
279
|
render(targetId) {
|
|
303
280
|
const container = this._setupContainer(targetId);
|
|
304
281
|
const { style, class: className } = this.state;
|
|
@@ -309,13 +286,15 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
309
286
|
wrapper.className += ` ${className}`;
|
|
310
287
|
if (style)
|
|
311
288
|
wrapper.setAttribute('style', style);
|
|
312
|
-
// Inline upload (if withUpload was called)
|
|
313
289
|
if (this._inlineUpload) {
|
|
314
|
-
const
|
|
290
|
+
const uploadOpts = {
|
|
315
291
|
label: this._inlineUpload.label,
|
|
316
|
-
accept: this._inlineUpload.accept
|
|
317
|
-
}
|
|
318
|
-
|
|
292
|
+
accept: this._inlineUpload.accept,
|
|
293
|
+
};
|
|
294
|
+
if (this._inlineUpload.icon) {
|
|
295
|
+
uploadOpts.icon = this._inlineUpload.icon;
|
|
296
|
+
}
|
|
297
|
+
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
319
298
|
this._uploadRef = upload;
|
|
320
299
|
this._pendingSource = async () => {
|
|
321
300
|
upload.bind('change', async (files) => {
|
|
@@ -323,7 +302,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
323
302
|
return;
|
|
324
303
|
const file = files[0];
|
|
325
304
|
this.state.loading = true;
|
|
326
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...');
|
|
305
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
327
306
|
try {
|
|
328
307
|
const df = await this._driver.streamFile(file);
|
|
329
308
|
await this._driver.store(file.name, df, { source: file.name });
|
|
@@ -332,11 +311,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
332
311
|
catch (err) {
|
|
333
312
|
this._triggerCallback('error', err.message, null, this);
|
|
334
313
|
this.state.loading = false;
|
|
335
|
-
this._updateStatus('❌ ' + err.message);
|
|
314
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
336
315
|
}
|
|
337
316
|
});
|
|
338
317
|
};
|
|
339
|
-
// Render upload into wrapper before status/table
|
|
340
318
|
const uploadContainer = document.createElement('div');
|
|
341
319
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
342
320
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
@@ -347,25 +325,23 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
347
325
|
else {
|
|
348
326
|
container.appendChild(wrapper);
|
|
349
327
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// ✅ Create internal table — this establishes the DOM structure
|
|
328
|
+
if (this._showStatus) {
|
|
329
|
+
const statusBar = document.createElement('div');
|
|
330
|
+
statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
|
|
331
|
+
statusBar.id = `${this._id}-status`;
|
|
332
|
+
statusBar.textContent = 'No data loaded.';
|
|
333
|
+
wrapper.appendChild(statusBar);
|
|
334
|
+
}
|
|
358
335
|
const tbl = new Table(`${this._id}-table`, {
|
|
359
336
|
striped: this._tableOptions.striped,
|
|
360
337
|
hoverable: this._tableOptions.hoverable,
|
|
361
338
|
sortable: this._tableOptions.sortable,
|
|
362
|
-
filterable: false,
|
|
339
|
+
filterable: false,
|
|
363
340
|
paginated: this._tableOptions.paginated,
|
|
364
341
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
365
342
|
});
|
|
366
|
-
tbl.render(wrapper);
|
|
343
|
+
tbl.render(wrapper);
|
|
367
344
|
this._table = tbl;
|
|
368
|
-
// Execute pending data source
|
|
369
345
|
if (this._pendingSource) {
|
|
370
346
|
const fn = this._pendingSource;
|
|
371
347
|
this._pendingSource = null;
|
|
@@ -17,6 +17,8 @@ export interface DataFrameOptions {
|
|
|
17
17
|
filterable?: boolean;
|
|
18
18
|
paginated?: boolean;
|
|
19
19
|
rowsPerPage?: number;
|
|
20
|
+
showStatus?: boolean;
|
|
21
|
+
icon?: string;
|
|
20
22
|
style?: string;
|
|
21
23
|
class?: string;
|
|
22
24
|
}
|
|
@@ -43,7 +45,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
43
45
|
private _uploadRef: FileUpload | null = null;
|
|
44
46
|
private _storageKey: string | null = null;
|
|
45
47
|
private _pendingSource: (() => Promise<void>) | null = null;
|
|
46
|
-
private _inlineUpload: { label: string; accept: string } | null = null;
|
|
48
|
+
private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
|
|
49
|
+
private _showStatus: boolean = true;
|
|
50
|
+
private _icon: string = '';
|
|
47
51
|
|
|
48
52
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
49
53
|
super(id, {
|
|
@@ -63,11 +67,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
63
67
|
options.storeName ?? 'frames'
|
|
64
68
|
);
|
|
65
69
|
|
|
70
|
+
this._showStatus = options.showStatus ?? true;
|
|
71
|
+
this._icon = options.icon ?? '';
|
|
72
|
+
|
|
66
73
|
this._tableOptions = {
|
|
67
74
|
striped: options.striped ?? true,
|
|
68
75
|
hoverable: options.hoverable ?? true,
|
|
69
76
|
sortable: options.sortable ?? true,
|
|
70
|
-
filterable: options.filterable ??
|
|
77
|
+
filterable: options.filterable ?? false,
|
|
71
78
|
paginated: options.paginated ?? true,
|
|
72
79
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
73
80
|
};
|
|
@@ -80,91 +87,85 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
80
87
|
* DATA SOURCES
|
|
81
88
|
* ═══════════════════════════════════════════════════ */
|
|
82
89
|
|
|
83
|
-
/**
|
|
84
|
-
* Load from IndexedDB by storage key
|
|
85
|
-
*/
|
|
86
90
|
fromStorage(key: string): this {
|
|
87
91
|
this._storageKey = key;
|
|
88
|
-
|
|
92
|
+
const loadFn = async () => {
|
|
89
93
|
this.state.loading = true;
|
|
94
|
+
this._updateStatus('⏳ Loading...', 'loading');
|
|
90
95
|
try {
|
|
91
|
-
|
|
96
|
+
const df = await this._driver.loadByName(key);
|
|
92
97
|
if (!df) {
|
|
93
98
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
94
99
|
this.state.loading = false;
|
|
100
|
+
this._updateStatus('No table found: ' + key, 'empty');
|
|
95
101
|
return;
|
|
96
102
|
}
|
|
97
103
|
this._setDataFrame(df, 'storage: ' + key);
|
|
98
104
|
} catch (err: any) {
|
|
99
105
|
this._triggerCallback('error', err.message, null, this);
|
|
100
106
|
this.state.loading = false;
|
|
107
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
101
108
|
}
|
|
102
109
|
};
|
|
110
|
+
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
103
111
|
return this;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
/**
|
|
107
|
-
* Load from a FileUpload component — auto-wires change event
|
|
108
|
-
*/
|
|
109
114
|
fromUpload(upload: FileUpload): this {
|
|
110
115
|
this._uploadRef = upload;
|
|
111
116
|
this._pendingSource = async () => {
|
|
112
|
-
// Wire upload's change event to parse incoming files
|
|
113
117
|
upload.bind('change', async (files: File[]) => {
|
|
114
118
|
if (!files || files.length === 0) return;
|
|
115
119
|
const file = files[0];
|
|
116
120
|
this.state.loading = true;
|
|
117
|
-
|
|
121
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
118
122
|
try {
|
|
119
123
|
const df = await this._driver.streamFile(file);
|
|
120
|
-
// Auto-persist to IndexedDB
|
|
121
124
|
await this._driver.store(file.name, df, { source: file.name });
|
|
122
|
-
this._setDataFrame(df,
|
|
125
|
+
this._setDataFrame(df, file.name);
|
|
123
126
|
} catch (err: any) {
|
|
124
127
|
this._triggerCallback('error', err.message, null, this);
|
|
125
128
|
this.state.loading = false;
|
|
129
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
126
130
|
}
|
|
127
131
|
});
|
|
128
132
|
};
|
|
129
133
|
return this;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
/**
|
|
133
|
-
* Load from raw data — array of objects or Record<string, any[]>
|
|
134
|
-
*/
|
|
135
136
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this {
|
|
136
|
-
|
|
137
|
+
const loadFn = async () => {
|
|
137
138
|
this.state.loading = true;
|
|
139
|
+
this._updateStatus('⏳ Loading data...', 'loading');
|
|
138
140
|
try {
|
|
139
141
|
const df = new DataFrame(data);
|
|
140
142
|
this._setDataFrame(df, 'inline data');
|
|
141
143
|
} catch (err: any) {
|
|
142
144
|
this._triggerCallback('error', err.message, null, this);
|
|
143
145
|
this.state.loading = false;
|
|
146
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
144
147
|
}
|
|
145
148
|
};
|
|
149
|
+
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
146
150
|
return this;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* Auto-wires parsing, storage, and display — zero callbacks needed.
|
|
152
|
-
*
|
|
153
|
-
* @param label - Button/label text (default: 'Upload File')
|
|
154
|
-
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
155
|
-
*/
|
|
156
|
-
withUpload(label: string = 'Upload File', accept: string = '.csv,.tsv,.txt,.xlsx,.xls'): this {
|
|
157
|
-
this._inlineUpload = { label, accept };
|
|
153
|
+
withUpload(label: string = 'Upload File', accept: string = '.csv,.tsv,.txt,.xlsx,.xls', icon: string = 'upload'): this {
|
|
154
|
+
this._inlineUpload = { label, accept, icon };
|
|
158
155
|
return this;
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
/* ═══════════════════════════════════════════════════
|
|
162
|
-
*
|
|
159
|
+
* UI TOGGLES
|
|
160
|
+
* ═══════════════════════════════════════════════════ */
|
|
161
|
+
|
|
162
|
+
showStatus(v: boolean): this { this._showStatus = v; return this; }
|
|
163
|
+
statusIcon(v: string): this { this._icon = v; return this; }
|
|
164
|
+
|
|
165
|
+
/* ═══════════════════════════════════════════════════
|
|
166
|
+
* TRANSFORM API
|
|
163
167
|
* ═══════════════════════════════════════════════════ */
|
|
164
168
|
|
|
165
|
-
/**
|
|
166
|
-
* Apply a transform to the current DataFrame and update the table
|
|
167
|
-
*/
|
|
168
169
|
apply(fn: (df: DataFrame) => DataFrame): this {
|
|
169
170
|
if (!this._df) return this;
|
|
170
171
|
const result = fn(this._df);
|
|
@@ -173,51 +174,30 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
173
174
|
return this;
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
/**
|
|
177
|
-
* Filter rows
|
|
178
|
-
*/
|
|
179
177
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
|
|
180
178
|
return this.apply(df => df.filter(predicate));
|
|
181
179
|
}
|
|
182
180
|
|
|
183
|
-
/**
|
|
184
|
-
* Select columns
|
|
185
|
-
*/
|
|
186
181
|
select(...cols: string[]): this {
|
|
187
182
|
return this.apply(df => df.select(...cols));
|
|
188
183
|
}
|
|
189
184
|
|
|
190
|
-
/**
|
|
191
|
-
* Sort by column
|
|
192
|
-
*/
|
|
193
185
|
sort(col: string, descending?: boolean): this {
|
|
194
186
|
return this.apply(df => df.sort(col, descending));
|
|
195
187
|
}
|
|
196
188
|
|
|
197
|
-
/**
|
|
198
|
-
* Show first N rows
|
|
199
|
-
*/
|
|
200
189
|
head(n: number = 5): this {
|
|
201
190
|
return this.apply(df => df.head(n));
|
|
202
191
|
}
|
|
203
192
|
|
|
204
|
-
/**
|
|
205
|
-
* Show last N rows
|
|
206
|
-
*/
|
|
207
193
|
tail(n: number = 5): this {
|
|
208
194
|
return this.apply(df => df.tail(n));
|
|
209
195
|
}
|
|
210
196
|
|
|
211
|
-
/**
|
|
212
|
-
* Add a computed column
|
|
213
|
-
*/
|
|
214
197
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
|
|
215
198
|
return this.apply(df => df.withColumn(name, fn));
|
|
216
199
|
}
|
|
217
200
|
|
|
218
|
-
/**
|
|
219
|
-
* Where clause
|
|
220
|
-
*/
|
|
221
201
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this {
|
|
222
202
|
return this.apply(df => df.where(col, op, value));
|
|
223
203
|
}
|
|
@@ -226,41 +206,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
226
206
|
* ACCESSORS
|
|
227
207
|
* ═══════════════════════════════════════════════════ */
|
|
228
208
|
|
|
229
|
-
/** Get the underlying DataFrame */
|
|
230
209
|
get df(): DataFrame | null { return this._df; }
|
|
231
|
-
|
|
232
|
-
/** Get the underlying TabularDriver */
|
|
233
210
|
get driver(): TabularDriver { return this._driver; }
|
|
234
|
-
|
|
235
|
-
/** Get the internal Table component */
|
|
236
211
|
get table(): Table | null { return this._table; }
|
|
212
|
+
describe(): Record<string, any> | null { return this._df?.describe() ?? null; }
|
|
213
|
+
toCSV(delimiter?: string): string { return this._df?.toCSV(delimiter) ?? ''; }
|
|
214
|
+
toRows(): Record<string, any>[] { return this._df?.toRows() ?? []; }
|
|
215
|
+
get shape(): [number, number] { return this._df?.shape ?? [0, 0]; }
|
|
216
|
+
get columns(): string[] { return this._df?.columns ?? []; }
|
|
237
217
|
|
|
238
|
-
/** Get describe() stats */
|
|
239
|
-
describe(): Record<string, any> | null {
|
|
240
|
-
return this._df?.describe() ?? null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Export to CSV string */
|
|
244
|
-
toCSV(delimiter?: string): string {
|
|
245
|
-
return this._df?.toCSV(delimiter) ?? '';
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Export to row objects */
|
|
249
|
-
toRows(): Record<string, any>[] {
|
|
250
|
-
return this._df?.toRows() ?? [];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Get shape */
|
|
254
|
-
get shape(): [number, number] {
|
|
255
|
-
return this._df?.shape ?? [0, 0];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Get column names */
|
|
259
|
-
get columns(): string[] {
|
|
260
|
-
return this._df?.columns ?? [];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Save current DataFrame to IndexedDB */
|
|
264
218
|
async save(key?: string): Promise<string | null> {
|
|
265
219
|
if (!this._df) return null;
|
|
266
220
|
const name = key ?? this._storageKey ?? this._id;
|
|
@@ -282,9 +236,27 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
282
236
|
* INTERNAL
|
|
283
237
|
* ═══════════════════════════════════════════════════ */
|
|
284
238
|
|
|
285
|
-
private _updateStatus(text: string): void {
|
|
239
|
+
private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' = 'empty'): void {
|
|
286
240
|
const el = document.getElementById(`${this._id}-status`);
|
|
287
|
-
if (el)
|
|
241
|
+
if (!el) return;
|
|
242
|
+
|
|
243
|
+
el.className = 'jux-dataframe-status';
|
|
244
|
+
if (type) el.classList.add(`jux-dataframe-status-${type}`);
|
|
245
|
+
|
|
246
|
+
el.innerHTML = '';
|
|
247
|
+
|
|
248
|
+
if (this._icon && type === 'success') {
|
|
249
|
+
const iconEl = renderIcon(this._icon);
|
|
250
|
+
iconEl.style.width = '16px';
|
|
251
|
+
iconEl.style.height = '16px';
|
|
252
|
+
iconEl.style.marginRight = '6px';
|
|
253
|
+
iconEl.style.verticalAlign = 'middle';
|
|
254
|
+
el.appendChild(iconEl);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const span = document.createElement('span');
|
|
258
|
+
span.textContent = text;
|
|
259
|
+
el.appendChild(span);
|
|
288
260
|
}
|
|
289
261
|
|
|
290
262
|
private _setDataFrame(df: DataFrame, sourceName: string): void {
|
|
@@ -301,13 +273,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
301
273
|
}
|
|
302
274
|
|
|
303
275
|
this._updateTable();
|
|
304
|
-
this._updateStatus(
|
|
305
|
-
|
|
276
|
+
this._updateStatus(
|
|
277
|
+
`${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
|
|
278
|
+
'success'
|
|
279
|
+
);
|
|
306
280
|
|
|
307
|
-
// ✅ Enable filter now that data exists (render it into DOM)
|
|
308
281
|
if (this._tableOptions.filterable) {
|
|
309
282
|
this._showFilterInput();
|
|
310
283
|
}
|
|
284
|
+
|
|
285
|
+
this._triggerCallback('load', this._df, null, this);
|
|
311
286
|
}
|
|
312
287
|
|
|
313
288
|
private _updateTable(): void {
|
|
@@ -315,17 +290,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
315
290
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
316
291
|
}
|
|
317
292
|
|
|
318
|
-
update(prop: string, value: any): void { }
|
|
319
|
-
|
|
320
|
-
/* ═══════════════════════════════════════════════════
|
|
321
|
-
* RENDER
|
|
322
|
-
* ═══════════════════════════════════════════════════ */
|
|
323
|
-
|
|
324
293
|
private _showFilterInput(): void {
|
|
325
294
|
const wrapper = document.getElementById(this._id);
|
|
326
295
|
if (!wrapper) return;
|
|
327
|
-
|
|
328
|
-
// Only add once
|
|
329
296
|
if (wrapper.querySelector('.jux-dataframe-filter')) return;
|
|
330
297
|
|
|
331
298
|
const filterContainer = document.createElement('div');
|
|
@@ -347,14 +314,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
347
314
|
filterContainer.appendChild(iconWrap);
|
|
348
315
|
filterContainer.appendChild(input);
|
|
349
316
|
|
|
350
|
-
// ✅ FIX: Find the table element (we know it exists because render() created it)
|
|
351
317
|
const tableElement = wrapper.querySelector('.jux-table-wrapper table');
|
|
352
|
-
|
|
353
318
|
if (tableElement && tableElement.parentElement) {
|
|
354
|
-
// Insert filter BEFORE the table element
|
|
355
319
|
tableElement.parentElement.insertBefore(filterContainer, tableElement);
|
|
356
320
|
} else {
|
|
357
|
-
// Fallback: append to wrapper (shouldn't happen, but safe)
|
|
358
321
|
wrapper.appendChild(filterContainer);
|
|
359
322
|
}
|
|
360
323
|
|
|
@@ -374,6 +337,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
374
337
|
});
|
|
375
338
|
}
|
|
376
339
|
|
|
340
|
+
update(prop: string, value: any): void { }
|
|
341
|
+
|
|
342
|
+
/* ═══════════════════════════════════════════════════
|
|
343
|
+
* RENDER
|
|
344
|
+
* ═══════════════════════════════════════════════════ */
|
|
345
|
+
|
|
377
346
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
378
347
|
const container = this._setupContainer(targetId);
|
|
379
348
|
const { style, class: className } = this.state;
|
|
@@ -384,22 +353,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
384
353
|
if (className) wrapper.className += ` ${className}`;
|
|
385
354
|
if (style) wrapper.setAttribute('style', style);
|
|
386
355
|
|
|
387
|
-
// Inline upload (if withUpload was called)
|
|
388
356
|
if (this._inlineUpload) {
|
|
389
|
-
const
|
|
357
|
+
const uploadOpts: any = {
|
|
390
358
|
label: this._inlineUpload.label,
|
|
391
|
-
accept: this._inlineUpload.accept
|
|
392
|
-
}
|
|
359
|
+
accept: this._inlineUpload.accept,
|
|
360
|
+
};
|
|
361
|
+
if (this._inlineUpload.icon) {
|
|
362
|
+
uploadOpts.icon = this._inlineUpload.icon;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
393
366
|
|
|
394
|
-
// Wire it as a fromUpload source
|
|
395
367
|
this._uploadRef = upload;
|
|
396
368
|
this._pendingSource = async () => {
|
|
397
369
|
upload.bind('change', async (files: File[]) => {
|
|
398
370
|
if (!files || files.length === 0) return;
|
|
399
371
|
const file = files[0];
|
|
400
372
|
this.state.loading = true;
|
|
401
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...');
|
|
402
|
-
|
|
373
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
403
374
|
try {
|
|
404
375
|
const df = await this._driver.streamFile(file);
|
|
405
376
|
await this._driver.store(file.name, df, { source: file.name });
|
|
@@ -407,12 +378,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
407
378
|
} catch (err: any) {
|
|
408
379
|
this._triggerCallback('error', err.message, null, this);
|
|
409
380
|
this.state.loading = false;
|
|
410
|
-
this._updateStatus('❌ ' + err.message);
|
|
381
|
+
this._updateStatus('❌ ' + err.message, 'error');
|
|
411
382
|
}
|
|
412
383
|
});
|
|
413
384
|
};
|
|
414
385
|
|
|
415
|
-
// Render upload into wrapper before status/table
|
|
416
386
|
const uploadContainer = document.createElement('div');
|
|
417
387
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
418
388
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
@@ -423,27 +393,25 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
423
393
|
container.appendChild(wrapper);
|
|
424
394
|
}
|
|
425
395
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
396
|
+
if (this._showStatus) {
|
|
397
|
+
const statusBar = document.createElement('div');
|
|
398
|
+
statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
|
|
399
|
+
statusBar.id = `${this._id}-status`;
|
|
400
|
+
statusBar.textContent = 'No data loaded.';
|
|
401
|
+
wrapper.appendChild(statusBar);
|
|
402
|
+
}
|
|
433
403
|
|
|
434
|
-
// ✅ Create internal table — this establishes the DOM structure
|
|
435
404
|
const tbl = new Table(`${this._id}-table`, {
|
|
436
405
|
striped: this._tableOptions.striped,
|
|
437
406
|
hoverable: this._tableOptions.hoverable,
|
|
438
407
|
sortable: this._tableOptions.sortable,
|
|
439
|
-
filterable: false,
|
|
408
|
+
filterable: false,
|
|
440
409
|
paginated: this._tableOptions.paginated,
|
|
441
410
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
442
411
|
});
|
|
443
|
-
tbl.render(wrapper);
|
|
412
|
+
tbl.render(wrapper);
|
|
444
413
|
this._table = tbl;
|
|
445
414
|
|
|
446
|
-
// Execute pending data source
|
|
447
415
|
if (this._pendingSource) {
|
|
448
416
|
const fn = this._pendingSource;
|
|
449
417
|
this._pendingSource = null;
|