This tutorial will show you how to create a standalone JupyterLab application using the public Jupyter components and a standard notebook server. At the end of this tutorial you will have a notebook server for serving notebooks and executing notebook commands and your own custom web application for visualising and editing a notebook along with an interactive terminal.
There are reasons you might want to do this, but one reason is you might already have an existing application where you would like to integrate the JupyterLab notebook interface / widgets.
Let's get started!
For this tutorial you'll need to the following tools:
- NodeJs
- Python
- Virtualenv for managing Python environments
Notebook server
We will be using the standard Jupyter notebook server.
Start off by installing virtualenv. virtualenv is a tool to create isolated Python environments. You could also use conda
Make sure you have pip installed:
sudo apt install python3-pip
Then install virtualenv using pip3:
sudo pip3 install virtualenv
Create a new virtualenv inside a directory of your choice:
virtualenv venv
Activate the new environment:
source venc/bin/activate
Now we need to install the jupyter dependency
pip install jupyter
Inside the same directory where you created the virtualenv environment, create a new folder called notebooks. We will also create another file called main.py with the following content:
from notebook.notebookapp import NotebookApp
if __name__ == '__main__':
# Allow CORS requests from this origin
NotebookApp.allow_origin = 'http://localhost:9000'
# Path to the location of the notebooks
NotebookApp.notebook_dir = 'notebooks'
# The authentication token
NotebookApp.token = 'abc'
# Don't open the browser when launching the notebook server
NotebookApp.open_browser = False
# Start the server
NotebookApp.launch_instance()
http://localhost:9000 will be the URL to the application that we will develop later on in this tutorial
Now let's grab an example notebook from the official Jupyter repository and place it into our notebooks directory:
wget -O notebooks/notebook.ipynb wget -O notebooks/notebook.ipynb https://raw.githubusercontent.com/jupyter/notebook/master/docs/source/examples/Notebook/Running%20Code.ipynb
See here for more official notebook examples.
Now we have an example notebook to use we can go ahead and start the notebook server:
python main.py
You should have a notebook server running at this URL: http://localhost:8888/tree?token=abc
Great! Let's move onto writing the web application.
Web application
For the web application we'll be using webpack to manage the build process and NPM to manage the dependencies. The application will be developed using Typescript and SCSS for the basic styling.
JupyterLab is built on different components and libraries. The team has made a lot of effort to decouple components/modules into individual packages and thanks to this you can pick and choose which components to you wish to use inside your application. For the layout, JupyterLab uses the phosphorjs widget library.
For this tutorial, we will be only using a few of the Jupyter components to build our application. You will end up with three panels, one for the command palette, one for the notebook and one which will give you access to a terminal.
Initialising the project
Before you begin, please make sure you have NodeJs and NPM installed
Start by creating a new directory and then inside the directory create a new project:
npm init
Answer the questions it asks and then type yes to confirm. You should now have a package.json file that we can use to start adding our dependencies.
Install the following dependencies to the dependencies section of the package.json:
"@jupyterlab/codemirror": "^1.2.0",
"@jupyterlab/completer": "^1.2.0",
"@jupyterlab/docmanager": "^1.2.0",
"@jupyterlab/docregistry": "^1.2.0",
"@jupyterlab/documentsearch": "^1.2.0",
"@jupyterlab/mathjax2": "^1.2.0",
"@jupyterlab/notebook": "^1.2.0",
"@jupyterlab/rendermime": "^1.2.0",
"@jupyterlab/services": "^4.2.0",
"@jupyterlab/theme-light-extension": "^1.2.0",
"@jupyterlab/terminal": "^1.2.0",
"@phosphor/commands": "^1.7.1",
"@phosphor/widgets": "^1.9.1"
We also need the following development dependencies, add them to the devDependencies section:
"css-loader": "^2.1.1",
"file-loader": "~1.1.11",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.13.0",
"raw-loader": "~0.5.1",
"rimraf": "~2.6.2",
"sass-loader": "^8.0.0",
"style-loader": "~0.21.0",
"ts-loader": "^6.0.4",
"typescript": "^3.6.2",
"url-loader": "~1.0.1",
"watch": "~1.0.2",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-concat-plugin": "^3.0.0",
"webpack-dev-server": "^3.2.1"
And to build to the application we'll need to add some scripts inside the scripts section:
"build": "webpack",
"clean": "rimraf build",
"start": "webpack-dev-server -d --config webpack.config.js"
Your full package.json should look similar to this:
{
"name": "jupyter-standalone-application-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@jupyterlab/codemirror": "^1.2.0",
"@jupyterlab/completer": "^1.2.0",
"@jupyterlab/docmanager": "^1.2.0",
"@jupyterlab/docregistry": "^1.2.0",
"@jupyterlab/documentsearch": "^1.2.0",
"@jupyterlab/mathjax2": "^1.2.0",
"@jupyterlab/notebook": "^1.2.0",
"@jupyterlab/rendermime": "^1.2.0",
"@jupyterlab/services": "^4.2.0",
"@jupyterlab/theme-light-extension": "^1.2.0",
"@jupyterlab/terminal": "^1.2.0",
"@phosphor/commands": "^1.7.1",
"@phosphor/widgets": "^1.9.1"
},
"devDependencies": {
"css-loader": "^2.1.1",
"file-loader": "~1.1.11",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.13.0",
"raw-loader": "~0.5.1",
"rimraf": "~2.6.2",
"sass-loader": "^8.0.0",
"style-loader": "~0.21.0",
"ts-loader": "^6.0.4",
"typescript": "^3.6.2",
"url-loader": "~1.0.1",
"watch": "~1.0.2",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-concat-plugin": "^3.0.0",
"webpack-dev-server": "^3.2.1"
},
"scripts": {
"build": "webpack",
"clean": "rimraf build",
"start": "webpack-dev-server -d --config webpack.config.js"
},
"author": "",
"license": "ISC"
}
Now it's time to install the dependencies:
npm install
Configuring webpack
Create a new file called webpack.config.js in the root of your project with the following content:
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
context: resolve(__dirname, 'src'),
entry: {
app: ['./index.ts']
},
output: {
filename: '[hash].bundle.js',
path: resolve(__dirname, 'dist')
},
devtool: 'inline-source-map',
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
watch: false,
devServer: {
watchContentBase: false,
compress: false,
stats: {
colors: true
},
port: 9000
},
module: {
rules: [
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: 'url-loader?limit=10000&mimetype=image/svg+xml'
},
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.md$/, use: 'raw-loader' }, {
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{ test: /\.(jpg|png|gif)$/, use: 'file-loader' },
{ test: /\.js.map$/, use: 'file-loader' },
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: 'url-loader?limit=10000&mimetype=application/font-woff'
},
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: 'url-loader?limit=10000&mimetype=application/font-woff'
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: 'url-loader?limit=10000&mimetype=application/octet-stream'
},
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
]
},
plugins: [
new HtmlWebpackPlugin({
templateParameters: {
JUPYTER_BASE_URL: 'http://localhost:8888',
JUPYTER_TOKEN: 'abc',
JUPYTER_NOTEBOOK_PATH: 'notebook.ipynb',
JUPYTER_MATHJAX_URL: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js'
},
template: 'index.html',
inject: true,
})
]
};
The configuration does the following:
- Setups the context to look for the source code of our application inside the src directory
- It sets the entry point of our application to index.ts which resides inside the src directory
- When the application is built it will create an inline-source-map which is very useful for debugging
- Look for source files ending in ts, tsx or js.
- The dev server is configured so every time we make a new change to the source code, the page is reloaded with the new changes. The server will be listening on port 9000.
- Rules to parse different types of files i.e svgs, scss, and images.
- Configuration options that will be used to interact with the notebook server that we setup earlier.
Now we have finished configuring webpack, we will need to create some files so that we can start developing our web application.
In the root of your project, create a new file called tsconfig.json and add the following content:
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "es6",
"target": "es5",
"strict": false,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
}
}
The tsconfig.json file specifies the compiler options required to compile our application.
Again in the root directory, create a new folder called src. Create four new files inside this folder:
- index.ts
- index.html
- commands.ts
- index.scss
Inside index.html, add the following content:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Jupyter Notebook Standalone</title>
</head>
<body>
<!-- The jupyter components will use these configuration values -->
<script id="jupyter-config-data" type="application/json">
{
"baseUrl": "<%= JUPYTER_BASE_URL %>",
"token": "<%= JUPYTER_TOKEN %>",
"terminalsAvailable": true,
"notebookPath": "<%= JUPYTER_NOTEBOOK_PATH %>",
"mathjaxUrl": "<%= JUPYTER_MATHJAX_URL %>",
"mathjaxConfig": "TeX-AMS_CHTML-full,Safe"
}
</script>
</body>
</html>
The Jupyter configuration values that we have defined in the webpack file will be injected into the HTML file when we create a build or run the development server.
Inside the index.scss file, we need to import some JupyterLab styling and add some basic CSS rules:
@import '~@jupyterlab/application/style/index.css';
@import '~@jupyterlab/codemirror/style/index.css';
@import '~@jupyterlab/notebook/style/index.css';
@import '~@jupyterlab/theme-light-extension/style/index.css';
@import '~@jupyterlab/terminal/style/index.css';
body {
background: orange;
margin: 0;
padding: 0;
}
#main {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
bottom: 20px;
}
The commands.ts file binds keyboard commands for the Jupyter notebook.
To keep the tutorial short, I will not go into detail about what all of the commands do, however, this file is necessary :)
Add the following content to the file:
/**
* Set up keyboard shortcuts & commands for notebook
*/
import { CommandRegistry } from '@phosphor/commands';
import { CompletionHandler } from '@jupyterlab/completer';
import { NotebookPanel, NotebookActions } from '@jupyterlab/notebook';
import {
SearchInstance,
NotebookSearchProvider
} from '@jupyterlab/documentsearch';
import { CommandPalette } from '@phosphor/widgets';
/**
* The map of command ids used by the notebook.
*/
const cmdIds = {
invoke: 'completer:invoke',
select: 'completer:select',
invokeNotebook: 'completer:invoke-notebook',
selectNotebook: 'completer:select-notebook',
startSearch: 'documentsearch:start-search',
findNext: 'documentsearch:find-next',
findPrevious: 'documentsearch:find-previous',
save: 'notebook:save',
interrupt: 'notebook:interrupt-kernel',
restart: 'notebook:restart-kernel',
switchKernel: 'notebook:switch-kernel',
runAndAdvance: 'notebook-cells:run-and-advance',
deleteCell: 'notebook-cells:delete',
selectAbove: 'notebook-cells:select-above',
selectBelow: 'notebook-cells:select-below',
extendAbove: 'notebook-cells:extend-above',
extendBelow: 'notebook-cells:extend-below',
editMode: 'notebook:edit-mode',
merge: 'notebook-cells:merge',
split: 'notebook-cells:split',
commandMode: 'notebook:command-mode',
undo: 'notebook-cells:undo',
redo: 'notebook-cells:redo'
};
export const SetupCommands = (
commands: CommandRegistry,
palette: CommandPalette,
nbWidget: NotebookPanel,
handler: CompletionHandler
) => {
// Add commands.
commands.addCommand(cmdIds.invoke, {
label: 'Completer: Invoke',
execute: () => handler.invoke()
});
commands.addCommand(cmdIds.select, {
label: 'Completer: Select',
execute: () => handler.completer.selectActive()
});
commands.addCommand(cmdIds.invokeNotebook, {
label: 'Invoke Notebook',
execute: () => {
if (nbWidget.content.activeCell.model.type === 'code') {
return commands.execute(cmdIds.invoke);
}
}
});
commands.addCommand(cmdIds.selectNotebook, {
label: 'Select Notebook',
execute: () => {
if (nbWidget.content.activeCell.model.type === 'code') {
return commands.execute(cmdIds.select);
}
}
});
commands.addCommand(cmdIds.save, {
label: 'Save',
execute: () => nbWidget.context.save()
});
let searchInstance: SearchInstance;
commands.addCommand(cmdIds.startSearch, {
label: 'Find...',
execute: () => {
if (searchInstance) {
searchInstance.focusInput();
return;
}
const provider = new NotebookSearchProvider();
searchInstance = new SearchInstance(nbWidget, provider);
searchInstance.disposed.connect(() => {
searchInstance = undefined;
// find next and previous are now not enabled
commands.notifyCommandChanged();
});
// find next and previous are now enabled
commands.notifyCommandChanged();
searchInstance.focusInput();
}
});
commands.addCommand(cmdIds.findNext, {
label: 'Find Next',
isEnabled: () => !!searchInstance,
execute: async () => {
if (!searchInstance) {
return;
}
await searchInstance.provider.highlightNext();
searchInstance.updateIndices();
}
});
commands.addCommand(cmdIds.findPrevious, {
label: 'Find Previous',
isEnabled: () => !!searchInstance,
execute: async () => {
if (!searchInstance) {
return;
}
await searchInstance.provider.highlightPrevious();
searchInstance.updateIndices();
}
});
commands.addCommand(cmdIds.interrupt, {
label: 'Interrupt',
execute: async () => {
if (nbWidget.context.session.kernel) {
await nbWidget.context.session.kernel.interrupt();
}
}
});
commands.addCommand(cmdIds.restart, {
label: 'Restart Kernel',
execute: () => nbWidget.context.session.restart()
});
commands.addCommand(cmdIds.switchKernel, {
label: 'Switch Kernel',
execute: () => nbWidget.context.session.selectKernel()
});
commands.addCommand(cmdIds.runAndAdvance, {
label: 'Run and Advance',
execute: () => {
return NotebookActions.runAndAdvance(
nbWidget.content,
nbWidget.context.session
);
}
});
commands.addCommand(cmdIds.editMode, {
label: 'Edit Mode',
execute: () => {
nbWidget.content.mode = 'edit';
}
});
commands.addCommand(cmdIds.commandMode, {
label: 'Command Mode',
execute: () => {
nbWidget.content.mode = 'command';
}
});
commands.addCommand(cmdIds.selectBelow, {
label: 'Select Below',
execute: () => NotebookActions.selectBelow(nbWidget.content)
});
commands.addCommand(cmdIds.selectAbove, {
label: 'Select Above',
execute: () => NotebookActions.selectAbove(nbWidget.content)
});
commands.addCommand(cmdIds.extendAbove, {
label: 'Extend Above',
execute: () => NotebookActions.extendSelectionAbove(nbWidget.content)
});
commands.addCommand(cmdIds.extendBelow, {
label: 'Extend Below',
execute: () => NotebookActions.extendSelectionBelow(nbWidget.content)
});
commands.addCommand(cmdIds.merge, {
label: 'Merge Cells',
execute: () => NotebookActions.mergeCells(nbWidget.content)
});
commands.addCommand(cmdIds.split, {
label: 'Split Cell',
execute: () => NotebookActions.splitCell(nbWidget.content)
});
commands.addCommand(cmdIds.undo, {
label: 'Undo',
execute: () => NotebookActions.undo(nbWidget.content)
});
commands.addCommand(cmdIds.redo, {
label: 'Redo',
execute: () => NotebookActions.redo(nbWidget.content)
});
let category = 'Notebook Operations';
[
cmdIds.interrupt,
cmdIds.restart,
cmdIds.editMode,
cmdIds.commandMode,
cmdIds.switchKernel,
cmdIds.startSearch,
cmdIds.findNext,
cmdIds.findPrevious
].forEach(command => palette.addItem({ command, category }));
category = 'Notebook Cell Operations';
[
cmdIds.runAndAdvance,
cmdIds.split,
cmdIds.merge,
cmdIds.selectAbove,
cmdIds.selectBelow,
cmdIds.extendAbove,
cmdIds.extendBelow,
cmdIds.undo,
cmdIds.redo
].forEach(command => palette.addItem({ command, category }));
let bindings = [
{
selector: '.jp-Notebook.jp-mod-editMode .jp-mod-completer-enabled',
keys: ['Tab'],
command: cmdIds.invokeNotebook
},
{
selector: `.jp-mod-completer-active`,
keys: ['Enter'],
command: cmdIds.selectNotebook
},
{
selector: '.jp-Notebook',
keys: ['Shift Enter'],
command: cmdIds.runAndAdvance
},
{
selector: '.jp-Notebook',
keys: ['Accel S'],
command: cmdIds.save
},
{
selector: '.jp-Notebook',
keys: ['Accel F'],
command: cmdIds.startSearch
},
{
selector: '.jp-Notebook',
keys: ['Accel G'],
command: cmdIds.findNext
},
{
selector: '.jp-Notebook',
keys: ['Accel Shift G'],
command: cmdIds.findPrevious
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['I', 'I'],
command: cmdIds.interrupt
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['0', '0'],
command: cmdIds.restart
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Enter'],
command: cmdIds.editMode
},
{
selector: '.jp-Notebook.jp-mod-editMode',
keys: ['Escape'],
command: cmdIds.commandMode
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Shift M'],
command: cmdIds.merge
},
{
selector: '.jp-Notebook.jp-mod-editMode',
keys: ['Ctrl Shift -'],
command: cmdIds.split
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['J'],
command: cmdIds.selectBelow
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['ArrowDown'],
command: cmdIds.selectBelow
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['K'],
command: cmdIds.selectAbove
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['ArrowUp'],
command: cmdIds.selectAbove
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Shift K'],
command: cmdIds.extendAbove
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Shift J'],
command: cmdIds.extendBelow
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Z'],
command: cmdIds.undo
},
{
selector: '.jp-Notebook.jp-mod-commandMode:focus',
keys: ['Y'],
command: cmdIds.redo
}
];
bindings.map(binding => commands.addKeyBinding(binding));
};
We have now finished scaffolding the application.
Let's start to code!
It's worth noting that there will be some code here that might not make much sense. If you would like some more information please go ahead and dive into the source code :)
Inside index.ts we will need to add a few imports:
import './index.scss';
import { CommandRegistry } from '@phosphor/commands';
import { CommandPalette, SplitPanel, Widget } from '@phosphor/widgets';
import { ServiceManager } from '@jupyterlab/services';
import { MathJaxTypesetter } from '@jupyterlab/mathjax2';
import { PageConfig } from '@jupyterlab/coreutils';
import { TerminalSession } from '@jupyterlab/services';
import { Terminal } from '@jupyterlab/terminal';
import { NotebookPanel, NotebookWidgetFactory, NotebookModelFactory} from '@jupyterlab/notebook';
import { CompleterModel, Completer, CompletionHandler, KernelConnector } from '@jupyterlab/completer';
import { editorServices } from '@jupyterlab/codemirror';
import { DocumentManager } from '@jupyterlab/docmanager';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { RenderMimeRegistry, standardRendererFactories as initialFactories} from '@jupyterlab/rendermime';
import { SetupCommands } from './commands';
After the imports create a new function called main:
function main(): void {
let manager = new ServiceManager();
void manager.ready.then(() => {
createApp(manager);
});
}
This function will create a new ServiceManager, once ready it will then bootstrap our application.
Create another function called createApp that accepts a parameter called manager of type ServiceManager.IManager
async function createApp(manager: ServiceManager.IManager) {
}
All the code below will live inside the function we have defined above. Let's start by binding the keyboard commands we defined earlier. Inside the createApp function add the following:
// Initialize the command registry with the bindings.
const commands = new CommandRegistry();
// Setup the keydown listener for the document.
document.addEventListener('keydown', event => commands.processKeydownEvent(event), true);
Now we will initialise a DocumentRegistry and DocumentManager
// Instansiate a document registry and manager
const documentRegistry = new DocumentRegistry();
const documentManager = new DocumentManager({
registry: documentRegistry,
manager,
opener: {
open: (widget: Widget) => {
console.log('Opening widget');
}
}
});
Now it's time to create a inlineEditor and associate it to a notebook panel. We will also setup the mime registry that the application will use to parse different mime types (markdown, mathjax etc.) and create two factories, one for the notebook model and one for the notebook widget.
const editorFactory = editorServices.factoryService.newInlineEditor;
const contentFactory = new NotebookPanel.ContentFactory({ editorFactory });
const notebookModelFactory = new NotebookModelFactory({});
const renderMimeRegistry = new RenderMimeRegistry({
initialFactories,
latexTypesetter: new MathJaxTypesetter({
url: PageConfig.getOption('mathjaxUrl'),
config: PageConfig.getOption('mathjaxConfig')
})
});
const notebookWidgetFactory = new NotebookWidgetFactory({
name: 'Notebook',
modelName: 'notebook',
fileTypes: ['notebook'],
defaultFor: ['notebook'],
preferKernel: true,
canStartKernel: true,
rendermime: renderMimeRegistry,
contentFactory,
mimeTypeService: editorServices.mimeTypeService
});
Next we need to associate the model factory and notebook widget factory to the document registry:
documentRegistry.addModelFactory(notebookModelFactory);
documentRegistry.addWidgetFactory(notebookWidgetFactory);
Cool. Time to create the command palette:
Add this line of code:
const commandPalette = new CommandPalette({ commands });
Let's now tell the application where to fetch the notebook we would like to interact with. This code will do a call to your notebook server and retrieve the notebook.ipynb file:
const notebookPath = PageConfig.getOption('notebookPath');
const notebookWidget = documentManager.open(notebookPath) as NotebookPanel;
Let's setup a new editor, completer (a widget that enables text completion) and bind a completion handler to the editor (yups, this code is a bit obscure).
const editor = notebookWidget.content.activeCell && notebookWidget.content.activeCell.editor;
const completer = new Completer({ editor, model: new CompleterModel() });
const kernelConnector = new KernelConnector({ session: notebookWidget.session });
const handler = new CompletionHandler({ completer, connector: kernelConnector });
handler.editor = editor;
// Listen for active cell changes.
notebookWidget.content.activeCellChanged.connect((sender, cell) => {
handler.editor = cell && cell.editor;
});
Nice! Now we're getting somewhere. Let's create a new dark themed terminal (or light if you prefer).
const terminalSession = await TerminalSession.startNew();
const terminal = new Terminal(terminalSession, { theme: 'dark' });
Time to put this all altogether into a nice split panel. Our split panel will have three panes. Command palette at the top, notebook pane and then a terminal pane.
Let's create and configure a split panel:
const panel = new SplitPanel({
orientation: 'vertical',
spacing: 0,
});
panel.id = 'main';
// Don't expand
SplitPanel.setStretch(commandPalette, 0);
// Expand to equally fill the vertical and horizontal space
SplitPanel.setStretch(notebookWidget, 1);
SplitPanel.setStretch(terminal, 1);
Add the widgets to our panel:
panel.addWidget(commandPalette);
panel.addWidget(notebookWidget);
panel.addWidget(terminal);
Time to render everything to the screen and attach a resize handler for when the window is resized:
// Attach the panel to the DOM.
Widget.attach(panel, document.body);
Widget.attach(completer, document.body);
// Handle resize events.
window.addEventListener('resize', () => panel.update());
SetupCommands(commands, commandPalette, notebookWidget, handler);
One last thing, we need to call our main function once the DOM is ready.
Outside of the main function add the following code:
window.addEventListener('load', main);
Start the development server and navigate to http://localhost:9000
If all went well, you should see a screen like this:
You can find the full source code to this tutorial here: https://code.ill.fr/panosc/data-analysis-services/jupyter-minimal-client
Thanks for reading. Happy coding!