From 970566f0f2af48100798935a6ed34d5a35991692 Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 11:20:25 -0800 Subject: [PATCH 1/6] Add tornado handler for checking the validity of a dashboard URL server side. This allows us to avoid CORS or mixed content errors when determining whether a user-provided dashboard is valid. --- dask_labextension/__init__.py | 6 ++++- dask_labextension/clusterhandler.py | 2 +- dask_labextension/dashboardhandler.py | 36 +++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/dask_labextension/__init__.py b/dask_labextension/__init__.py index 802b688..f60b8d4 100644 --- a/dask_labextension/__init__.py +++ b/dask_labextension/__init__.py @@ -4,7 +4,7 @@ from . import config from .clusterhandler import DaskClusterHandler -from .dashboardhandler import DaskDashboardHandler +from .dashboardhandler import DaskDashboardCheckHandler, DaskDashboardHandler from ._version import get_versions @@ -32,9 +32,13 @@ def load_jupyter_server_extension(nb_server_app): get_dashboard_path = url_path_join( base_url, f"dask/dashboard/{cluster_id_regex}(?P.+)" ) + check_dashboard_path = url_path_join( + base_url, "dask/dashboard-check/(?P.+)" + ) handlers = [ (get_cluster_path, DaskClusterHandler), (list_clusters_path, DaskClusterHandler), (get_dashboard_path, DaskDashboardHandler), + (check_dashboard_path, DaskDashboardCheckHandler), ] web_app.add_handlers(".*$", handlers) diff --git a/dask_labextension/clusterhandler.py b/dask_labextension/clusterhandler.py index 000ad58..380e6a9 100644 --- a/dask_labextension/clusterhandler.py +++ b/dask_labextension/clusterhandler.py @@ -33,7 +33,7 @@ async def delete(self, cluster_id: str) -> None: raise web.HTTPError(500, str(e)) @web.authenticated - def get(self, cluster_id: str = "") -> None: + async def get(self, cluster_id: str = "") -> None: """ Get a cluster by id. If no id is given, lists known clusters. """ diff --git a/dask_labextension/dashboardhandler.py b/dask_labextension/dashboardhandler.py index 8e09a0e..88bab54 100644 --- a/dask_labextension/dashboardhandler.py +++ b/dask_labextension/dashboardhandler.py @@ -3,16 +3,48 @@ This proxies the bokeh server http and ws requests through the notebook server, preventing CORS issues. """ +import json from urllib import parse -from tornado import web +from tornado import httpclient, web +from notebook.base.handlers import APIHandler from notebook.utils import url_path_join from jupyter_server_proxy.handlers import ProxyHandler from .manager import manager +class DaskDashboardCheckHandler(APIHandler): + """ + A handler for checking validity of a dask dashboard. + """ + + @web.authenticated + async def get(self, url) -> None: + """ + Test if a given url string hosts a dask dashboard. Should always return a + 200 code, any errors are presumed to result from an invalid/inactive dashboard. + """ + try: + url = _normalize_dashboard_link(parse.unquote(url), self.request) + client = httpclient.AsyncHTTPClient() + response = await client.fetch(url) + effective_url = response.effective_url if response.effective_url != url else None + self.set_status(200) + self.finish(json.dumps({ + "url": url, + "isActive": response.code == 200, + "effectiveUrl": effective_url, + })) + except: + self.set_status(200) + self.finish(json.dumps({ + "url": url, + "isActive": False, + })) + + class DaskDashboardHandler(ProxyHandler): """ A handler that proxies the dask dashboard to the notebook server. @@ -92,6 +124,6 @@ def _normalize_dashboard_link(link, request): # as the application, and prepend that. link = url_path_join(f"{request.protocol}://{request.host}", link) if link.endswith("/status"): - # If the default "status" dashboard is give, strip it. + # If the default "status" dashboard is given, strip it. link = link[:-len("/status")] return link From 42bec00fd5018a14c1b0759f29283d5cc1577a9d Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 11:23:30 -0800 Subject: [PATCH 2/6] Check for dashboard validity through the server. If there are redirects, that is captured in `effectiveUrl`, which is used to construct dashboard chart urls. --- src/dashboard.tsx | 162 +++++++++++++++++++++++----------------------- src/index.ts | 29 +++++---- 2 files changed, 99 insertions(+), 92 deletions(-) diff --git a/src/dashboard.tsx b/src/dashboard.tsx index ce15771..5575793 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -8,8 +8,6 @@ import { searchIcon } from '@jupyterlab/ui-components'; import { JSONExt, JSONObject } from '@lumino/coreutils'; -import { Message } from '@lumino/messaging'; - import { Poll } from '@lumino/polling'; import { ISignal, Signal } from '@lumino/signaling'; @@ -19,6 +17,26 @@ import { Widget, PanelLayout } from '@lumino/widgets'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; +/** + * Info for a a given dashboard URL. + */ +export type DashboardURLInfo = { + /** + * The user provided url in the search box. + */ + url: string; + + /** + * Whether there is a live dashboard at the URL. + */ + isActive: boolean; + + /** + * A new URL to use after redirects or proxies. + */ + effectiveUrl?: string; +}; + /** * A class for hosting a Dask dashboard in an iframe. */ @@ -121,16 +139,19 @@ export class DaskDashboardLauncher extends Widget { this.addClass('dask-DaskDashboardLauncher'); this._items = options.items || DaskDashboardLauncher.DEFAULT_ITEMS; this._launchItem = options.launchItem; - this._input.urlChanged.connect(this.updateLinks, this); + this._input.urlInfoChanged.connect(this.updateLinks, this); } - private async updateLinks(): Promise { - if (!this._input.isValid) { + private async updateLinks( + _: URLInput, + change: URLInput.IChangedArgs + ): Promise { + if (!change.newValue.isActive) { this.update(); return; } const result = await Private.getItems( - this._input.url, + this._input.urlInfo.effectiveUrl || this._input.urlInfo.url, this._serverSettings ); this._items = result || DaskDashboardLauncher.DEFAULT_ITEMS; @@ -154,7 +175,7 @@ export class DaskDashboardLauncher extends Widget { /** * Handle an update request. */ - protected onUpdateRequest(msg: Message): void { + protected onUpdateRequest(): void { // Don't bother if the sidebar is not visible if (!this.isVisible) { return; @@ -163,7 +184,7 @@ export class DaskDashboardLauncher extends Widget { ReactDOM.render( , this._dashboard.node @@ -173,7 +194,7 @@ export class DaskDashboardLauncher extends Widget { /** * Rerender after showing. */ - protected onAfterShow(msg: Message): void { + protected onAfterShow(): void { this.update(); } @@ -240,20 +261,16 @@ export class URLInput extends Widget { * as it first checks to see whether the url is pointing * at a valid dask dashboard server. */ - get url(): string { - return this._url; - } set url(newValue: string) { this._input.value = newValue; - const oldValue = this._url; - if (newValue === oldValue) { + const oldValue = this._urlInfo; + if (newValue === oldValue.url) { return; } void Private.testDaskDashboard(newValue, this._serverSettings).then( result => { - this._url = newValue; - this._isValid = result; - this._urlChanged.emit({ isValid: result, oldValue, newValue }); + this._urlInfo = result; + this._urlChanged.emit({ oldValue, newValue: result }); this._input.blur(); this.update(); if (!result) { @@ -266,16 +283,17 @@ export class URLInput extends Widget { } /** - * Whether the current url is pointing to a valid dask dashboard. + * The URL information for the dashboard. This should be set via the url setter, + * but read through this getter, as it brings in some extra information. */ - get isValid(): boolean { - return this._isValid; + get urlInfo(): DashboardURLInfo { + return this._urlInfo; } /** * A signal emitted when the url changes. */ - get urlChanged(): ISignal { + get urlInfoChanged(): ISignal { return this._urlChanged; } @@ -329,14 +347,14 @@ export class URLInput extends Widget { /** * Handle `after-attach` messages for the widget. */ - protected onAfterAttach(msg: Message): void { + protected onAfterAttach(): void { this._input.addEventListener('keydown', this, true); } /** * Handle `before-detach` messages for the widget. */ - protected onBeforeDetach(msg: Message): void { + protected onBeforeDetach(): void { this._input.removeEventListener('keydown', this, true); } @@ -346,39 +364,34 @@ export class URLInput extends Widget { private _startUrlCheckTimer(): void { this._poll = new Poll({ factory: async () => { - const url = this._url; + const urlInfo = this._urlInfo; // Don't bother checking if there is no url. - if (!url) { + if (!urlInfo.url) { return; } const result = await Private.testDaskDashboard( - url, + urlInfo.url, this._serverSettings ); // No change - valid case - if (result && this._isValid) { + if (result.isActive && urlInfo.isActive) { return; } // Show an error if the connection died. - if (!result && this._isValid) { - console.warn(`The connection to dask dashboard ${url} has been lost`); + if (!result.isActive && urlInfo.isActive) { + console.warn( + `The connection to dask dashboard ${urlInfo.url} has been lost` + ); } // No change - invalid case - if (!result && !this._isValid) { - // unset url - this._urlChanged.emit({ - oldValue: url, - newValue: '', - isValid: result - }); + if (!result.isActive && !urlInfo.isActive) { + return; } // Connection died or started - if (result !== this._isValid) { - this._isValid = result; + if (result.isActive !== urlInfo.isActive) { this._urlChanged.emit({ - oldValue: url, - newValue: url, - isValid: result + oldValue: urlInfo, + newValue: result }); } }, @@ -388,8 +401,7 @@ export class URLInput extends Widget { } private _urlChanged = new Signal(this); - private _url = ''; - private _isValid = false; + private _urlInfo: DashboardURLInfo = { isActive: false, url: '' }; private _input: HTMLInputElement; private _poll: Poll; private _isDisposed: boolean; @@ -405,19 +417,14 @@ export namespace URLInput { */ export interface IChangedArgs { /** - * The old url. + * The old url info. */ - oldValue: string; + oldValue: DashboardURLInfo; /** - * The new url. + * The new url info. */ - newValue: string; - - /** - * Whether the URL is pointing at a valid dask webserver. - */ - isValid: boolean; + newValue: DashboardURLInfo; } } @@ -601,10 +608,10 @@ namespace Private { /** * Test whether a given URL hosts a dask dashboard. */ - export function testDaskDashboard( + export async function testDaskDashboard( url: string, settings: ServerConnection.ISettings - ): Promise { + ): Promise { url = normalizeDashboardUrl(url, settings.baseUrl); // If this is a url that we are proxying under the notebook server, @@ -616,37 +623,32 @@ namespace Private { settings ).then(response => { if (response.status === 200) { - return true; + return { + url, + isActive: true + }; } else { - return false; + return { + url, + isActive: false + }; } }); } - return new Promise(resolve => { - // Hack Alert! We would like to test whether a given URL is actually - // a dask dashboard, since we will be iframe-ing it sight-unseen. - // However, CORS policies prevent us from doing a normal fetch - // to an arbitrary URL. We *can*, however, request an image from - // an arbitrary location. So let's request the dask logo from the - // bokeh server statics directory and check whether that was successful. - // - // If the logo ever moves or changes names, or if there is a different - // server with an identical file path, then this will fail. - let logoUrl = URLExt.join(url, 'statics/images/dask-logo.svg'); - // Bust caching for src attr - // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache - logoUrl += (/\?/.test(logoUrl) ? '&' : '?') + new Date().getTime(); - - let img = document.createElement('img'); - img.onload = () => { - resolve(true); - }; - img.onerror = () => { - resolve(false); - }; - img.src = logoUrl; - }); + const response = await ServerConnection.makeRequest( + URLExt.join( + settings.baseUrl, + 'dask', + 'dashboard-check', + encodeURIComponent(url) + ), + {}, + settings + ); + const info = (await response.json()) as DashboardURLInfo; + console.log(info); + return info; } export function createInactivePanel(): HTMLElement { diff --git a/src/index.ts b/src/index.ts index 0e7c938..8218166 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,30 +178,31 @@ async function activate( const input = sidebar.dashboardLauncher.input; // Update the urls of open dashboards. tracker.forEach(widget => { - if (!input.isValid) { + if (!input.urlInfo.isActive) { widget.dashboardUrl = ''; widget.active = false; return; } - widget.dashboardUrl = input.url; + widget.dashboardUrl = input.urlInfo.effectiveUrl || input.urlInfo.url; widget.active = true; }); }; - sidebar.dashboardLauncher.input.urlChanged.connect(async (sender, args) => { + sidebar.dashboardLauncher.input.urlInfoChanged.connect(async (_, args) => { updateDashboards(); // Save the current url to the state DB so it can be - // reloaded on refresh. + // reloaded on refresh. Save url instead of effectiveUrl to continue + // showing user intent. const active = sidebar.clusterManager.activeCluster; return state.save(id, { - url: args.newValue, + url: args.newValue.url, cluster: active ? active.id : '' }); }); sidebar.clusterManager.activeClusterChanged.connect(async () => { const active = sidebar.clusterManager.activeCluster; return state.save(id, { - url: sidebar.dashboardLauncher.input.url, + url: sidebar.dashboardLauncher.input.urlInfo.url, cluster: active ? active.id : '' }); }); @@ -243,7 +244,7 @@ async function activate( // A function to inject a dask client when a new session owner is added. const injectOnWidgetAdded = ( - sender: IWidgetTracker, + _: IWidgetTracker, widget: SessionOwner ) => { widget.sessionContext.statusChanged.connect(injectOnSessionStatusChanged); @@ -309,7 +310,10 @@ async function activate( const state = res[1] as { url?: string; cluster?: string } | undefined; const url = state ? state.url : ''; const cluster = state ? state.cluster : ''; - if (url && !sidebar.dashboardLauncher.input.url) { + const dashboardUrl = + sidebar.dashboardLauncher.input.urlInfo.effectiveUrl || + sidebar.dashboardLauncher.input.urlInfo.url; + if (url && !dashboardUrl) { // If there is a URL in the statedb, let it have priority. sidebar.dashboardLauncher.input.url = url; } else { @@ -341,8 +345,9 @@ async function activate( caption: 'Launch a Dask dashboard', execute: args => { // Construct the url for the dashboard. - const dashboardUrl = sidebar.dashboardLauncher.input.url; - const active = sidebar.dashboardLauncher.input.isValid; + const urlInfo = sidebar.dashboardLauncher.input.urlInfo; + const dashboardUrl = urlInfo.effectiveUrl || urlInfo.url; + const active = urlInfo.isActive; const dashboardItem = args as IDashboardItem; // If we already have a dashboard open to this url, activate it @@ -512,7 +517,7 @@ namespace Private { store_history: false, code }; - return new Promise((resolve, reject) => { + return new Promise((resolve, _) => { const future = kernel.requestExecute(content); future.onIOPub = msg => { if (msg.header.msg_type !== 'display_data') { @@ -540,7 +545,7 @@ client = Client()`; store_history: false, code }; - return new Promise((resolve, reject) => { + return new Promise((resolve, _) => { const future = kernel.requestExecute(content); future.onIOPub = msg => { if (msg.header.msg_type !== 'display_data') { From b7134558d46a036ac7d6bc17a7b792c74ca02a88 Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 12:06:01 -0800 Subject: [PATCH 3/6] Always get individual-plots from the bokeh server. --- dask_labextension/dashboardhandler.py | 18 ++++++++- src/dashboard.tsx | 55 ++++++++++++--------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/dask_labextension/dashboardhandler.py b/dask_labextension/dashboardhandler.py index 88bab54..3fbb505 100644 --- a/dask_labextension/dashboardhandler.py +++ b/dask_labextension/dashboardhandler.py @@ -27,15 +27,31 @@ async def get(self, url) -> None: 200 code, any errors are presumed to result from an invalid/inactive dashboard. """ try: - url = _normalize_dashboard_link(parse.unquote(url), self.request) client = httpclient.AsyncHTTPClient() + + # Check the user-provided url, following any redirects. + url = _normalize_dashboard_link(parse.unquote(url), self.request) response = await client.fetch(url) effective_url = response.effective_url if response.effective_url != url else None + + # Fetch the individual plots + individual_plots_response = await client.fetch( + url_path_join( + _normalize_dashboard_link(effective_url, self.request), + "individual-plots.json" + ) + ) + # If we didn't get individual plots, it may not be a dask dashboard + if individual_plots_response.code != 200: + raise ValueError("Does not seem to host a dask dashboard") + individual_plots = json.loads(individual_plots_response.body) + self.set_status(200) self.finish(json.dumps({ "url": url, "isActive": response.code == 200, "effectiveUrl": effective_url, + "plots": individual_plots, })) except: self.set_status(200) diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 5575793..df17079 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -35,6 +35,11 @@ export type DashboardURLInfo = { * A new URL to use after redirects or proxies. */ effectiveUrl?: string; + + /** + * A mapping from individual dashboard plot names to their sub-path. + */ + plots: { [name: string]: string }; }; /** @@ -150,11 +155,11 @@ export class DaskDashboardLauncher extends Widget { this.update(); return; } - const result = await Private.getItems( + const result = await Private.getDashboardPlots( this._input.urlInfo.effectiveUrl || this._input.urlInfo.url, this._serverSettings ); - this._items = result || DaskDashboardLauncher.DEFAULT_ITEMS; + this._items = result; this.update(); } @@ -401,7 +406,7 @@ export class URLInput extends Widget { } private _urlChanged = new Signal(this); - private _urlInfo: DashboardURLInfo = { isActive: false, url: '' }; + private _urlInfo: DashboardURLInfo = { isActive: false, url: '', plots: {} }; private _input: HTMLInputElement; private _poll: Poll; private _isDisposed: boolean; @@ -576,33 +581,19 @@ namespace Private { /** * Return the json result of /individual-plots.json */ - export async function getItems( + export async function getDashboardPlots( url: string, settings: ServerConnection.ISettings ): Promise { - // We can't fetch items from a non-local URL due to CORS policies. - if (!isLocal(url)) { - return DaskDashboardLauncher.DEFAULT_ITEMS; - } - url = normalizeDashboardUrl(url, settings.baseUrl); - const response = await ServerConnection.makeRequest( - URLExt.join(url, 'individual-plots.json'), - {}, - settings - ); - if (response.status === 200) { - let result = await response.json(); - const newItems: IDashboardItem[] = []; - for (let key in result) { - let label = key.replace('Individual ', ''); - let route = String(result[key]); - let item = { route: route, label: label, key: label }; - newItems.push(item); - } - return newItems; - } else { - return []; + const info = await testDaskDashboard(url, settings); + const plots: IDashboardItem[] = []; + for (let key in info.plots) { + const label = key.replace('Individual ', ''); + const route = String(info.plots[key]); + const plot = { route: route, label: label, key: label }; + plots.push(plot); } + return plots; } /** @@ -615,22 +606,25 @@ namespace Private { url = normalizeDashboardUrl(url, settings.baseUrl); // If this is a url that we are proxying under the notebook server, - // it is easier to check for a valid dashboard. + // check for the individual charts directly. if (url.indexOf(settings.baseUrl) === 0) { return ServerConnection.makeRequest( URLExt.join(url, 'individual-plots.json'), {}, settings - ).then(response => { + ).then(async response => { if (response.status === 200) { + const plots = (await response.json()) as { [plot: string]: string }; return { url, - isActive: true + isActive: true, + plots }; } else { return { url, - isActive: false + isActive: false, + plots: {} }; } }); @@ -647,7 +641,6 @@ namespace Private { settings ); const info = (await response.json()) as DashboardURLInfo; - console.log(info); return info; } From 6eb77b6585819c61ee2d93e5cb1d399fdaccd534 Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 13:01:39 -0800 Subject: [PATCH 4/6] Clean up control flow a little bit when getting dashboard listing from bokeh server. --- dask_labextension/dashboardhandler.py | 1 + src/dashboard.tsx | 18 ++++-------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/dask_labextension/dashboardhandler.py b/dask_labextension/dashboardhandler.py index 3fbb505..996feba 100644 --- a/dask_labextension/dashboardhandler.py +++ b/dask_labextension/dashboardhandler.py @@ -43,6 +43,7 @@ async def get(self, url) -> None: ) # If we didn't get individual plots, it may not be a dask dashboard if individual_plots_response.code != 200: + self.log.warn(f"{url} does not seem to host a dask dashboard") raise ValueError("Does not seem to host a dask dashboard") individual_plots = json.loads(individual_plots_response.body) diff --git a/src/dashboard.tsx b/src/dashboard.tsx index df17079..9d84137 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -144,21 +144,15 @@ export class DaskDashboardLauncher extends Widget { this.addClass('dask-DaskDashboardLauncher'); this._items = options.items || DaskDashboardLauncher.DEFAULT_ITEMS; this._launchItem = options.launchItem; - this._input.urlInfoChanged.connect(this.updateLinks, this); + this._input.urlInfoChanged.connect(this._updateLinks, this); } - private async updateLinks( - _: URLInput, - change: URLInput.IChangedArgs - ): Promise { + private _updateLinks(_: URLInput, change: URLInput.IChangedArgs): void { if (!change.newValue.isActive) { this.update(); return; } - const result = await Private.getDashboardPlots( - this._input.urlInfo.effectiveUrl || this._input.urlInfo.url, - this._serverSettings - ); + const result = Private.getDashboardPlots(change.newValue); this._items = result; this.update(); } @@ -581,11 +575,7 @@ namespace Private { /** * Return the json result of /individual-plots.json */ - export async function getDashboardPlots( - url: string, - settings: ServerConnection.ISettings - ): Promise { - const info = await testDaskDashboard(url, settings); + export function getDashboardPlots(info: DashboardURLInfo): IDashboardItem[] { const plots: IDashboardItem[] = []; for (let key in info.plots) { const label = key.replace('Individual ', ''); From 3f8e56d6b226f26f4f9eb3180dcea1ebec1ec18b Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 14:00:35 -0800 Subject: [PATCH 5/6] Handle dynamic changes to the listing of dashboard charts a bit better. Close nonexistent panes, rename ones that have a different label. --- src/index.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 8218166..6724d56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -176,13 +176,30 @@ async function activate( const updateDashboards = () => { const input = sidebar.dashboardLauncher.input; + const dashboards = sidebar.dashboardLauncher.items; // Update the urls of open dashboards. tracker.forEach(widget => { + // Identify the dashboard item associated with the widget + const dashboard = dashboards.find(d => widget.item?.route === d.route); + + // If the dashboard item doesn't exist in the new listing, close the pane. + if (!dashboard) { + widget.dispose(); + return; + } + + // Possibly update the name of the existing dashboard pane. + if (`Dask ${dashboard.label}` !== widget.title.label) { + widget.title.label = `Dask ${dashboard.label}`; + } + + // If the dashboard server is inactive, mark it as such. if (!input.urlInfo.isActive) { widget.dashboardUrl = ''; widget.active = false; return; } + widget.dashboardUrl = input.urlInfo.effectiveUrl || input.urlInfo.url; widget.active = true; }); @@ -436,7 +453,7 @@ async function activate( app.commands.addCommand(CommandIDs.toggleAutoStartClient, { label: 'Auto-Start Dask', isToggled: () => autoStartClient, - execute: () => { + execute: async () => { const value = !autoStartClient; const key = 'autoStartClient'; return settingRegistry From 14dbd4f118098dd21b0da3f843fde636774761a6 Mon Sep 17 00:00:00 2001 From: Ian Rose Date: Wed, 9 Dec 2020 14:58:45 -0800 Subject: [PATCH 6/6] Minor cleanup. --- dask_labextension/dashboardhandler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dask_labextension/dashboardhandler.py b/dask_labextension/dashboardhandler.py index 996feba..09aa59e 100644 --- a/dask_labextension/dashboardhandler.py +++ b/dask_labextension/dashboardhandler.py @@ -37,13 +37,12 @@ async def get(self, url) -> None: # Fetch the individual plots individual_plots_response = await client.fetch( url_path_join( - _normalize_dashboard_link(effective_url, self.request), + _normalize_dashboard_link(effective_url or url, self.request), "individual-plots.json" ) ) # If we didn't get individual plots, it may not be a dask dashboard if individual_plots_response.code != 200: - self.log.warn(f"{url} does not seem to host a dask dashboard") raise ValueError("Does not seem to host a dask dashboard") individual_plots = json.loads(individual_plots_response.body) @@ -55,10 +54,12 @@ async def get(self, url) -> None: "plots": individual_plots, })) except: + self.log.warn(f"{url} does not seem to host a dask dashboard") self.set_status(200) self.finish(json.dumps({ "url": url, "isActive": False, + "plots": {}, }))