├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── notebooks └── dev.nb ├── package-lock.json ├── package.json ├── src ├── additionalStyles.css ├── components │ └── kernelManager.ts ├── flatPromise.ts ├── index.ts ├── output.ts ├── styles.ts └── types.ts ├── tsconfig.json └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: gzuidhof 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | 108 | stats.json 109 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Guido Zuidhof 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # starboard-jupyter 2 | Experiment for support of Starboard cells that are backed by a Jupyter kernel. 3 | 4 | ## Screenshot 5 | ![](https://i.imgur.com/WFyAi5R.png) 6 | 7 | ## Setup 8 | 9 | 10 | ### 1. Install Jupyter Kernel Gateway 11 | 12 | ``` 13 | pip install jupyter_kernel_gateway 14 | ``` 15 | 16 | ### 2. Start the Jupyter Kernel gateway: 17 | ```shell 18 | # For some reason we need 19 | KG_ALLOW_METHODS="*" \ 20 | jupyter kernelgateway \ 21 | --KernelGatewayApp.allow_origin="https://gz.starboard.host" \ 22 | --KernelGatewayApp.allow_headers="authorization,content-type,x-xsrftoken" \ 23 | --KernelGatewayApp.max_age=3600 \\ 24 | --JupyterWebsocketPersonality.list_kernels=True 25 | 26 | # or for a specific localhost port 27 | KG_ALLOW_METHODS="*" \ 28 | jupyter kernelgateway \ 29 | --KernelGatewayApp.allow_origin="http://localhost:9001" \ 30 | --KernelGatewayApp.allow_headers="authorization,content-type,x-xsrftoken" \ 31 | --KernelGatewayApp.max_age=3600 \ 32 | --JupyterWebsocketPersonality.list_kernels=True 33 | ``` 34 | *(change the origin to be the origin where you are hosting your notebook sandbox)* 35 | 36 | > Note 1: right now auth with a token doesn't work for the websocket connection. The token simply doesn't get passed from the client, so for now there is no auth other than the origin check (and the fact that you can stay entirely within localhost). When that is fixed you should add `--KernelGatewayApp.auth_token="my-super-strong-secret-example"` 37 | 38 | > Note 2: For some reason `--KernelGateWayApp.allow_methods="POST,GET,OPTIONS,DELETE"` does not work, so we use the `KG_ALLOW_METHODS` env variable. 39 | 40 | > Note 3: It also works with a plain Jupyter Notebook or JupyterLab, but you will need to allow the origins and set the auth token with another method. 41 | 42 | ### 3. Initialize the plugin in your notebook 43 | 44 | You can use [this notebook](https://starboard.gg/nb/nA3wm87) (or without Starboard's interface around it [here](https://gz.starboard.host/v1/embed/0.8.9/br0qtd223akg00eiaos0/nA3wm87/)) to try it out. 45 | 46 | ```javascript 47 | // Or wherever you are hosting it, could be from some CDN. 48 | import "http://localhost:8080/dist/starboard-jupyter.js"; 49 | 50 | registerJupyterPlugin({ 51 | serverSettings: { 52 | baseUrl: "http://localhost:8888", 53 | token: "my-super-strong-secret-example", 54 | } 55 | }) 56 | ``` 57 | 58 | ### 4. Create Jupyter cells 59 | 60 | Who even needs Jupyter's interface anymore? ;) 61 | 62 | ## License 63 | 64 | [MIT](./LICENSE) -------------------------------------------------------------------------------- /notebooks/dev.nb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gzuidhof/starboard-jupyter/35e5bae13113473be6615b4cfd9c85d0cece7e46/notebooks/dev.nb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starboard-jupyter", 3 | "version": "0.2.10", 4 | "description": "Jupyter-backed cells for Starboard Notebook", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "scripts": { 10 | "build": "rimraf dist && webpack", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "format": "prettier -w src", 13 | "prepublishOnly": "npm i && npm run build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/gzuidhof/starboard-jupyter.git" 18 | }, 19 | "keywords": [ 20 | "jupyter", 21 | "starboard", 22 | "python", 23 | "starboard-notebook", 24 | "notebook" 25 | ], 26 | "author": "Guido Zuidhof ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/gzuidhof/starboard-jupyter/issues" 30 | }, 31 | "homepage": "https://github.com/gzuidhof/starboard-jupyter#readme", 32 | "devDependencies": { 33 | "@types/markdown-it": "^10.0.2", 34 | "@types/requirejs": "^2.1.32", 35 | "clean-css": "^4.2.3", 36 | "css-loader": "^5.2.0", 37 | "file-loader": "^6.2.0", 38 | "prettier": "^2.2.1", 39 | "rimraf": "^3.0.2", 40 | "source-map-loader": "^2.0.1", 41 | "starboard-notebook": "^0.12.0", 42 | "style-loader": "^2.0.0", 43 | "ts-loader": "^8.1.0", 44 | "typescript": "^4.2.3", 45 | "url-loader": "^4.1.1", 46 | "webpack": "^5.31.0", 47 | "webpack-cli": "^4.6.0", 48 | "webpack-dev-server": "^3.11.2" 49 | }, 50 | "dependencies": { 51 | "@jupyterlab/outputarea": "^3.0.7", 52 | "@jupyterlab/rendermime": "^3.0.7", 53 | "@jupyterlab/services": "^6.0.6", 54 | "@jupyterlab/theme-light-extension": "^3.0.8", 55 | "@types/katex": "^0.11.0", 56 | "lit": "*", 57 | "requirejs": "^2.3.6" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/additionalStyles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --jp-cell-prompt-width: calc(var(--cell-margin-left)); 7 | } 8 | 9 | /* The prompt can extend into the margin */ 10 | .jp-OutputArea { 11 | margin-left: calc(-1 * min(var(--cell-margin-left), var(--jp-cell-prompt-width))); 12 | } 13 | 14 | .jp-OutputArea code, 15 | .jp-OutputArea pre { 16 | background-color: unset; 17 | } 18 | 19 | .jp-OutputPrompt { 20 | padding-left: 1px; 21 | padding-right: 0; 22 | } 23 | 24 | .celltype-jupyter .cell-bottom { 25 | overflow-y: visible; 26 | } 27 | 28 | starboard-jupyter-manager { 29 | display: grid; 30 | grid-template-columns: [content-start] auto [content-end]; 31 | } 32 | 33 | starboard-notebook > starboard-jupyter-manager { 34 | /* This matches the notebook itself.. this probably deserves it's own class in starboard-notebook for easier re-use.*/ 35 | grid-template-columns: [full-width-start] auto [margin-left-start] var(--cell-margin-left) [margin-left-end content-start] minmax( 36 | auto, 37 | calc(min(var(--cell-base-width), 100% - var(--cell-margin-left) - var(--cell-margin-right))) 38 | ) [content-end margin-right-start] var(--cell-margin-right) [margin-right-end] auto [full-width-end]; 39 | } 40 | 41 | .starboard-jupyter-interface { 42 | grid-column: content-start / content-end; 43 | border-radius: var(--border-radius); 44 | border: 1px solid var(--border-color); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/kernelManager.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, KernelManager, KernelMessage } from "@jupyterlab/services"; 2 | import { JupyterPluginSettings } from "../types"; 3 | 4 | import * as P from "@jupyterlab/services/lib/serverconnection"; 5 | import { IKernelConnection } from "@jupyterlab/services/lib/kernel/kernel"; 6 | import { OutputArea } from "@jupyterlab/outputarea"; 7 | import { customElement } from "lit/decorators/custom-element"; 8 | 9 | const lit = window.runtime.exports.libraries.lit; 10 | const html = lit.html; 11 | 12 | @customElement("starboard-jupyter-manager") 13 | export class StarboardJupyterManager extends lit.LitElement { 14 | private settings: JupyterPluginSettings; 15 | private manager: KernelManager; 16 | 17 | private isReady = false; 18 | private runningKernels: Kernel.IModel[] = []; 19 | 20 | private currentKernel?: IKernelConnection; 21 | private connectionError: Error | undefined; 22 | 23 | constructor(jupyterSettings: JupyterPluginSettings) { 24 | super(); 25 | this.settings = jupyterSettings; 26 | 27 | this.manager = new KernelManager({ 28 | standby: "when-hidden", 29 | serverSettings: P.ServerConnection.makeSettings(jupyterSettings.serverSettings), 30 | }); 31 | 32 | this.manager.connectionFailure.connect((km, err) => { 33 | console.warn("Jupyter Connection Failure", err); 34 | this.connectionError = err; 35 | this.performUpdate(); 36 | }, this); 37 | 38 | this.manager.ready.then( 39 | () => { 40 | console.log("Jupyter manager is now ready"); 41 | this.isReady = true; 42 | this.performUpdate(); 43 | }, 44 | (err) => { 45 | console.warn("Jupyter manager failed to ready", err); 46 | this.isReady = false; 47 | this.connectionError = err; 48 | this.performUpdate(); 49 | } 50 | ); 51 | 52 | this.manager.runningChanged.connect((km, running) => { 53 | this.runningKernels = running; 54 | this.connectionError = undefined; 55 | this.performUpdate(); 56 | }, this); 57 | } 58 | 59 | private setupKernelConnection() { 60 | this.currentKernel!.statusChanged.connect((kc, status) => { 61 | if (status === "dead" && this.currentKernel) { 62 | this.currentKernel.dispose(); 63 | this.currentKernel = undefined; 64 | } 65 | this.performUpdate(); 66 | }); 67 | this.currentKernel!.connectionStatusChanged.connect((kc, status) => { 68 | this.performUpdate(); 69 | }); 70 | this.manager.refreshRunning().catch((e) => console.error("Failed to refresh running kernels:", e)); 71 | } 72 | 73 | createRenderRoot() { 74 | return this; 75 | } 76 | 77 | async startKernel(name?: string, shutdownCurrentKernel?: boolean) { 78 | if (shutdownCurrentKernel && this.currentKernel && !this.currentKernel.isDisposed) { 79 | console.error("Already connected to a kernel, shutting down existing kernel"); 80 | await this.currentKernel.shutdown(); 81 | this.currentKernel.dispose(); 82 | } 83 | this.currentKernel = await this.manager.startNew({ name: name }); 84 | this.setupKernelConnection(); 85 | this.performUpdate(); 86 | } 87 | 88 | async connectToKernel(id: string) { 89 | if (this.currentKernel && !this.currentKernel.isDisposed) { 90 | this.currentKernel.dispose(); 91 | this.currentKernel = undefined; 92 | } 93 | 94 | this.currentKernel = this.manager.connectTo({ model: { name: "", id } }); 95 | this.setupKernelConnection(); 96 | this.performUpdate(); 97 | } 98 | 99 | async shutdownKernel(id: string) { 100 | this.manager.shutdown(id); 101 | } 102 | 103 | async interruptKernel() { 104 | if (this.currentKernel) { 105 | this.currentKernel.interrupt(); 106 | } 107 | } 108 | 109 | async disconnectFromKernel() { 110 | if (this.currentKernel) { 111 | this.currentKernel.dispose(); 112 | this.currentKernel = undefined; 113 | this.performUpdate(); 114 | } 115 | } 116 | 117 | /** 118 | * Takes an object with a `code` field. 119 | * There are more parameters which you probably won't need. 120 | */ 121 | async runCode(content: KernelMessage.IExecuteRequestMsg["content"], output: OutputArea) { 122 | if (!this.currentKernel) { 123 | await this.startKernel(); 124 | } 125 | 126 | output.future = this.currentKernel!.requestExecute(content); 127 | } 128 | 129 | disconnectedCallback() { 130 | super.disconnectedCallback(); 131 | 132 | (async () => { 133 | if (this.currentKernel) { 134 | await this.manager.shutdown(this.currentKernel.id); 135 | } 136 | this.manager.dispose(); 137 | })(); 138 | } 139 | 140 | render() { 141 | return html` 142 |
143 |
144 | 145 |
146 | ${this.settings.headerText ? html`

${this.settings.headerText}

` : undefined} 147 | ${this.connectionError 148 | ? html`
Connection Error
` 149 | : this.isReady 150 | ? html`
✅ OK
` 151 | : html`
Connecting to Jupyter..
`} 152 |
153 |
154 | ${this.currentKernel 155 | ? html` 160 | ${this.currentKernel.connectionStatus} 161 | 162 | ${this.currentKernel.status} 163 | ` 170 | : html`Not connected to a kernel`} 171 |
172 |
173 | ${this.isReady 174 | ? html` ${ 175 | // this.connectionError ? 176 | // html`` : 177 | html`` 180 | } 181 |
    182 | ${this.runningKernels.map((v) => { 183 | if (this.currentKernel && this.currentKernel.id === v.id) { 184 | return html`
  • 187 | 🔗 ${v.name} ${v.id} 188 | 189 |
    190 | 196 | 200 | 204 | ${this.currentKernel.status} 205 | 206 |
    207 |
  • `; 208 | } else { 209 | return html`
    210 | ${v.name} ${v.id} 211 |
    212 | 214 | 216 | 219 | ${(v as any).execution_state} 220 | 221 |
    222 |
    223 | `; 224 | } 225 | })} 226 |
` 227 | : undefined} 228 | ${this.connectionError 229 | ? html`
230 | Connection Error 231 |

${this.connectionError}

232 |
233 |

Check the Network tab in your browser's developer console for more details.

234 |
` 235 | : undefined} 236 |
237 |
238 | `; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/flatPromise.ts: -------------------------------------------------------------------------------- 1 | export interface FlatPromise { 2 | resolve: (value?: T) => void; 3 | reject: (reason?: E) => void; 4 | promise: Promise; 5 | } 6 | 7 | /** 8 | * Creates a promise with the resolve and reject function outside of it, useful for tasks that may complete at any time. 9 | * Based on MIT licensed https://github.com/arikw/flat-promise, with typings added by gzuidhof. 10 | * @param executor 11 | */ 12 | export function flatPromise( 13 | executor?: (resolve: (value?: T) => void, reject: (reason?: E) => void) => void | Promise 14 | ): FlatPromise { 15 | let resolve!: (value?: T) => void; 16 | let reject!: (reason?: E) => void; 17 | 18 | const promise: Promise = new Promise((res, rej) => { 19 | // Is this any cast necessary? 20 | (resolve as any) = res; 21 | reject = rej; 22 | }); 23 | 24 | if (executor) { 25 | // This is actually valid.. as in the spec the function above the Promise gets executed immediately. 26 | executor(resolve, reject); 27 | } 28 | 29 | return { promise, resolve, reject }; 30 | } 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CellTypeDefinition, 3 | CellHandlerAttachParameters, 4 | CellElements, 5 | Cell, 6 | StarboardPlugin, 7 | } from "starboard-notebook/dist/src/types"; 8 | import { Runtime, ControlButton } from "starboard-notebook/dist/src/types"; 9 | 10 | import "./styles"; 11 | import { JupyterPluginSettings } from "./types"; 12 | import { StarboardJupyterManager } from "./components/kernelManager"; 13 | import { OutputArea } from "@jupyterlab/outputarea"; 14 | import { createJupyterOutputArea } from "./output"; 15 | import { TemplateResult } from "lit-element/lit-element"; 16 | export { createJupyterOutputArea } from "./output"; 17 | 18 | declare global { 19 | interface Window { 20 | runtime: Runtime; 21 | $_: any; 22 | } 23 | } 24 | 25 | // Singleton global kernel manager. 26 | let globalKernelManager: StarboardJupyterManager; 27 | 28 | function registerJupyter(runtime: Runtime, jupyterOpts: JupyterPluginSettings = { headerText: "Jupyter Plugin" }) { 29 | /* These globals are exposed by Starboard Notebook. We can re-use them so we don't have to bundle them again. */ 30 | const lit = runtime.exports.libraries.lit; 31 | 32 | const StarboardTextEditor = runtime.exports.elements.StarboardTextEditor; 33 | const cellControlsTemplate = runtime.exports.templates.cellControls; 34 | const icons = runtime.exports.templates.icons; 35 | 36 | globalKernelManager = new StarboardJupyterManager(jupyterOpts); 37 | 38 | const JUPYTER_CELL_TYPE_DEFINITION: CellTypeDefinition = { 39 | name: "Jupyter", 40 | cellType: ["jupyter"], 41 | createHandler: (cell: Cell, runtime: Runtime) => new JupyterCellHandler(cell, runtime), 42 | }; 43 | 44 | class JupyterCellHandler { 45 | private elements!: CellElements; 46 | private editor: any; 47 | private outputArea: OutputArea; 48 | 49 | private lastRunId = 0; 50 | private isCurrentlyRunning: boolean = false; 51 | 52 | cell: Cell; 53 | runtime: Runtime; 54 | 55 | constructor(cell: Cell, runtime: Runtime) { 56 | this.cell = cell; 57 | this.runtime = runtime; 58 | 59 | this.outputArea = createJupyterOutputArea(); 60 | } 61 | 62 | private getControls(): TemplateResult | string { 63 | const icon = this.isCurrentlyRunning ? icons.ClockIcon : icons.PlayCircleIcon; 64 | const tooltip = this.isCurrentlyRunning ? "Cell is running" : "Run Cell"; 65 | const runButton: ControlButton = { 66 | icon, 67 | tooltip, 68 | callback: () => this.runtime.controls.emit({ id: this.cell.id, type: "RUN_CELL" }), 69 | }; 70 | let buttons = [runButton]; 71 | 72 | return cellControlsTemplate({ buttons }); 73 | } 74 | 75 | attach(params: CellHandlerAttachParameters): void { 76 | this.elements = params.elements; 77 | 78 | const topElement = this.elements.topElement; 79 | lit.render(this.getControls(), this.elements.topControlsElement); 80 | 81 | this.editor = new StarboardTextEditor(this.cell, this.runtime, { 82 | language: "python", 83 | }); 84 | topElement.appendChild(this.editor); 85 | 86 | this.elements.bottomElement.appendChild(this.outputArea.node); 87 | } 88 | 89 | async run() { 90 | const codeToRun = this.cell.textContent; 91 | 92 | this.lastRunId++; 93 | const currentRunId = this.lastRunId; 94 | this.isCurrentlyRunning = true; 95 | lit.render(this.getControls(), this.elements.topControlsElement); 96 | 97 | await globalKernelManager.runCode({ code: codeToRun }, this.outputArea); 98 | await this.outputArea.future.done; 99 | 100 | if (this.lastRunId === currentRunId) { 101 | this.isCurrentlyRunning = false; 102 | lit.render(this.getControls(), this.elements.topControlsElement); 103 | } 104 | 105 | const val = this.outputArea.model.toJSON(); 106 | window.$_ = val; 107 | return val; 108 | } 109 | 110 | focusEditor() { 111 | this.editor.focus(); 112 | } 113 | 114 | async dispose() { 115 | this.editor.remove(); 116 | } 117 | 118 | clear() { 119 | this.outputArea.model.clear(); 120 | } 121 | } 122 | 123 | runtime.definitions.cellTypes.register(JUPYTER_CELL_TYPE_DEFINITION.cellType, JUPYTER_CELL_TYPE_DEFINITION); 124 | 125 | const existingKernelUI = document.querySelector("starboard-jupyter-manager"); 126 | if (existingKernelUI) { 127 | (existingKernelUI as StarboardJupyterManager).remove(); 128 | } 129 | 130 | if (jupyterOpts.mount) { 131 | jupyterOpts.mount.appendChild(globalKernelManager); 132 | } else { 133 | const nb = document.querySelector("starboard-notebook"); 134 | if (nb) nb.prepend(globalKernelManager); 135 | } 136 | } 137 | 138 | const pluginExports = { 139 | createJupyterOutputArea: createJupyterOutputArea, 140 | getGlobalKernelManager: () => { 141 | return globalKernelManager; 142 | }, 143 | }; 144 | 145 | export const plugin: StarboardPlugin = { 146 | id: "starboard-jupyter", 147 | metadata: { 148 | name: "Jupyter for Starboard", 149 | }, 150 | exports: pluginExports, 151 | async register(runtime: Runtime, opts?: JupyterPluginSettings) { 152 | if (opts === undefined) { 153 | opts = { headerText: "Jupyter Plugin" }; 154 | } 155 | registerJupyter(runtime, opts); 156 | }, 157 | }; 158 | 159 | export default plugin; 160 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | import { OutputArea, OutputAreaModel } from "@jupyterlab/outputarea"; 2 | 3 | import { RenderMimeRegistry, standardRendererFactories } from "@jupyterlab/rendermime"; 4 | 5 | export function createJupyterOutputArea() { 6 | const model = new OutputAreaModel(); 7 | const rendermime = new RenderMimeRegistry({ initialFactories: standardRendererFactories }); 8 | const outputArea = new OutputArea({ model, rendermime }); 9 | return outputArea; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles.ts: -------------------------------------------------------------------------------- 1 | import "@jupyterlab/rendermime/style/index.js"; 2 | import "@jupyterlab/outputarea/style/index.js"; 3 | import "@jupyterlab/theme-light-extension/style/index.js"; 4 | 5 | // Oh my god this is awful, but as Jupyter has its own reset it's kind of necessary.. 6 | import "starboard-notebook/dist/starboard-notebook.css"; 7 | import "./additionalStyles.css"; 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface JupyterPluginSettings { 2 | serverSettings?: { 3 | baseUrl: string; 4 | token: string; 5 | }; 6 | /** HTML Element to mount the Jupyter Manager UI on */ 7 | mount?: HTMLElement; 8 | /** Hides the header of the widget */ 9 | headerText: string; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "lib": ["es2020", "es2019", "es2018", "dom"], 7 | "declaration": true, 8 | "sourceMap": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "outDir": "./dist", 12 | "skipLibCheck": true, 13 | "esModuleInterop": false, 14 | "experimentalDecorators": true 15 | }, 16 | "include": [ 17 | "src" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ], 22 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import webpack from "webpack" 3 | const {DefinePlugin} = webpack; 4 | 5 | // const path = require('path'); 6 | // const webpack = require('webpack'); 7 | export default { 8 | entry: ['./src/index.ts'], 9 | output: { 10 | path: path.resolve('dist'), 11 | filename: 'index.js', 12 | module: true, 13 | libraryTarget: "module" 14 | }, 15 | experiments: { 16 | outputModule: true 17 | }, 18 | optimization: { 19 | minimize: false, 20 | usedExports: true, 21 | }, 22 | resolve: { 23 | extensions: ['.ts', '.tsx', '.js', '.d.ts'], 24 | }, 25 | mode: "production", 26 | module: { 27 | rules: [ 28 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 29 | { test: /\.md$/, use: 'raw-loader' }, 30 | { test: /\.txt$/, use: 'raw-loader' }, 31 | { 32 | test: /\.js$/, 33 | use: [/*'source-map-loader'*/], 34 | //enforce: 'pre', 35 | // eslint-disable-next-line no-undef 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.ts$/, 40 | use: [/*'source-map-loader', */'ts-loader'], 41 | //enforce: 'pre', 42 | // eslint-disable-next-line no-undef 43 | exclude: /node_modules/ 44 | }, 45 | { test: /\.(jpg|png|gif)$/, use: 'file-loader' }, 46 | { test: /\.js.map$/, use: 'file-loader' }, 47 | { 48 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 49 | use: 'url-loader?limit=10000&mimetype=application/font-woff' 50 | }, 51 | { 52 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 53 | use: 'url-loader?limit=10000&mimetype=application/font-woff' 54 | }, 55 | { 56 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 57 | use: 'url-loader?limit=10000&mimetype=application/octet-stream' 58 | }, 59 | { 60 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 61 | use: 'url-loader?limit=10000&mimetype=application/octet-stream' 62 | }, 63 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' }, 64 | { 65 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 66 | use: 'url-loader?limit=10000&mimetype=image/svg+xml' 67 | } 68 | ] 69 | }, 70 | plugins: [ 71 | new DefinePlugin({ 72 | // // Needed for Blueprint. See https://github.com/palantir/blueprint/issues/4393 73 | 'process.env': '{}', 74 | // // Needed for various packages using cwd(), like the path polyfill 75 | // // process: { cwd: () => '/' } 76 | }) 77 | ], 78 | }; --------------------------------------------------------------------------------