jupyterlab_tabular_data_viewer_extension 1.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/widget.js ADDED
@@ -0,0 +1,644 @@
1
+ import { Widget } from '@lumino/widgets';
2
+ import { requestAPI } from './request';
3
+ /**
4
+ * Parquet viewer widget
5
+ */
6
+ export class TabularDataViewer extends Widget {
7
+ constructor(filePath, setLastContextMenuRow) {
8
+ super();
9
+ this._columns = [];
10
+ this._data = [];
11
+ this._totalRows = 0;
12
+ this._unfilteredTotalRows = 0;
13
+ this._currentOffset = 0;
14
+ this._limit = 500;
15
+ this._loading = false;
16
+ this._hasMore = true;
17
+ this._filters = {};
18
+ this._sortBy = null;
19
+ this._sortOrder = 'asc';
20
+ this._fileSize = 0;
21
+ this._caseInsensitive = false;
22
+ this._useRegex = false;
23
+ this._contextMenuOpen = false;
24
+ this._columnWidths = new Map();
25
+ this._resizing = null;
26
+ this._cleanupHighlight = null;
27
+ this._menuObserver = null;
28
+ /**
29
+ * Handle column resize drag
30
+ */
31
+ this._doResize = (e) => {
32
+ if (!this._resizing) {
33
+ return;
34
+ }
35
+ const deltaX = e.clientX - this._resizing.startX;
36
+ const newWidth = Math.max(80, this._resizing.startWidth + deltaX); // Minimum width of 80px
37
+ // Store the new width
38
+ this._columnWidths.set(this._resizing.columnName, newWidth);
39
+ // Apply the new width to the column header and filter cell only
40
+ // With table-layout: fixed, this automatically applies to all cells in the column
41
+ const columnIndex = this._columns.findIndex(col => col.name === this._resizing.columnName);
42
+ if (columnIndex !== -1) {
43
+ const headerCell = this._headerRow.children[columnIndex];
44
+ const filterCell = this._filterRow.children[columnIndex];
45
+ if (headerCell) {
46
+ headerCell.style.width = `${newWidth}px`;
47
+ }
48
+ if (filterCell) {
49
+ filterCell.style.width = `${newWidth}px`;
50
+ }
51
+ // Update table width to sum of all column widths
52
+ const totalWidth = Array.from(this._columnWidths.values()).reduce((sum, w) => sum + w, 0);
53
+ this._table.style.width = `${totalWidth}px`;
54
+ }
55
+ };
56
+ /**
57
+ * Stop column resize
58
+ */
59
+ this._stopResize = () => {
60
+ if (!this._resizing) {
61
+ return;
62
+ }
63
+ this._resizing = null;
64
+ // Remove global mouse event listeners
65
+ document.removeEventListener('mousemove', this._doResize);
66
+ document.removeEventListener('mouseup', this._stopResize);
67
+ // Restore user selection and cursor
68
+ document.body.style.userSelect = '';
69
+ document.body.style.cursor = '';
70
+ };
71
+ this._filePath = filePath;
72
+ this._setLastContextMenuRow = setLastContextMenuRow;
73
+ this.addClass('jp-TabularDataViewer');
74
+ // Create table container (scrollable)
75
+ this._tableContainer = document.createElement('div');
76
+ this._tableContainer.className = 'jp-TabularDataViewer-container';
77
+ // Create table
78
+ this._table = document.createElement('table');
79
+ this._table.className = 'jp-TabularDataViewer-table';
80
+ this._thead = document.createElement('thead');
81
+ this._thead.className = 'jp-TabularDataViewer-thead';
82
+ this._tbody = document.createElement('tbody');
83
+ this._tbody.className = 'jp-TabularDataViewer-tbody';
84
+ // Create filter row
85
+ this._filterRow = document.createElement('tr');
86
+ this._filterRow.className = 'jp-TabularDataViewer-filterRow';
87
+ // Create header row
88
+ this._headerRow = document.createElement('tr');
89
+ this._headerRow.className = 'jp-TabularDataViewer-headerRow';
90
+ this._thead.appendChild(this._filterRow);
91
+ this._thead.appendChild(this._headerRow);
92
+ this._table.appendChild(this._thead);
93
+ this._table.appendChild(this._tbody);
94
+ this._tableContainer.appendChild(this._table);
95
+ // Create status bar (outside scroll container)
96
+ this._statusBar = document.createElement('div');
97
+ this._statusBar.className = 'jp-TabularDataViewer-statusBar';
98
+ this._statusLeft = document.createElement('div');
99
+ this._statusLeft.className = 'jp-TabularDataViewer-statusLeft';
100
+ // Create middle section with case-insensitive checkbox
101
+ const statusMiddle = document.createElement('div');
102
+ statusMiddle.className = 'jp-TabularDataViewer-statusMiddle';
103
+ const checkboxLabel = document.createElement('label');
104
+ checkboxLabel.className = 'jp-TabularDataViewer-caseInsensitiveLabel';
105
+ this._caseInsensitiveCheckbox = document.createElement('input');
106
+ this._caseInsensitiveCheckbox.type = 'checkbox';
107
+ this._caseInsensitiveCheckbox.className = 'jp-TabularDataViewer-caseInsensitiveCheckbox';
108
+ this._caseInsensitiveCheckbox.checked = this._caseInsensitive;
109
+ this._caseInsensitiveCheckbox.addEventListener('change', () => {
110
+ this._caseInsensitive = this._caseInsensitiveCheckbox.checked;
111
+ this._loadData(true);
112
+ });
113
+ const checkboxText = document.createElement('span');
114
+ checkboxText.textContent = ' Case insensitive';
115
+ checkboxLabel.appendChild(this._caseInsensitiveCheckbox);
116
+ checkboxLabel.appendChild(checkboxText);
117
+ statusMiddle.appendChild(checkboxLabel);
118
+ // Create regex checkbox
119
+ const regexLabel = document.createElement('label');
120
+ regexLabel.className = 'jp-TabularDataViewer-regexLabel';
121
+ this._regexCheckbox = document.createElement('input');
122
+ this._regexCheckbox.type = 'checkbox';
123
+ this._regexCheckbox.className = 'jp-TabularDataViewer-regexCheckbox';
124
+ this._regexCheckbox.checked = this._useRegex;
125
+ this._regexCheckbox.addEventListener('change', () => {
126
+ this._useRegex = this._regexCheckbox.checked;
127
+ this._loadData(true);
128
+ });
129
+ const regexText = document.createElement('span');
130
+ regexText.textContent = ' Use regex';
131
+ regexLabel.appendChild(this._regexCheckbox);
132
+ regexLabel.appendChild(regexText);
133
+ statusMiddle.appendChild(regexLabel);
134
+ this._statusRight = document.createElement('div');
135
+ this._statusRight.className = 'jp-TabularDataViewer-statusRight';
136
+ this._statusBar.appendChild(this._statusLeft);
137
+ this._statusBar.appendChild(statusMiddle);
138
+ this._statusBar.appendChild(this._statusRight);
139
+ // Append table container and status bar directly to widget node
140
+ this.node.appendChild(this._tableContainer);
141
+ this.node.appendChild(this._statusBar);
142
+ // Set up scroll listener for progressive loading
143
+ this._tableContainer.addEventListener('scroll', () => {
144
+ this._onScroll();
145
+ });
146
+ // Remove context-active class when clicking anywhere or dismissing context menu
147
+ const removeHighlight = () => {
148
+ this._contextMenuOpen = false;
149
+ this._tbody.classList.remove('jp-TabularDataViewer-context-menu-open');
150
+ this._tbody.querySelectorAll('tr').forEach(r => {
151
+ r.classList.remove('jp-TabularDataViewer-row-context-active');
152
+ });
153
+ // Stop observing when highlight is removed
154
+ if (this._menuObserver) {
155
+ this._menuObserver.disconnect();
156
+ this._menuObserver = null;
157
+ }
158
+ };
159
+ // Store cleanup function for external access
160
+ this._cleanupHighlight = removeHighlight;
161
+ // Initialize
162
+ this._initialize();
163
+ }
164
+ /**
165
+ * Start observing for menu removal when context menu opens
166
+ */
167
+ _startMenuObserver() {
168
+ // Stop any existing observer
169
+ if (this._menuObserver) {
170
+ this._menuObserver.disconnect();
171
+ }
172
+ // Create observer to watch for menu removal from DOM
173
+ this._menuObserver = new MutationObserver((mutations) => {
174
+ for (const mutation of mutations) {
175
+ if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
176
+ // Check if any removed node is a Lumino menu
177
+ for (const node of Array.from(mutation.removedNodes)) {
178
+ if (node instanceof HTMLElement &&
179
+ (node.classList.contains('lm-Menu') || node.classList.contains('p-Menu'))) {
180
+ // Menu was removed, clear highlight
181
+ if (this._cleanupHighlight) {
182
+ this._cleanupHighlight();
183
+ }
184
+ return;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ });
190
+ // Observe document body for child removals
191
+ this._menuObserver.observe(document.body, {
192
+ childList: true,
193
+ subtree: false
194
+ });
195
+ }
196
+ /**
197
+ * Get cleanup function to clear highlight (for external use)
198
+ */
199
+ getCleanupHighlight() {
200
+ return this._cleanupHighlight || (() => { });
201
+ }
202
+ /**
203
+ * Initialize the viewer by loading metadata and initial data
204
+ */
205
+ async _initialize() {
206
+ try {
207
+ await this._loadMetadata();
208
+ await this._loadData(true);
209
+ }
210
+ catch (error) {
211
+ this._showError(`Failed to load file: ${error}`);
212
+ }
213
+ }
214
+ /**
215
+ * Load file metadata (columns, types, row count)
216
+ */
217
+ async _loadMetadata() {
218
+ const response = await requestAPI('metadata', {
219
+ method: 'POST',
220
+ body: JSON.stringify({ path: this._filePath })
221
+ });
222
+ this._columns = response.columns;
223
+ this._totalRows = response.totalRows;
224
+ this._unfilteredTotalRows = response.totalRows;
225
+ this._fileSize = response.fileSize || 0;
226
+ this._renderHeaders();
227
+ this._updateStatusBar();
228
+ }
229
+ /**
230
+ * Load data from server
231
+ */
232
+ async _loadData(reset = false) {
233
+ if (this._loading) {
234
+ return;
235
+ }
236
+ this._loading = true;
237
+ this._updateStatusBar('Loading...');
238
+ try {
239
+ if (reset) {
240
+ this._currentOffset = 0;
241
+ this._data = [];
242
+ this._tbody.innerHTML = '';
243
+ }
244
+ const response = await requestAPI('data', {
245
+ method: 'POST',
246
+ body: JSON.stringify({
247
+ path: this._filePath,
248
+ offset: this._currentOffset,
249
+ limit: this._limit,
250
+ filters: this._filters,
251
+ sortBy: this._sortBy,
252
+ sortOrder: this._sortOrder,
253
+ caseInsensitive: this._caseInsensitive,
254
+ useRegex: this._useRegex
255
+ })
256
+ });
257
+ this._data = this._data.concat(response.data);
258
+ this._hasMore = response.hasMore;
259
+ this._currentOffset += response.data.length;
260
+ if (reset) {
261
+ this._totalRows = response.totalRows;
262
+ }
263
+ this._renderData(response.data);
264
+ this._updateStatusBar();
265
+ }
266
+ catch (error) {
267
+ this._showError(`Failed to load data: ${error}`);
268
+ }
269
+ finally {
270
+ this._loading = false;
271
+ }
272
+ }
273
+ /**
274
+ * Render table headers with filter inputs
275
+ */
276
+ _renderHeaders() {
277
+ this._filterRow.innerHTML = '';
278
+ this._headerRow.innerHTML = '';
279
+ this._columns.forEach(col => {
280
+ // Create filter cell
281
+ const filterCell = document.createElement('th');
282
+ filterCell.className = 'jp-TabularDataViewer-filterCell';
283
+ const filterInput = document.createElement('input');
284
+ filterInput.type = 'text';
285
+ filterInput.className = 'jp-TabularDataViewer-filterInput';
286
+ filterInput.placeholder = this._getFilterPlaceholder(col.type);
287
+ filterInput.dataset.columnName = col.name;
288
+ filterInput.dataset.columnType = col.type;
289
+ filterInput.addEventListener('keyup', (e) => {
290
+ if (e.key === 'Enter') {
291
+ this._applyFilter(col.name, filterInput.value, col.type);
292
+ }
293
+ });
294
+ filterCell.appendChild(filterInput);
295
+ this._filterRow.appendChild(filterCell);
296
+ // Create header cell with column name and type
297
+ const headerCell = document.createElement('th');
298
+ headerCell.className = 'jp-TabularDataViewer-headerCell';
299
+ headerCell.style.cursor = 'pointer';
300
+ headerCell.dataset.columnName = col.name;
301
+ // Add click handler for sorting
302
+ headerCell.addEventListener('click', () => {
303
+ this._toggleSort(col.name);
304
+ });
305
+ const headerContent = document.createElement('div');
306
+ headerContent.className = 'jp-TabularDataViewer-headerContent';
307
+ const nameSpan = document.createElement('div');
308
+ nameSpan.className = 'jp-TabularDataViewer-columnName';
309
+ nameSpan.textContent = col.name;
310
+ const typeSpan = document.createElement('div');
311
+ typeSpan.className = 'jp-TabularDataViewer-columnType';
312
+ typeSpan.textContent = this._simplifyType(col.type);
313
+ const sortIndicator = document.createElement('span');
314
+ sortIndicator.className = 'jp-TabularDataViewer-sortIndicator';
315
+ sortIndicator.textContent = '';
316
+ headerContent.appendChild(nameSpan);
317
+ headerContent.appendChild(typeSpan);
318
+ headerCell.appendChild(headerContent);
319
+ headerCell.appendChild(sortIndicator);
320
+ // Add resize handle
321
+ const resizeHandle = document.createElement('div');
322
+ resizeHandle.className = 'jp-TabularDataViewer-resizeHandle';
323
+ resizeHandle.addEventListener('mousedown', (e) => {
324
+ e.preventDefault();
325
+ e.stopPropagation();
326
+ this._startResize(col.name, e.clientX, headerCell);
327
+ });
328
+ // Prevent click events from triggering sort
329
+ resizeHandle.addEventListener('click', (e) => {
330
+ e.preventDefault();
331
+ e.stopPropagation();
332
+ });
333
+ headerCell.appendChild(resizeHandle);
334
+ // Set width - use stored width or default to 200px
335
+ const columnWidth = this._columnWidths.get(col.name) || 200;
336
+ if (!this._columnWidths.has(col.name)) {
337
+ this._columnWidths.set(col.name, columnWidth);
338
+ }
339
+ headerCell.style.width = `${columnWidth}px`;
340
+ filterCell.style.width = `${columnWidth}px`;
341
+ this._headerRow.appendChild(headerCell);
342
+ });
343
+ // Set table width to sum of all column widths
344
+ const totalWidth = Array.from(this._columnWidths.values()).reduce((sum, w) => sum + w, 0);
345
+ this._table.style.width = `${totalWidth}px`;
346
+ }
347
+ /**
348
+ * Render data rows
349
+ */
350
+ _renderData(rows) {
351
+ rows.forEach(row => {
352
+ const tr = document.createElement('tr');
353
+ tr.className = 'jp-TabularDataViewer-row';
354
+ this._columns.forEach(col => {
355
+ const td = document.createElement('td');
356
+ td.className = 'jp-TabularDataViewer-cell';
357
+ const value = row[col.name];
358
+ td.textContent = value !== null && value !== undefined ? String(value) : '';
359
+ tr.appendChild(td);
360
+ });
361
+ // Remove context-active class when hovering over any row (only if context menu not open)
362
+ tr.addEventListener('mouseenter', () => {
363
+ // Don't clear highlight while context menu is open
364
+ if (!this._contextMenuOpen) {
365
+ this._tbody.querySelectorAll('tr').forEach(r => {
366
+ r.classList.remove('jp-TabularDataViewer-row-context-active');
367
+ });
368
+ }
369
+ });
370
+ // Add right-click handler to store row data and maintain hover styling
371
+ tr.addEventListener('contextmenu', (e) => {
372
+ // Mark context menu as open
373
+ this._contextMenuOpen = true;
374
+ // Add class to tbody to disable hover on other rows
375
+ this._tbody.classList.add('jp-TabularDataViewer-context-menu-open');
376
+ // Remove context-active class from all rows
377
+ this._tbody.querySelectorAll('tr').forEach(r => {
378
+ r.classList.remove('jp-TabularDataViewer-row-context-active');
379
+ });
380
+ // Add context-active class to keep hover styling visible
381
+ tr.classList.add('jp-TabularDataViewer-row-context-active');
382
+ // Store row data for context menu
383
+ this._setLastContextMenuRow(row);
384
+ // Start observing for menu removal
385
+ this._startMenuObserver();
386
+ });
387
+ this._tbody.appendChild(tr);
388
+ });
389
+ }
390
+ /**
391
+ * Start column resize
392
+ */
393
+ _startResize(columnName, startX, headerCell) {
394
+ this._resizing = {
395
+ columnName,
396
+ startX,
397
+ startWidth: headerCell.offsetWidth
398
+ };
399
+ // Add global mouse event listeners
400
+ document.addEventListener('mousemove', this._doResize);
401
+ document.addEventListener('mouseup', this._stopResize);
402
+ // Prevent text selection during resize
403
+ document.body.style.userSelect = 'none';
404
+ document.body.style.cursor = 'col-resize';
405
+ }
406
+ /**
407
+ * Apply filter to a column
408
+ */
409
+ _applyFilter(columnName, value, columnType) {
410
+ if (!value.trim()) {
411
+ // Remove filter if empty
412
+ delete this._filters[columnName];
413
+ }
414
+ else {
415
+ const isNumeric = this._isNumericType(columnType);
416
+ if (isNumeric) {
417
+ // Parse numerical filter with operator
418
+ const match = value.match(/^([><=]+)?\s*(.+)$/);
419
+ if (match) {
420
+ const operator = match[1] || '=';
421
+ const numValue = match[2].trim();
422
+ this._filters[columnName] = {
423
+ type: 'number',
424
+ value: numValue,
425
+ operator: operator
426
+ };
427
+ }
428
+ }
429
+ else {
430
+ // Text filter
431
+ this._filters[columnName] = {
432
+ type: 'text',
433
+ value: value
434
+ };
435
+ }
436
+ }
437
+ // Reload data with filters
438
+ this._loadData(true);
439
+ }
440
+ /**
441
+ * Simplify type names for display
442
+ */
443
+ _simplifyType(type) {
444
+ const lowerType = type.toLowerCase();
445
+ // Date types
446
+ if (lowerType.includes('date32') || lowerType.includes('date64')) {
447
+ return 'date';
448
+ }
449
+ // Timestamp/datetime types
450
+ if (lowerType.includes('timestamp')) {
451
+ return 'datetime';
452
+ }
453
+ // Integer types
454
+ if (lowerType.match(/^u?int(8|16|32|64)$/)) {
455
+ return 'int';
456
+ }
457
+ // Float types
458
+ if (lowerType.match(/^(float|double)(16|32|64)?$/)) {
459
+ return 'float';
460
+ }
461
+ // Decimal types
462
+ if (lowerType.includes('decimal')) {
463
+ return 'decimal';
464
+ }
465
+ // Boolean
466
+ if (lowerType === 'bool') {
467
+ return 'boolean';
468
+ }
469
+ // String types
470
+ if (lowerType === 'string' || lowerType === 'utf8' || lowerType === 'large_string' || lowerType === 'large_utf8') {
471
+ return 'string';
472
+ }
473
+ // Binary types
474
+ if (lowerType === 'binary' || lowerType === 'large_binary') {
475
+ return 'binary';
476
+ }
477
+ // List types
478
+ if (lowerType.startsWith('list')) {
479
+ return 'list';
480
+ }
481
+ // Struct types
482
+ if (lowerType.startsWith('struct')) {
483
+ return 'struct';
484
+ }
485
+ // Default: return as-is
486
+ return type;
487
+ }
488
+ /**
489
+ * Check if column type is numeric
490
+ */
491
+ _isNumericType(type) {
492
+ const numericTypes = ['int', 'float', 'double', 'decimal', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'];
493
+ return numericTypes.some(t => type.toLowerCase().includes(t));
494
+ }
495
+ /**
496
+ * Get filter placeholder based on column type
497
+ */
498
+ _getFilterPlaceholder(type) {
499
+ if (this._isNumericType(type)) {
500
+ return '=, >, <, >=, <=';
501
+ }
502
+ return 'text or regex...';
503
+ }
504
+ /**
505
+ * Handle scroll event for progressive loading
506
+ */
507
+ _onScroll() {
508
+ const container = this._tableContainer;
509
+ const scrollPosition = container.scrollTop + container.clientHeight;
510
+ const scrollHeight = container.scrollHeight;
511
+ // Load more when scrolled to within 200px of bottom
512
+ if (scrollPosition >= scrollHeight - 200 && this._hasMore && !this._loading) {
513
+ this._loadData(false);
514
+ }
515
+ }
516
+ /**
517
+ * Toggle sort on a column
518
+ */
519
+ _toggleSort(columnName) {
520
+ if (this._sortBy === columnName) {
521
+ // Cycle through: asc -> desc -> off
522
+ if (this._sortOrder === 'asc') {
523
+ this._sortOrder = 'desc';
524
+ }
525
+ else if (this._sortOrder === 'desc') {
526
+ // Turn off sorting
527
+ this._sortBy = null;
528
+ this._sortOrder = 'asc';
529
+ }
530
+ }
531
+ else {
532
+ // Sort by new column (ascending)
533
+ this._sortBy = columnName;
534
+ this._sortOrder = 'asc';
535
+ }
536
+ this._updateSortIndicators();
537
+ this._loadData(true);
538
+ }
539
+ /**
540
+ * Update sort indicators in column headers
541
+ */
542
+ _updateSortIndicators() {
543
+ const headers = this._headerRow.querySelectorAll('th');
544
+ headers.forEach(header => {
545
+ const columnName = header.dataset.columnName;
546
+ const indicator = header.querySelector('.jp-TabularDataViewer-sortIndicator');
547
+ if (columnName === this._sortBy) {
548
+ indicator.textContent = this._sortOrder === 'asc' ? ' ▲' : ' ▼';
549
+ }
550
+ else {
551
+ indicator.textContent = '';
552
+ }
553
+ });
554
+ }
555
+ /**
556
+ * Clear all filters
557
+ */
558
+ _clearFilters() {
559
+ this._filters = {};
560
+ // Clear filter inputs
561
+ const filterInputs = this._filterRow.querySelectorAll('input');
562
+ filterInputs.forEach(input => {
563
+ input.value = '';
564
+ });
565
+ this._loadData(true);
566
+ }
567
+ /**
568
+ * Format file size to human-readable string
569
+ */
570
+ _formatFileSize(bytes) {
571
+ if (bytes === 0)
572
+ return '0 B';
573
+ const k = 1024;
574
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
575
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
576
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
577
+ }
578
+ /**
579
+ * Update status bar
580
+ */
581
+ _updateStatusBar(message) {
582
+ if (message) {
583
+ this._statusLeft.textContent = '';
584
+ this._statusRight.textContent = message;
585
+ }
586
+ else {
587
+ // Left side: file stats (always show unfiltered total)
588
+ const numColumns = this._columns.length;
589
+ const fileSize = this._formatFileSize(this._fileSize);
590
+ this._statusLeft.textContent = `${numColumns} columns • ${this._unfilteredTotalRows} rows • ${fileSize}`;
591
+ // Right side: showing info and clear filters link
592
+ const filterCount = Object.keys(this._filters).length;
593
+ let rightText = `Showing ${this._data.length} of ${this._totalRows} rows`;
594
+ if (filterCount > 0) {
595
+ rightText += ` (${filterCount} filter${filterCount > 1 ? 's' : ''} active)`;
596
+ }
597
+ this._statusRight.innerHTML = rightText;
598
+ if (filterCount > 0) {
599
+ const clearLink = document.createElement('a');
600
+ clearLink.href = '#';
601
+ clearLink.className = 'jp-TabularDataViewer-clearFilters';
602
+ clearLink.textContent = 'Clear filters';
603
+ clearLink.addEventListener('click', (e) => {
604
+ e.preventDefault();
605
+ this._clearFilters();
606
+ });
607
+ this._statusRight.appendChild(document.createTextNode(' • '));
608
+ this._statusRight.appendChild(clearLink);
609
+ }
610
+ }
611
+ }
612
+ /**
613
+ * Show error message
614
+ */
615
+ _showError(message) {
616
+ this._tbody.innerHTML = '';
617
+ const tr = document.createElement('tr');
618
+ const td = document.createElement('td');
619
+ td.colSpan = this._columns.length || 1;
620
+ td.className = 'jp-TabularDataViewer-error';
621
+ td.textContent = message;
622
+ tr.appendChild(td);
623
+ this._tbody.appendChild(tr);
624
+ }
625
+ /**
626
+ * Dispose of the widget
627
+ */
628
+ dispose() {
629
+ this._tableContainer.removeEventListener('scroll', this._onScroll);
630
+ // Clean up menu observer
631
+ if (this._menuObserver) {
632
+ this._menuObserver.disconnect();
633
+ this._menuObserver = null;
634
+ }
635
+ // Clean up resize event listeners if still active
636
+ if (this._resizing) {
637
+ document.removeEventListener('mousemove', this._doResize);
638
+ document.removeEventListener('mouseup', this._stopResize);
639
+ document.body.style.userSelect = '';
640
+ document.body.style.cursor = '';
641
+ }
642
+ super.dispose();
643
+ }
644
+ }