Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable multiple launchers (with paths) #267

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ def _load_jupyter_server_extension(nbapp):
launcher_entries = []
icons = {}
for sp in server_processes:
if sp.launcher_entry.enabled and sp.launcher_entry.icon_path:
icons[sp.name] = sp.launcher_entry.icon_path
for le in sp.launcher_entries:
if le.enabled and le.icon_path:
icons[(sp.name, le.name)] = le.icon_path


nbapp.web_app.add_handlers('.*', [
(ujoin(base_url, 'server-proxy/servers-info'), ServersInfoHandler, {'server_processes': server_processes}),
Expand Down
28 changes: 18 additions & 10 deletions jupyter_server_proxy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ async def get(self):
# Pick out and send only metadata
# Don't send anything that might be a callable, or leak sensitive info
for sp in self.server_processes:
# Manually recurse to convert namedtuples into JSONable structures
item = {
'name': sp.name,
'launcher_entry': {
'enabled': sp.launcher_entry.enabled,
'title': sp.launcher_entry.title
},
'new_browser_tab' : sp.new_browser_tab
'launcher_entries': []
}
if sp.launcher_entry.icon_path:
icon_url = ujoin(self.base_url, 'server-proxy', 'icon', sp.name)
item['launcher_entry']['icon_url'] = icon_url

for le in sp.launcher_entries:
litem = {
'enabled': le.enabled,
'title': le.title,
'new_browser_tab': le.new_browser_tab,
'path': le.path
}

# Manually recurse to convert namedtuples into JSONable structures
if le.icon_path:
icon_url = ujoin(self.base_url, 'server-proxy', 'icon', sp.name, le.name)
litem['icon_url'] = icon_url

item['launcher_entries'].append(litem)

data.append(item)

Expand All @@ -44,6 +51,7 @@ def initialize(self, icons):
self.icons = icons

async def get(self, name):
name = tuple(name.split("/", 1))
if name not in self.icons:
raise web.HTTPError(404)
path = self.icons[name]
Expand All @@ -64,6 +72,6 @@ async def get(self, name):
else:
content_type = "application/octet-stream"

with open(self.icons[name]) as f:
with open(self.icons[name], "rb") as f:
self.write(f.read())
self.set_header('Content-Type', content_type)
56 changes: 40 additions & 16 deletions jupyter_server_proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,13 @@ def make_handlers(base_url, server_processes):
))
return handlers

LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title'])
LauncherEntry = namedtuple('LauncherEntry', ['enabled', 'icon_path', 'title', 'name', 'new_browser_tab', 'path'])
ServerProcess = namedtuple('ServerProcess', [
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'mappath', 'launcher_entry', 'new_browser_tab', 'request_headers_override'])
'name', 'command', 'environment', 'timeout', 'absolute_url', 'port', 'mappath', 'launcher_entries', 'request_headers_override'])



def make_server_process(name, server_process_config):
le = server_process_config.get('launcher_entry', {})
return ServerProcess(
name=name,
command=server_process_config['command'],
Expand All @@ -120,15 +121,27 @@ def make_server_process(name, server_process_config):
absolute_url=server_process_config.get('absolute_url', False),
port=server_process_config.get('port', 0),
mappath=server_process_config.get('mappath', {}),
launcher_entry=LauncherEntry(
enabled=le.get('enabled', True),
icon_path=le.get('icon_path'),
title=le.get('title', name)
),
new_browser_tab=server_process_config.get('new_browser_tab', True),
launcher_entries=[*make_launcher_entries(name, server_process_config)],
request_headers_override=server_process_config.get('request_headers_override', {})
)


def make_launcher_entries(name, server_process_config):
le = server_process_config.get('launcher_entry', {})
les = [le] if isinstance(le, dict) else le
new_browser_tab = server_process_config.get('new_browser_tab', True)

for le in les:
yield LauncherEntry(
name=le.get('name', 'default'),
enabled=le.get('enabled', True),
icon_path=le.get('icon_path'),
title=le.get('title', le.get('name', name)),
path=le.get('path', '/'),
new_browser_tab=le.get('new_browser_tab', new_browser_tab)
)


class ServerProxy(Configurable):
servers = Dict(
{},
Expand Down Expand Up @@ -167,8 +180,17 @@ class ServerProxy(Configurable):
Either a dictionary of request paths to proxied paths,
or a callable that takes parameter ``path`` and returns the proxied path.

new_browser_tab
Set to True (default) to make the proxied server interface opened as a new browser tab. Set to False
to have it open a new JupyterLab tab. This has no effect in classic notebook.

request_headers_override
A dictionary of additional HTTP headers for the proxy request. As with
the command traitlet, {{port}} and {{base_url}} will be substituted.

launcher_entry
A dictionary of various options for entries in classic notebook / jupyterlab launchers.
May also be a list of the same, where each must have a ``name`` key.

Keys recognized are:

Expand All @@ -178,18 +200,20 @@ class ServerProxy(Configurable):

icon_path
Full path to an svg icon that could be used with a launcher. Currently only used by the
JupyterLab launcher
JupyterLab launcher.

title
Title to be used for the launcher entry. Defaults to the name of the server if missing.

new_browser_tab
Set to True (default) to make the proxied server interface opened as a new browser tab. Set to False
to have it open a new JupyterLab tab. This has no effect in classic notebook.
new_browser_tab
Override the default tab behavior from the root config.

name:
A name (default: default) used for constructing URLs and DOM ids for the launcher

path:
A path (default: /) to open. May include ``?`` params and ``#`` fragments.

request_headers_override
A dictionary of additional HTTP headers for the proxy request. As with
the command traitlet, {{port}} and {{base_url}} will be substituted.
""",
config=True
)
Expand Down
47 changes: 24 additions & 23 deletions jupyter_server_proxy/static/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,34 @@ define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, Jupyter, ut
.attr('role', 'presentation')
.addClass('divider');


/* add the divider */
$menu.append($divider);

$.each(data.server_processes, function(_, server_process) {
if (!server_process.launcher_entry.enabled) {
return;
}

/* create our list item */
var $entry_container = $('<li>')
.attr('role', 'presentation')
.addClass('new-' + server_process.name);

/* create our list item's link */
var $entry_link = $('<a>')
.attr('role', 'menuitem')
.attr('tabindex', '-1')
.attr('href', base_url + server_process.name + '/')
.attr('target', '_blank')
.text(server_process.launcher_entry.title);

/* add the link to the item and
* the item to the menu */
$entry_container.append($entry_link);
$menu.append($entry_container);

$.each(server_process.launcher_entries, function(_, launcher_entry) {
if (!launcher_entry.enabled) {
return;
}

/* create our list item */
var $entry_container = $('<li>')
.attr('role', 'presentation')
.addClass('new-' + server_process.name + '-' + launcher_entry.name);

/* create our list item's link */
var $entry_link = $('<a>')
.attr('role', 'menuitem')
.attr('tabindex', '-1')
.attr('href', base_url + server_process.name + launcher_entry.path)
.attr('target', '_blank')
.text(launcher_entry.title);

/* add the link to the item and
* the item to the menu */
$entry_container.append($entry_link);
$menu.append($entry_container);
});
});
});
}
Expand Down
10 changes: 6 additions & 4 deletions jupyterlab-server-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,20 @@
"install:extension": "jupyter labextension develop --overwrite .",
"prepare": "jlpm run clean && jlpm run build:prod",
"watch": "run-p watch:src watch:labextension",
"watch:src": "tsc -w",
"watch:src": "tsc -w --preserveWatchOutput",
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@jupyterlab/application": "^2.0.0 || ^3.0.0",
"@jupyterlab/apputils": "^2.0.0 || ^3.0.0",
"@jupyterlab/launcher": "^2.0.0 || ^3.0.0"
"@jupyterlab/launcher": "^2.0.0 || ^3.0.0",
"@jupyterlab/mainmenu": "^2.0.0 || ^3.0.0"
},
"devDependencies": {
"@jupyterlab/builder": "^3.0.2",
"rimraf": "^2.6.1",
"typescript": "~3.7.0"
"rimraf": "^3.0.2",
"typescript": "~4.2.4",
"npm-run-all": "~4.1.5"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes watch work as advertised. the typescript/rimraf stuff is just a ride-along.

},
"jupyterlab": {
"extension": true,
Expand Down
83 changes: 58 additions & 25 deletions jupyterlab-server-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Menu } from '@lumino/widgets';

import { JupyterFrontEnd, JupyterFrontEndPlugin, ILayoutRestorer } from '@jupyterlab/application';
import { ILauncher } from '@jupyterlab/launcher';
import { PageConfig } from '@jupyterlab/coreutils';
import { IFrame, MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils';
import { ICommandPalette, IFrame, MainAreaWidget, WidgetTracker } from '@jupyterlab/apputils';
import { IMainMenu } from '@jupyterlab/mainmenu';

import '../style/index.css';

Expand All @@ -19,7 +22,13 @@ function newServerProxyWidget(id: string, url: string, text: string): MainAreaWi
return widget;
}

async function activate(app: JupyterFrontEnd, launcher: ILauncher, restorer: ILayoutRestorer) : Promise<void> {
async function activate(
app: JupyterFrontEnd,
launcher?: ILauncher,
restorer?: ILayoutRestorer,
palette?: ICommandPalette,
mainMenu?: IMainMenu
) : Promise<void> {
const response = await fetch(PageConfig.getBaseUrl() + 'server-proxy/servers-info');
if (!response.ok) {
console.log('Could not fetch metadata about registered servers. Make sure jupyter-server-proxy is installed.');
Expand Down Expand Up @@ -75,30 +84,54 @@ async function activate(app: JupyterFrontEnd, launcher: ILauncher, restorer: ILa
}
});

const menuItems: Menu.IItemOptions[] = [];

for (let server_process of data.server_processes) {
if (!server_process.launcher_entry.enabled) {
continue;
}
for (let launcher_entry of server_process.launcher_entries) {
if (!launcher_entry.enabled) {
continue;
}

const url = PageConfig.getBaseUrl() + server_process.name + '/';
const title = server_process.launcher_entry.title;
const newBrowserTab = server_process.new_browser_tab;
const id = namespace + ':' + server_process.name;
const launcher_item : ILauncher.IItemOptions = {
command: command,
args: {
url: url,
title: title + (newBrowserTab ? ' [↗]': ''),
newBrowserTab: newBrowserTab,
id: id
},
category: 'Notebook'
};

if (server_process.launcher_entry.icon_url) {
launcher_item.kernelIconUrl = server_process.launcher_entry.icon_url;
const url = PageConfig.getBaseUrl() + server_process.name + launcher_entry.path;
const newBrowserTab = launcher_entry.new_browser_tab;
const title = launcher_entry.title + (newBrowserTab ? ' [↗]': '');
const id = namespace + ':' + server_process.name + ':' + launcher_entry.name;
const launcher_item : ILauncher.IItemOptions = {
command: command,
args: { url, title, newBrowserTab, id },
category: 'Notebook'
};

if (launcher_entry.icon_url) {
launcher_item.kernelIconUrl = launcher_entry.icon_url;
}

if (launcher) {
launcher.add(launcher_item);
}

if (palette) {
palette.addItem({
command,
args: {
...launcher_item.args,
title: `Launch ${title}`
},
category: 'Server Proxies'
});
}

if (mainMenu) {
menuItems.push({
command,
args: launcher_item.args
});
}
}
launcher.add(launcher_item);
}

if (mainMenu && menuItems) {
mainMenu.fileMenu.newMenu.addGroup(menuItems);
}
}

Expand All @@ -108,8 +141,8 @@ async function activate(app: JupyterFrontEnd, launcher: ILauncher, restorer: ILa
const extension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-server-proxy',
autoStart: true,
requires: [ILauncher, ILayoutRestorer],
activate: activate
optional: [ILauncher, ILayoutRestorer, ICommandPalette, IMainMenu],
activate
};

export default extension;