├── .prettierignore
├── .prettierrc
├── basic.png
├── .gitignore
├── resources.png
├── style
├── chart-dark.svg
├── chart-light.svg
└── index.css
├── jupyterlab_bokeh_server
├── __init__.py
└── server.py
├── tsconfig.json
├── setup.py
├── package.json
├── LICENSE.txt
├── src
├── index.ts
└── dashboard.tsx
├── tslint.json
├── README.md
└── yarn.lock
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/lib
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/basic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-r-rose/jupyterlab-bokeh-server/HEAD/basic.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | lib/
3 | node_modules/
4 | *.egg-info/
5 | .ipynb_checkpoints
6 |
--------------------------------------------------------------------------------
/resources.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-r-rose/jupyterlab-bokeh-server/HEAD/resources.png
--------------------------------------------------------------------------------
/style/chart-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/style/chart-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/jupyterlab_bokeh_server/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Return config on servers to start for bokeh
3 |
4 | See https://jupyter-server-proxy.readthedocs.io/en/latest/server-process.html
5 | for more information.
6 | """
7 | import os
8 | import sys
9 |
10 | serverfile = os.path.join(os.path.dirname(__file__), "server.py")
11 |
12 |
13 | def launch_server():
14 | return {"command": [sys.executable, serverfile, '{port}']}
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "lib": ["es2015", "dom"],
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "noEmitOnError": true,
8 | "noUnusedLocals": true,
9 | "outDir": "lib",
10 | "rootDir": "src",
11 | "strict": true,
12 | "strictNullChecks": true,
13 | "skipLibCheck": true,
14 | "target": "es2015",
15 | "jsx": "react",
16 | "types": []
17 | },
18 | "include": ["src/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup(
4 | name="jupyterlab-bokeh-server",
5 | version='0.1.0',
6 | url="https://github.com/ian-r-rose/jupyterlab-bokeh-server",
7 | author="Matt Rocklin and Ian Rose",
8 | description="projectjupyter@gmail.com",
9 | license="BSD",
10 | packages=setuptools.find_packages(),
11 | keywords=['Jupyter'],
12 | classifiers=['Framework :: Jupyter'],
13 | install_requires=[
14 | 'jupyter-server-proxy'
15 | ],
16 | entry_points={
17 | 'jupyter_serverproxy_servers': [
18 | 'bokeh-dashboard = jupyterlab_bokeh_server:launch_server',
19 | ]
20 | },
21 | package_data={
22 | 'jupyterlab_bokeh_server': ['icons/*'],
23 | },
24 | )
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jupyterlab-bokeh-server",
3 | "version": "0.1.0",
4 | "description": "A JupyterLab extension for displaying the contents of a Bokeh server",
5 | "keywords": [
6 | "jupyter",
7 | "jupyterlab",
8 | "jupyterlab-extension"
9 | ],
10 | "homepage": "https://github.com/my_name/myextension",
11 | "bugs": {
12 | "url": "https://github.com/my_name/myextension/issues"
13 | },
14 | "license": "BSD-3-Clause",
15 | "author": "Matt Rocklin and Ian Rose",
16 | "files": [
17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
19 | ],
20 | "main": "lib/index.js",
21 | "types": "lib/index.d.ts",
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/my_name/myextension.git"
25 | },
26 | "scripts": {
27 | "build": "tsc",
28 | "clean": "rimraf lib",
29 | "watch": "tsc -w"
30 | },
31 | "dependencies": {
32 | "@jupyterlab/application": "^1.0.0-alpha.6",
33 | "@jupyterlab/apputils": "^1.0.0-alpha.6",
34 | "@jupyterlab/coreutils": "3.0.0-alpha.6",
35 | "react": "^16.4.2",
36 | "react-dom": "^16.4.2"
37 | },
38 | "devDependencies": {
39 | "@types/react": "~16.4.13",
40 | "@types/react-dom": "~16.0.7",
41 | "rimraf": "^2.6.1",
42 | "typescript": "~3.3.1"
43 | },
44 | "jupyterlab": {
45 | "extension": true
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2019 Ian Rose and Matthew Rocklin
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 |
7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8 |
9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
13 |
14 |
--------------------------------------------------------------------------------
/jupyterlab_bokeh_server/server.py:
--------------------------------------------------------------------------------
1 | from bokeh.server.server import Server
2 | from bokeh.plotting import figure, ColumnDataSource
3 | from tornado import web
4 |
5 | import random
6 | import sys
7 | import time
8 |
9 |
10 | def lineplot(doc):
11 | fig = figure(title="Line plot!", sizing_mode="stretch_both", x_axis_type="datetime")
12 | source = ColumnDataSource({"x": [], "y": []})
13 | fig.line(source=source, x="x", y="y")
14 |
15 | doc.title = "Line Plot!"
16 | doc.add_root(fig)
17 |
18 | y = 0
19 |
20 | def cb():
21 | nonlocal y
22 | now = time.time() * 1000 # bokeh measures in ms
23 | y += random.random() - 0.5
24 |
25 | source.stream({"x": [now], "y": [y]}, 100)
26 |
27 | doc.add_periodic_callback(cb, 100)
28 |
29 |
30 | def histogram(doc):
31 | fig = figure(title="Histogram!", sizing_mode="stretch_both")
32 |
33 | left = [0, 1, 2, 3, 4, 5]
34 | right = [l + 0.9 for l in left]
35 | ys = [0 for _ in left]
36 |
37 | source = ColumnDataSource({"left": left, "right": right, "y": [ys]})
38 |
39 | fig.quad(source=source, left="left", right="right", bottom=0, top="y", color="blue")
40 |
41 | doc.title = "Histogram!"
42 | doc.add_root(fig)
43 |
44 | def cb():
45 | nonlocal ys
46 |
47 | for i, y in enumerate(ys):
48 | ys[i] = 0.9 * y + 0.1 * random.random()
49 |
50 | source.data.update({"y": ys})
51 |
52 | doc.add_periodic_callback(cb, 100)
53 |
54 | routes = {
55 | "/line": lineplot,
56 | "/histogram": histogram,
57 | }
58 |
59 |
60 | class RouteIndex(web.RequestHandler):
61 | """ A JSON index of all routes present on the Bokeh Server """
62 |
63 | def get(self):
64 | self.write({route: route.strip("/").title() for route in routes})
65 |
66 |
67 | if __name__ == "__main__":
68 | from tornado.ioloop import IOLoop
69 |
70 | server = Server(routes, port=int(sys.argv[1]), allow_websocket_origin=["*"])
71 | server.start()
72 |
73 | server._tornado.add_handlers(
74 | r".*", [(server.prefix + "/" + "index.json", RouteIndex, {})]
75 | )
76 |
77 | IOLoop.current().start()
78 |
--------------------------------------------------------------------------------
/style/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bokeh-launch-button-height: 28px;
3 | }
4 |
5 | /**
6 | * Rules related to the overall sidebar panel.
7 | */
8 | .bokeh-BokehDashboardLauncher {
9 | background: var(--jp-layout-color1);
10 | color: var(--jp-ui-font-color1);
11 | font-size: var(--jp-ui-font-size1);
12 | overflow: auto;
13 | }
14 |
15 | .bokeh-BokehDashboardLauncher-header {
16 | font-size: var(--jp-ui-font-size0);
17 | padding: 8px 12px;
18 | font-weight: 600;
19 | border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
20 | letter-spacing: 1px;
21 | margin: 0px;
22 | text-transform: uppercase;
23 | }
24 |
25 | /**
26 | * Rules related to the dashboard launcher.
27 | */
28 | .bokeh-DashboardListing-list {
29 | display: flex;
30 | flex-direction: column;
31 | justify-content: center;
32 | align-items: center;
33 | margin: 8px;
34 | padding: 0;
35 | list-style-type: none;
36 | }
37 |
38 | .bokeh-DashboardListing-item {
39 | margin: 4px 0px 4px 0px;
40 | width: 100%;
41 | }
42 |
43 | .bokeh-DashboardListing-item button {
44 | font-size: var(--jp-ui-font-size1);
45 | line-height: 24px;
46 | padding: 0px 8px;
47 | width: 100%;
48 | }
49 |
50 | .bokeh-DashboardListing-item button.jp-mod-styled.jp-mod-accept {
51 | height: var(--bokeh-launch-button-height);
52 | }
53 |
54 | /**
55 | * Rules for the dashboard panels.
56 | */
57 | .bokeh-BokehDashboard-inactive {
58 | position: absolute;
59 | top: 0;
60 | left: 0;
61 | width: 100%;
62 | height: 100%;
63 | display: flex;
64 | align-items: center;
65 | justify-content: center;
66 | color: var(--jp-ui-font-color3);
67 | font-size: var(--jp-ui-font-size3);
68 | background-color: var(--jp-layout-color0);
69 | z-index: 10;
70 | }
71 |
72 | .bokeh-BokehDashboard-inactive:before {
73 | content: '';
74 | position: absolute;
75 | top: 0;
76 | left: 0;
77 | width: 100%;
78 | height: 100%;
79 | background-image: url(chart-light.svg);
80 | background-repeat: no-repeat;
81 | background-position: center;
82 | opacity: 0.1;
83 | }
84 |
85 | /**
86 | * Icons
87 | */
88 |
89 | [data-theme-light="true"] .bokeh-ChartIcon {
90 | background-image: url(./chart-light.svg);
91 | }
92 | [data-theme-light="false"] .bokeh-ChartIcon {
93 | background-image: url(./chart-dark.svg);
94 | }
95 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ILabShell,
3 | ILayoutRestorer,
4 | JupyterFrontEnd,
5 | JupyterFrontEndPlugin
6 | } from '@jupyterlab/application';
7 |
8 | import { InstanceTracker } from '@jupyterlab/apputils';
9 |
10 | import { BokehDashboard, BokehDashboardLauncher, IDashboardItem } from './dashboard';
11 |
12 | import '../style/index.css';
13 |
14 | const COMMAND_ID = 'bokeh-server:launch-document';
15 |
16 | /**
17 | * Initialization data for the jupyterlab-bokeh-server extension.
18 | */
19 | const extension: JupyterFrontEndPlugin = {
20 | id: 'jupyterlab-bokeh-server',
21 | autoStart: true,
22 | requires: [ILabShell],
23 | optional: [ILayoutRestorer],
24 | activate: (
25 | app: JupyterFrontEnd,
26 | labShell: ILabShell,
27 | restorer: ILayoutRestorer | null
28 | ) => {
29 |
30 | const sidebar = new BokehDashboardLauncher({
31 | launchItem: (item: IDashboardItem) => {
32 | app.commands.execute(COMMAND_ID, item);
33 | }
34 | });
35 | sidebar.id = 'bokeh-dashboard-launcher';
36 | sidebar.title.iconClass ='bokeh-ChartIcon jp-SideBar-tabIcon';
37 | sidebar.title.caption = 'My Cool Plots';
38 | labShell.add(sidebar, 'left');
39 |
40 | // An instance tracker which is used for state restoration.
41 | const tracker = new InstanceTracker({
42 | namespace: 'bokeh-dashboard-launcher'
43 | });
44 |
45 | app.commands.addCommand(COMMAND_ID, {
46 | label: 'Open Bokeh document',
47 | execute: args => {
48 | const item = args as IDashboardItem;
49 | // If we already have a dashboard open to this url, activate it
50 | // but don't create a duplicate.
51 | const w = tracker.find(w => {
52 | return !!(w && w.item && w.item.route === item.route);
53 | });
54 | if (w) {
55 | labShell.activateById(w.id);
56 | return;
57 | }
58 |
59 | const widget = new BokehDashboard();
60 | widget.title.label = item.label;
61 | widget.item = item;
62 | labShell.add(widget, 'main');
63 | tracker.add(widget);
64 | }
65 | });
66 |
67 | if (restorer) {
68 | // Add state restoration for the dashboard items.
69 | restorer.add(sidebar, sidebar.id);
70 | restorer.restore(tracker, {
71 | command: COMMAND_ID,
72 | args: widget => widget.item,
73 | name: widget => widget.item && widget.item.route
74 | });
75 | }
76 |
77 | labShell.add(sidebar, 'left', { rank: 200 });
78 |
79 | }
80 | };
81 |
82 | export default extension;
83 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": ["tslint-plugin-prettier"],
3 | "rules": {
4 | "prettier": [true, { "singleQuote": true }],
5 | "align": [true, "parameters", "statements"],
6 | "ban": [
7 | true,
8 | ["_", "forEach"],
9 | ["_", "each"],
10 | ["$", "each"],
11 | ["angular", "forEach"]
12 | ],
13 | "class-name": true,
14 | "comment-format": [true, "check-space"],
15 | "curly": true,
16 | "eofline": true,
17 | "forin": false,
18 | "indent": [true, "spaces", 2],
19 | "interface-name": [true, "always-prefix"],
20 | "jsdoc-format": true,
21 | "label-position": true,
22 | "max-line-length": [false],
23 | "member-access": false,
24 | "member-ordering": [false],
25 | "new-parens": true,
26 | "no-angle-bracket-type-assertion": true,
27 | "no-any": false,
28 | "no-arg": true,
29 | "no-bitwise": true,
30 | "no-conditional-assignment": true,
31 | "no-consecutive-blank-lines": false,
32 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
33 | "no-construct": true,
34 | "no-debugger": true,
35 | "no-default-export": false,
36 | "no-duplicate-variable": true,
37 | "no-empty": true,
38 | "no-eval": true,
39 | "no-inferrable-types": false,
40 | "no-internal-module": true,
41 | "no-invalid-this": [true, "check-function-in-method"],
42 | "no-null-keyword": false,
43 | "no-reference": true,
44 | "no-require-imports": false,
45 | "no-shadowed-variable": false,
46 | "no-string-literal": false,
47 | "no-switch-case-fall-through": true,
48 | "no-trailing-whitespace": true,
49 | "no-unused-expression": true,
50 | "no-use-before-declare": false,
51 | "no-var-keyword": true,
52 | "no-var-requires": true,
53 | "object-literal-sort-keys": false,
54 | "one-line": [
55 | true,
56 | "check-open-brace",
57 | "check-catch",
58 | "check-else",
59 | "check-finally",
60 | "check-whitespace"
61 | ],
62 | "one-variable-per-declaration": [true, "ignore-for-loop"],
63 | "quotemark": [true, "single", "avoid-escape"],
64 | "radix": true,
65 | "semicolon": [true, "always", "ignore-bound-class-methods"],
66 | "switch-default": true,
67 | "trailing-comma": [
68 | false,
69 | {
70 | "multiline": "never",
71 | "singleline": "never"
72 | }
73 | ],
74 | "triple-equals": [true, "allow-null-check", "allow-undefined-check"],
75 | "typedef": [false],
76 | "typedef-whitespace": [
77 | false,
78 | {
79 | "call-signature": "nospace",
80 | "index-signature": "nospace",
81 | "parameter": "nospace",
82 | "property-declaration": "nospace",
83 | "variable-declaration": "nospace"
84 | },
85 | {
86 | "call-signature": "space",
87 | "index-signature": "space",
88 | "parameter": "space",
89 | "property-declaration": "space",
90 | "variable-declaration": "space"
91 | }
92 | ],
93 | "use-isnan": true,
94 | "use-strict": [false],
95 | "variable-name": [
96 | true,
97 | "check-format",
98 | "allow-leading-underscore",
99 | "ban-keywords"
100 | ],
101 | "whitespace": [
102 | true,
103 | "check-branch",
104 | "check-operator",
105 | "check-separator",
106 | "check-type"
107 | ]
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | JupyterLab and Bokeh Server
2 | ===========================
3 |
4 | A JupyterLab extension for displaying the contents of a Bokeh server.
5 |
6 | This project serves as an example for how to integrate a Bokeh server
7 | application into JupyterLab. This makes it easy for Python developers to
8 | develop rich dashboards and integrate them directly into Jupyter environments.
9 |
10 | Motivation
11 | ----------
12 |
13 | We want to give Jupyter users rich and real-time dashboards while they work.
14 | This can be useful for tracking quantities like the following:
15 |
16 | - Computational resources like CPU, Memory, and Network
17 | - External instruments or detectors
18 | - Other resources like GPU accelerators
19 | - Web APIs
20 | - ...
21 |
22 | [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) extensions provide
23 | a platform for these dashboards, but require some JavaScript expertise. For
24 | Python-only developers, this requirement may prove to be a bottleneck.
25 | Fortunately the [Bokeh](https://bokeh.pydata.org) plotting library makes it
26 | easy to produce rich and interactive browser-based visuals from Python,
27 | effectively crossing this boundary.
28 |
29 | This repository serves as an example on how to create rich dashboards in Python
30 | with Bokeh and then smoothly integrate those dashboards into JupyterLab for a
31 | first-class dashboarding experience that is accessible to Python-only
32 | developers.
33 |
34 |
35 | What's here
36 | -----------
37 |
38 | This repository contains two sets of code:
39 |
40 | - Python code defining a Bokeh Server application that generates a couple of
41 | live plots in the `jupyterlab_bokeh_server/` directory
42 | - TypeScript code integrating these plots into JupyterLab in the `src/`
43 | directory
44 |
45 | You should be able to modify only the Python code to produce a dashboard system
46 | that works well for you without modifying the TypeScript code.
47 |
48 | There are also two branches in this repository:
49 |
50 | 1. **master** contains a basic dashboard with two plots, a line plot and
51 | a histogram, that display randomly varying data:
52 | 
53 | And is available in a live notebook here: `TODO binder link`
54 |
55 | - **system-resouces** expands on the toy system above to create a real-world example
56 | that uses the `psutil` module to show CPU, memory, network, and storage
57 | activity:
58 | 
59 | And is available in a live notebook here: `TODO binder link`
60 |
61 | You can view the [difference between these two branches](https://github.com/ian-r-rose/jupyterlab-bokeh-server/compare/system-resources).
62 | This should give a sense for what you need to do to construct your own
63 | JupyterLab enabled dashboard.
64 |
65 |
66 | History
67 | -------
68 |
69 | This project is a generalization of the approach taken by the
70 | [Dask JupyterLab Extension](https://github.com/dask/dask-labextension) which
71 | integrates a rich dashboard for distributed computing into JupyterLab, which
72 | has demonstrated value for many Dask and Jupyter users in the past.
73 |
74 | 
75 |
76 |
77 | ## Prerequisites
78 |
79 | * JupyterLab 1.0.0a3
80 | * bokeh
81 |
82 | ## Installation
83 |
84 | This extension has a server-side (Python) and a client-side (Typescript) component,
85 | and we must install both in order for it to work.
86 |
87 | To install the server-side component, run the following in your terminal
88 |
89 | ```bash
90 | pip install jupyterlab-bokeh-server
91 | ```
92 |
93 | To install the client-side component, run
94 |
95 | ```bash
96 | jupyter labextension install jupyterlab-bokeh-server
97 | ```
98 |
99 | ## Development
100 |
101 | To install the server-side part, run the following in your terminal from the repository directory:
102 |
103 | ```bash
104 | pip install -e .
105 | ```
106 |
107 | In order to install the client-side component (requires node version 8 or later), run the following in the repository directory:
108 |
109 | ```bash
110 | jlpm install
111 | jlpm run build
112 | jupyter labextension install .
113 | ```
114 |
115 | To rebuild the package and the JupyterLab app:
116 |
117 | ```bash
118 | jlpm run build
119 | jupyter lab build
120 | ```
121 |
122 | ## Publishing
123 |
124 | In order to distribute your bokeh dashboard application,
125 | you must publish the two subpackages.
126 | The JupyterLab frontend part should be published to [npm](https://npmjs.org),
127 | and the server-side part to [PyPI](https://pypi.org)
128 | or [conda-forge](https://conda-forge.org) (or both).
129 |
130 | Instructions for publishing the JupyterLab extension can be found
131 | [here](https://jupyterlab.readthedocs.io/en/stable/developer/xkcd_extension_tutorial.html#publish-your-extension-to-npmjs-org).
132 | A nice write-up for how to publish a package to PyPI can be found in the
133 | [nbconvert documentation](https://nbconvert.readthedocs.io/en/latest/development_release.html).
134 |
--------------------------------------------------------------------------------
/src/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { IFrame, MainAreaWidget } from '@jupyterlab/apputils';
2 |
3 | import { URLExt } from '@jupyterlab/coreutils';
4 |
5 | import { ServerConnection } from '@jupyterlab/services';
6 |
7 | import { JSONExt, JSONObject } from '@phosphor/coreutils';
8 |
9 | import { Message } from '@phosphor/messaging';
10 |
11 | import { Widget, PanelLayout } from '@phosphor/widgets';
12 |
13 | import * as React from 'react';
14 | import * as ReactDOM from 'react-dom';
15 |
16 | /**
17 | * A class for hosting a Bokeh dashboard in an iframe.
18 | */
19 | export class BokehDashboard extends MainAreaWidget