jupyterlab_tabular_data_viewer_extension 1.5.14 → 1.6.10
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/README.md +3 -4
- package/lib/index.js +2 -2
- package/lib/modal.d.ts +2 -1
- package/lib/modal.js +15 -2
- package/lib/request.d.ts +5 -2
- package/lib/request.js +22 -11
- package/lib/widget.d.ts +19 -1
- package/lib/widget.js +116 -18
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/modal.ts +25 -2
- package/src/request.ts +24 -11
- package/src/widget.ts +137 -19
- package/style/base.css +66 -8
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ View and browse Parquet, Excel, CSV, and TSV files directly in JupyterLab. Doubl
|
|
|
31
31
|
|
|
32
32
|

|
|
33
33
|
|
|
34
|
-
**
|
|
34
|
+
**Export:** Click the **Export** link in the status bar (or right-click on the viewer) to export the current view in your choice of format - original, Excel (.xlsx), CSV, Parquet (.parquet), or JSONL (.jsonl). When filters are active, the export popup notes that only filtered rows will be exported.
|
|
35
35
|
|
|
36
36
|

|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ View and browse Parquet, Excel, CSV, and TSV files directly in JupyterLab. Doubl
|
|
|
40
40
|
**Supported File Formats:**
|
|
41
41
|
|
|
42
42
|
- **Parquet files** (.parquet) - Full support with efficient columnar data reading
|
|
43
|
-
- **Excel files** (.xlsx) -
|
|
43
|
+
- **Excel files** (.xlsx) - Multi-sheet support: a sheet bar appears at the bottom for workbooks with more than one sheet, and switching sheets resets all filters/sort/selection (each sheet behaves like a separate file). Mixed-type columns (e.g. integers and strings in the same column) are handled via per-column cascading type inference rather than failing to open. Excel files must still be simple tabular data without merged cells, complex formulas, or advanced formatting
|
|
44
44
|
- **CSV files** (.csv) - Comma-separated values with UTF-8 encoding (fallback to latin1)
|
|
45
45
|
- **TSV files** (.tsv) - Tab-separated values with UTF-8 encoding (fallback to latin1)
|
|
46
46
|
|
|
@@ -69,7 +69,7 @@ View and browse Parquet, Excel, CSV, and TSV files directly in JupyterLab. Doubl
|
|
|
69
69
|
**Additional features:**
|
|
70
70
|
|
|
71
71
|
- Column statistics modal - View comprehensive statistics including data type, row counts, null values, unique counts, and type-specific metrics (numeric: min/max/mean/median/std dev/outliers; string: most common value/length stats; date: earliest/latest dates). Includes scrollable list of unique values sorted by frequency with counts and percentages. Copy statistics as JSON with one click
|
|
72
|
-
-
|
|
72
|
+
- Export - **Export** link in the status bar (or right-click on the viewer) opens a format picker: original, Excel (.xlsx), CSV, Parquet (.parquet), or JSONL (.jsonl). Exports preserve active filters and sort order. Filename includes the slugified sheet name for multi-sheet Excel and a `_filtered` suffix when filters are applied
|
|
73
73
|
- Right-click context menu on rows to copy data as JSON
|
|
74
74
|
- Refresh view - Right-click on viewer and select "Refresh View" to reload data from file while preserving scroll position, filters, and sorting
|
|
75
75
|
- Cell text truncation - Configurable maximum character limit for cell display (default: 100 characters). Text longer than limit shows "..." ellipsis. Set to 0 for unlimited display
|
|
@@ -107,4 +107,3 @@ Configure extension behavior through JupyterLab Settings:
|
|
|
107
107
|
- **Maximum Unique Values** - Default: 100. Maximum number of unique values to display in filter dialog and column statistics. Set to 0 for no limit
|
|
108
108
|
|
|
109
109
|
When a file type is disabled, files open with JupyterLab's default handler instead.
|
|
110
|
-
|
package/lib/index.js
CHANGED
|
@@ -101,8 +101,8 @@ const plugin = {
|
|
|
101
101
|
// Download command
|
|
102
102
|
const downloadCommand = 'tabular-data-viewer:download';
|
|
103
103
|
commands.addCommand(downloadCommand, {
|
|
104
|
-
label: '
|
|
105
|
-
caption: '
|
|
104
|
+
label: 'Export',
|
|
105
|
+
caption: 'Export the current view in various formats',
|
|
106
106
|
isEnabled: () => {
|
|
107
107
|
return activeWidget !== null;
|
|
108
108
|
},
|
package/lib/modal.d.ts
CHANGED
|
@@ -83,7 +83,8 @@ export declare class FilterModal extends Widget {
|
|
|
83
83
|
*/
|
|
84
84
|
export declare class DownloadModal extends Widget {
|
|
85
85
|
private _onDownload;
|
|
86
|
-
|
|
86
|
+
private _hasFilters;
|
|
87
|
+
constructor(onDownload: (format: string) => void, hasFilters?: boolean);
|
|
87
88
|
/**
|
|
88
89
|
* Render the modal content
|
|
89
90
|
*/
|
package/lib/modal.js
CHANGED
|
@@ -416,9 +416,10 @@ export class FilterModal extends Widget {
|
|
|
416
416
|
* Modal dialog widget for selecting download format
|
|
417
417
|
*/
|
|
418
418
|
export class DownloadModal extends Widget {
|
|
419
|
-
constructor(onDownload) {
|
|
419
|
+
constructor(onDownload, hasFilters = false) {
|
|
420
420
|
super();
|
|
421
421
|
this._onDownload = onDownload;
|
|
422
|
+
this._hasFilters = hasFilters;
|
|
422
423
|
this.addClass('jp-FilterModal');
|
|
423
424
|
this._render();
|
|
424
425
|
this._setupEventListeners();
|
|
@@ -433,7 +434,7 @@ export class DownloadModal extends Widget {
|
|
|
433
434
|
const header = document.createElement('div');
|
|
434
435
|
header.className = 'jp-FilterModal-header';
|
|
435
436
|
const title = document.createElement('h3');
|
|
436
|
-
title.textContent = '
|
|
437
|
+
title.textContent = 'Export';
|
|
437
438
|
const closeBtn = document.createElement('button');
|
|
438
439
|
closeBtn.className = 'jp-FilterModal-close';
|
|
439
440
|
closeBtn.textContent = '×';
|
|
@@ -441,6 +442,14 @@ export class DownloadModal extends Widget {
|
|
|
441
442
|
header.appendChild(title);
|
|
442
443
|
header.appendChild(closeBtn);
|
|
443
444
|
content.appendChild(header);
|
|
445
|
+
// Filter notice (only when filters are active)
|
|
446
|
+
if (this._hasFilters) {
|
|
447
|
+
const notice = document.createElement('div');
|
|
448
|
+
notice.className = 'jp-FilterModal-notice';
|
|
449
|
+
notice.textContent =
|
|
450
|
+
'Filters are active - export will include only the filtered rows.';
|
|
451
|
+
content.appendChild(notice);
|
|
452
|
+
}
|
|
444
453
|
// Format buttons
|
|
445
454
|
const buttonsDiv = document.createElement('div');
|
|
446
455
|
buttonsDiv.className = 'jp-FilterModal-buttons';
|
|
@@ -450,6 +459,10 @@ export class DownloadModal extends Widget {
|
|
|
450
459
|
buttonsDiv.appendChild(excelBtn);
|
|
451
460
|
const csvBtn = this._createFormatButton('Download as CSV', 'csv');
|
|
452
461
|
buttonsDiv.appendChild(csvBtn);
|
|
462
|
+
const parquetBtn = this._createFormatButton('Download as Parquet (.parquet)', 'parquet');
|
|
463
|
+
buttonsDiv.appendChild(parquetBtn);
|
|
464
|
+
const jsonlBtn = this._createFormatButton('Download as JSONL (.jsonl)', 'jsonl');
|
|
465
|
+
buttonsDiv.appendChild(jsonlBtn);
|
|
453
466
|
content.appendChild(buttonsDiv);
|
|
454
467
|
// Cancel button
|
|
455
468
|
const cancelDiv = document.createElement('div');
|
package/lib/request.d.ts
CHANGED
|
@@ -42,9 +42,10 @@ export interface IColumnStats {
|
|
|
42
42
|
*
|
|
43
43
|
* @param filePath Path to the data file
|
|
44
44
|
* @param columnName Name of column to analyze
|
|
45
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
45
46
|
* @returns Column statistics
|
|
46
47
|
*/
|
|
47
|
-
export declare function fetchColumnStats(filePath: string, columnName: string): Promise<IColumnStats>;
|
|
48
|
+
export declare function fetchColumnStats(filePath: string, columnName: string, sheet?: string | null): Promise<IColumnStats>;
|
|
48
49
|
/**
|
|
49
50
|
* Unique values interface
|
|
50
51
|
*/
|
|
@@ -59,6 +60,8 @@ export interface IUniqueValues {
|
|
|
59
60
|
*
|
|
60
61
|
* @param filePath Path to the data file
|
|
61
62
|
* @param columnName Name of column to get unique values for
|
|
63
|
+
* @param limit Maximum number of unique values to return
|
|
64
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
62
65
|
* @returns Unique values
|
|
63
66
|
*/
|
|
64
|
-
export declare function fetchUniqueValues(filePath: string, columnName: string, limit?: number): Promise<IUniqueValues>;
|
|
67
|
+
export declare function fetchUniqueValues(filePath: string, columnName: string, limit?: number, sheet?: string | null): Promise<IUniqueValues>;
|
package/lib/request.js
CHANGED
|
@@ -38,15 +38,20 @@ export async function requestAPI(endPoint = '', init = {}) {
|
|
|
38
38
|
*
|
|
39
39
|
* @param filePath Path to the data file
|
|
40
40
|
* @param columnName Name of column to analyze
|
|
41
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
41
42
|
* @returns Column statistics
|
|
42
43
|
*/
|
|
43
|
-
export async function fetchColumnStats(filePath, columnName) {
|
|
44
|
+
export async function fetchColumnStats(filePath, columnName, sheet) {
|
|
45
|
+
const body = {
|
|
46
|
+
path: filePath,
|
|
47
|
+
columnName: columnName
|
|
48
|
+
};
|
|
49
|
+
if (sheet) {
|
|
50
|
+
body.sheet = sheet;
|
|
51
|
+
}
|
|
44
52
|
return requestAPI('column-stats', {
|
|
45
53
|
method: 'POST',
|
|
46
|
-
body: JSON.stringify(
|
|
47
|
-
path: filePath,
|
|
48
|
-
columnName: columnName
|
|
49
|
-
})
|
|
54
|
+
body: JSON.stringify(body)
|
|
50
55
|
});
|
|
51
56
|
}
|
|
52
57
|
/**
|
|
@@ -54,15 +59,21 @@ export async function fetchColumnStats(filePath, columnName) {
|
|
|
54
59
|
*
|
|
55
60
|
* @param filePath Path to the data file
|
|
56
61
|
* @param columnName Name of column to get unique values for
|
|
62
|
+
* @param limit Maximum number of unique values to return
|
|
63
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
57
64
|
* @returns Unique values
|
|
58
65
|
*/
|
|
59
|
-
export async function fetchUniqueValues(filePath, columnName, limit = 100) {
|
|
66
|
+
export async function fetchUniqueValues(filePath, columnName, limit = 100, sheet) {
|
|
67
|
+
const body = {
|
|
68
|
+
path: filePath,
|
|
69
|
+
columnName: columnName,
|
|
70
|
+
limit: limit
|
|
71
|
+
};
|
|
72
|
+
if (sheet) {
|
|
73
|
+
body.sheet = sheet;
|
|
74
|
+
}
|
|
60
75
|
return requestAPI('unique-values', {
|
|
61
76
|
method: 'POST',
|
|
62
|
-
body: JSON.stringify(
|
|
63
|
-
path: filePath,
|
|
64
|
-
columnName: columnName,
|
|
65
|
-
limit: limit
|
|
66
|
-
})
|
|
77
|
+
body: JSON.stringify(body)
|
|
67
78
|
});
|
|
68
79
|
}
|
package/lib/widget.d.ts
CHANGED
|
@@ -38,6 +38,9 @@ export declare class TabularDataViewer extends Widget {
|
|
|
38
38
|
private _maxCellCharacters;
|
|
39
39
|
private _maxUniqueValues;
|
|
40
40
|
private _selectedRow;
|
|
41
|
+
private _sheets;
|
|
42
|
+
private _activeSheet;
|
|
43
|
+
private _sheetBar;
|
|
41
44
|
constructor(filePath: string, setLastContextMenuRow: (row: any) => void, maxCellCharacters?: number, maxUniqueValues?: number);
|
|
42
45
|
/**
|
|
43
46
|
* Start observing for menu removal when context menu opens
|
|
@@ -61,7 +64,22 @@ export declare class TabularDataViewer extends Widget {
|
|
|
61
64
|
*/
|
|
62
65
|
private _initialize;
|
|
63
66
|
/**
|
|
64
|
-
*
|
|
67
|
+
* Render the sheet bar. Visible only when the file has more than one sheet.
|
|
68
|
+
* Each sheet is a button; the active one carries `jp-mod-active`.
|
|
69
|
+
*/
|
|
70
|
+
private _renderSheetBar;
|
|
71
|
+
/**
|
|
72
|
+
* Switch to a different sheet. Resets all user state and reloads metadata
|
|
73
|
+
* + data so the experience matches "opening a new file".
|
|
74
|
+
*/
|
|
75
|
+
private _switchSheet;
|
|
76
|
+
/**
|
|
77
|
+
* Reset all user-applied state (filters, sort, selection, scroll position,
|
|
78
|
+
* column widths, paging cursor). Used when switching sheets.
|
|
79
|
+
*/
|
|
80
|
+
private _resetState;
|
|
81
|
+
/**
|
|
82
|
+
* Load file metadata (columns, types, row count, sheets)
|
|
65
83
|
*/
|
|
66
84
|
private _loadMetadata;
|
|
67
85
|
/**
|
package/lib/widget.js
CHANGED
|
@@ -31,6 +31,8 @@ export class TabularDataViewer extends Widget {
|
|
|
31
31
|
this._maxCellCharacters = 100;
|
|
32
32
|
this._maxUniqueValues = 100;
|
|
33
33
|
this._selectedRow = null;
|
|
34
|
+
this._sheets = [];
|
|
35
|
+
this._activeSheet = null;
|
|
34
36
|
/**
|
|
35
37
|
* Handle column resize drag
|
|
36
38
|
*/
|
|
@@ -147,8 +149,13 @@ export class TabularDataViewer extends Widget {
|
|
|
147
149
|
this._statusBar.appendChild(this._statusLeft);
|
|
148
150
|
this._statusBar.appendChild(statusMiddle);
|
|
149
151
|
this._statusBar.appendChild(this._statusRight);
|
|
150
|
-
//
|
|
152
|
+
// Sheet bar (only shown when multi-sheet Excel; sits above status bar)
|
|
153
|
+
this._sheetBar = document.createElement('div');
|
|
154
|
+
this._sheetBar.className = 'jp-TabularDataViewer-sheetBar';
|
|
155
|
+
this._sheetBar.style.display = 'none';
|
|
156
|
+
// Append table container, sheet bar, status bar directly to widget node
|
|
151
157
|
this.node.appendChild(this._tableContainer);
|
|
158
|
+
this.node.appendChild(this._sheetBar);
|
|
152
159
|
this.node.appendChild(this._statusBar);
|
|
153
160
|
// Set up scroll listener for progressive loading
|
|
154
161
|
this._tableContainer.addEventListener('scroll', () => {
|
|
@@ -239,7 +246,8 @@ export class TabularDataViewer extends Widget {
|
|
|
239
246
|
* Show download modal dialog
|
|
240
247
|
*/
|
|
241
248
|
showDownloadModal() {
|
|
242
|
-
const
|
|
249
|
+
const hasFilters = Object.keys(this._filters).length > 0;
|
|
250
|
+
const modal = new DownloadModal((format) => this.downloadFilteredData(format), hasFilters);
|
|
243
251
|
modal.show();
|
|
244
252
|
}
|
|
245
253
|
/**
|
|
@@ -255,17 +263,86 @@ export class TabularDataViewer extends Widget {
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
/**
|
|
258
|
-
*
|
|
266
|
+
* Render the sheet bar. Visible only when the file has more than one sheet.
|
|
267
|
+
* Each sheet is a button; the active one carries `jp-mod-active`.
|
|
268
|
+
*/
|
|
269
|
+
_renderSheetBar() {
|
|
270
|
+
this._sheetBar.innerHTML = '';
|
|
271
|
+
if (this._sheets.length <= 1) {
|
|
272
|
+
this._sheetBar.style.display = 'none';
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this._sheetBar.style.display = '';
|
|
276
|
+
for (const name of this._sheets) {
|
|
277
|
+
const tab = document.createElement('button');
|
|
278
|
+
tab.className = 'jp-TabularDataViewer-sheetTab';
|
|
279
|
+
if (name === this._activeSheet) {
|
|
280
|
+
tab.classList.add('jp-mod-active');
|
|
281
|
+
}
|
|
282
|
+
tab.textContent = name;
|
|
283
|
+
tab.title = name;
|
|
284
|
+
tab.addEventListener('click', () => this._switchSheet(name));
|
|
285
|
+
this._sheetBar.appendChild(tab);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Switch to a different sheet. Resets all user state and reloads metadata
|
|
290
|
+
* + data so the experience matches "opening a new file".
|
|
291
|
+
*/
|
|
292
|
+
_switchSheet(name) {
|
|
293
|
+
if (name === this._activeSheet) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this._activeSheet = name;
|
|
297
|
+
this._resetState();
|
|
298
|
+
this._renderSheetBar();
|
|
299
|
+
this._initialize();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Reset all user-applied state (filters, sort, selection, scroll position,
|
|
303
|
+
* column widths, paging cursor). Used when switching sheets.
|
|
304
|
+
*/
|
|
305
|
+
_resetState() {
|
|
306
|
+
this._filters = {};
|
|
307
|
+
this._sortBy = null;
|
|
308
|
+
this._sortOrder = 'asc';
|
|
309
|
+
this._caseInsensitive = false;
|
|
310
|
+
this._useRegex = false;
|
|
311
|
+
this._caseInsensitiveCheckbox.checked = false;
|
|
312
|
+
this._regexCheckbox.checked = false;
|
|
313
|
+
this._selectedRow = null;
|
|
314
|
+
this._currentOffset = 0;
|
|
315
|
+
this._data = [];
|
|
316
|
+
this._hasMore = true;
|
|
317
|
+
this._totalRows = 0;
|
|
318
|
+
this._unfilteredTotalRows = 0;
|
|
319
|
+
this._columnWidths.clear();
|
|
320
|
+
this._tbody.innerHTML = '';
|
|
321
|
+
this._tableContainer.scrollTop = 0;
|
|
322
|
+
this._tableContainer.scrollLeft = 0;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Load file metadata (columns, types, row count, sheets)
|
|
259
326
|
*/
|
|
260
327
|
async _loadMetadata() {
|
|
328
|
+
const body = { path: this._filePath };
|
|
329
|
+
if (this._activeSheet) {
|
|
330
|
+
body.sheet = this._activeSheet;
|
|
331
|
+
}
|
|
261
332
|
const response = await requestAPI('metadata', {
|
|
262
333
|
method: 'POST',
|
|
263
|
-
body: JSON.stringify(
|
|
334
|
+
body: JSON.stringify(body)
|
|
264
335
|
});
|
|
265
336
|
this._columns = response.columns;
|
|
266
337
|
this._totalRows = response.totalRows;
|
|
267
338
|
this._unfilteredTotalRows = response.totalRows;
|
|
268
339
|
this._fileSize = response.fileSize || 0;
|
|
340
|
+
this._sheets = response.sheets || [];
|
|
341
|
+
// First load: pick the first sheet (if any) as active
|
|
342
|
+
if (this._activeSheet === null && this._sheets.length > 0) {
|
|
343
|
+
this._activeSheet = this._sheets[0];
|
|
344
|
+
}
|
|
345
|
+
this._renderSheetBar();
|
|
269
346
|
this._renderHeaders();
|
|
270
347
|
this._updateStatusBar();
|
|
271
348
|
}
|
|
@@ -285,18 +362,22 @@ export class TabularDataViewer extends Widget {
|
|
|
285
362
|
this._tbody.innerHTML = '';
|
|
286
363
|
this._selectedRow = null; // Clear row selection on reset
|
|
287
364
|
}
|
|
365
|
+
const dataBody = {
|
|
366
|
+
path: this._filePath,
|
|
367
|
+
offset: this._currentOffset,
|
|
368
|
+
limit: this._limit,
|
|
369
|
+
filters: this._filters,
|
|
370
|
+
sortBy: this._sortBy,
|
|
371
|
+
sortOrder: this._sortOrder,
|
|
372
|
+
caseInsensitive: this._caseInsensitive,
|
|
373
|
+
useRegex: this._useRegex
|
|
374
|
+
};
|
|
375
|
+
if (this._activeSheet) {
|
|
376
|
+
dataBody.sheet = this._activeSheet;
|
|
377
|
+
}
|
|
288
378
|
const response = await requestAPI('data', {
|
|
289
379
|
method: 'POST',
|
|
290
|
-
body: JSON.stringify(
|
|
291
|
-
path: this._filePath,
|
|
292
|
-
offset: this._currentOffset,
|
|
293
|
-
limit: this._limit,
|
|
294
|
-
filters: this._filters,
|
|
295
|
-
sortBy: this._sortBy,
|
|
296
|
-
sortOrder: this._sortOrder,
|
|
297
|
-
caseInsensitive: this._caseInsensitive,
|
|
298
|
-
useRegex: this._useRegex
|
|
299
|
-
})
|
|
380
|
+
body: JSON.stringify(dataBody)
|
|
300
381
|
});
|
|
301
382
|
this._data = this._data.concat(response.data);
|
|
302
383
|
this._hasMore = response.hasMore;
|
|
@@ -596,7 +677,7 @@ export class TabularDataViewer extends Widget {
|
|
|
596
677
|
async _openFilterModal(columnName, filterInput, filterButton) {
|
|
597
678
|
try {
|
|
598
679
|
// Fetch unique values for this column with the limit from settings
|
|
599
|
-
const uniqueValues = await fetchUniqueValues(this._filePath, columnName, this._maxUniqueValues);
|
|
680
|
+
const uniqueValues = await fetchUniqueValues(this._filePath, columnName, this._maxUniqueValues, this._activeSheet);
|
|
600
681
|
// Get current filter values if any
|
|
601
682
|
const currentFilter = this._filters[columnName];
|
|
602
683
|
const currentValueList = (currentFilter === null || currentFilter === void 0 ? void 0 : currentFilter.valueList) || [];
|
|
@@ -764,9 +845,9 @@ export class TabularDataViewer extends Widget {
|
|
|
764
845
|
async _showColumnStats(columnName) {
|
|
765
846
|
try {
|
|
766
847
|
// Show loading indicator (we could add a spinner here)
|
|
767
|
-
const stats = await fetchColumnStats(this._filePath, columnName);
|
|
848
|
+
const stats = await fetchColumnStats(this._filePath, columnName, this._activeSheet);
|
|
768
849
|
// Fetch unique values with the limit from settings
|
|
769
|
-
const uniqueValues = await fetchUniqueValues(this._filePath, columnName, this._maxUniqueValues);
|
|
850
|
+
const uniqueValues = await fetchUniqueValues(this._filePath, columnName, this._maxUniqueValues, this._activeSheet);
|
|
770
851
|
const modal = new ColumnStatsModal(stats, uniqueValues);
|
|
771
852
|
modal.show();
|
|
772
853
|
}
|
|
@@ -812,8 +893,9 @@ export class TabularDataViewer extends Widget {
|
|
|
812
893
|
* Format file size to human-readable string
|
|
813
894
|
*/
|
|
814
895
|
_formatFileSize(bytes) {
|
|
815
|
-
if (bytes === 0)
|
|
896
|
+
if (bytes === 0) {
|
|
816
897
|
return '0 B';
|
|
898
|
+
}
|
|
817
899
|
const k = 1024;
|
|
818
900
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
819
901
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
@@ -839,6 +921,18 @@ export class TabularDataViewer extends Widget {
|
|
|
839
921
|
rightText += ` (${filterCount} filter${filterCount > 1 ? 's' : ''} active)`;
|
|
840
922
|
}
|
|
841
923
|
this._statusRight.innerHTML = rightText;
|
|
924
|
+
// Export link (always available; opens export popup)
|
|
925
|
+
const exportLink = document.createElement('a');
|
|
926
|
+
exportLink.href = '#';
|
|
927
|
+
exportLink.className = 'jp-TabularDataViewer-exportLink';
|
|
928
|
+
exportLink.textContent = 'Export';
|
|
929
|
+
exportLink.title = 'Export the current view';
|
|
930
|
+
exportLink.addEventListener('click', e => {
|
|
931
|
+
e.preventDefault();
|
|
932
|
+
this.showDownloadModal();
|
|
933
|
+
});
|
|
934
|
+
this._statusRight.appendChild(document.createTextNode(' • '));
|
|
935
|
+
this._statusRight.appendChild(exportLink);
|
|
842
936
|
if (filterCount > 0) {
|
|
843
937
|
const clearLink = document.createElement('a');
|
|
844
938
|
clearLink.href = '#';
|
|
@@ -877,6 +971,10 @@ export class TabularDataViewer extends Widget {
|
|
|
877
971
|
const params = new URLSearchParams();
|
|
878
972
|
params.append('path', this._filePath);
|
|
879
973
|
params.append('format', format);
|
|
974
|
+
// Active sheet (multi-sheet Excel only)
|
|
975
|
+
if (this._activeSheet) {
|
|
976
|
+
params.append('sheet', this._activeSheet);
|
|
977
|
+
}
|
|
880
978
|
// Add filters
|
|
881
979
|
if (Object.keys(this._filters).length > 0) {
|
|
882
980
|
params.append('filters', JSON.stringify(this._filters));
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -161,8 +161,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
161
161
|
// Download command
|
|
162
162
|
const downloadCommand = 'tabular-data-viewer:download';
|
|
163
163
|
commands.addCommand(downloadCommand, {
|
|
164
|
-
label: '
|
|
165
|
-
caption: '
|
|
164
|
+
label: 'Export',
|
|
165
|
+
caption: 'Export the current view in various formats',
|
|
166
166
|
isEnabled: () => {
|
|
167
167
|
return activeWidget !== null;
|
|
168
168
|
},
|
package/src/modal.ts
CHANGED
|
@@ -508,10 +508,12 @@ export class FilterModal extends Widget {
|
|
|
508
508
|
*/
|
|
509
509
|
export class DownloadModal extends Widget {
|
|
510
510
|
private _onDownload: (format: string) => void;
|
|
511
|
+
private _hasFilters: boolean;
|
|
511
512
|
|
|
512
|
-
constructor(onDownload: (format: string) => void) {
|
|
513
|
+
constructor(onDownload: (format: string) => void, hasFilters = false) {
|
|
513
514
|
super();
|
|
514
515
|
this._onDownload = onDownload;
|
|
516
|
+
this._hasFilters = hasFilters;
|
|
515
517
|
this.addClass('jp-FilterModal');
|
|
516
518
|
this._render();
|
|
517
519
|
this._setupEventListeners();
|
|
@@ -528,7 +530,7 @@ export class DownloadModal extends Widget {
|
|
|
528
530
|
const header = document.createElement('div');
|
|
529
531
|
header.className = 'jp-FilterModal-header';
|
|
530
532
|
const title = document.createElement('h3');
|
|
531
|
-
title.textContent = '
|
|
533
|
+
title.textContent = 'Export';
|
|
532
534
|
const closeBtn = document.createElement('button');
|
|
533
535
|
closeBtn.className = 'jp-FilterModal-close';
|
|
534
536
|
closeBtn.textContent = '×';
|
|
@@ -537,6 +539,15 @@ export class DownloadModal extends Widget {
|
|
|
537
539
|
header.appendChild(closeBtn);
|
|
538
540
|
content.appendChild(header);
|
|
539
541
|
|
|
542
|
+
// Filter notice (only when filters are active)
|
|
543
|
+
if (this._hasFilters) {
|
|
544
|
+
const notice = document.createElement('div');
|
|
545
|
+
notice.className = 'jp-FilterModal-notice';
|
|
546
|
+
notice.textContent =
|
|
547
|
+
'Filters are active - export will include only the filtered rows.';
|
|
548
|
+
content.appendChild(notice);
|
|
549
|
+
}
|
|
550
|
+
|
|
540
551
|
// Format buttons
|
|
541
552
|
const buttonsDiv = document.createElement('div');
|
|
542
553
|
buttonsDiv.className = 'jp-FilterModal-buttons';
|
|
@@ -556,6 +567,18 @@ export class DownloadModal extends Widget {
|
|
|
556
567
|
const csvBtn = this._createFormatButton('Download as CSV', 'csv');
|
|
557
568
|
buttonsDiv.appendChild(csvBtn);
|
|
558
569
|
|
|
570
|
+
const parquetBtn = this._createFormatButton(
|
|
571
|
+
'Download as Parquet (.parquet)',
|
|
572
|
+
'parquet'
|
|
573
|
+
);
|
|
574
|
+
buttonsDiv.appendChild(parquetBtn);
|
|
575
|
+
|
|
576
|
+
const jsonlBtn = this._createFormatButton(
|
|
577
|
+
'Download as JSONL (.jsonl)',
|
|
578
|
+
'jsonl'
|
|
579
|
+
);
|
|
580
|
+
buttonsDiv.appendChild(jsonlBtn);
|
|
581
|
+
|
|
559
582
|
content.appendChild(buttonsDiv);
|
|
560
583
|
|
|
561
584
|
// Cancel button
|
package/src/request.ts
CHANGED
|
@@ -85,18 +85,24 @@ export interface IColumnStats {
|
|
|
85
85
|
*
|
|
86
86
|
* @param filePath Path to the data file
|
|
87
87
|
* @param columnName Name of column to analyze
|
|
88
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
88
89
|
* @returns Column statistics
|
|
89
90
|
*/
|
|
90
91
|
export async function fetchColumnStats(
|
|
91
92
|
filePath: string,
|
|
92
|
-
columnName: string
|
|
93
|
+
columnName: string,
|
|
94
|
+
sheet?: string | null
|
|
93
95
|
): Promise<IColumnStats> {
|
|
96
|
+
const body: Record<string, unknown> = {
|
|
97
|
+
path: filePath,
|
|
98
|
+
columnName: columnName
|
|
99
|
+
};
|
|
100
|
+
if (sheet) {
|
|
101
|
+
body.sheet = sheet;
|
|
102
|
+
}
|
|
94
103
|
return requestAPI<IColumnStats>('column-stats', {
|
|
95
104
|
method: 'POST',
|
|
96
|
-
body: JSON.stringify(
|
|
97
|
-
path: filePath,
|
|
98
|
-
columnName: columnName
|
|
99
|
-
})
|
|
105
|
+
body: JSON.stringify(body)
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -115,19 +121,26 @@ export interface IUniqueValues {
|
|
|
115
121
|
*
|
|
116
122
|
* @param filePath Path to the data file
|
|
117
123
|
* @param columnName Name of column to get unique values for
|
|
124
|
+
* @param limit Maximum number of unique values to return
|
|
125
|
+
* @param sheet Active sheet name (optional, multi-sheet Excel only)
|
|
118
126
|
* @returns Unique values
|
|
119
127
|
*/
|
|
120
128
|
export async function fetchUniqueValues(
|
|
121
129
|
filePath: string,
|
|
122
130
|
columnName: string,
|
|
123
|
-
limit: number = 100
|
|
131
|
+
limit: number = 100,
|
|
132
|
+
sheet?: string | null
|
|
124
133
|
): Promise<IUniqueValues> {
|
|
134
|
+
const body: Record<string, unknown> = {
|
|
135
|
+
path: filePath,
|
|
136
|
+
columnName: columnName,
|
|
137
|
+
limit: limit
|
|
138
|
+
};
|
|
139
|
+
if (sheet) {
|
|
140
|
+
body.sheet = sheet;
|
|
141
|
+
}
|
|
125
142
|
return requestAPI<IUniqueValues>('unique-values', {
|
|
126
143
|
method: 'POST',
|
|
127
|
-
body: JSON.stringify(
|
|
128
|
-
path: filePath,
|
|
129
|
-
columnName: columnName,
|
|
130
|
-
limit: limit
|
|
131
|
-
})
|
|
144
|
+
body: JSON.stringify(body)
|
|
132
145
|
});
|
|
133
146
|
}
|
package/src/widget.ts
CHANGED
|
@@ -66,6 +66,9 @@ export class TabularDataViewer extends Widget {
|
|
|
66
66
|
private _maxCellCharacters: number = 100;
|
|
67
67
|
private _maxUniqueValues: number = 100;
|
|
68
68
|
private _selectedRow: HTMLTableRowElement | null = null;
|
|
69
|
+
private _sheets: string[] = [];
|
|
70
|
+
private _activeSheet: string | null = null;
|
|
71
|
+
private _sheetBar: HTMLDivElement;
|
|
69
72
|
|
|
70
73
|
constructor(
|
|
71
74
|
filePath: string,
|
|
@@ -167,8 +170,14 @@ export class TabularDataViewer extends Widget {
|
|
|
167
170
|
this._statusBar.appendChild(statusMiddle);
|
|
168
171
|
this._statusBar.appendChild(this._statusRight);
|
|
169
172
|
|
|
170
|
-
//
|
|
173
|
+
// Sheet bar (only shown when multi-sheet Excel; sits above status bar)
|
|
174
|
+
this._sheetBar = document.createElement('div');
|
|
175
|
+
this._sheetBar.className = 'jp-TabularDataViewer-sheetBar';
|
|
176
|
+
this._sheetBar.style.display = 'none';
|
|
177
|
+
|
|
178
|
+
// Append table container, sheet bar, status bar directly to widget node
|
|
171
179
|
this.node.appendChild(this._tableContainer);
|
|
180
|
+
this.node.appendChild(this._sheetBar);
|
|
172
181
|
this.node.appendChild(this._statusBar);
|
|
173
182
|
|
|
174
183
|
// Set up scroll listener for progressive loading
|
|
@@ -275,8 +284,10 @@ export class TabularDataViewer extends Widget {
|
|
|
275
284
|
* Show download modal dialog
|
|
276
285
|
*/
|
|
277
286
|
public showDownloadModal(): void {
|
|
278
|
-
const
|
|
279
|
-
|
|
287
|
+
const hasFilters = Object.keys(this._filters).length > 0;
|
|
288
|
+
const modal = new DownloadModal(
|
|
289
|
+
(format: string) => this.downloadFilteredData(format),
|
|
290
|
+
hasFilters
|
|
280
291
|
);
|
|
281
292
|
modal.show();
|
|
282
293
|
}
|
|
@@ -294,19 +305,96 @@ export class TabularDataViewer extends Widget {
|
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
/**
|
|
297
|
-
*
|
|
308
|
+
* Render the sheet bar. Visible only when the file has more than one sheet.
|
|
309
|
+
* Each sheet is a button; the active one carries `jp-mod-active`.
|
|
310
|
+
*/
|
|
311
|
+
private _renderSheetBar(): void {
|
|
312
|
+
this._sheetBar.innerHTML = '';
|
|
313
|
+
|
|
314
|
+
if (this._sheets.length <= 1) {
|
|
315
|
+
this._sheetBar.style.display = 'none';
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this._sheetBar.style.display = '';
|
|
320
|
+
|
|
321
|
+
for (const name of this._sheets) {
|
|
322
|
+
const tab = document.createElement('button');
|
|
323
|
+
tab.className = 'jp-TabularDataViewer-sheetTab';
|
|
324
|
+
if (name === this._activeSheet) {
|
|
325
|
+
tab.classList.add('jp-mod-active');
|
|
326
|
+
}
|
|
327
|
+
tab.textContent = name;
|
|
328
|
+
tab.title = name;
|
|
329
|
+
tab.addEventListener('click', () => this._switchSheet(name));
|
|
330
|
+
this._sheetBar.appendChild(tab);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Switch to a different sheet. Resets all user state and reloads metadata
|
|
336
|
+
* + data so the experience matches "opening a new file".
|
|
337
|
+
*/
|
|
338
|
+
private _switchSheet(name: string): void {
|
|
339
|
+
if (name === this._activeSheet) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this._activeSheet = name;
|
|
343
|
+
this._resetState();
|
|
344
|
+
this._renderSheetBar();
|
|
345
|
+
this._initialize();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Reset all user-applied state (filters, sort, selection, scroll position,
|
|
350
|
+
* column widths, paging cursor). Used when switching sheets.
|
|
351
|
+
*/
|
|
352
|
+
private _resetState(): void {
|
|
353
|
+
this._filters = {};
|
|
354
|
+
this._sortBy = null;
|
|
355
|
+
this._sortOrder = 'asc';
|
|
356
|
+
this._caseInsensitive = false;
|
|
357
|
+
this._useRegex = false;
|
|
358
|
+
this._caseInsensitiveCheckbox.checked = false;
|
|
359
|
+
this._regexCheckbox.checked = false;
|
|
360
|
+
this._selectedRow = null;
|
|
361
|
+
this._currentOffset = 0;
|
|
362
|
+
this._data = [];
|
|
363
|
+
this._hasMore = true;
|
|
364
|
+
this._totalRows = 0;
|
|
365
|
+
this._unfilteredTotalRows = 0;
|
|
366
|
+
this._columnWidths.clear();
|
|
367
|
+
this._tbody.innerHTML = '';
|
|
368
|
+
this._tableContainer.scrollTop = 0;
|
|
369
|
+
this._tableContainer.scrollLeft = 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Load file metadata (columns, types, row count, sheets)
|
|
298
374
|
*/
|
|
299
375
|
private async _loadMetadata(): Promise<void> {
|
|
376
|
+
const body: Record<string, unknown> = { path: this._filePath };
|
|
377
|
+
if (this._activeSheet) {
|
|
378
|
+
body.sheet = this._activeSheet;
|
|
379
|
+
}
|
|
380
|
+
|
|
300
381
|
const response = await requestAPI<any>('metadata', {
|
|
301
382
|
method: 'POST',
|
|
302
|
-
body: JSON.stringify(
|
|
383
|
+
body: JSON.stringify(body)
|
|
303
384
|
});
|
|
304
385
|
|
|
305
386
|
this._columns = response.columns;
|
|
306
387
|
this._totalRows = response.totalRows;
|
|
307
388
|
this._unfilteredTotalRows = response.totalRows;
|
|
308
389
|
this._fileSize = response.fileSize || 0;
|
|
390
|
+
this._sheets = response.sheets || [];
|
|
309
391
|
|
|
392
|
+
// First load: pick the first sheet (if any) as active
|
|
393
|
+
if (this._activeSheet === null && this._sheets.length > 0) {
|
|
394
|
+
this._activeSheet = this._sheets[0];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this._renderSheetBar();
|
|
310
398
|
this._renderHeaders();
|
|
311
399
|
this._updateStatusBar();
|
|
312
400
|
}
|
|
@@ -330,18 +418,22 @@ export class TabularDataViewer extends Widget {
|
|
|
330
418
|
this._selectedRow = null; // Clear row selection on reset
|
|
331
419
|
}
|
|
332
420
|
|
|
421
|
+
const dataBody: Record<string, unknown> = {
|
|
422
|
+
path: this._filePath,
|
|
423
|
+
offset: this._currentOffset,
|
|
424
|
+
limit: this._limit,
|
|
425
|
+
filters: this._filters,
|
|
426
|
+
sortBy: this._sortBy,
|
|
427
|
+
sortOrder: this._sortOrder,
|
|
428
|
+
caseInsensitive: this._caseInsensitive,
|
|
429
|
+
useRegex: this._useRegex
|
|
430
|
+
};
|
|
431
|
+
if (this._activeSheet) {
|
|
432
|
+
dataBody.sheet = this._activeSheet;
|
|
433
|
+
}
|
|
333
434
|
const response = await requestAPI<any>('data', {
|
|
334
435
|
method: 'POST',
|
|
335
|
-
body: JSON.stringify(
|
|
336
|
-
path: this._filePath,
|
|
337
|
-
offset: this._currentOffset,
|
|
338
|
-
limit: this._limit,
|
|
339
|
-
filters: this._filters,
|
|
340
|
-
sortBy: this._sortBy,
|
|
341
|
-
sortOrder: this._sortOrder,
|
|
342
|
-
caseInsensitive: this._caseInsensitive,
|
|
343
|
-
useRegex: this._useRegex
|
|
344
|
-
})
|
|
436
|
+
body: JSON.stringify(dataBody)
|
|
345
437
|
});
|
|
346
438
|
|
|
347
439
|
this._data = this._data.concat(response.data);
|
|
@@ -783,7 +875,8 @@ export class TabularDataViewer extends Widget {
|
|
|
783
875
|
const uniqueValues = await fetchUniqueValues(
|
|
784
876
|
this._filePath,
|
|
785
877
|
columnName,
|
|
786
|
-
this._maxUniqueValues
|
|
878
|
+
this._maxUniqueValues,
|
|
879
|
+
this._activeSheet
|
|
787
880
|
);
|
|
788
881
|
|
|
789
882
|
// Get current filter values if any
|
|
@@ -992,12 +1085,17 @@ export class TabularDataViewer extends Widget {
|
|
|
992
1085
|
private async _showColumnStats(columnName: string): Promise<void> {
|
|
993
1086
|
try {
|
|
994
1087
|
// Show loading indicator (we could add a spinner here)
|
|
995
|
-
const stats = await fetchColumnStats(
|
|
1088
|
+
const stats = await fetchColumnStats(
|
|
1089
|
+
this._filePath,
|
|
1090
|
+
columnName,
|
|
1091
|
+
this._activeSheet
|
|
1092
|
+
);
|
|
996
1093
|
// Fetch unique values with the limit from settings
|
|
997
1094
|
const uniqueValues = await fetchUniqueValues(
|
|
998
1095
|
this._filePath,
|
|
999
1096
|
columnName,
|
|
1000
|
-
this._maxUniqueValues
|
|
1097
|
+
this._maxUniqueValues,
|
|
1098
|
+
this._activeSheet
|
|
1001
1099
|
);
|
|
1002
1100
|
const modal = new ColumnStatsModal(stats, uniqueValues);
|
|
1003
1101
|
modal.show();
|
|
@@ -1051,7 +1149,9 @@ export class TabularDataViewer extends Widget {
|
|
|
1051
1149
|
* Format file size to human-readable string
|
|
1052
1150
|
*/
|
|
1053
1151
|
private _formatFileSize(bytes: number): string {
|
|
1054
|
-
if (bytes === 0)
|
|
1152
|
+
if (bytes === 0) {
|
|
1153
|
+
return '0 B';
|
|
1154
|
+
}
|
|
1055
1155
|
const k = 1024;
|
|
1056
1156
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1057
1157
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
@@ -1081,6 +1181,19 @@ export class TabularDataViewer extends Widget {
|
|
|
1081
1181
|
|
|
1082
1182
|
this._statusRight.innerHTML = rightText;
|
|
1083
1183
|
|
|
1184
|
+
// Export link (always available; opens export popup)
|
|
1185
|
+
const exportLink = document.createElement('a');
|
|
1186
|
+
exportLink.href = '#';
|
|
1187
|
+
exportLink.className = 'jp-TabularDataViewer-exportLink';
|
|
1188
|
+
exportLink.textContent = 'Export';
|
|
1189
|
+
exportLink.title = 'Export the current view';
|
|
1190
|
+
exportLink.addEventListener('click', e => {
|
|
1191
|
+
e.preventDefault();
|
|
1192
|
+
this.showDownloadModal();
|
|
1193
|
+
});
|
|
1194
|
+
this._statusRight.appendChild(document.createTextNode(' • '));
|
|
1195
|
+
this._statusRight.appendChild(exportLink);
|
|
1196
|
+
|
|
1084
1197
|
if (filterCount > 0) {
|
|
1085
1198
|
const clearLink = document.createElement('a');
|
|
1086
1199
|
clearLink.href = '#';
|
|
@@ -1125,6 +1238,11 @@ export class TabularDataViewer extends Widget {
|
|
|
1125
1238
|
params.append('path', this._filePath);
|
|
1126
1239
|
params.append('format', format);
|
|
1127
1240
|
|
|
1241
|
+
// Active sheet (multi-sheet Excel only)
|
|
1242
|
+
if (this._activeSheet) {
|
|
1243
|
+
params.append('sheet', this._activeSheet);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1128
1246
|
// Add filters
|
|
1129
1247
|
if (Object.keys(this._filters).length > 0) {
|
|
1130
1248
|
params.append('filters', JSON.stringify(this._filters));
|
package/style/base.css
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
top: 0;
|
|
35
35
|
z-index: 10;
|
|
36
36
|
background-color: var(--jp-layout-color2);
|
|
37
|
-
box-shadow: 0 2px 4px
|
|
37
|
+
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/* Filter Row */
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
position: sticky;
|
|
243
243
|
left: 0;
|
|
244
244
|
z-index: 5;
|
|
245
|
-
box-shadow: 2px 0 4px
|
|
245
|
+
box-shadow: 2px 0 4px rgb(0 0 0 / 10%);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/* Row number cells in header (thead) need higher z-index to appear above sticky header */
|
|
@@ -256,6 +256,7 @@
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/* Clickable rows */
|
|
259
|
+
/* stylelint-disable-next-line no-descending-specificity */
|
|
259
260
|
.jp-TabularDataViewer-tbody .jp-TabularDataViewer-row {
|
|
260
261
|
cursor: pointer;
|
|
261
262
|
}
|
|
@@ -282,6 +283,7 @@
|
|
|
282
283
|
) !important;
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
/* stylelint-disable-next-line no-descending-specificity */
|
|
285
287
|
.jp-TabularDataViewer-row-selected .jp-TabularDataViewer-rowNumberCell {
|
|
286
288
|
background-color: color-mix(
|
|
287
289
|
in srgb,
|
|
@@ -294,6 +296,39 @@
|
|
|
294
296
|
border-right: none;
|
|
295
297
|
}
|
|
296
298
|
|
|
299
|
+
/* Sheet Bar (multi-sheet Excel only) */
|
|
300
|
+
.jp-TabularDataViewer-sheetBar {
|
|
301
|
+
flex-shrink: 0;
|
|
302
|
+
display: flex;
|
|
303
|
+
overflow-x: auto;
|
|
304
|
+
border-top: 1px solid var(--jp-border-color2);
|
|
305
|
+
background-color: var(--jp-layout-color1);
|
|
306
|
+
font-family: var(--jp-ui-font-family);
|
|
307
|
+
font-size: var(--jp-ui-font-size0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.jp-TabularDataViewer-sheetTab {
|
|
311
|
+
padding: 2px 12px;
|
|
312
|
+
border: none;
|
|
313
|
+
border-right: 1px solid var(--jp-border-color2);
|
|
314
|
+
background: transparent;
|
|
315
|
+
cursor: pointer;
|
|
316
|
+
color: var(--jp-ui-font-color1);
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
font-family: inherit;
|
|
319
|
+
font-size: inherit;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.jp-TabularDataViewer-sheetTab:hover {
|
|
323
|
+
background-color: var(--jp-layout-color2);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.jp-TabularDataViewer-sheetTab.jp-mod-active {
|
|
327
|
+
background-color: var(--jp-layout-color2);
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
border-bottom: 2px solid var(--jp-brand-color1);
|
|
330
|
+
}
|
|
331
|
+
|
|
297
332
|
/* Status Bar */
|
|
298
333
|
.jp-TabularDataViewer-statusBar {
|
|
299
334
|
flex-shrink: 0;
|
|
@@ -357,6 +392,19 @@
|
|
|
357
392
|
text-decoration: underline !important;
|
|
358
393
|
}
|
|
359
394
|
|
|
395
|
+
.jp-TabularDataViewer-exportLink {
|
|
396
|
+
color: var(--jp-brand-color1) !important;
|
|
397
|
+
text-decoration: none !important;
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
margin-left: 4px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.jp-TabularDataViewer-exportLink:hover {
|
|
404
|
+
color: var(--jp-brand-color1) !important;
|
|
405
|
+
text-decoration: underline !important;
|
|
406
|
+
}
|
|
407
|
+
|
|
360
408
|
/* Error Message */
|
|
361
409
|
.jp-TabularDataViewer-error {
|
|
362
410
|
padding: 16px;
|
|
@@ -410,6 +458,7 @@
|
|
|
410
458
|
opacity: 1;
|
|
411
459
|
}
|
|
412
460
|
|
|
461
|
+
/* stylelint-disable-next-line no-descending-specificity */
|
|
413
462
|
.jp-TabularDataViewer-infoIcon:hover {
|
|
414
463
|
color: var(--jp-brand-color1) !important;
|
|
415
464
|
opacity: 1;
|
|
@@ -422,7 +471,7 @@
|
|
|
422
471
|
left: 0;
|
|
423
472
|
width: 100%;
|
|
424
473
|
height: 100%;
|
|
425
|
-
background-color:
|
|
474
|
+
background-color: rgb(0 0 0 / 50%);
|
|
426
475
|
display: flex;
|
|
427
476
|
align-items: center;
|
|
428
477
|
justify-content: center;
|
|
@@ -432,7 +481,7 @@
|
|
|
432
481
|
.jp-ColumnStatsModal-content {
|
|
433
482
|
background-color: var(--jp-layout-color1);
|
|
434
483
|
border-radius: 8px;
|
|
435
|
-
box-shadow: 0 4px 12px
|
|
484
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 30%);
|
|
436
485
|
max-width: 500px;
|
|
437
486
|
max-height: 80vh;
|
|
438
487
|
overflow-y: auto;
|
|
@@ -516,7 +565,7 @@
|
|
|
516
565
|
}
|
|
517
566
|
|
|
518
567
|
.jp-ColumnStatsModal-section h4 {
|
|
519
|
-
margin: 0 0 8px
|
|
568
|
+
margin: 0 0 8px;
|
|
520
569
|
font-size: 15px;
|
|
521
570
|
font-weight: 600;
|
|
522
571
|
color: var(--jp-ui-font-color1);
|
|
@@ -576,7 +625,7 @@
|
|
|
576
625
|
left: 0;
|
|
577
626
|
width: 100%;
|
|
578
627
|
height: 100%;
|
|
579
|
-
background-color:
|
|
628
|
+
background-color: rgb(0 0 0 / 50%);
|
|
580
629
|
display: flex;
|
|
581
630
|
align-items: center;
|
|
582
631
|
justify-content: center;
|
|
@@ -586,7 +635,7 @@
|
|
|
586
635
|
.jp-FilterModal-content {
|
|
587
636
|
background-color: var(--jp-layout-color1);
|
|
588
637
|
border-radius: 8px;
|
|
589
|
-
box-shadow: 0 4px 12px
|
|
638
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 30%);
|
|
590
639
|
max-width: 500px;
|
|
591
640
|
width: 90%;
|
|
592
641
|
max-height: 80vh;
|
|
@@ -600,7 +649,7 @@
|
|
|
600
649
|
display: flex;
|
|
601
650
|
justify-content: space-between;
|
|
602
651
|
align-items: center;
|
|
603
|
-
padding: 20px 20px 12px
|
|
652
|
+
padding: 20px 20px 12px;
|
|
604
653
|
border-bottom: 2px solid var(--jp-border-color1);
|
|
605
654
|
}
|
|
606
655
|
|
|
@@ -611,6 +660,15 @@
|
|
|
611
660
|
color: var(--jp-ui-font-color1);
|
|
612
661
|
}
|
|
613
662
|
|
|
663
|
+
.jp-FilterModal-notice {
|
|
664
|
+
padding: 10px 20px;
|
|
665
|
+
font-size: var(--jp-ui-font-size1);
|
|
666
|
+
color: var(--jp-ui-font-color2);
|
|
667
|
+
background-color: var(--jp-layout-color2);
|
|
668
|
+
border-bottom: 1px solid var(--jp-border-color2);
|
|
669
|
+
font-style: italic;
|
|
670
|
+
}
|
|
671
|
+
|
|
614
672
|
.jp-FilterModal-close {
|
|
615
673
|
background: none;
|
|
616
674
|
border: none;
|