diff --git a/docs/_static/s3fs_inactive.png b/docs/_static/s3fs_inactive.png new file mode 100644 index 0000000..7960c8a Binary files /dev/null and b/docs/_static/s3fs_inactive.png differ diff --git a/docs/index.md b/docs/index.md index 2dd6a77..da0146d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,6 +123,13 @@ the server's working directory in both the browser UI and in the kernel side. Since the kernel is usually a fully privileged process, this restriction only applies to the automatic behavior of jupyter_fsspec. ::: +### Inactive Filesystems + +Filesystems that are not instantiated due to an error will appear grayed out and will display an error message on hover. +On click, there will be more information logged to the browser console. + +![Jupyter FSSpec Inactive Filesystem](_static/s3fs_inactive.png 'Jupyter FSSpec Inactive Filesystem') + ## The `helper` module You can import the `jupyter_fsspec.helper` module into your notebooks to interact with diff --git a/jupyter_fsspec/handlers.py b/jupyter_fsspec/handlers.py index 15357c1..9993ee7 100644 --- a/jupyter_fsspec/handlers.py +++ b/jupyter_fsspec/handlers.py @@ -463,10 +463,10 @@ async def get(self): fs_instance = fs["instance"] response = {} - is_async = fs_instance.async_impl try: with handle_exception(self): + is_async = fs_instance.async_impl result = ( await fs_instance._ls(item_path, detail=True) if is_async diff --git a/src/FssFilesysItem.ts b/src/FssFilesysItem.ts index 351a270..72d9af2 100644 --- a/src/FssFilesysItem.ts +++ b/src/FssFilesysItem.ts @@ -36,17 +36,24 @@ class FssFilesysItem { const fsItem = document.createElement('div'); fsItem.classList.add('jfss-fsitem-root'); + this.root = fsItem; if ('error' in fsInfo) { fsItem.classList.add('jfss-fsitem-error'); + fsItem.dataset.errorMessage = fsInfo.error.short_traceback; + fsItem.addEventListener( + 'mouseenter', + this.handleDisplayFSError.bind(this) + ); + } else { + fsItem.addEventListener('mouseenter', this.handleFsysHover.bind(this)); + fsItem.addEventListener('mouseleave', this.handleFsysHover.bind(this)); + + // Set the tooltip + this.root.title = `Root Path: ${fsInfo.path}`; } - fsItem.addEventListener('mouseenter', this.handleFsysHover.bind(this)); - fsItem.addEventListener('mouseleave', this.handleFsysHover.bind(this)); - fsItem.dataset.fssname = fsInfo.name; - this.root = fsItem; - // Set the tooltip - this.root.title = `Root Path: ${fsInfo.path}`; + fsItem.dataset.fssname = fsInfo.name; this.nameField = document.createElement('div'); this.nameField.classList.add('jfss-fsitem-name'); @@ -176,6 +183,48 @@ class FssFilesysItem { } } + handleDisplayFSError(event: MouseEvent): void { + const fsItem = event.currentTarget as HTMLElement; + const errorMessage = fsItem.dataset.errorMessage; + + const tooltip = document.createElement('div'); + tooltip.className = 'jfss-fsitem-tooltip'; + tooltip.textContent = `[Inactive] ${errorMessage}`; + + Object.assign(tooltip.style, { + position: 'fixed', + backgroundColor: 'rgba(242, 159, 159, 0.85)', + color: 'rgb(77, 16, 16)', + padding: '4px 8px', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + zIndex: '9999', + fontSize: '12px', + visibility: 'hidden' + }); + + document.body.appendChild(tooltip); + + // Measure and position tooltip + const offset = 10; + const { clientX: x, clientY: y } = event; + const { width, height } = tooltip.getBoundingClientRect(); + const maxX = window.innerWidth - width - offset; + const maxY = window.innerHeight - height - offset; + + const left = Math.min(x + offset, maxX); + const top = Math.min(y + offset, maxY); + + tooltip.style.left = `${left}px`; + tooltip.style.top = `${top}px`; + tooltip.style.visibility = 'visible'; + + const removeTooltip = () => { + tooltip.remove(); + fsItem.removeEventListener('mouseleave', removeTooltip); + }; + fsItem.addEventListener('mouseleave', removeTooltip); + } + handleFsysHover(event: any) { if (event.type === 'mouseenter') { this.hovered = true; @@ -190,10 +239,11 @@ class FssFilesysItem { protocol: this.filesysProtocol, path: this.fsInfo.path }); - if (this.fsInfo.error) { + if ('error' in this.fsInfo) { this.logger.error('Inactive filesystem', { ...this.fsInfo.error }); + return; } this.selected = true; this.filesysClicked.emit(this.fsInfo); diff --git a/ui-tests/tests/filesystem_interaction.test.ts b/ui-tests/tests/filesystem_interaction.test.ts index 7a83d64..a297331 100644 --- a/ui-tests/tests/filesystem_interaction.test.ts +++ b/ui-tests/tests/filesystem_interaction.test.ts @@ -192,6 +192,33 @@ test('test open jupyterFsspec with hdfs config', async ({ page }) => { .toHaveText('Path: myhdfs'); }); +test('test filesystem error on hover', async ({ page }) => { + await page.route('http://localhost:8888/jupyter_fsspec/config?**', route => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(hdfsConfig) + }); + }); + + await page.goto(); + await page.getByText('FSSpec', { exact: true }).click(); + + // verify filesystem item was created + await expect.soft(page.locator('.jfss-fsitem-root')).toBeVisible(); + + // filesystem details should match as expected + await expect.soft(page.locator('.jfss-fsitem-error')).toBeVisible(); + await expect.soft(page.locator('.jfss-fsitem-name')).toBeVisible(); + page.locator('.jfss-fsitem-error').hover(); + await expect.soft(page.locator('.jfss-fsitem-tooltip')).toBeVisible(); + await expect + .soft(page.locator('.jfss-fsitem-tooltip')) + .toContainText('[Inactive]'); + page.mouse.move(0, 0); + await expect.soft(page.locator('.jfss-fsitem-tooltip')).toBeHidden(); +}); + test('test memory filesystem with mock config data', async ({ page }) => { await page.goto(); await page.getByText('FSSpec', { exact: true }).click();