forked from jupyter-server/jupyter_server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
External Links Extension (jupyter-server#148)
* WIP: initial extension implementation * add server extension * link frontend and server extension * refactor JS code * disconnect the signal properly * clean up and add testing * fix disconnect * formatting Co-authored-by: Steven Silvester <ssilvester@apple.com>
- Loading branch information
1 parent
b4f8448
commit acf5c8d
Showing
15 changed files
with
1,353 additions
and
3,453 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
data_studio_jupyter_extensions/extensions/external_links/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from data_studio_jupyter_extensions.extensions.external_links.extension import ( | ||
ExternalLinksExtension, | ||
) | ||
|
||
|
||
def _jupyter_server_extension_points(): # pragma: no cover | ||
return [ | ||
{ | ||
"module": "data_studio_jupyter_extensions.extensions.external_links.extension", | ||
"app": ExternalLinksExtension, | ||
} | ||
] |
12 changes: 12 additions & 0 deletions
12
data_studio_jupyter_extensions/extensions/external_links/extension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from jupyter_server.extension.application import ExtensionApp | ||
|
||
from .handlers import handlers | ||
|
||
|
||
class ExternalLinksExtension(ExtensionApp): | ||
"""Jupyter Server extension that verifies | ||
the health of the server. | ||
""" | ||
|
||
name = "external_links" | ||
handlers = handlers |
23 changes: 23 additions & 0 deletions
23
data_studio_jupyter_extensions/extensions/external_links/handlers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
"""Application Heartbeat for PIE Health check""" | ||
import json | ||
|
||
from jupyter_server.base.handlers import JupyterHandler | ||
from jupyter_server.services.kernels.handlers import _kernel_id_regex | ||
|
||
from data_studio_jupyter_extensions.base_handler import DSExtensionHandlerMixin | ||
|
||
|
||
class ExternalLinksHandler(DSExtensionHandlerMixin, JupyterHandler): | ||
async def get(self, kernel_id): | ||
# Look up kernel. | ||
km = self.kernel_manager.get_kernel(kernel_id) | ||
# Get the process ID to hand off to the notebook_service. | ||
process_id = km.provisioner.process_id | ||
resp = await self.nbservice_client.get_external_links_for_kernel(process_id) | ||
data = json.dumps(resp) | ||
self.finish(data) | ||
|
||
|
||
handlers = [ | ||
(rf"/api/kernels/{_kernel_id_regex}/links", ExternalLinksHandler), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
data_studio_jupyter_extensions/tests/extensions/test_external_links.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import json | ||
|
||
import pytest | ||
from jupyter_client.provisioning.provisioner_base import KernelProvisionerBase | ||
|
||
from data_studio_jupyter_extensions.tests.mock.client import MockNotebookServiceClient | ||
|
||
|
||
@pytest.fixture | ||
def provisioner_process_id_patch(monkeypatch): | ||
"""The tests use a LocalProvisioner, which won't have a process_id attribute. | ||
We'll temporarily patch in this attribute here. | ||
""" | ||
monkeypatch.setattr( | ||
KernelProvisionerBase, "process_id", "testprocess", raising=False | ||
) | ||
|
||
|
||
@pytest.fixture | ||
def jp_server_config(datastudio_env): | ||
config = { | ||
"ServerApp": { | ||
"jpserver_extensions": { | ||
"data_studio_jupyter_extensions": True, | ||
"data_studio_jupyter_extensions.extensions.telemetry": True, | ||
"data_studio_jupyter_extensions.extensions.heartbeat": True, | ||
"data_studio_jupyter_extensions.extensions.external_links": True, | ||
"nbclassic": True, | ||
}, | ||
}, | ||
# Use the Mock Notebook Service Client. | ||
"DataStudioJupyterExtensions": { | ||
"nbservice_client_class": MockNotebookServiceClient | ||
}, | ||
} | ||
return config | ||
|
||
|
||
async def test_endpoint(clear_singletons, provisioner_process_id_patch, jp_fetch): | ||
# Start a kernel to use. | ||
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True) | ||
kernel_id = json.loads(r.body.decode())["id"] | ||
external_links_url = f"/api/kernels/{kernel_id}/links" | ||
resp = await jp_fetch(external_links_url) | ||
data = json.loads(resp.body) | ||
assert resp.code == 200 | ||
for item in data: | ||
assert "label" in item | ||
assert "description" in item or "details" in item | ||
assert "url" in item |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { Menu } from '@lumino/widgets'; | ||
import { Signal } from '@lumino/signaling'; | ||
import { INotebookTracker } from '@jupyterlab/notebook'; | ||
import { ISessionContext } from '@jupyterlab/apputils'; | ||
import { IMainMenu } from '@jupyterlab/mainmenu'; | ||
import { | ||
JupyterFrontEnd, | ||
JupyterFrontEndPlugin | ||
} from '@jupyterlab/application'; | ||
import { requestAPI } from './handler'; | ||
|
||
class ExternalLinksMenu { | ||
// Notebook witch | ||
app: JupyterFrontEnd; | ||
menu: Menu; | ||
|
||
constructor(app: JupyterFrontEnd, mainmenu: IMainMenu) { | ||
this.app = app; | ||
this.menu = new Menu({ commands: this.app.commands }); | ||
this.menu.title.label = 'External Links'; | ||
mainmenu.addMenu(this.menu, { rank: 4000 }); | ||
} | ||
|
||
registerCommand(name: string, enabled: boolean) { | ||
if (this.app.commands.hasCommand(name)) { | ||
return; | ||
} | ||
|
||
this.app.commands.addCommand(name, { | ||
label: args => args['details'] as string, | ||
isEnabled: () => enabled, | ||
isVisible: () => true, | ||
execute: args => { | ||
const url = args['url'] as string; | ||
window.open(url); | ||
return; | ||
} | ||
}); | ||
|
||
this.app.commands.notifyCommandChanged(name); | ||
} | ||
|
||
async addLinks(kernel_id: string | undefined) { | ||
this.menu.clearItems(); | ||
if (kernel_id) { | ||
// Hit the server extension to get the links. | ||
const url = '/api/kernels/' + kernel_id + '/links'; | ||
const links: any = await requestAPI(url); | ||
|
||
// Define a new command for this link. | ||
for (const link of links) { | ||
const openExternalLink = 'externallinks:' + link['label']; | ||
|
||
if (!this.app.commands.hasCommand(openExternalLink)) { | ||
this.registerCommand(openExternalLink, true); | ||
} | ||
|
||
// Add an item to the menu that executes the above command. | ||
this.menu.addItem({ | ||
command: openExternalLink, | ||
args: link | ||
}); | ||
} | ||
} else { | ||
this.registerCommand('externallinks:none', false); | ||
this.menu.addItem({ | ||
command: 'externallinks:none', | ||
args: { details: 'No Links Found', url: '/' } | ||
}); | ||
} | ||
} | ||
|
||
onKernelChange(sender: ISessionContext) { | ||
// Clear the menu of previous content. | ||
// Get the current menu item. | ||
const kernel_id = sender.session?.kernel?.id; | ||
// Populate the menu. | ||
this.addLinks(kernel_id); | ||
} | ||
|
||
onNotebookChange(sender: INotebookTracker) { | ||
// Get current notebook. | ||
const current = sender.currentWidget; | ||
// If there is no notebook open, return null. | ||
if (!current) { | ||
return; | ||
} | ||
// Get the current menu item. | ||
const kernel_id = current.sessionContext.session?.kernel?.id; | ||
// Populate the menu. | ||
this.addLinks(kernel_id); | ||
// Add a signal that looks for kernel changes (after removing any old signals). | ||
Signal.disconnectReceiver(this.onKernelChange); | ||
current.sessionContext.kernelChanged.connect(this.onKernelChange, this); | ||
} | ||
} | ||
|
||
/** | ||
* Build an external links menu item. | ||
*/ | ||
export const ExternalLinksPlugin: JupyterFrontEndPlugin<void> = { | ||
id: 'data_studio:external_links_plugin', | ||
autoStart: true, | ||
requires: [IMainMenu, INotebookTracker], | ||
activate: ( | ||
app: JupyterFrontEnd, | ||
mainmenu: IMainMenu, | ||
notebookTracker: INotebookTracker | ||
) => { | ||
console.log('JupyterLab extension external links is activated!'); | ||
// Build an "External Links" menu. | ||
const menu = new ExternalLinksMenu(app, mainmenu); | ||
// Populate that menu when it changes. | ||
notebookTracker.currentChanged.connect(menu.onNotebookChange, menu); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.