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:

The command palette
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:

A screenshot of our application
A screenshot of our application

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!