From 9c7f6d2dea9214a6a232fc35dc40f684f12a2dd9 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 10 Jul 2025 18:18:21 -0700 Subject: [PATCH 1/3] update file size with binary prefixes --- src/FssTreeItem.ts | 13 ++++++++++++- src/utils.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/utils.ts diff --git a/src/FssTreeItem.ts b/src/FssTreeItem.ts index 7a54bfc..5c02bc4 100644 --- a/src/FssTreeItem.ts +++ b/src/FssTreeItem.ts @@ -12,6 +12,7 @@ import { INotebookTracker } from '@jupyterlab/notebook'; import { fileIcon, folderIcon } from '@jupyterlab/ui-components'; import { FssTreeItemContext } from './FssTreeItemContext'; +import { formatBytes } from './utils'; import { Logger } from './logger'; export class FssTreeItem { @@ -168,7 +169,17 @@ export class FssTreeItem { this.root.dataset.fss = user_path; this.root.dataset.fsize = size; - const sizeDisplay = `(${size.toLocaleString()})`; + // cast size to number + const sizeNumber = Number(size); + if (isNaN(sizeNumber)) { + this.logger.error('Invalid size', { size }); + return; + } + const formattedSize = formatBytes(sizeNumber); + // cast formattedSize to string + const formattedSizeString = formattedSize.toString(); + + const sizeDisplay = `(${formattedSizeString})`; this.sizeLbl.innerText = sizeDisplay; } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..884d49c --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,28 @@ +export function formatBytes(n: number): string { + /** + * Examples: + * formatBytes(1) // '1 B' + * formatBytes(1234) // '1.21 kiB' + * formatBytes(12345678) // '11.77 MiB' + * formatBytes(1234567890) // '1.15 GiB' + * formatBytes(1234567890000) // '1.12 TiB' + * formatBytes(1234567890000000) // '1.10 PiB' + * + * For all values < 2^60, the output is always <= 10 characters. + * Note:Code adapted from dask.utils.format_bytes + */ + const units: [string, number][] = [ + ['Pi', Math.pow(2, 50)], + ['Ti', Math.pow(2, 40)], + ['Gi', Math.pow(2, 30)], + ['Mi', Math.pow(2, 20)], + ['ki', Math.pow(2, 10)] + ]; + + for (const [prefix, k] of units) { + if (n >= k * 0.9) { + return `${(n / k).toFixed(2)} ${prefix}B`; + } + } + return `${n} B`; +} From 1c4e79316d215894e77d441342db8585dfaa523e Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 11 Jul 2025 11:26:08 -0700 Subject: [PATCH 2/3] set directory children count once items are loaded --- src/FssTreeItem.ts | 36 ++++++++++++++++++++++------------ src/index.ts | 48 ++++++++++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/FssTreeItem.ts b/src/FssTreeItem.ts index 5c02bc4..49f1ec5 100644 --- a/src/FssTreeItem.ts +++ b/src/FssTreeItem.ts @@ -160,27 +160,38 @@ export class FssTreeItem { }); } - setMetadata(user_path: string, size: string) { + setMetadata(user_path: string, size: string, childrenCount?: number) { this.logger.debug('Setting item metadata', { path: user_path, - size + size, + childrenCount }); this.root.dataset.fss = user_path; this.root.dataset.fsize = size; - // cast size to number - const sizeNumber = Number(size); - if (isNaN(sizeNumber)) { - this.logger.error('Invalid size', { size }); + if (childrenCount !== undefined) { + // For directories, show children count + const childrenDisplay = `(${childrenCount} items)`; + this.sizeLbl.innerText = childrenDisplay; + this.sizeLbl.style.display = 'block'; + } else if (this.isDir) { + // For directories without known count, hide the size label + this.sizeLbl.style.display = 'none'; return; - } - const formattedSize = formatBytes(sizeNumber); - // cast formattedSize to string - const formattedSizeString = formattedSize.toString(); + } else { + // For files, show size + const sizeNumber = Number(size); + if (isNaN(sizeNumber)) { + this.logger.error('Invalid size', { size }); + return; + } + const formattedSize = formatBytes(sizeNumber); + const formattedSizeString = formattedSize.toString(); - const sizeDisplay = `(${formattedSizeString})`; - this.sizeLbl.innerText = sizeDisplay; + const sizeDisplay = `(${formattedSizeString})`; + this.sizeLbl.innerText = sizeDisplay; + } } setText(value: string) { @@ -202,6 +213,7 @@ export class FssTreeItem { if (symbol === 'file') { fileIcon.element({ container: this.dirSymbol }); this.isDir = false; + this.sizeLbl.style.display = 'block'; } } diff --git a/src/index.ts b/src/index.ts index 1cce824..1d49894 100644 --- a/src/index.ts +++ b/src/index.ts @@ -903,13 +903,21 @@ class FsspecWidget extends Widget { // Update the TreeView in the UI await this.updateFileBrowserView(nodeForPath); + // Update the children count for the expanded directory if (nodeForPath.id.toString() in this.elementHeap) { const uiElement = this.elementHeap[nodeForPath.id.toString()]; + const actualChildrenCount = Object.keys(nodeForPath.children).length; + uiElement.setMetadata( + nodeForPath.path, + nodeForPath.metadata.size, + actualChildrenCount + ); uiElement.expandItem(); this.logger.debug('Auto-expanded directory node', { path: source_path, - nodeId: nodeForPath.id + nodeId: nodeForPath.id, + childrenCount: actualChildrenCount }); } } @@ -991,10 +999,28 @@ class FsspecWidget extends Widget { true, this.notebookTracker ); - item.setMetadata( - (pathInfo as any).path, - (pathInfo as any).metadata.size - ); + const isDirectory = + Object.keys((pathInfo as any).children).length > 0 || + ('type' in (pathInfo as any).metadata && + (pathInfo as any).metadata.type === 'directory'); + + if (isDirectory) { + // Since directroy children are lazy-loaded: set children count to undefined + // The count will be updated once the directory is expanded + item.setMetadata( + (pathInfo as any).path, + (pathInfo as any).metadata.size, + undefined + ); + item.setType('dir'); + } else { + item.setMetadata( + (pathInfo as any).path, + (pathInfo as any).metadata.size + ); + item.setType('file'); + } + item.setText(pathSegment); item.treeItemClicked.connect(this.handleTreeItemClicked.bind(this)); item.getBytesRequested.connect( @@ -1008,18 +1034,6 @@ class FsspecWidget extends Widget { (pathInfo as any).id = item_id; this.elementHeap[item_id.toString()] = item; - // Set the item type (file or directory) - const isDirectory = - Object.keys((pathInfo as any).children).length > 0 || - ('type' in (pathInfo as any).metadata && - (pathInfo as any).metadata.type === 'directory'); - - if (isDirectory) { - item.setType('dir'); - } else { - item.setType('file'); - } - // Add children to build targets if needed if (Object.keys((pathInfo as any).children).length > 0) { buildTargets[(pathInfo as any).path] = [ From 16b9d4ee7247c1d4335f51a51a40a348dd99b87a Mon Sep 17 00:00:00 2001 From: RRosio Date: Fri, 11 Jul 2025 11:40:29 -0700 Subject: [PATCH 3/3] use singular item for 1 item --- src/FssTreeItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FssTreeItem.ts b/src/FssTreeItem.ts index 49f1ec5..18a0d0b 100644 --- a/src/FssTreeItem.ts +++ b/src/FssTreeItem.ts @@ -172,7 +172,7 @@ export class FssTreeItem { if (childrenCount !== undefined) { // For directories, show children count - const childrenDisplay = `(${childrenCount} items)`; + const childrenDisplay = `(${childrenCount} item${childrenCount === 1 ? '' : 's'})`; this.sizeLbl.innerText = childrenDisplay; this.sizeLbl.style.display = 'block'; } else if (this.isDir) {