├── nbtools ├── tests │ ├── __init__.py │ ├── test_example.py │ ├── test_nbextension_path.py │ └── conftest.py ├── nbtools.json ├── _frontend.py ├── nbextension │ ├── __init__.py │ └── static │ │ ├── extension.js │ │ └── toolbox.js ├── _version.py ├── __init__.py ├── event_manager.py ├── utils.py ├── settings.py ├── uioutput.py ├── basewidget.py ├── parsing_manager.py └── uibuilder.py ├── style ├── index.js ├── g2nb_logo.png ├── index.css ├── uioutput.css ├── basewidget.css ├── toolbox.css ├── uibuilder.css └── icon.svg ├── .coveragerc ├── .idea ├── .gitignore ├── rSettings.xml ├── vcs.xml ├── modules.xml ├── misc.xml ├── compiler.xml └── nbtools.iml ├── .eslintignore ├── .prettierignore ├── jupyter-config └── nbtools.json ├── setup.py ├── .prettierrc ├── .npmignore ├── pytest.ini ├── postBuild ├── config └── overrides.json ├── readthedocs.yml ├── install.json ├── codecov.yml ├── src ├── version.ts ├── index.ts ├── extension.ts ├── utils.ts ├── plugin.ts ├── registry.ts ├── toolbox.ts ├── dataregistry.ts ├── uioutput.ts └── databank.ts ├── tests ├── tsconfig.json ├── src │ ├── index.spec.ts │ └── utils.spec.ts └── karma.conf.js ├── tsconfig.json ├── schema └── plugin.json ├── docs ├── uioutput.md ├── wysiwyg.md └── toolmanager.md ├── docker_build.sh ├── .eslintrc.js ├── LICENSE.txt ├── .github └── workflows │ └── build.yml ├── appveyor.yml ├── .gitignore ├── Dockerfile ├── DEPLOY.md ├── webpack.config.js ├── pyproject.toml ├── package.json ├── README.md └── dev.Dockerfile /nbtools/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = nbtools/tests/* 3 | -------------------------------------------------------------------------------- /nbtools/nbtools.json: -------------------------------------------------------------------------------- 1 | { 2 | "load": ["nbtools"] 3 | } -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | -------------------------------------------------------------------------------- /style/g2nb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g2nb/nbtools/HEAD/style/g2nb_logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | nbtools 6 | -------------------------------------------------------------------------------- /jupyter-config/nbtools.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "nbtools/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import './basewidget.css'; 2 | @import './uioutput.css'; 3 | @import './uibuilder.css'; 4 | @import './toolbox.css'; 5 | -------------------------------------------------------------------------------- /.idea/rSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tests/ 4 | .jshintrc 5 | # Ignore any build output from python: 6 | dist/*.tar.gz 7 | dist/*.wheel 8 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = nbtools/tests examples 3 | norecursedirs = node_modules .ipynb_checkpoints 4 | addopts = --nbval --current-env 5 | -------------------------------------------------------------------------------- /postBuild: -------------------------------------------------------------------------------- 1 | pip install --pre nbtools 2 | cp ./nbtools/config/overrides.json /opt/conda/share/jupyter/lab/settings/overrides.json # Enable automatically saving widget state -------------------------------------------------------------------------------- /config/overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "@jupyterlab/apputils-extension:themes": { 3 | "theme": "g2nb" 4 | }, 5 | "@jupyter-widgets/jupyterlab-manager:plugin": { 6 | "saveState": true 7 | } 8 | } -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | type: sphinx 2 | python: 3 | version: 3.8 4 | pip_install: true 5 | extra_requirements: 6 | - examples 7 | - docs 8 | conda: 9 | file: docs/environment.yml 10 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "nbtools", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package nbtools" 5 | } 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | # show coverage in CI status, but never consider it a failure 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: 0% 8 | patch: 9 | default: 10 | target: 0% 11 | ignore: 12 | - "nbtools/tests" 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | const data = require('../package.json'); 2 | 3 | /** 4 | * The html widget manager assumes that this is the same as the npm package 5 | * version number. 6 | */ 7 | export const MODULE_VERSION = data.version; 8 | 9 | /* 10 | * The current package name. 11 | */ 12 | export const MODULE_NAME = data.name; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Regents of the University of California & the Broad Institute 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | export * from './version'; 5 | export * from './context'; 6 | export * from './basewidget'; 7 | export * from './uioutput'; 8 | export * from './uibuilder'; 9 | export * from './registry'; 10 | export * from './toolbox'; -------------------------------------------------------------------------------- /nbtools/tests/test_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Regents of the University of California & the Broad Institute. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | import pytest 8 | 9 | from ..uioutput import UIOutput 10 | 11 | 12 | def test_example_creation_blank(): 13 | w = UIOutput() 14 | assert w.name == 'Python Results' 15 | -------------------------------------------------------------------------------- /nbtools/_frontend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Regents of the University of California & the Broad Institute. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | """ 8 | Information about the frontend package of the widgets. 9 | """ 10 | from ._version import __version__ 11 | 12 | module_name = "@g2nb/nbtools" 13 | module_version = f">={__version__}" 14 | -------------------------------------------------------------------------------- /nbtools/nbextension/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Regents of the University of California & the Broad Institute 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | def _jupyter_nbextension_paths(): 8 | return [{ 9 | 'section': 'notebook', 10 | 'src': 'nbextension/static', 11 | 'dest': 'nbtools', 12 | 'require': 'nbtools/extension' 13 | }] 14 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noImplicitAny": true, 5 | "lib": ["dom", "es5", "es2015.promise", "es2015.iterable"], 6 | "noEmitOnError": true, 7 | "strictNullChecks": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "target": "ES5", 11 | "outDir": "build", 12 | "skipLibCheck": true, 13 | "sourceMap": true 14 | }, 15 | "include": [ 16 | "src/*.ts", 17 | "../src/**/*.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /nbtools/tests/test_nbextension_path.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Regents of the University of California & the Broad Institute. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | 8 | def test_nbextension_path(): 9 | # Check that magic function can be imported from package root: 10 | from nbtools import _jupyter_nbextension_paths 11 | # Ensure that it can be called without incident: 12 | path = _jupyter_nbextension_paths() 13 | # Some sanity checks: 14 | assert len(path) == 1 15 | assert isinstance(path[0], dict) 16 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | // Entry point for the notebook bundle containing custom model definitions. 5 | // 6 | // Setup notebook base URL 7 | // 8 | // Some static assets may be required by the custom widget javascript. The base 9 | // url for the notebook is not known at build time and is therefore computed 10 | // dynamically. 11 | 12 | (window as any).__webpack_public_path__ = document.querySelector('body')!.getAttribute('data-base-url') + 'nbextensions/nbtools'; 13 | 14 | export * from './index'; -------------------------------------------------------------------------------- /.idea/nbtools.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /nbtools/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Regents of the University of California & the Broad Institute. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import json 5 | from pathlib import Path 6 | 7 | __all__ = ["__version__"] 8 | 9 | 10 | def _fetch_version(): 11 | HERE = Path(__file__).parent.resolve() 12 | 13 | for settings in HERE.rglob("package.json"): 14 | try: 15 | with settings.open() as f: 16 | return json.load(f)["version"] 17 | except FileNotFoundError: 18 | pass 19 | 20 | raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") 21 | 22 | 23 | __version__ = _fetch_version() 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "strictNullChecks": false, 21 | "target": "es2017", 22 | "types": ["node"] 23 | }, 24 | "include": ["src/*"] 25 | } 26 | -------------------------------------------------------------------------------- /schema/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "nbtools", 3 | "description": "Settings for nbtools", 4 | "type": "object", 5 | "jupyter.lab.shortcuts": [ 6 | { 7 | "command": "nbtools:insert-tool", 8 | "keys": [ 9 | "G" 10 | ], 11 | "selector": ".jp-Notebook:focus" 12 | } 13 | ], 14 | "properties": { 15 | "force_render": { 16 | "type": "boolean", 17 | "title": "Ensure Widget Rendering", 18 | "description": "If true, cells containing nbtools widgets will be executed when a notebook is opened, ensuring that they render appropriately.", 19 | "default": true 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /nbtools/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from .tool_manager import ToolManager, NBTool, tool, DataManager, Data, NBOrigin, data 5 | from .event_manager import EventManager 6 | from .nbextension import _jupyter_nbextension_paths 7 | from .uioutput import UIOutput 8 | from .uibuilder import UIBuilder, build_ui 9 | from .settings import load_settings, import_defaults 10 | from .utils import open, python_safe 11 | from ._version import __version__ 12 | 13 | HERE = Path(__file__).parent.resolve() 14 | 15 | with (HERE / "labextension" / "package.json").open() as fid: 16 | package_data = json.load(fid) 17 | 18 | def _jupyter_labextension_paths(): 19 | return [{ 20 | "src": "labextension", 21 | "dest": package_data["name"] 22 | }] 23 | 24 | -------------------------------------------------------------------------------- /docs/uioutput.md: -------------------------------------------------------------------------------- 1 | # UI Output 2 | 3 | UI Output is a widget that can be displayed to present a function's output in an interactive interface, similar to the UI Builder. 4 | 5 | To use this widget a function's developer just needs to instantiate and display or return a `UIOutput` object. A code example is given below. 6 | 7 | ``` 8 | import nbtools 9 | nbtools.UIOutput(name='Example Output', text='stdout or your choice of message can go here.') 10 | ``` 11 | 12 | ## Parameters 13 | 14 | The UI Output widget supports several parameters which can be used to provide content. They are: 15 | 16 | * **Name:** Specifies the name of the UI Output widget. 17 | * **Description:** Used to set the description that is displayed at the top of the widget. 18 | * **Files:** An array of URLs or file paths. Used to integrate file outputs with the file input parameters found in the UI Builder. 19 | * **Text:** Display the contents of this parameter as output text. 20 | -------------------------------------------------------------------------------- /docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # PARSE THE OPTS 4 | dfile='Dockerfile' 5 | while getopts v:dp flag; do 6 | case "${flag}" in 7 | v) version=${OPTARG};; 8 | p) push='1';; 9 | d) dfile='dev.Dockerfile';; 10 | *) echo "Option not recognized"; exit 1;; 11 | esac 12 | done 13 | 14 | # ENSURE NECESSARY OPTS HAVE BEEN SET 15 | if ! [ "$version" ]; then 16 | echo 'ERROR: A version flag has not been set; ABORTING SCRIPT!' 17 | echo ' -v Set target version' 18 | echo ' -p (optional) If set, push to Dockerhub' 19 | exit 1 20 | fi 21 | 22 | # BUILD THE IMAGES 23 | docker build -t g2nb/lab:"$version" -f $dfile --target=lab . 24 | docker build -t g2nb/lab:latest -f $dfile --target=lab . 25 | docker build -t g2nb/lab:secure -f $dfile --target=secure . 26 | 27 | # IF PUSH FLAG SET, PUSH TO DOCKERHUB 28 | if [ "$push" ]; then 29 | docker push g2nb/lab:"$version" 30 | docker push g2nb/lab:latest 31 | docker push g2nb/lab:secure 32 | fi -------------------------------------------------------------------------------- /tests/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import expect = require('expect.js'); 5 | 6 | import { 7 | // Add any needed widget imports here (or from controls) 8 | } from '@jupyter-widgets/base'; 9 | 10 | import { 11 | createTestModel 12 | } from './utils.spec'; 13 | 14 | import { 15 | UIOutputModel 16 | } from '../../src/' 17 | 18 | 19 | describe('UIOutput', () => { 20 | 21 | describe('UIOutputModel', () => { 22 | 23 | it('should be createable', () => { 24 | let model = createTestModel(UIOutputModel); 25 | expect(model).to.be.an(UIOutputModel); 26 | expect(model.get('name')).to.be('Python Results'); 27 | }); 28 | 29 | it('should be createable with a value', () => { 30 | let state = { name: 'Foo Bar!' }; 31 | let model = createTestModel(UIOutputModel, state); 32 | expect(model).to.be.an(UIOutputModel); 33 | expect(model.get('name')).to.be('Foo Bar!'); 34 | }); 35 | 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '..', 4 | frameworks: ['mocha', 'karma-typescript'], 5 | reporters: ['mocha', 'karma-typescript'], 6 | client: { 7 | mocha: { 8 | timeout : 10000, // 10 seconds - upped from 2 seconds 9 | retries: 3 // Allow for slow server on CI. 10 | } 11 | }, 12 | files: [ 13 | { pattern: "tests/src/**/*.ts" }, 14 | { pattern: "src/**/*.ts" }, 15 | ], 16 | exclude: [ 17 | "src/extension.ts", 18 | ], 19 | preprocessors: { 20 | '**/*.ts': ['karma-typescript'] 21 | }, 22 | browserNoActivityTimeout: 31000, // 31 seconds - upped from 10 seconds 23 | port: 9876, 24 | colors: true, 25 | singleRun: true, 26 | logLevel: config.LOG_INFO, 27 | 28 | 29 | karmaTypescriptConfig: { 30 | tsconfig: 'tests/tsconfig.json', 31 | reports: { 32 | "text-summary": "", 33 | "html": "coverage", 34 | "lcovonly": { 35 | "directory": "coverage", 36 | "filename": "coverage.lcov" 37 | } 38 | } 39 | } 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /nbtools/event_manager.py: -------------------------------------------------------------------------------- 1 | class EventManager(object): 2 | _instance = None # EventManager singleton 3 | 4 | @staticmethod 5 | def instance(): 6 | if EventManager._instance is None: 7 | EventManager._instance = EventManager() 8 | return EventManager._instance 9 | 10 | def __init__(self): 11 | self.events = {} # A map of event names -> list of registered callbacks 12 | 13 | def register(self, event, callback): 14 | """Register an event callback with the event manager""" 15 | # Lazily create empty list of callbacks 16 | if event not in self.events: self.events[event] = [] 17 | 18 | self.events[event].append(callback) # Add callback to the list 19 | 20 | def dispatch(self, event, data=None): 21 | """Dispatch an event to trigger registered callbacks""" 22 | if event in self.events: # If callbacks are registered for this event 23 | for callback in self.events[event]: # Loop over each registered callback 24 | callback(data=data) # Run the callback, passing in the provided data 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/naming-convention': [ 16 | 'error', 17 | { 18 | 'selector': 'interface', 19 | 'format': ['PascalCase'], 20 | 'custom': { 21 | 'regex': '^I[A-Z]', 22 | 'match': true 23 | } 24 | } 25 | ], 26 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-namespace': 'off', 29 | '@typescript-eslint/no-use-before-define': 'off', 30 | '@typescript-eslint/quotes': [ 31 | 'error', 32 | 'single', 33 | { avoidEscape: true, allowTemplateLiterals: false } 34 | ], 35 | curly: ['error', 'all'], 36 | eqeqeq: 'error', 37 | 'prefer-arrow-callback': 'error' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /nbtools/nbextension/static/extension.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // 3 | define(function () { 4 | "use strict"; 5 | 6 | const base_path = document.querySelector('body').getAttribute('data-base-url') + 7 | 'nbextensions/nbtools/'; 8 | 9 | function load_css(url) { 10 | const link = document.createElement("link"); 11 | link.type = "text/css"; 12 | link.rel = "stylesheet"; 13 | link.href = url; 14 | document.getElementsByTagName("head")[0].appendChild(link); 15 | } 16 | load_css(base_path + 'notebook.css'); 17 | 18 | window['requirejs'].config({ 19 | map: { 20 | '*': { 21 | '@g2nb/nbtools': 'nbextensions/nbtools/index', 22 | }, 23 | } 24 | }); 25 | 26 | // Load the toolbox 27 | require(['nbextensions/nbtools/toolbox'], function() { 28 | require(['nbtools/toolbox'], function(toolbox) { 29 | toolbox.init(); 30 | }) 31 | }); 32 | 33 | // Export the required load_ipython_extension function 34 | return { 35 | load_ipython_extension: function () { 36 | } 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /docs/wysiwyg.md: -------------------------------------------------------------------------------- 1 | # WYSIWYG Editing 2 | 3 | The WYSIWYG Editor allows a user to format notes and documentation in a notebook in much the same way that one might use Microsoft Word or Libre Office — 4 | without the need to write a single HTML tag or line of markdown. 5 | 6 | **NOTE: This package is setill being ported to JupyterLab. For now, only Jupyter Notebook is supported.** 7 | 8 | ## Installation 9 | 10 | NBTools' accompanying HTML/markdown WYSIWYG editor is distributed in its own package: [jupyter-wysiwyg](https://github.com/genepattern/jupyter-wysiwyg). It can 11 | be installed through either PIP or conda. 12 | 13 | > pip install jupyter-wysiwyg 14 | 15 | > conda install -c g2nb jupyter-wysiwyg 16 | 17 | To enable the nbextension in Jupyter Notebook 5.2 and earlier you will need to run the following command lines. In Jupyter Notebook 5.3 and later, this is 18 | automatic and will not be necessary. 19 | 20 | > jupyter nbextension install --py jupyter_wysiwyg 21 | 22 | > jupyter nbextension enable --py jupyter_wysiwyg 23 | 24 | 25 | ## Getting Started 26 | 27 | To use the WYSIWYG Editor, first insert a markdown cell. Once a cell has been changed to the markdown type, two buttons should appear to the left of the cell. 28 | The `` button opens the WYSIWYG Editor and the `>|` button finalizes the cell and renders the text. 29 | 30 | Click the `` button and the editor will appear. A toolbar will show above the cell, allowing the user to format text, insert headers or add links. Style the 31 | text as desired, and when finished, click the button to finish editing. 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Regents of the University of California & Broad Institute 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Install node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '12.x' 19 | - name: Install Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.7' 23 | architecture: 'x64' 24 | 25 | 26 | - name: Setup pip cache 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.cache/pip 30 | key: pip-3.7-${{ hashFiles('package.json') }} 31 | restore-keys: | 32 | pip-3.7- 33 | pip- 34 | 35 | - name: Get yarn cache directory path 36 | id: yarn-cache-dir-path 37 | run: echo "::set-output name=dir::$(yarn cache dir)" 38 | - name: Setup yarn cache 39 | uses: actions/cache@v2 40 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 41 | with: 42 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 43 | key: yarn-${{ hashFiles('**/yarn.lock') }} 44 | restore-keys: | 45 | yarn- 46 | 47 | - name: Install dependencies 48 | run: python -m pip install -U jupyterlab~=3.3 jupyter_packaging~=0.12.0 49 | - name: Build the extension 50 | run: | 51 | jlpm 52 | jlpm run eslint:check 53 | python -m pip install . 54 | 55 | jupyter labextension list 2>&1 | grep -ie "@g2nb/nbtools.*OK" 56 | python -m jupyterlab.browser_check 57 | -------------------------------------------------------------------------------- /nbtools/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Regents of the University of California & the Broad Institute. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | import pytest 8 | 9 | from ipykernel.comm import Comm 10 | from ipywidgets import Widget 11 | 12 | class MockComm(Comm): 13 | """A mock Comm object. 14 | 15 | Can be used to inspect calls to Comm's open/send/close methods. 16 | """ 17 | comm_id = 'a-b-c-d' 18 | kernel = 'Truthy' 19 | 20 | def __init__(self, *args, **kwargs): 21 | self.log_open = [] 22 | self.log_send = [] 23 | self.log_close = [] 24 | super(MockComm, self).__init__(*args, **kwargs) 25 | 26 | def open(self, *args, **kwargs): 27 | self.log_open.append((args, kwargs)) 28 | 29 | def send(self, *args, **kwargs): 30 | self.log_send.append((args, kwargs)) 31 | 32 | def close(self, *args, **kwargs): 33 | self.log_close.append((args, kwargs)) 34 | 35 | _widget_attrs = {} 36 | undefined = object() 37 | 38 | 39 | @pytest.fixture 40 | def mock_comm(): 41 | _widget_attrs['_comm_default'] = getattr(Widget, '_comm_default', undefined) 42 | Widget._comm_default = lambda self: MockComm() 43 | _widget_attrs['_ipython_display_'] = Widget._ipython_display_ 44 | def raise_not_implemented(*args, **kwargs): 45 | raise NotImplementedError() 46 | Widget._ipython_display_ = raise_not_implemented 47 | 48 | yield MockComm() 49 | 50 | for attr, value in _widget_attrs.items(): 51 | if value is undefined: 52 | delattr(Widget, attr) 53 | else: 54 | setattr(Widget, attr, value) 55 | -------------------------------------------------------------------------------- /nbtools/utils.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import re 3 | import urllib 4 | import requests 5 | import threading 6 | 7 | 8 | def is_url(path_or_url): 9 | matches_url = re.compile( 10 | r'^(?:http|ftp)s?://' # http:// or https:// 11 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 12 | r'localhost|' # localhost... 13 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 14 | r'(?::\d+)?' # optional port 15 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 16 | return re.match(matches_url, path_or_url) 17 | 18 | 19 | def open(path_or_url): 20 | """ 21 | Wrapper for opening an IO object to a local file or URL 22 | 23 | :param path_or_url: 24 | :return: 25 | """ 26 | if is_url(path_or_url): 27 | return urllib.request.urlopen(path_or_url) 28 | else: 29 | return builtins.open(path_or_url) 30 | 31 | 32 | def python_safe(raw_name): 33 | """Make a string safe to use in a Python namespace""" 34 | return re.sub('[^0-9a-zA-Z]', '_', raw_name) 35 | 36 | 37 | def usage_tracker(event_token, description='', endpoint='https://workspace.g2nb.org/services/usage/'): 38 | """We maintain a basic counter of how many times our tools are used; this helps us secure funding. 39 | No identifying information is sent.""" 40 | 41 | # Call the usage tracker endpoint, don't break aything if there is any kind of error at all 42 | def make_request_async(): 43 | try: requests.get(f'{endpoint}{event_token}/', data=description) 44 | except: pass 45 | 46 | # Ping the usage tracker in its own thread, so as not to make the user wait 47 | usage_thread = threading.Thread(target=make_request_async) 48 | usage_thread.start() 49 | -------------------------------------------------------------------------------- /style/uioutput.css: -------------------------------------------------------------------------------- 1 | .nbtools-status { 2 | width: 33%; 3 | float: left; 4 | padding-right: 10px; 5 | text-align: right; 6 | color: var(--jp-cell-inprompt-font-color); 7 | font-family: monospace; 8 | } 9 | 10 | .nbtools-files { 11 | width: calc(66% - 30px); 12 | float: right; 13 | padding-left: 20px; 14 | position: relative; 15 | } 16 | 17 | .nbtools-file { 18 | display: block; 19 | color: var(--jp-content-link-color); 20 | text-decoration: none; 21 | line-break: anywhere; 22 | } 23 | 24 | .nbtools-file-menu { 25 | left: 0; 26 | right: auto; 27 | top: 20px; 28 | z-index: 7; 29 | } 30 | 31 | .nbtools-menu-header, 32 | .nbtools-menu-header:hover { 33 | background-color: var(--jp-layout-color0) !important; 34 | cursor: text; 35 | color: var(--jp-ui-font-color2); 36 | font-weight: bold; 37 | } 38 | 39 | .nbtools-menu-subitem { 40 | padding-left: 20px; 41 | } 42 | 43 | .nbtools-status::after, 44 | .nbtools-files::after { 45 | content: ""; 46 | clear: both; 47 | display: table; 48 | } 49 | 50 | .nbtools-text { 51 | font-family: monospace; 52 | padding-top: 5px !important; 53 | width: 100%; 54 | } 55 | 56 | .nbtools-visualization { 57 | margin-top: 0; 58 | } 59 | 60 | .nbtools-visualization-iframe { 61 | width: 100%; 62 | height: 500px; 63 | overflow: auto; 64 | border: 1px solid var(--jp-border-color2); 65 | margin-top: 10px; 66 | } 67 | 68 | .nbtools-appendix .nbtools-logo, 69 | .nbtools-appendix .nbtools-gear, 70 | .nbtools-appendix .nbtools-description { 71 | display: none !important; 72 | } 73 | 74 | .nbtools-appendix .nbtools-header { 75 | background-color: var(--jp-layout-color4) !important; 76 | min-height: 23px; 77 | } 78 | 79 | .nbtools-appendix .nbtools { 80 | margin-bottom: 5px; 81 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Do not build feature branch with open Pull Requests 2 | skip_branch_with_pr: true 3 | 4 | # environment variables 5 | environment: 6 | nodejs_version: "8" 7 | matrix: 8 | - PYTHON: "C:\\Miniconda3-x64" 9 | PYTHON_VERSION: "3.7" 10 | PYTHON_MAJOR: 3 11 | PYTHON_ARCH: "64" 12 | - PYTHON: "C:\\Miniconda3" 13 | PYTHON_VERSION: "3.4" 14 | PYTHON_MAJOR: 3 15 | PYTHON_ARCH: "32" 16 | 17 | # build cache to preserve files/folders between builds 18 | cache: 19 | - '%AppData%/npm-cache' 20 | - '%PYTHON%/pkgs' 21 | - '%LOCALAPPDATA%\pip\Cache' 22 | 23 | # scripts that run after cloning repository 24 | install: 25 | # Install node: 26 | - ps: Install-Product node $env:nodejs_version 27 | # Ensure python scripts are from right version: 28 | - 'SET "PATH=%PYTHON%\Scripts;%PYTHON%;%PATH%"' 29 | # Setup conda: 30 | - 'conda list' 31 | - 'conda update conda -y' 32 | # If 32 bit, force conda to use it: 33 | - 'IF %PYTHON_ARCH% EQU 32 SET CONDA_FORCE_32BIT=1' 34 | - 'conda create -n test_env python=%PYTHON_VERSION% -y' 35 | - 'activate test_env' 36 | # Update install tools: 37 | - 'conda install setuptools pip -y' 38 | - 'python -m pip install --upgrade pip' 39 | - 'python -m easy_install --upgrade setuptools' 40 | # Install coverage utilities: 41 | - 'pip install codecov' 42 | # Install our package: 43 | - 'pip install --upgrade ".[test]" -v' 44 | 45 | build: off 46 | 47 | # scripts to run before tests 48 | before_test: 49 | - git config --global user.email appveyor@fake.com 50 | - git config --global user.name "AppVeyor CI" 51 | - set "tmptestdir=%tmp%\nbtools-%RANDOM%" 52 | - mkdir "%tmptestdir%" 53 | - cd "%tmptestdir%" 54 | 55 | 56 | # to run your custom scripts instead of automatic tests 57 | test_script: 58 | - 'py.test -l --cov-report xml:"%APPVEYOR_BUILD_FOLDER%\coverage.xml" --cov=nbtools --pyargs nbtools' 59 | 60 | on_success: 61 | - cd "%APPVEYOR_BUILD_FOLDER%" 62 | - codecov -X gcov --file "%APPVEYOR_BUILD_FOLDER%\coverage.xml" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | *.tsbuildinfo 7 | nbtools/labextension 8 | .idea/inspectionProfiles/ 9 | 10 | # Created by https://www.gitignore.io/api/python 11 | # Edit at https://www.gitignore.io/?templates=python 12 | 13 | ### Python ### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Mr Developer 95 | .mr.developer.cfg 96 | .project 97 | .pydevproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | .dmypy.json 105 | dmypy.json 106 | 107 | # Pyre type checker 108 | .pyre/ 109 | 110 | # End of https://www.gitignore.io/api/python 111 | 112 | # OSX files 113 | .DS_Store 114 | /GPNBAntiCryptominer/ 115 | -------------------------------------------------------------------------------- /nbtools/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | import jupyter_core.paths 5 | from IPython import get_ipython 6 | from .tool_manager import ToolManager 7 | 8 | 9 | def load_settings(): 10 | """Attempt to load the nbtools settings files, fall back to default if not available""" 11 | load = [] 12 | for p in jupyter_core.paths.jupyter_path(): # Get Jupyter data paths 13 | nbtools_path = os.path.join(p, 'nbtools') 14 | if os.path.exists(nbtools_path) and os.path.isdir(nbtools_path): # Check for nbtools config 15 | json_files = [j for j in os.listdir(nbtools_path) if j.endswith('.json')] # Check for json in config dir 16 | for jf in json_files: # Loop over json files 17 | try: 18 | with open(os.path.join(nbtools_path, jf)) as json_file: # Load and parse 19 | data = json.load(json_file) 20 | if 'load' in data and type(data['load']) is list: # Ensure correct json format 21 | load += data['load'] # Add packages to load list 22 | except FileNotFoundError as e: 23 | logging.debug(f'nbtools setting file not found: {e}') 24 | except json.JSONDecodeError as e: 25 | logging.debug(f'unable to parse nbtools setting file: {e}') 26 | 27 | # If packages were read, return the list to load 28 | if len(load): return {"load": list(set(load))} 29 | # If it couldn't be loaded, return the default settings 30 | else: return {"load": ["nbtools"]} 31 | 32 | 33 | def import_defaults(): 34 | ToolManager.instance() # Lazily initialize, if not already done 35 | settings = load_settings() 36 | for module in settings['load']: 37 | if module == 'nbtools': # Special case so that nbtools import detection works 38 | get_ipython().run_cell(f'import nbtools as _nbtools') 39 | else: 40 | get_ipython().run_cell(f'import {module}') 41 | -------------------------------------------------------------------------------- /nbtools/uioutput.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import VBox, widget_serialization 2 | from traitlets import Unicode, List, Dict, Instance, Bool 3 | from ._frontend import module_name, module_version 4 | from .basewidget import BaseWidget 5 | 6 | 7 | class UIOutput(BaseWidget): 8 | """ 9 | Widget used to render Python output in a UI 10 | """ 11 | _model_name = Unicode('UIOutputModel').tag(sync=True) 12 | _model_module = Unicode(module_name).tag(sync=True) 13 | _model_module_version = Unicode(module_version).tag(sync=True) 14 | 15 | _view_name = Unicode('UIOutputView').tag(sync=True) 16 | _view_module = Unicode(module_name).tag(sync=True) 17 | _view_module_version = Unicode(module_version).tag(sync=True) 18 | 19 | name = Unicode('Python Results').tag(sync=True) 20 | status = Unicode('').tag(sync=True) 21 | files = List(default_value=[]).tag(sync=True) 22 | text = Unicode('').tag(sync=True) 23 | visualization = Unicode('').tag(sync=True) 24 | appendix = Instance(VBox).tag(sync=True, **widget_serialization) 25 | extra_file_menu_items = Dict().tag(sync=True) 26 | default_file_menu_items = Bool(True).tag(sync=True) 27 | attach_file_prefixes = Bool(True).tag(sync=True) 28 | 29 | def __init__(self, **kwargs): 30 | # Initialize the child widget container 31 | self.appendix = VBox() 32 | 33 | BaseWidget.__init__(self, **kwargs) # Call the superclass 34 | self.register_data() # Register any output files 35 | 36 | def register_data(self, group=None): 37 | group = self.name if group is None else group 38 | if len(self.files): 39 | from .tool_manager import DataManager, Data 40 | all_data = [] 41 | for f in self.files: 42 | if isinstance(f, tuple) or isinstance(f, list): # Handle (uri, label, kind) tuples 43 | kwargs = {} 44 | if len(f) >= 1: kwargs['uri'] = f[0] 45 | else: raise Exception('Empty tuple or list passed to UIOutput.files') 46 | if len(f) >= 2: kwargs['label'] = f[1] 47 | if len(f) >= 3: kwargs['kind'] = f[2] 48 | all_data.append(Data(origin=self.origin, group=group, **kwargs)) 49 | else: all_data.append(Data(origin=self.origin, group=group, uri=f)) # Handle uri strings 50 | DataManager.instance().group_widget(origin=self.origin, group=group, widget=self) 51 | DataManager.instance().register_all(all_data) 52 | -------------------------------------------------------------------------------- /nbtools/basewidget.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import DOMWidget 2 | from traitlets import Bool, Unicode, Dict, Int 3 | from ._frontend import module_name, module_version 4 | import json 5 | import warnings 6 | 7 | 8 | class BaseWidget(DOMWidget): 9 | _model_name = Unicode('BaseWidgettModel').tag(sync=True) 10 | _model_module = Unicode(module_name).tag(sync=True) 11 | _model_module_version = Unicode(module_version).tag(sync=True) 12 | 13 | _view_name = Unicode('BaseWidgetView').tag(sync=True) 14 | _view_module = Unicode(module_name).tag(sync=True) 15 | _view_module_version = Unicode(module_version).tag(sync=True) 16 | _view_count = Int(0).tag(sync=True) 17 | 18 | _id = Unicode(sync=True) 19 | origin = Unicode('Notebook').tag(sync=True) 20 | name = Unicode('').tag(sync=True) 21 | subtitle = Unicode('').tag(sync=True) 22 | description = Unicode('').tag(sync=True) 23 | collapsed = Bool(False).tag(sync=True) 24 | color = Unicode('var(--jp-layout-color4)').tag(sync=True) 25 | logo = Unicode('').tag(sync=True) 26 | info = Unicode('', sync=True) 27 | error = Unicode('', sync=True) 28 | extra_menu_items = Dict(sync=True) 29 | 30 | def handle_messages(self, _, content, buffers): 31 | """Handle messages sent from the client-side""" 32 | if content.get('event', '') == 'method': # Handle method call events 33 | method_name = content.get('method', '') 34 | params = content.get('params', None) 35 | if method_name and hasattr(self, method_name) and not params: 36 | getattr(self, method_name)() 37 | elif method_name and hasattr(self, method_name) and params: 38 | try: 39 | kwargs = json.loads(params) 40 | getattr(self, method_name)(**kwargs) 41 | except json.JSONDecodeError: 42 | pass 43 | elif method_name and hasattr(self, '_parent') and hasattr(self._parent, method_name) and not params: 44 | getattr(self._parent, method_name)() 45 | elif method_name and hasattr(self, '_parent') and hasattr(self._parent, method_name) and params: 46 | try: 47 | kwargs = json.loads(params) 48 | getattr(self._parent, method_name)(**kwargs) 49 | except json.JSONDecodeError: 50 | pass 51 | 52 | def __init__(self, **kwargs): 53 | super(BaseWidget, self).__init__(**kwargs) 54 | 55 | # Assign keyword parameters to this object 56 | recognized_keys = dir(self.__class__) 57 | for key, value in kwargs.items(): 58 | if key not in recognized_keys and f'_{key}' not in recognized_keys: 59 | warnings.warn(RuntimeWarning(f'Keyword parameter {key} not recognized')) 60 | setattr(self, key, value) 61 | 62 | # Attach the callback event handler 63 | self.on_msg(self.handle_messages) 64 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for running nbtools from a pip install 2 | 3 | ################################################################################### 4 | ## NOTE ## 5 | ## This Dockerfile mimics a pip install. The Dockerfile that mimics a dev ## 6 | ## install has moved to dev.Dockerfile. This prevents an issue where the dev ## 7 | ## Dockerfile runs out of memory when transpiling JS dependencies on Binder. ## 8 | ################################################################################### 9 | 10 | # Pull the latest known good scipy notebook image from the official Jupyter stacks 11 | FROM jupyter/scipy-notebook:2023-04-10 AS lab 12 | 13 | MAINTAINER Thorin Tabor 14 | EXPOSE 8888 15 | 16 | ############################################# 17 | ## ROOT ## 18 | ## Install npm ## 19 | ############################################# 20 | 21 | USER root 22 | 23 | RUN apt-get update && apt-get install -y npm 24 | 25 | ############################################# 26 | ## $NB_USER ## 27 | ## Install libraries & config ## 28 | ############################################# 29 | 30 | USER $NB_USER 31 | 32 | RUN conda install -c conda-forge beautifulsoup4 blas bokeh cloudpickle dask dill h5py hdf5 jedi jinja2 libblas libcurl \ 33 | matplotlib nodejs numba numexpr numpy pandas patsy pickleshare pillow pycurl requests scikit-image scikit-learn \ 34 | scipy seaborn sqlalchemy sqlite statsmodels sympy traitlets vincent jupyter-archive jupyterlab-git && \ 35 | conda install plotly openpyxl sphinx && \ 36 | npm install -g yarn && \ 37 | pip install plotnine bioblend py4cytoscape ndex2 qgrid ipycytoscape firecloud globus-jupyterlab 38 | 39 | RUN jupyter labextension install @g2nb/cy-jupyterlab && \ 40 | jupyter labextension install @g2nb/jupyterlab-theme && \ 41 | pip install g2nb 42 | 43 | COPY ./config/overrides.json /opt/conda/share/jupyter/lab/settings/overrides.json 44 | 45 | ############################################# 46 | ## $NB_USER ## 47 | ## Launch lab by default ## 48 | ############################################# 49 | 50 | ENV JUPYTER_ENABLE_LAB="true" 51 | ENV TERM xterm 52 | 53 | ############################################# 54 | ## ROOT ## 55 | ## Install security measures ## 56 | ############################################# 57 | 58 | FROM lab AS secure 59 | USER root 60 | 61 | RUN mv /usr/bin/wget /usr/bin/.drgf && \ 62 | # mv /usr/bin/curl /usr/bin/.cdfg && \ 63 | mkdir -p /tmp/..drgf/patterns 64 | 65 | COPY GPNBAntiCryptominer/wget_and_curl/wget /usr/bin/wget 66 | # COPY GPNBAntiCryptominer/wget_and_curl/curl /usr/bin/curl 67 | COPY GPNBAntiCryptominer/wget_and_curl/encrypted_patterns.zip /tmp/..drgf/patterns/ 68 | 69 | RUN chmod a+x /usr/bin/wget && \ 70 | mkdir -p /tmp/.wg && \ 71 | chmod a+rw /tmp/.wg && \ 72 | chmod -R a+rw /tmp/..drgf/patterns 73 | 74 | USER $NB_USER 75 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # First Time 2 | 3 | 1. Make sure you have twine and build installed: 4 | > pip install twine build 5 | 2. Make sure you have added your [PyPI credentials](https://docs.python.org/3.3/distutils/packageindex.html#pypirc) to `~/.pypirc` 6 | 3. Make sure you have anaconda-client installed: 7 | > conda install anaconda-client 8 | 4. Log into Anaconda Cloud 9 | > anaconda login 10 | 11 | # How to Deploy to PyPi Test 12 | 13 | 1. Make sure that the version number is updated in `package.json`. 14 | 2. Navigate to the directory where the repository was checked out: 15 | > cd nbtools 16 | 3. Remove any residual build artifacts from the last time nbtools was built. This step is not necessary the first time the package is built. 17 | > rm dist/\*.tar.gz; rm dist/\*.whl 18 | 4. Build the sdist and wheel artifacts. 19 | > python -m build . 20 | 5. Upload the files by running: 21 | > twine upload -r pypitest dist/\*.tar.gz; twine upload -r pypitest dist/\*.whl 22 | 6. If the upload fails go to [https://testpypi.python.org/pypi](https://testpypi.python.org/pypi) and manually upload dist/nbtools-*.tar.gz. 23 | 7. Test the deploy by uninstalling and reinstalling the package: 24 | > pip uninstall nbtools; 25 | > pip install -i https://test.pypi.org/simple/ nbtools 26 | 27 | # How to Deploy to Production PyPi 28 | 29 | 1. First deploy to test and ensure everything is working correctly (see above). 30 | 2. Make sure that the version number is updated in `package.json`. 31 | 3. Navigate to the directory where the repository was checked out: 32 | > cd nbtools 33 | 4. Remove any residual build artifacts from the last time nbtools was built. This step is not necessary the first time the package is built. 34 | > rm dist/\*.tar.gz; rm dist/\*.whl 35 | 5. Build the sdist and wheel artifacts. 36 | > python -m build . 37 | 6. Upload the files by running: 38 | > twine upload dist/\*.tar.gz; twine upload dist/\*.whl 39 | 7. If the upload fails go to [https://testpypi.python.org/pypi](https://testpypi.python.org/pypi) and manually upload dist/nbtools-*.tar.gz. 40 | 8. Test the deploy by uninstalling and reinstalling the package: 41 | > pip uninstall nbtools; 42 | > pip install nbtools 43 | 44 | # How to Deploy to Conda 45 | 46 | 1. Deploy to Production PyPi 47 | 2. Navigate to Anaconda directory 48 | > cd /anaconda3 49 | 3. Activate a clean environment. 50 | > conda activate clean 51 | 4. Run the following, removing the existing directory if necessary: 52 | > conda skeleton pypi nbtools --version XXX 53 | 5. Build the package: 54 | > conda build nbtools 55 | 6. Converting this package to builds for other operating systems can be done as shown below. You will need to upload each 56 | built version using a separate upload command. 57 | > conda convert --platform all /anaconda3/conda-bld/osx-64/nbtools-XXX-py37_0.tar.bz2 -o conda-bld/ 58 | 7. Upload the newly built package: 59 | > anaconda upload /anaconda3/conda-bld/*/nbtools-XXX-py37_0.tar.bz2 -u g2nb 60 | 8. Log into the [Anaconda website](https://anaconda.org/) to make sure everything is good. 61 | 62 | # How to deploy to NPM 63 | 64 | 1. Make sure that the version number is updated in `package.json`. 65 | 2. Navigate to the directory where the repository was checked out: 66 | > cd nbtools 67 | 3. Log in to NPM is necessary 68 | > npm login 69 | 4. Build and upload the package. Be prepared to enter a two-factor authentication code when prompted. 70 | > npm publish -------------------------------------------------------------------------------- /nbtools/parsing_manager.py: -------------------------------------------------------------------------------- 1 | from IPython import get_ipython 2 | 3 | 4 | class ParsingManager: 5 | """Parse parameter inputs for supported syntax""" 6 | 7 | @staticmethod 8 | def parse_value(value): 9 | """Test each form of supported syntax and return value""" 10 | if ParsingManager._is_pass_by_value(value): 11 | return ParsingManager._extract_pass_by_value(value) 12 | elif ParsingManager._is_force_string(value): 13 | return ParsingManager._extract_force_string(value) 14 | elif ParsingManager._is_var_ref(value): 15 | return ParsingManager._extract_var_ref(value) 16 | else: 17 | return value 18 | 19 | @staticmethod 20 | def _is_pass_by_value(value): 21 | """Is this a pass by value reference?""" 22 | if not type(value) == str: # Not a string, not pass by value 23 | return False 24 | 25 | trimmed_value = value.strip() # Ignore leading or trailing whitespace 26 | return trimmed_value.startswith("{{") and trimmed_value.endswith("}}") # Test moustache syntax 27 | 28 | @staticmethod 29 | def _is_force_string(value): 30 | """Is this a forced string literal?""" 31 | if not type(value) == str: # Not a string, not a forced string 32 | return False 33 | 34 | trimmed_value = value.strip() # Ignore leading or trailing whitespace 35 | if trimmed_value.startswith("'") and trimmed_value.endswith("'"): # Test single quotes 36 | return True 37 | elif trimmed_value.startswith('"') and trimmed_value.endswith('"'): # Test double quotes 38 | return True 39 | else: 40 | return False 41 | 42 | @staticmethod 43 | def _is_var_ref(value): 44 | """Is the value the name of a global variable?""" 45 | if not type(value) == str: return False # Not a string, not a variable ref 46 | trimmed_value = value.strip() # Ignore leading or trailing whitespace 47 | 48 | return trimmed_value in get_ipython().user_global_ns 49 | 50 | @staticmethod 51 | def _extract_pass_by_value(value): 52 | """Return the value of a pass by value string""" 53 | 54 | # Remove leading and trailing whitespace, then remove the moustaches and inner whitespace 55 | trimmed_value = value.strip()[2:-2].strip() 56 | 57 | try: # Attempt to retrieve variable reference 58 | var_ref = ParsingManager._extract_var_ref(trimmed_value) 59 | # Return the variable cast as a string 60 | return str(var_ref) 61 | except KeyError: # If an error was encountered, just return the trimmed value 62 | return trimmed_value 63 | 64 | @staticmethod 65 | def _extract_force_string(value): 66 | """Return the string of a forced string literal""" 67 | # Remove leading and trailing whitespace, then remove enclosing quotes 68 | return value.strip()[1:-1] 69 | 70 | @staticmethod 71 | def _extract_var_ref(value): 72 | """Look up variable by name and return a reference""" 73 | trimmed_value = value.strip() # Ignore leading or trailing whitespace 74 | if trimmed_value in get_ipython().user_global_ns: 75 | return get_ipython().user_global_ns[trimmed_value] 76 | else: 77 | raise KeyError('Unknown global variable') 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const version = require('./package.json').version; 4 | 5 | // Custom webpack rules 6 | const rules = [ 7 | { test: /\.ts$/, loader: 'ts-loader' }, 8 | { test: /\.js$/, loader: 'source-map-loader' }, 9 | { test: /\.css$/i, use: ['style-loader', 'css-loader'] }, 10 | { test: /\.(png|svg|jpg)$/i, use: ['file-loader'] } 11 | ]; 12 | 13 | // Packages that shouldn't be bundled but loaded at runtime 14 | const externals = ['@jupyter-widgets/base']; 15 | 16 | const resolve = { 17 | // Add '.ts' and '.tsx' as resolvable extensions. 18 | extensions: [".webpack.js", ".web.js", ".ts", ".js", ".css"] 19 | }; 20 | 21 | module.exports = [ 22 | /** 23 | * Notebook extension 24 | * 25 | * This bundle only contains the part of the JavaScript that is run on load of 26 | * the notebook. 27 | */ 28 | { 29 | entry: './src/extension.ts', 30 | target: 'web', 31 | output: { 32 | filename: 'index.js', 33 | path: path.resolve(__dirname, 'nbtools', 'nbextension', 'static'), 34 | libraryTarget: 'amd', 35 | // TODO: Replace after release to unpkg.org 36 | publicPath: '' // 'https://unpkg.com/@g2nb/nbtools@' + version + '/dist/' 37 | }, 38 | module: { 39 | rules: rules 40 | }, 41 | devtool: 'source-map', 42 | externals, 43 | resolve, 44 | plugins: [ 45 | new webpack.ProvidePlugin({ 46 | process: 'process/browser.js', 47 | }), 48 | ], 49 | }, 50 | 51 | /** 52 | * Embeddable nbtools bundle 53 | * 54 | * This bundle is almost identical to the notebook extension bundle. The only 55 | * difference is in the configuration of the webpack public path for the 56 | * static assets. 57 | * 58 | * The target bundle is always `dist/index.js`, which is the path required by 59 | * the custom widget embedder. 60 | */ 61 | { 62 | entry: './src/index.ts', 63 | target: 'web', 64 | output: { 65 | filename: 'index.js', 66 | path: path.resolve(__dirname, 'dist'), 67 | libraryTarget: 'amd', 68 | library: "@g2nb/nbtools", 69 | // TODO: Replace after release to unpkg.org 70 | publicPath: '' // 'https://unpkg.com/@g2nb/nbtools@' + version + '/dist/' 71 | }, 72 | devtool: 'source-map', 73 | module: { 74 | rules: rules 75 | }, 76 | externals, 77 | resolve, 78 | plugins: [ 79 | new webpack.ProvidePlugin({ 80 | process: 'process/browser.js', 81 | }), 82 | ], 83 | }, 84 | 85 | 86 | /** 87 | * Documentation widget bundle 88 | * 89 | * This bundle is used to embed widgets in the package documentation. 90 | */ 91 | { 92 | entry: './src/index.ts', 93 | target: 'web', 94 | output: { 95 | filename: 'embed-bundle.js', 96 | path: path.resolve(__dirname, 'docs', 'source', '_static'), 97 | library: "@g2nb/nbtools", 98 | libraryTarget: 'amd', 99 | // TODO: Replace after release to unpkg.org 100 | publicPath: '' // 'https://unpkg.com/@g2nb/nbtools@' + version + '/dist/' 101 | }, 102 | module: { 103 | rules: rules 104 | }, 105 | devtool: 'source-map', 106 | externals, 107 | resolve, 108 | plugins: [ 109 | new webpack.ProvidePlugin({ 110 | process: 'process/browser.js', 111 | }), 112 | ], 113 | } 114 | 115 | ]; 116 | -------------------------------------------------------------------------------- /tests/src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import * as widgets from '@jupyter-widgets/base'; 5 | import * as services from '@jupyterlab/services'; 6 | import * as Backbone from 'backbone'; 7 | 8 | let numComms = 0; 9 | 10 | export 11 | class MockComm { 12 | target_name = 'dummy'; 13 | 14 | constructor() { 15 | this.comm_id = `mock-comm-id-${numComms}`; 16 | numComms += 1; 17 | } 18 | on_close(fn: Function | null) { 19 | this._on_close = fn; 20 | } 21 | on_msg(fn: Function | null) { 22 | this._on_msg = fn; 23 | } 24 | _process_msg(msg: services.KernelMessage.ICommMsgMsg) { 25 | if (this._on_msg) { 26 | return this._on_msg(msg); 27 | } else { 28 | return Promise.resolve(); 29 | } 30 | } 31 | close(): string { 32 | if (this._on_close) { 33 | this._on_close(); 34 | } 35 | return 'dummy'; 36 | } 37 | send(): string { 38 | return 'dummy'; 39 | } 40 | 41 | open(): string { 42 | return 'dummy'; 43 | } 44 | comm_id: string; 45 | _on_msg: Function | null = null; 46 | _on_close: Function | null = null; 47 | } 48 | 49 | export 50 | class DummyManager extends widgets.ManagerBase { 51 | constructor() { 52 | super(); 53 | this.el = window.document.createElement('div'); 54 | } 55 | 56 | display_view(msg: services.KernelMessage.IMessage, view: Backbone.View, options: any) { 57 | // TODO: make this a spy 58 | // TODO: return an html element 59 | return Promise.resolve(view).then(view => { 60 | this.el.appendChild(view.el); 61 | view.on('remove', () => console.log('view removed', view)); 62 | return view.el; 63 | }); 64 | } 65 | 66 | protected loadClass(className: string, moduleName: string, moduleVersion: string): Promise { 67 | if (moduleName === '@jupyter-widgets/base') { 68 | if ((widgets as any)[className]) { 69 | return Promise.resolve((widgets as any)[className]); 70 | } else { 71 | return Promise.reject(`Cannot find class ${className}`) 72 | } 73 | } else if (moduleName === 'jupyter-datawidgets') { 74 | if (this.testClasses[className]) { 75 | return Promise.resolve(this.testClasses[className]); 76 | } else { 77 | return Promise.reject(`Cannot find class ${className}`) 78 | } 79 | } else { 80 | return Promise.reject(`Cannot find module ${moduleName}`); 81 | } 82 | } 83 | 84 | _get_comm_info() { 85 | return Promise.resolve({}); 86 | } 87 | 88 | _create_comm() { 89 | return Promise.resolve(new MockComm()); 90 | } 91 | 92 | el: HTMLElement; 93 | 94 | testClasses: { [key: string]: any } = {}; 95 | } 96 | 97 | 98 | export 99 | interface Constructor { 100 | new (attributes?: any, options?: any): T; 101 | } 102 | 103 | export 104 | function createTestModel(constructor: Constructor, attributes?: any): T { 105 | let id = widgets.uuid(); 106 | let widget_manager = new DummyManager(); 107 | let modelOptions = { 108 | widget_manager: widget_manager, 109 | model_id: id, 110 | }; 111 | 112 | return new constructor(attributes, modelOptions); 113 | } 114 | -------------------------------------------------------------------------------- /docs/toolmanager.md: -------------------------------------------------------------------------------- 1 | # Notebook Tool Manager API 2 | 3 | The Notebook Tool Manager API consists of two components: a singleton manager which registers and lists tools, 4 | as well as a simple interface a tool can implement to provide its metadata and rendering instructions. 5 | 6 | ### Tool Manager Singleton 7 | * **register(tool): id** 8 | * Registers a tool with the manager, passing in an object that implements the Notebook Tool interface (see below). 9 | Returns an ID unique to this tool’s registration. 10 | * **unregister(id): boolean** 11 | * Unregisters a tool with the tool manager. Accepts the ID returned by the register() function and returns True 12 | if the tool was successfully unregistered 13 | * **list(): list** 14 | * Lists all currently registered tools 15 | * **modified(): timestamp** 16 | * Returns a timestamp of the last time the list of registered tools was modified (register or unregister). This is 17 | useful when caching the list of tools. 18 | 19 | ### Notebook Tool Interface 20 | * **load(): boolean** 21 | * Function to call when a notebook first loads (for example, import dependencies, add new cell type to the menu, 22 | add buttons to the toolbar, etc.). 23 | * **render(cell): boolean** 24 | * Function to call when you click on a tool in the navigation. Returns true if it successfully 25 | rendered. 26 | 27 | > The following is metadata the tool defines and which may be used to render a description of the tool 28 | 29 | * **origin: string** 30 | * Identifier for the origin of the tool (local execution, specific GenePattern server, Galaxy, etc.) 31 | * **id: string** 32 | * identifier unique within an origin (example: LSID) 33 | * **name: string** 34 | * What we display to the user 35 | * **version: string** (optional) 36 | * To identify particular versions of a tool 37 | * **description: string** (optional) 38 | * Brief description of the tool 39 | * **tags: list** (optional) 40 | * Categories or other navigation aids 41 | * **attributes: dict** (optional) 42 | * Tool-specific metadata which may be useful to the tool. 43 | 44 | # Hello World Examples 45 | 46 | Below are two examples of how to define and register a new notebook tool. One example uses Javascript; its intended use is to define tools inside an nbextension or within a Javascript cell. The other example uses Python, and its intended use is to define tools either inside a Jupyter server exension or within a cell in a specific notebook. 47 | 48 | ## Javascript Example 49 | 50 | ```javascript 51 | // Import nbtools using RequireJS 52 | require(["nbtools"], function(nbtools) { 53 | 54 | // Instantiate the NBTool object with necessary metadata 55 | const hello_tool = new nbtools.NBTool({ 56 | id: 'hello_tool', 57 | name: 'Hello World Tool', 58 | origin: 'Notebook', 59 | 60 | load: function() {}, // Called when registered 61 | 62 | render: function() { // Called when the tool is selected 63 | const cell = Jupyter.notebook.get_selected_cell(); 64 | cell.set_text('Hello World'); 65 | } 66 | }); 67 | 68 | // Register the tool 69 | nbtools.instance().register(hello_tool); 70 | }); 71 | ``` 72 | 73 | ## Python Example 74 | 75 | ```python 76 | import nbtools 77 | 78 | # Instantiate the NBTool object with necessary metadata 79 | hello_tool = nbtools.NBTool(id='hello_tool', name='Hello World Tool', origin='Notebook') 80 | hello_tool.load = lambda: None # Called when registered 81 | hello_tool.render = lambda: print('Hello World') # Called when the tool is selected 82 | 83 | # Register the tool with the tool manager 84 | nbtools.register(hello_tool) 85 | ``` 86 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling", 4 | "ipywidgets>7,<9", 5 | "jupyterlab>=3.4,<4", 6 | ] 7 | build-backend = "hatchling.build" 8 | 9 | [project] 10 | name = "nbtools" 11 | description = "Framework for creating user-friendly Jupyter notebooks, accessible to both programming and non-programming users alike." 12 | readme = "README.md" 13 | requires-python = ">=3.6" 14 | authors = [ 15 | { name = "Thorin Tabor", email = "tmtabor@cloud.ucsd.edu" }, 16 | ] 17 | keywords = [ 18 | "Jupyter", 19 | "JupyterLab", 20 | "JupyterLab3", 21 | ] 22 | classifiers = [ 23 | "Framework :: Jupyter", 24 | "Intended Audience :: Developers", 25 | "Intended Audience :: Science/Research", 26 | "License :: OSI Approved :: BSD License", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | ] 34 | dependencies = [ 35 | "ipyuploads", 36 | "ipywidgets>7,<9", 37 | "jupyterlab>=3.4,<4", 38 | ] 39 | version = "24.8.0" 40 | 41 | [project.license] 42 | file = "LICENSE.txt" 43 | 44 | [project.urls] 45 | Homepage = "https://github.com/g2nb/nbtools" 46 | 47 | [tool.hatch.build] 48 | artifacts = [ 49 | "nbtools/labextension", 50 | ] 51 | 52 | [tool.hatch.build.targets.wheel.shared-data] 53 | "nbtools/nbextension/static" = "share/jupyter/nbextensions/nbtools" 54 | "nbtools/labextension/static" = "share/jupyter/labextensions/@g2nb/nbtools/static" 55 | "nbtools/labextension/schemas/@g2nb/nbtools" = "share/jupyter/labextensions/@g2nb/nbtools/schemas/@g2nb/nbtools" 56 | "install.json" = "share/jupyter/labextensions/@g2nb/nbtools/install.json" 57 | "nbtools/labextension/build_log.json" = "share/jupyter/labextensions/@g2nb/nbtools/build_log.json" 58 | "nbtools/labextension/package.json" = "share/jupyter/labextensions/@g2nb/nbtools/package.json" 59 | "nbtools/nbtools.json" = "share/jupyter/nbtools/nbtools.json" 60 | jupyter-config = "etc/jupyter/nbconfig/notebook.d" 61 | 62 | [tool.hatch.build.targets.sdist] 63 | exclude = [ 64 | ".github", 65 | ] 66 | 67 | [tool.hatch.build.hooks.jupyter-builder] 68 | dependencies = [ 69 | "hatch-jupyter-builder>=0.8.2", 70 | ] 71 | build-function = "hatch_jupyter_builder.npm_builder" 72 | ensured-targets = [ 73 | "nbtools/labextension/static/style.js", 74 | "nbtools/labextension/package.json", 75 | ] 76 | skip-if-exists = [ 77 | "nbtools/labextension/static/style.js", 78 | ] 79 | 80 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 81 | build_dir = "nbtools/labextension" 82 | source_dir = "src" 83 | build_cmd = "install:extension" 84 | npm = [ 85 | "jlpm", 86 | ] 87 | 88 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 89 | build_cmd = "build:all" 90 | npm = [ 91 | "jlpm", 92 | ] 93 | 94 | [tool.tbump] 95 | github_url = "https://github.com/g2nb/nbtools" 96 | 97 | [tool.tbump.version] 98 | current = "23.7.0" 99 | regex = ''' 100 | (?P\d+) 101 | \. 102 | (?P\d+) 103 | \. 104 | (?P\d+) 105 | (?P
((a|b|rc)\d+))?
106 |   (\.
107 |     (?Pdev\d*)
108 |   )?
109 |   '''
110 | 
111 | [tool.tbump.git]
112 | message_template = "Bump to {new_version}"
113 | tag_template = "v{new_version}"
114 | 
115 | [[tool.tbump.file]]
116 | src = "pyproject.toml"
117 | version_template = "version = \"{major}.{minor}.{patch}\""
118 | 
119 | [[tool.tbump.file]]
120 | src = "package.json"
121 | version_template = "\"version\": \"{major}.{minor}.{patch}\""
122 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "name": "@g2nb/nbtools",
  3 |   "version": "24.8.0",
  4 |   "description": "Framework for creating user-friendly Jupyter notebooks, accessible to both programming and non-programming users alike.",
  5 |   "keywords": [
  6 |     "jupyter",
  7 |     "jupyterlab",
  8 |     "jupyterlab-extension",
  9 |     "widgets"
 10 |   ],
 11 |   "files": [
 12 |     "lib/**/*.{js,css}",
 13 |     "style/*.{js,css,png,svg}",
 14 |     "schema/**/*.json",
 15 |     "dist/*.{js,css}",
 16 |     "style/index.js"
 17 |   ],
 18 |   "homepage": "https://github.com/g2nb/nbtools",
 19 |   "bugs": {
 20 |     "url": "https://github.com/g2nb/nbtools/issues"
 21 |   },
 22 |   "license": "BSD-3-Clause",
 23 |   "author": {
 24 |     "name": "Thorin Tabor",
 25 |     "email": "tmtabor@cloud.ucsd.edu"
 26 |   },
 27 |   "main": "lib/index.js",
 28 |   "types": "./lib/index.d.ts",
 29 |   "style": "style/index.css",
 30 |   "repository": {
 31 |     "type": "git",
 32 |     "url": "https://github.com/g2nb/nbtools"
 33 |   },
 34 |   "scripts": {
 35 |     "build": "jlpm run build:lib && jlpm run build:labextension:dev",
 36 |     "build:all": "jlpm run build:lib && jlpm run build:labextension && jlpm run build:nbextension",
 37 |     "build:labextension": "jupyter labextension build .",
 38 |     "build:labextension:dev": "jupyter labextension build --development True .",
 39 |     "build:lib": "tsc",
 40 |     "build:nbextension": "webpack --mode production",
 41 |     "clean": "jlpm run clean:lib",
 42 |     "clean:all": "jlpm run clean:lib && jlpm run clean:labextension && jlpm run clean:nbextension",
 43 |     "clean:labextension": "rimraf nbtools/labextension",
 44 |     "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
 45 |     "clean:nbextension": "rimraf nbtools/nbextension/static/index.js",
 46 |     "eslint": "eslint . --ext .ts,.tsx --fix",
 47 |     "eslint:check": "eslint . --ext .ts,.tsx",
 48 |     "install:extension": "jlpm run build",
 49 |     "prepack": "jlpm run build:lib",
 50 |     "prepare": "jlpm run clean && jlpm run build:all",
 51 |     "test": "jlpm run test:firefox",
 52 |     "test:chrome": "karma start --browsers=Chrome tests/karma.conf.js",
 53 |     "test:debug": "karma start --browsers=Chrome --singleRun=false --debug=true tests/karma.conf.js",
 54 |     "test:firefox": "karma start --browsers=Firefox tests/karma.conf.js",
 55 |     "test:ie": "karma start --browsers=IE tests/karma.conf.js",
 56 |     "watch": "run-p watch:src watch:labextension",
 57 |     "watch:labextension": "jupyter labextension watch .",
 58 |     "watch:lib": "tsc -w",
 59 |     "watch:nbextension": "webpack --watch",
 60 |     "watch:src": "tsc -w"
 61 |   },
 62 |   "dependencies": {
 63 |     "@jupyter-widgets/base": "^6",
 64 |     "@jupyterlab/application": "^3.0.4",
 65 |     "@jupyterlab/mainmenu": "^3.0.3",
 66 |     "@jupyterlab/notebook": "^3.0.4",
 67 |     "yarn": "^1.22.19"
 68 |   },
 69 |   "devDependencies": {
 70 |     "@jupyterlab/apputils": "^3.0.3",
 71 |     "@jupyterlab/builder": "^3.0.0",
 72 |     "@jupyterlab/ui-components": "^3.0.3",
 73 |     "@lumino/application": "^1.13.1",
 74 |     "@lumino/widgets": "^1.16.1",
 75 |     "@types/backbone": "^1.4.4",
 76 |     "@types/node": "^14.14.27",
 77 |     "@typescript-eslint/eslint-plugin": "^4.8.1",
 78 |     "@typescript-eslint/parser": "^4.8.1",
 79 |     "backbone": "^1.2.3",
 80 |     "css-loader": "^5.2.7",
 81 |     "eslint": "^7.14.0",
 82 |     "eslint-config-prettier": "^6.15.0",
 83 |     "eslint-plugin-prettier": "^3.1.4",
 84 |     "expect.js": "^0.3.1",
 85 |     "file-loader": "^6.2.0",
 86 |     "fs-extra": "^9.1.0",
 87 |     "karma": "^6.1.1",
 88 |     "karma-typescript": "^5.3.0",
 89 |     "mkdirp": "^1.0.4",
 90 |     "mocha": "^8.3.0",
 91 |     "npm-run-all": "^4.1.5",
 92 |     "prettier": "^2.1.1",
 93 |     "rimraf": "^3.0.2",
 94 |     "source-map-loader": "^2.0.1",
 95 |     "style-loader": "^2.0.0",
 96 |     "ts-loader": "^8.0.17",
 97 |     "typescript": "~4.5.2",
 98 |     "webpack": "^5.21.2",
 99 |     "webpack-cli": "^4.5.0"
100 |   },
101 |   "jupyterlab": {
102 |     "extension": "lib/plugin",
103 |     "schemaDir": "schema",
104 |     "sharedPackages": {
105 |       "@jupyter-widgets/base": {
106 |         "bundled": false,
107 |         "singleton": true
108 |       }
109 |     },
110 |     "discovery": {
111 |       "kernel": [
112 |         {
113 |           "kernel_spec": {
114 |             "language": "^python"
115 |           },
116 |           "base": {
117 |             "name": "nbtools"
118 |           },
119 |           "managers": [
120 |             "pip",
121 |             "conda"
122 |           ]
123 |         }
124 |       ]
125 |     },
126 |     "outputDir": "nbtools/labextension"
127 |   },
128 |   "styleModule": "style/index.js"
129 | }
130 | 


--------------------------------------------------------------------------------
/style/basewidget.css:
--------------------------------------------------------------------------------
  1 | .nbtools {
  2 |     margin-bottom: 0;
  3 |     width: 100%;
  4 |     max-width: 100%;
  5 |     overflow: visible !important;
  6 |     border: 1px solid var(--jp-border-color1);
  7 |     border-radius: 2px;
  8 |     background: var(--jp-layout-color0);
  9 |     font-size: 10pt;
 10 | }
 11 | 
 12 | /* Fixes CSS issues in JupyterLab 3.4 */
 13 | .nbtools *, .nbtools ::before, ::after {
 14 |     box-sizing: unset;
 15 | }
 16 | 
 17 | .nbtools .nbtools-header {
 18 |     background-color: var(--jp-layout-color4);
 19 |     color: #FFFFFF;
 20 |     padding: 7px;
 21 | }
 22 | 
 23 | .nbtools-logo {
 24 |     height: 20px !important;
 25 |     padding-right: 10px;
 26 | }
 27 | 
 28 | .nbtools-logo-hidden {
 29 |     visibility: hidden;
 30 |     width: 0;
 31 | }
 32 | 
 33 | .nbtools-title {
 34 |     margin: 0;
 35 |     line-height: 25px;
 36 |     position: absolute;
 37 |     font-size: 1.1em;
 38 | }
 39 | 
 40 | .nbtools-subtitle {
 41 |     position: absolute;
 42 |     right: 75px;
 43 |     text-align: right;
 44 |     line-height: 25px;
 45 |     font-size: 0.9em;
 46 | }
 47 | 
 48 | .nbtools-controls {
 49 |     position: absolute;
 50 |     right: 5px;
 51 |     top: 5px;
 52 |     text-align: right;
 53 | }
 54 | 
 55 | .nbtools-controls > button {
 56 |     border-width: 1px;
 57 |     width: 25px;
 58 |     padding-right: 0;
 59 |     padding-left: 0;
 60 |     height: 25px;
 61 |     border-radius: 2px;
 62 |     color: var(--jp-ui-font-color1);
 63 |     background-color: var(--jp-layout-color0);
 64 |     border-color: var(--jp-border-color2);
 65 | }
 66 | 
 67 | .nbtools-controls > button.nbtools-gear {
 68 |     width: 30px;
 69 | }
 70 | 
 71 | .nbtools-menu {
 72 |     display: block;
 73 |     position: absolute;
 74 |     top: 28px;
 75 |     right: 0;
 76 |     margin: 0;
 77 |     background: var(--jp-layout-color0);
 78 |     color: var(--jp-ui-font-color0);
 79 |     border: solid 1px var(--jp-border-color1);
 80 |     padding: 5px 0 5px 0;
 81 |     list-style-type: none;
 82 |     cursor: pointer;
 83 |     z-index: 4;
 84 |     white-space: nowrap;
 85 | }
 86 | 
 87 | .nbtools-menu > li {
 88 |     padding: 3px 10px 3px 10px;
 89 | }
 90 | 
 91 | .nbtools-menu > li:hover {
 92 |     background-color: var(--jp-layout-color2);
 93 | }
 94 | 
 95 | .nbtools-body {
 96 |     padding: 10px;
 97 |     position: relative;
 98 | }
 99 | 
100 | .nbtools .nbtools-description {
101 |     position: relative;
102 |     left: -10px;
103 |     top: -10px;
104 |     width: 100%;
105 |     background-color: var(--jp-layout-color2);
106 |     padding: 10px;
107 | }
108 | 
109 | .nbtools-toggle {
110 | 	display: block;
111 | 	height: auto;
112 | 	opacity: 1;
113 | 	overflow: hidden;
114 | 	transition: height 350ms ease-in-out, opacity 750ms ease-in-out;
115 | }
116 | 
117 | .nbtools-toggle.nbtools-hidden {
118 | 	display: none;
119 | 	height: 0;
120 |     min-height: 0;
121 | 	opacity: 0;
122 |     overflow: hidden;
123 | }
124 | 
125 | div.jp-Cell-inputWrapper.nbtools-hidden ~ div.jp-Cell-outputWrapper {
126 |   margin-top: 30px;
127 | }
128 | 
129 | .nbtools div.widget-box,
130 | .nbtools div.widget-gridbox {
131 |     overflow: visible !important;
132 | }
133 | 
134 | .nbtools.jupyter-widgets-disconnected::before,
135 | .nbtools .jupyter-widgets-disconnected::before {
136 |     position: absolute;
137 | }
138 | 
139 | .nbtools-disconnected {
140 |     display: none;
141 |     position: absolute;
142 |     top: 0;
143 |     bottom: 0;
144 |     left: 0;
145 |     right: 0;
146 |     background-color: var(--jp-ui-inverse-font-color3);
147 |     z-index: 2;
148 | }
149 | 
150 | .jupyter-widgets-disconnected > .nbtools-disconnected {
151 |     display: block;
152 | }
153 | 
154 | .nbtools-disconnected > .nbtools-panel {
155 |     width: 50%;
156 |     position: absolute;
157 |     top: 50%;
158 |     left: 50%;
159 |     transform: translate(-50%,-50%);
160 | }
161 | 
162 | .nbtools-panel {
163 |     margin-bottom: 0;
164 |     width: 100%;
165 |     max-width: 100%;
166 |     overflow: visible !important;
167 |     border: 1px solid var(--jp-border-color0);
168 |     border-radius: 2px;
169 |     background-color: var(--jp-layout-color1);
170 | }
171 | 
172 | div.nbtools-connect,
173 | div.nbtools-panel-button {
174 |     text-align: center;
175 |     margin-top: 10px;
176 | }
177 | 
178 | button.nbtools-connect,
179 | button.nbtools-panel-button {
180 |     background-color: var(--jp-layout-color4);
181 |     color: #FFFFFF;
182 |     border: 1px solid var(--jp-border-color1);
183 |     padding: 6px 12px;
184 |     font-size: 0.9em;
185 |     cursor: pointer;
186 | }
187 | 
188 | 
189 | /* Hack fix for floating menus */
190 | 
191 | div.jupyter-widgets,
192 | .lm-Widget.jp-OutputPrompt.jp-OutputArea-prompt,
193 | .lm-Widget.p-Widget.jp-OutputArea,
194 | .lm-Widget.lm-Widget.jp-OutputArea,
195 | .lm-Widget.lm-Panel.jp-OutputArea,
196 | .lm-Widget.lm-Panel.jp-OutputArea-child,
197 | .lm-Widget.lm-Panel.jp-OutputArea-output,
198 | .lm-Widget.jp-OutputArea.jp-Cell-outputArea {
199 |     overflow: visible !important;
200 | }


--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Send a browser notification
  3 |  *
  4 |  * @param message
  5 |  * @param sender
  6 |  * @param icon
  7 |  */
  8 | export function send_notification(message:string, sender:string = 'nbtools', icon:string = '') {
  9 |     // Internal function to display the notification
 10 |     function notification() {
 11 |         new Notification(sender, {
 12 |             body: message,
 13 |             badge: icon,
 14 |             icon: icon,
 15 |             silent: true
 16 |         });
 17 |     }
 18 | 
 19 |     // Browser supports notifications and permission is granted
 20 |     if ("Notification" in window && Notification.permission === "granted") {
 21 |         notification()
 22 |     }
 23 | 
 24 |     // Otherwise, we need to ask the user for permission
 25 |     else if ("Notification" in window && Notification.permission !== "denied") {
 26 |         Notification.requestPermission(function (permission) {
 27 |             // If the user accepts, let's create a notification
 28 |             if (permission === "granted") {
 29 |                 notification()
 30 |             }
 31 |         });
 32 |     }
 33 | }
 34 | 
 35 | /**
 36 |  * Determines ia a given string is an absolute file path
 37 |  *
 38 |  * @param path_or_url
 39 |  * @returns {boolean}
 40 |  */
 41 | export function is_absolute_path(path_or_url:string) {
 42 |     let path_exp = new RegExp('^/');
 43 |     return path_exp.test(path_or_url);
 44 | }
 45 | 
 46 | /**
 47 |  * Decides if a string represents a valid URL or not
 48 |  *
 49 |  * @param path_or_url
 50 |  * @returns {boolean}
 51 |  */
 52 | export function is_url(path_or_url:string) {
 53 |     const url_exp = new RegExp('^(?:http|ftp)s?://');
 54 |     return url_exp.test(path_or_url);
 55 | }
 56 | 
 57 | export function get_absolute_url(url:string) {
 58 |     try { return new URL(url).href; }
 59 |     catch (e) { return new URL(url, document.baseURI).href; }
 60 | }
 61 | 
 62 | /**
 63 |  * Extracts a file name from a URL
 64 |  *
 65 |  * @param path
 66 |  * @returns {*}
 67 |  */
 68 | export function extract_file_name(path:string) {
 69 |     if (is_url(path)) return path.split('/').pop();
 70 |     else return path;
 71 | }
 72 | 
 73 | /**
 74 |  * Extracts a file type from a path or URL
 75 |  *
 76 |  * @param {string} path
 77 |  * @returns {any}
 78 |  */
 79 | export function extract_file_type(path:string) {
 80 |     return path.split('.').pop().trim();
 81 | }
 82 | 
 83 | /**
 84 |  * Wait until the specified element is found in the DOM and then execute a promise
 85 |  *
 86 |  * @param {HTMLElement} el
 87 |  */
 88 | export function element_rendered(el:HTMLElement) {
 89 |     return new Promise((resolve, reject) => {
 90 |         (function element_in_dom() {
 91 |             if (document.body.contains(el)) return resolve(el);
 92 |             else setTimeout(element_in_dom, 200);
 93 |         })();
 94 |     });
 95 | }
 96 | 
 97 | /**
 98 |  * Show an element
 99 |  *
100 |  * @param {HTMLElement} elem
101 |  */
102 | export function show(elem:HTMLElement) {
103 |     if (!elem) return; // Protect against null elements
104 | 
105 | 	// Get the natural height of the element
106 | 	const getHeight = function () {
107 | 		elem.style.display = 'block'; // Make it visible
108 | 		const height = elem.scrollHeight + 'px'; // Get it's height
109 | 		elem.style.display = ''; //  Hide it again
110 | 		return height;
111 | 	};
112 | 
113 | 	const height = getHeight(); // Get the natural height
114 | 	elem.classList.remove('nbtools-hidden'); // Make the element visible
115 | 	elem.style.height = height; // Update the height
116 | 
117 | 	// Once the transition is complete, remove the inline height so the content can scale responsively
118 | 	setTimeout(function () {
119 | 		elem.style.height = '';
120 | 		elem.classList.remove('nbtools-toggle');
121 | 	}, 350);
122 | }
123 | 
124 | /**
125 |  * Hide an element
126 |  *
127 |  * @param elem
128 |  * @param min_height
129 |  */
130 | export function hide(elem:HTMLElement, min_height='0') {
131 |     if (!elem) return; // Protect against null elements
132 |     elem.classList.add('nbtools-toggle');
133 | 
134 | 	// Give the element a height to change from
135 | 	elem.style.height = elem.scrollHeight + 'px';
136 | 
137 | 	// Set the height back to 0
138 | 	setTimeout(function () {
139 | 		elem.style.height = min_height;
140 | 	}, 10);
141 | 
142 | 	// When the transition is complete, hide it
143 | 	setTimeout(function () {
144 | 		elem.classList.add('nbtools-hidden');
145 | 	}, 350);
146 | 
147 | }
148 | 
149 | /**
150 |  * Toggle element visibility
151 |  *
152 |  * @param elem
153 |  * @param min_height
154 |  */
155 | export function toggle(elem:HTMLElement, min_height='0') {
156 | 	// If the element is visible, hide it
157 | 	if (!elem.classList.contains('nbtools-hidden')) {
158 | 		hide(elem, min_height);
159 | 		return;
160 | 	}
161 | 
162 | 	// Otherwise, show it
163 | 	show(elem);
164 | }
165 | 
166 | export function process_template(template:string, template_vars:any) {
167 | 	Object.keys(template_vars).forEach((key_var) => {
168 | 		template = template.replace(new RegExp(`{{${key_var}}}`, 'g'), template_vars[key_var]);
169 | 	});
170 | 
171 | 	return template;
172 | }
173 | 
174 | export function pulse_red(element:HTMLElement, count:number=0, count_up:boolean=true) {
175 |     setTimeout(() => {
176 |         element.style.border = `rgba(255, 0, 0, ${count / 10}) solid ${Math.ceil(count / 2)}px`;
177 |         if (count_up && count < 10) pulse_red(element, count+1, count_up);
178 |         else if (count_up) pulse_red(element, count, false);
179 |         else if (count > 0) pulse_red(element, count-1, count_up);
180 |         else element.style.border = `none`;
181 |     }, 25);
182 | }
183 | 
184 | /**
185 |  * We maintain a basic counter of how many times our tools are used; this helps us secure funding.
186 |  * No identifying information is sent.
187 |  *
188 |  * @param event_token
189 |  * @param description
190 |  * @param endpoint
191 |  */
192 | export function usage_tracker(event_token:string, description='', endpoint='https://workspace.g2nb.org/services/usage/') {
193 |     fetch(`${endpoint}${event_token}/`, {
194 |         method: "POST",
195 |         body: description
196 |     }).then(r => r.text()).then(b => console.log(`usage response: ${b}`));
197 | }
198 | 
199 | export function escape_quotes(raw:String) {
200 |     return raw.replace(/'/g, "\\'").replace(/"/g, '\\"')
201 | }


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # nbtools for JupyterLab
  2 | 
  3 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/g2nb/nbtools/lab?urlpath=lab)
  4 | [![Build Status](https://travis-ci.org/g2nb/nbtools.svg?branch=lab)](https://travis-ci.org/genepattern/nbtools)
  5 | [![Documentation Status](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](https://gpnotebook-website-docs.readthedocs.io/en/latest/)
  6 | [![Docker Pulls](https://img.shields.io/docker/pulls/genepattern/genepattern-notebook.svg)](https://hub.docker.com/r/genepattern/lab/)
  7 | [![Join the chat at https://gitter.im/g2nb](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/genepattern/genepattern-notebook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
  8 | 
  9 | 
 10 | **nbtools** is a framework for creating user-friendly Jupyter notebooks that are accessible to both programming and non-programming users. It is a core component of the [g2nb project](https://g2nb.org). The package provides:
 11 | 
 12 | * A decorator which can transform any Python function into an interactive user interface.
 13 | * A toolbox interface for encapsulating and adding new computational steps to a notebook.
 14 | * Flexible theming and APIs to extend the nbtools functionality.
 15 | 
 16 | ### **Looking for classic Jupyter Notebook support?**
 17 | **Jupyter Notebook support is available, albeit not in active development. You can find it in its own branch. [Just click here!](https://github.com/g2nb/nbtools/tree/notebook)**
 18 | 
 19 | 
 20 | ## Requirements
 21 | 
 22 | * JupyterLab >= 3.0
 23 | * ipywidgets >= 7.5.0
 24 | 
 25 | ## Docker
 26 | 
 27 | A Docker image with nbtools and the full JupyterLab stack is available through DockerHub.
 28 | 
 29 | ```bash
 30 | docker pull g2nb/lab
 31 | docker run --rm -p 8888:8888 g2nb/lab
 32 | ```
 33 | 
 34 | ## Installation
 35 | 
 36 | At the moment you may install a prerelease version from pip or create a development install from GitHub:
 37 | 
 38 | ```bash
 39 | pip install --pre nbtools
 40 | ```
 41 | 
 42 | ***OR***
 43 | 
 44 | ```bash
 45 | # Install ipywidgets, if you haven't already
 46 | jupyter nbextension enable --py widgetsnbextension
 47 | jupyter labextension install @jupyter-widgets/jupyterlab-manager
 48 | 
 49 | # Clone the nbtools repository
 50 | git clone https://github.com/g2nb/nbtools.git
 51 | cd nbtools
 52 | 
 53 | # Install the nbtools JupyterLab prototype
 54 | pip install -e .
 55 | jupyter labextension develop . --overwrite
 56 | jupyter nbextension install --py nbtools --symlink --sys-prefix
 57 | jupyter nbextension enable --py nbtools --sys-prefix
 58 | ```
 59 | 
 60 | If installing from GitHub, before nbtools will load in your JupyterLab environment, you'll also need to build its 
 61 | labextension (see Development below). 
 62 | 
 63 | ## Development
 64 | 
 65 | To develop with nbtools, you will need to first install npm or yarn, as well as install nbtools' dependencies. One way 
 66 | to do this is through conda. An example is given below. Run these commands within the top-level directory of the repository.
 67 | 
 68 | ```bash
 69 | conda install npm.  # Install npm
 70 | npm install         # Install package requirements
 71 | npm run build       # Build the package
 72 | jupyter lab build   # Build JupyterLab with the extension installed
 73 | ```
 74 | 
 75 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in 
 76 | the extension's source and automatically rebuild the extension. To develop, run each of the following commands in a 
 77 | separate terminal. 
 78 | 
 79 | ```bash
 80 | jlpm run watch
 81 | jupyter lab
 82 | ```
 83 | 
 84 | The `jlpm` command is JupyterLab's pinned version of [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You 
 85 | may use `yarn` or `npm` in lieu of `jlpm`.
 86 | 
 87 | With the watch command running, every saved change will immediately be built locally and available in your running 
 88 | JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the 
 89 | extension to be rebuilt).
 90 | 
 91 | By default, the `jlpm run build` command generates the source maps for this extension to make it easier to debug using 
 92 | the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
 93 | 
 94 | ```bash
 95 | jupyter lab build --minimize=False
 96 | ```
 97 | 
 98 | ### Uninstall
 99 | 
100 | ```bash
101 | pip uninstall nbtools
102 | ```
103 | 
104 | ## Getting Started
105 | 
106 | Let's start by writing a simple Hello World function and turning it into an interactive widget. Go ahead and install nbtools, launch
107 | Jupyter and open a new, blank notebook.
108 | 
109 | Once that's completed, let's write a basic function. The function below accepts a string and prints a brief message. By default, the message addresses the world. For good measure we will also add a docstring to document the function.
110 | 
111 | ```python
112 | def say_hello(to_whom='World'):
113 |     """Say hello to the world or whomever."""
114 |     print('Hello ' + to_whom)
115 | ```
116 | 
117 | This is pretty basic Python and hopefully everything so far is familiar. Next, we will turn this function into an interactive widget with just an import statement and one line of code. Update your code to what is shown below and execute the cell.
118 | 
119 | ```python
120 | import nbtools
121 | 
122 | @nbtools.build_ui
123 | def say_hello(to_whom='World'):
124 |     """Say hello to the world or whomever."""
125 |     print('Hello ' + to_whom)
126 | ```
127 | 
128 | You should now see a widget containing a web form. This form will prompt for the value of the `to_whom` parameter. The docstring will also appear as a description near the top of the widget. Go ahead and change the `to_whom` value, then click the "Run" button. This will execute the function and print the results below. Meanwhile, the form will also collapse, making more room on your screen.
129 | 
130 | With the push of a button, you've run the `say_hello` function!
131 | 
132 | This is exciting, but it is far from the only feature of the nbtools package. You can edit markdown cells using a WYSIWYG editor, customize how your function displays, chain together multiple related functions, make widgets from existing third-party methods, create a library of interactive tools (just click the Tools button on the toolbar and you will see `say_hello` has already added itself) and more! Just see the documentation links below.
133 | 
134 | ## Features
135 | 
136 | * [UI Builder](docs/uibuilder.md)
137 | * [UI Output](docs/uioutput.md)
138 | * [Tool Manager API](docs/toolmanager.md)
139 | * [WYSWYG Editor](docs/wysiwyg.md)
140 | 


--------------------------------------------------------------------------------
/style/toolbox.css:
--------------------------------------------------------------------------------
  1 | .nbtools-icon {
  2 |     position: relative;
  3 |     font-size: 1.2em;
  4 |     color: var(--jp-inverse-layout-color3);
  5 | }
  6 | 
  7 | .jp-mod-left .nbtools-icon {
  8 |     top: 3px;
  9 |     left: -9px;
 10 | }
 11 | 
 12 | .jp-mod-right .nbtools-icon {
 13 |     top: -3px;
 14 |     left: 9px;
 15 | }
 16 | 
 17 | .nbtools-wrapper {
 18 |     padding: 4px;
 19 |     background-color: var(--jp-layout-color1);
 20 | }
 21 | 
 22 | .nbtools-outline {
 23 |     padding: 0 9px;
 24 |     background-color: var(--jp-input-active-background);
 25 |     height: 30px;
 26 |     box-shadow: inset 0 0 0 var(--jp-border-width) var(--jp-input-border-color);
 27 | }
 28 | 
 29 | .nbtools-outline::after {
 30 |     content: ' ';
 31 |     position: absolute;
 32 |     top: 4px;
 33 |     right: 4px;
 34 |     height: 30px;
 35 |     width: 10px;
 36 |     padding: 0 10px;
 37 |     background-image: var(--jp-icon-search);
 38 |     background-size: 20px;
 39 |     background-repeat: no-repeat;
 40 |     background-position: center;
 41 | }
 42 | 
 43 | .nbtools-search {
 44 |     background: transparent;
 45 |     width: calc(100% - 18px);
 46 |     float: left;
 47 |     border: none;
 48 |     outline: none;
 49 |     font-size: var(--jp-ui-font-size1);
 50 |     color: var(--jp-ui-font-color0);
 51 |     line-height: 28px;
 52 | }
 53 | 
 54 | #nbtools-tabs {
 55 |     min-height: calc( var(--jp-private-horizontal-tab-height) + 2 * var(--jp-border-width) );
 56 | }
 57 | 
 58 | #nbtools-tabs > .lm-TabBar-content {
 59 |     align-items: flex-end;
 60 |     min-width: 0;
 61 |     min-height: 0;
 62 | }
 63 | 
 64 | #nbtools-tabs .lm-TabBar-tab {
 65 |     flex: 0 1 var(--jp-private-horizontal-tab-width);
 66 |     min-height: calc( var(--jp-private-horizontal-tab-height) + var(--jp-border-width) );
 67 |     min-width: 0;
 68 |     margin-left: calc(-1 * var(--jp-border-width));
 69 |     line-height: var(--jp-private-horizontal-tab-height);
 70 |     padding: 0 8px;
 71 |     background: var(--jp-layout-color2);
 72 |     border: var(--jp-border-width) solid var(--jp-border-color1);
 73 |     border-bottom: none;
 74 |     position: relative;
 75 |     max-width: min-content;
 76 | }
 77 | 
 78 | #nbtools-tabs .lm-TabBar-tab.lm-mod-current {
 79 |     background: var(--jp-layout-color1);
 80 |     color: var(--jp-ui-font-color0);
 81 |     min-height: calc( var(--jp-private-horizontal-tab-height) + 2 * var(--jp-border-width) );
 82 |     transform: translateY(var(--jp-border-width));
 83 | }
 84 | 
 85 | .nbtools-toolbox,
 86 | .nbtools-databank {
 87 |     overflow-y: auto;
 88 |     overflow-x: hidden;
 89 |     position: absolute;
 90 |     bottom: 0;
 91 |     top: 35px;
 92 |     right: 0;
 93 |     left: 0;
 94 | }
 95 | 
 96 | header.nbtools-origin {
 97 |     margin: 0;
 98 |     font-weight: 600;
 99 |     text-transform: uppercase;
100 |     color: var(--jp-ui-font-color1);
101 |     border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
102 |     letter-spacing: 1px;
103 |     font-size: var(--jp-ui-font-size0);
104 |     line-height: 24px;
105 |     height: 24px;
106 |     padding: 4px;
107 | }
108 | 
109 | header.nbtools-origin > .nbtools-collapse {
110 |     position: relative;
111 |     top: -7px;
112 | }
113 | 
114 | header.nbtools-origin > span.nbtools-header-title {
115 |     display: inline-block;
116 |     white-space: nowrap;
117 |     text-overflow: ellipsis;
118 |     overflow: hidden;
119 |     width: calc(100% - 90px);
120 | }
121 | 
122 | header.nbtools-origin > button {
123 |     border: var(--jp-border-width) solid transparent;
124 |     background: transparent;
125 |     color: var(--jp-ui-font-color1);
126 |     font-size: 0.9em;
127 |     float: right;
128 |     position: relative;
129 |     top: -5px;
130 |     height: 25px;
131 | }
132 | 
133 | header.nbtools-origin > button > menu {
134 |     position: absolute;
135 |     right: 0;
136 |     top: 13px;
137 |     list-style: none;
138 |     display: none;
139 |     z-index: 10000;
140 |     background: var(--jp-layout-color0);
141 |     color: var(--jp-ui-font-color1);
142 |     border: var(--jp-border-width) solid var(--jp-border-color1);
143 |     font-size: 0.9em;
144 |     box-shadow: var(--jp-elevation-z6);
145 |     width: 120px;
146 |     text-align: left;
147 |     padding: 2px 0 3px 0;
148 |     line-height: 1.6em;
149 | }
150 | 
151 | header.nbtools-origin > button > menu > li {
152 |     padding: 0 5px 1px 5px;
153 | }
154 | 
155 | header.nbtools-origin > button > menu > li:hover {
156 |     background: var(--jp-layout-color2);
157 | }
158 | 
159 | header.nbtools-origin > button:hover {
160 |     border: var(--jp-border-width) solid var(--jp-border-color2);
161 |     background: var(--jp-layout-color2);
162 |     cursor: pointer;
163 | }
164 | 
165 | header.nbtools-origin > button > i {
166 |     margin: 0 3px 0 3px;
167 | }
168 | 
169 | .nbtools-collapse {
170 |     font-weight: 600;
171 | }
172 | 
173 | .nbtools-expanded::before {
174 |     font-family: "Font Awesome 5 Free";
175 |     content: "\f0d7";
176 | }
177 | 
178 | .nbtools-collapsed::before {
179 |     font-family: "Font Awesome 5 Free";
180 |     content: "\f0da";
181 | }
182 | 
183 | ul.nbtools-origin {
184 |     padding-left: 0;
185 |     margin-top: 0;
186 | }
187 | 
188 | ul.nbtools-group {
189 |     padding-left: 15px;
190 | }
191 | 
192 | .nbtools-tool {
193 |     padding: 8px;
194 |     border-bottom: solid var(--jp-border-width) var(--jp-border-color2);
195 |     list-style-type: none;
196 |     cursor: pointer;
197 | }
198 | 
199 | .nbtools-tool:hover {
200 |     background-color: var(--jp-layout-color2);
201 | }
202 | 
203 | .nbtools-tool > .nbtools-header {
204 |     overflow: hidden;
205 |     white-space: nowrap;
206 |     text-overflow: ellipsis;
207 |     color: var(--jp-ui-font-color1);
208 |     font-size: var(--jp-ui-font-size1);
209 |     font-weight: 400;
210 |     padding: 0 12px 4px 0;
211 | }
212 | 
213 | .nbtools-tool > .nbtools-add {
214 |     float: right;
215 |     font-size: 28px;
216 |     line-height: 15px;
217 |     visibility: hidden;
218 |     color: var(--jp-ui-font-color2)
219 | }
220 | 
221 | .nbtools-tool:hover > .nbtools-add {
222 |     visibility: visible;
223 | }
224 | 
225 | .nbtools-tool > .nbtools-add.nbtools-hidden {
226 |     display: none;
227 | }
228 | 
229 | .nbtools-tool > .nbtools-description {
230 |     padding: 0 12px 4px 0;
231 |     font-size: var(--jp-ui-font-size1);
232 |     color: var(--jp-ui-font-color2);
233 |     font-weight: 400;
234 |     max-height: 3.5em;
235 |     overflow: hidden;
236 | }
237 | 
238 | .nbtools-tool .nbtools-data {
239 |     text-indent: -10px;
240 |     padding: 0 12px 4px 20px;
241 |     font-size: var(--jp-ui-font-size1);
242 |     color: var(--jp-ui-font-color2);
243 |     font-weight: 400;
244 |     max-height: 3.5em;
245 |     overflow: hidden;
246 |     display: block;
247 |     text-decoration: none;
248 |     word-break: break-all;
249 |     line-height: 1.28581;
250 |     box-sizing: unset;
251 | }
252 | 
253 | .nbtools-tool .nbtools-data:hover {
254 |     cursor: pointer;
255 |     background-color: var(--jp-layout-color3);
256 | }


--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
  1 | import { Application, IPlugin } from '@lumino/application';
  2 | import { Widget } from '@lumino/widgets';
  3 | import { IJupyterWidgetRegistry } from '@jupyter-widgets/base';
  4 | import { ISettingRegistry } from '@jupyterlab/settingregistry';
  5 | import { MODULE_NAME, MODULE_VERSION } from './version';
  6 | import * as base_exports from './basewidget';
  7 | import * as uioutput_exports from './uioutput';
  8 | import * as uibuilder_exports from './uibuilder';
  9 | import { IMainMenu } from '@jupyterlab/mainmenu';
 10 | import { ToolBrowser, Toolbox } from "./toolbox";
 11 | import { DataBrowser } from "./databank";
 12 | import { IToolRegistry, ToolRegistry } from "./registry";
 13 | import { pulse_red, usage_tracker } from "./utils";
 14 | import { ILabShell, ILayoutRestorer, JupyterFrontEnd } from "@jupyterlab/application";
 15 | import { INotebookTracker } from '@jupyterlab/notebook';
 16 | import { ContextManager } from "./context";
 17 | import { DataRegistry, IDataRegistry } from "./dataregistry";
 18 | 
 19 | const module_exports = { ...base_exports, ...uioutput_exports, ...uibuilder_exports };
 20 | const EXTENSION_ID = '@g2nb/nbtools:plugin';
 21 | const NAMESPACE = 'nbtools';
 22 | 
 23 | 
 24 | /**
 25 |  * The nbtools plugin.
 26 |  */
 27 | const nbtools_plugin: IPlugin, [IToolRegistry, IDataRegistry]> = ({
 28 |     id: EXTENSION_ID,
 29 |     provides: [IToolRegistry, IDataRegistry],
 30 |     requires: [IJupyterWidgetRegistry, ISettingRegistry],
 31 |     optional: [IMainMenu, ILayoutRestorer, ILabShell, INotebookTracker],
 32 |     activate: activate_widget_extension,
 33 |     autoStart: true
 34 | } as unknown) as IPlugin, [IToolRegistry, IDataRegistry]>;
 35 | 
 36 | export default nbtools_plugin;
 37 | 
 38 | 
 39 | /**
 40 |  * Activate the widget extension.
 41 |  */
 42 | async function activate_widget_extension(app: Application,
 43 |                                    widget_registry: IJupyterWidgetRegistry,
 44 |                                    settings: ISettingRegistry,
 45 |                                    mainmenu: IMainMenu|null,
 46 |                                    restorer: ILayoutRestorer|null,
 47 |                                    shell: ILabShell|null,
 48 |                                    notebook_tracker: INotebookTracker|null): Promise<[IToolRegistry, IDataRegistry]> {
 49 | 
 50 |     // Initialize the ContextManager
 51 |     init_context(app as JupyterFrontEnd, notebook_tracker);
 52 | 
 53 |     // Initialize settings
 54 |     const setting_dict = await init_settings(settings);
 55 | 
 56 |     // Create the tool and data registries
 57 |     const tool_registry = new ToolRegistry(setting_dict);
 58 |     const data_registry = new DataRegistry();
 59 | 
 60 |     // Add items to the help menu
 61 |     add_help_links(app as JupyterFrontEnd, mainmenu);
 62 | 
 63 |     // Add keyboard shortcuts
 64 |     add_keyboard_shortcuts(app as JupyterFrontEnd, tool_registry);
 65 | 
 66 |     // Add the toolbox
 67 |     add_tool_browser(app as JupyterFrontEnd, restorer);
 68 | 
 69 |     // Add the databank
 70 |     add_data_browser(app as JupyterFrontEnd, restorer);
 71 | 
 72 |     // Register the nbtools widgets with the widget registry
 73 |     widget_registry.registerWidget({
 74 |         name: MODULE_NAME,
 75 |         version: MODULE_VERSION,
 76 |         exports: module_exports,
 77 |     });
 78 | 
 79 |     // Register the plugin as loaded
 80 |     usage_tracker('labextension_load', location.protocol + '//' + location.host + location.pathname);
 81 | 
 82 |     // Return the tool registry so that it is provided to other extensions
 83 |     return [tool_registry, data_registry];
 84 | }
 85 | 
 86 | async function init_settings(settings:ISettingRegistry) {
 87 |     let setting = null;
 88 |     try { setting = await settings.load(EXTENSION_ID); }
 89 |     catch { console.log('Unable to load nbtools settings'); }
 90 |     return { force_render: setting ? setting.get('force_render').composite as boolean : true };
 91 | }
 92 | 
 93 | function init_context(app:JupyterFrontEnd, notebook_tracker: INotebookTracker|null) {
 94 |     ContextManager.jupyter_app = app;
 95 |     ContextManager.notebook_tracker = notebook_tracker;
 96 |     ContextManager.context();
 97 |     (window as any).ContextManager = ContextManager;  // Left in for development purposes
 98 | }
 99 | 
100 | function add_keyboard_shortcuts(app:JupyterFrontEnd, tool_registry:ToolRegistry) {
101 |     app.commands.addCommand("nbtools:insert-tool", {
102 |         label: 'Insert Notebook Tool',
103 |         execute: () => {
104 |             // Open the tool manager, if necessary
105 |             app.shell.activateById('nbtools-browser');
106 |             pulse_red(document.getElementById('nbtools-browser'));
107 | 
108 |             // If only one tool is available, add it
109 |             const tools = tool_registry.list();
110 |             if (tools.length === 1) Toolbox.add_tool_cell(tools[0]);
111 | 
112 |             // Otherwise give the search box focus
113 |             else (document.querySelector('.nbtools-search') as HTMLElement).focus()
114 |         },
115 |     });
116 | }
117 | 
118 | function add_data_browser(app:JupyterFrontEnd, restorer:ILayoutRestorer|null) {
119 |     const data_browser = new DataBrowser();
120 |     data_browser.title.iconClass = 'nbtools-icon fas fa-database jp-SideBar-tabIcon';
121 |     data_browser.title.caption = 'Databank';
122 |     data_browser.id = 'nbtools-data-browser';
123 | 
124 |     // Add the data browser widget to the application restorer
125 |     if (restorer) restorer.add(data_browser, NAMESPACE);
126 |     app.shell.add(data_browser, 'left', { rank: 103 });
127 | }
128 | 
129 | function add_tool_browser(app:JupyterFrontEnd, restorer:ILayoutRestorer|null) {
130 |     const tool_browser = new ToolBrowser();
131 |     tool_browser.title.iconClass = 'nbtools-icon fa fa-th jp-SideBar-tabIcon';
132 |     tool_browser.title.caption = 'Toolbox';
133 |     tool_browser.id = 'nbtools-browser';
134 | 
135 |     // Add the tool browser widget to the application restorer
136 |     if (restorer) restorer.add(tool_browser, NAMESPACE);
137 |     app.shell.add(tool_browser, 'left', { rank: 102 });
138 | }
139 | 
140 | /**
141 |  * Add the nbtools documentation and feedback links to the help menu
142 |  *
143 |  * @param {Application} app
144 |  * @param {IMainMenu} mainmenu
145 |  */
146 | function add_help_links(app:JupyterFrontEnd, mainmenu:IMainMenu|null) {
147 |     const feedback = 'nbtools:feedback';
148 |     const documentation = 'nbtools:documentation';
149 | 
150 |     // Add feedback command to the command palette
151 |     app.commands.addCommand(feedback, {
152 |         label: 'g2nb Help Forum',
153 |         caption: 'Open the g2nb help forum',
154 |         isEnabled: () => !!app.shell,
155 |         execute: () => {
156 |             const url = 'https://community.mesirovlab.org/c/g2nb/';
157 |             let element = document.createElement('a');
158 |             element.href = url;
159 |             element.target = '_blank';
160 |             document.body.appendChild(element);
161 |             element.click();
162 |             document.body.removeChild(element);
163 |             return void 0;
164 |         }
165 |     });
166 | 
167 |     // Add documentation command to the command palette
168 |     app.commands.addCommand(documentation, {
169 |         label: 'nbtools Documentation',
170 |         caption: 'Open documentation for nbtools',
171 |         isEnabled: () => !!app.shell,
172 |         execute: () => {
173 |             const url = 'https://github.com/g2nb/nbtools#nbtools';
174 |             let element = document.createElement('a');
175 |             element.href = url;
176 |             element.target = '_blank';
177 |             document.body.appendChild(element);
178 |             element.click();
179 |             document.body.removeChild(element);
180 |             return void 0;
181 |         }
182 |     });
183 | 
184 |     // Add documentation link to the help menu
185 |     if (mainmenu) mainmenu.helpMenu.addGroup([{command: feedback}, {command: documentation}], 2);
186 | }
187 | 


--------------------------------------------------------------------------------
/src/registry.ts:
--------------------------------------------------------------------------------
  1 | import { Widget } from "@lumino/widgets";
  2 | import { NotebookPanel } from "@jupyterlab/notebook";
  3 | import { send_notification } from "./utils";
  4 | import { ContextManager } from "./context";
  5 | import { Token } from "@lumino/coreutils";
  6 | 
  7 | export const IToolRegistry = new Token("nbtools");
  8 | 
  9 | export interface IToolRegistry {}
 10 | 
 11 | export class ToolRegistry implements ToolRegistry {
 12 |     public comm:any = null;                         // Reference to the comm used to communicate with the kernel
 13 |     public current:Widget|null = null;              // Reference to the currently selected notebook or other widget
 14 |     private _update_callbacks:Array = []; // Functions to call when an update happens
 15 |     kernel_tool_cache:any = {};                     // Keep a cache of kernels to registered tools
 16 |     kernel_import_cache:any = {};                   // Keep a cache of whether nbtools has been imported
 17 | 
 18 |     /**
 19 |      * Initialize the ToolRegistry and connect event handlers
 20 |      */
 21 |     constructor(setting_dict:any) {
 22 |         // Lazily assign the tool registry to the context
 23 |         if (!ContextManager.tool_registry) ContextManager.tool_registry = this;
 24 | 
 25 |         ContextManager.context().notebook_focus((current_widget:any) => {
 26 |             // Current notebook hasn't changed, no need to do anything, return
 27 |             if (this.current === current_widget) return;
 28 | 
 29 |             // Otherwise, update the current notebook reference
 30 |             this.current = current_widget;
 31 | 
 32 |             // If the current selected widget isn't a notebook, no comm is needed
 33 |             if (!(this.current instanceof NotebookPanel) && ContextManager.is_lab()) return;
 34 | 
 35 |             // Initialize the comm
 36 |             this.init_comm();
 37 | 
 38 |             // Load the default tools
 39 |             this.import_default_tools();
 40 | 
 41 |             // Ensure rendering of tool cells
 42 |             if (setting_dict.force_render) this.ensure_rendering();
 43 |         });
 44 |     }
 45 | 
 46 |     ensure_rendering() {
 47 |         ContextManager.context().kernel_ready(this.current, () => {
 48 |             if (!this.current) return;                              // Return if no notebook is selected
 49 |             ContextManager.context().run_tool_cells();
 50 |         });
 51 |     }
 52 | 
 53 |     import_default_tools() {
 54 |         ContextManager.context().kernel_changed(this.current, () => {
 55 |             ContextManager.context().execute_code(this.current, 'from nbtools import import_defaults\nimport_defaults()');
 56 |         });
 57 |     }
 58 | 
 59 |     /**
 60 |      * Initialize the comm between the notebook widget kernel and the ToolManager
 61 |      */
 62 |     init_comm() {
 63 |         ContextManager.context().kernel_ready(this.current, () => {
 64 |             const current:any = this.current;
 65 | 
 66 |             // Create a new comm that connects to the nbtools_comm target
 67 |             const connect_comm = () => {
 68 |                 const comm = ContextManager.context().create_comm(current, 'nbtools_comm', (msg:any) => {
 69 |                     // Handle message sent by the kernel
 70 |                     const data = msg.content.data;
 71 | 
 72 |                     if (data.func === 'update') {
 73 |                         this.update_tools(data.payload);
 74 |                         ContextManager.data_registry.update_data(data.payload);
 75 |                     }
 76 |                     else if (data.func === 'notification') send_notification(data.payload.message, data.payload.sender,
 77 |                         ContextManager.context().default_logo());
 78 |                     else console.error('ToolRegistry received unknown message: ' + data);
 79 |                 });
 80 | 
 81 |                 this.comm = comm;
 82 | 
 83 |                 // (window as any).comm = comm;
 84 |                 // (window as any).ToolRegistry = ToolRegistry;
 85 | 
 86 |                 // Request the current tool list
 87 |                 this.request_update(comm);
 88 |             };
 89 | 
 90 |             // When the kernel restarts or is changed, reconnect the comm
 91 |             ContextManager.context().kernel_changed(current, () => connect_comm());
 92 | 
 93 |             // Connect to the comm upon initial startup
 94 |             connect_comm();
 95 | 
 96 |             // Update tools from the cache
 97 |             this.update_from_cache();
 98 |         });
 99 |     }
100 | 
101 |     /**
102 |      * Get tools from the cache and make registered callbacks
103 |      */
104 |     update_from_cache() {
105 |         // Get the kernel ID
106 |         const kernel_id = this.current_kernel_id();
107 |         if (!kernel_id) return; // Do nothing if null
108 | 
109 |         // Get tools from the cache
110 |         const tool_list = this.kernel_tool_cache[kernel_id];
111 | 
112 |         // Make registered callbacks for when tools are updated
113 |         this._update_callbacks.forEach((callback) => {
114 |             callback(tool_list);
115 |         });
116 |     }
117 | 
118 |     /**
119 |      * Message the kernel, requesting an update to the tools cache
120 |      *
121 |      * @param comm
122 |      */
123 |     request_update(comm:any) {
124 |         comm.send({'func': 'request_update'});
125 |     }
126 | 
127 |     /**
128 |      * Send a command the kernel (used for databank buttons, etc.)
129 |      *
130 |      * @param comm
131 |      * @param command
132 |      * @param payload
133 |      */
134 |     send_command(comm:any, command:string, payload:object) {
135 |         comm.send({'func': command, 'payload': payload});
136 |     }
137 | 
138 |     /**
139 |      * Register an update callback with the ToolRegistry
140 |      *
141 |      * @param callback
142 |      */
143 |     on_update(callback:Function) {
144 |         this._update_callbacks.push(callback);
145 |     }
146 | 
147 |     /**
148 |      * Retrieve the kernel ID from the currently selected notebook
149 |      * Return null if no kernel or no notebook selected
150 |      */
151 |     current_kernel_id() {
152 |         return ContextManager.context().kernel_id(this.current);
153 |     }
154 | 
155 |     /**
156 |      * Update the tools cache for the current kernel
157 |      *
158 |      * @param message
159 |      */
160 |     update_tools(message:any) {
161 |         const kernel_id = this.current_kernel_id();
162 |         if (!kernel_id) return; // Do nothing if no kernel
163 | 
164 |         // Parse the message
165 |         const tool_list = message['tools'];
166 |         const needs_import = !!message['import'];
167 | 
168 |         // Update the cache
169 |         this.kernel_tool_cache[kernel_id] = tool_list;
170 |         this.kernel_import_cache[kernel_id] = needs_import;
171 | 
172 |         // Make registered callbacks when tools are updated
173 |         this._update_callbacks.forEach((callback) => {
174 |             callback(tool_list);
175 |         });
176 |     }
177 | 
178 |     /**
179 |      * Query whether nbtools has been imported in this kernel
180 |      */
181 |     needs_import():Boolean {
182 |         const kernel_id = this.current_kernel_id();
183 |         if (!kernel_id) return true; // Assume true if no kernel
184 | 
185 |         // Get import status from the cache and protect against undefined
186 |         return !this.kernel_import_cache[kernel_id];
187 |     }
188 | 
189 |     /**
190 |      * Returns a list of all currently registered tools
191 |      *
192 |      * @returns {Array} - A list of registered tools
193 |      */
194 |     list():Array {
195 |         const kernel_id = this.current_kernel_id();
196 |         if (!kernel_id) return []; // Empty list if no kernel
197 | 
198 |         // Get tools from the cache and protect against undefined
199 |         const tools = this.kernel_tool_cache[kernel_id];
200 |         if (!tools) return [];
201 | 
202 |         return Object.keys(tools).map(function(key) {
203 |             return tools[key];
204 |         });
205 |     }
206 | 
207 |     /**
208 |      * Has this tool already been registered?
209 |      *
210 |      * @param origin
211 |      * @param id
212 |      * @returns {boolean}
213 |      */
214 |     has_tool(origin:string, id:string|number) {
215 |         let found_tool = false;
216 | 
217 |         this.list().forEach(tool => {
218 |             if (tool.id === id && tool.origin === origin) found_tool = true;
219 |         });
220 | 
221 |         return found_tool;
222 |     }
223 | }


--------------------------------------------------------------------------------
/style/uibuilder.css:
--------------------------------------------------------------------------------
  1 | .nbtools-uibuilder .nbtools-description {
  2 |     min-height: 18px;
  3 |     padding-right: 60px;
  4 |     width: calc(100% - 50px);
  5 | }
  6 | 
  7 | .nbtools-error {
  8 |     padding: var(--jp-notebook-padding);
  9 |     border: var(--jp-error-color2) var(--jp-border-width) solid transparent;
 10 |     border-radius: var(--jp-border-radius);
 11 |     color: var(--jp-error-color0);
 12 |     background-color: var(--jp-error-color3);
 13 |     margin-bottom: 10px;
 14 | }
 15 | 
 16 | .nbtools-info {
 17 |     padding: var(--jp-notebook-padding);
 18 |     border: var(--jp-info-color2) var(--jp-border-width) solid transparent;
 19 |     border-radius: var(--jp-border-radius);
 20 |     color: var(--jp-info-color0);
 21 |     background-color: var(--jp-info-color3);
 22 |     margin-bottom: 10px;
 23 | }
 24 | 
 25 | .nbtools-run {
 26 |     z-index: 1;
 27 |     background-color: var(--jp-layout-color4);
 28 |     color: #FFFFFF;
 29 |     border: 1px solid var(--jp-border-color1);
 30 |     padding: 6px 12px;
 31 |     font-size: 0.9em;
 32 |     cursor: pointer;
 33 | }
 34 | 
 35 | .nbtools-buttons {
 36 |     position: absolute;
 37 |     z-index: 1;
 38 | }
 39 | 
 40 | .nbtools-buttons > button {
 41 |     z-index: 1;
 42 |     background-color: var(--jp-layout-color3);
 43 |     border: 1px solid var(--jp-border-color1);
 44 |     padding: 4px 12px;
 45 |     font-size: 0.9em;
 46 |     cursor: pointer;
 47 |     color: #FFFFFF;
 48 | }
 49 | 
 50 | .nbtools-buttons > button:hover,
 51 | .nbtools-buttons > button:active {
 52 |     background-color: var(--jp-layout-color4);
 53 | }
 54 | 
 55 | .nbtools-buttons:first-child {
 56 |     right: 5px;
 57 |     top: 4px;
 58 | }
 59 | 
 60 | .nbtools-buttons:last-child {
 61 |     right: 5px;
 62 |     bottom: 5px;
 63 | }
 64 | 
 65 | .nbtools-uibuilder .widget-interact {
 66 |     overflow: visible;
 67 | }
 68 | 
 69 | .nbtools .jupyter-button.hidden {
 70 |     display: none;
 71 | }
 72 | 
 73 | .nbtools-input > div.widget-label:first-child {
 74 |     font-weight: bold;
 75 |     text-align: right;
 76 |     padding-right: 20px;
 77 |     text-wrap: wrap;
 78 |     line-height: 1;
 79 | }
 80 | 
 81 | .nbtools-input > div.widget-label:last-child {
 82 |     height: auto;
 83 |     min-height: 5px;
 84 |     max-width: 100%;
 85 |     overflow: hidden !important;
 86 |     text-wrap: wrap;
 87 |     line-height: 1.3;
 88 |     padding-bottom: 5px;
 89 |     padding-top: 5px;
 90 | }
 91 | 
 92 | .nbtools-input.missing {
 93 |     border: red solid 2px;
 94 | }
 95 | 
 96 | .nbtools-input .widget-upload {
 97 |     width: 100px;
 98 |     top: 2px;
 99 | }
100 | 
101 | .nbtools div.widget-text.nbtools-menu-attached.nbtools-dropdown {
102 |     background-color: transparent;
103 |     /*z-index: 0;*/
104 | }
105 | 
106 | .nbtools div.widget-text.nbtools-menu-attached.nbtools-dropdown > input {
107 |     background-color: transparent;
108 |     /*z-index: 0;*/
109 | }
110 | 
111 | .nbtools-passwordinput input[type=password] {
112 |     box-sizing: border-box;
113 |     border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);
114 |     background-color: var(--jp-widgets-input-background-color);
115 |     color: var(--jp-widgets-input-color);
116 |     font-size: var(--jp-widgets-font-size);
117 |     flex-grow: 1;
118 |     min-width: 0;
119 |     flex-shrink: 1;
120 |     outline: currentcolor none medium !important;
121 |     height: var(--jp-widgets-inline-height);
122 |     line-height: var(--jp-widgets-inline-height);
123 | }
124 | 
125 | .nbtools .widget-dropdown > select,
126 | .nbtools .widget-dropdown > input {
127 |   width: 100%;
128 | }
129 | 
130 | .nbtools .widget-upload {
131 |   margin-right: 2px;
132 | }
133 | 
134 | .nbtools-fileinput .widget-vbox,
135 | .nbtools-fileinput .widget-text {
136 |     width: 100%;
137 | }
138 | 
139 | .widget-interact > .nbtools-input:last-child {
140 |     position: relative;
141 |     top: 13px;
142 |     z-index: 1;
143 |     height: 0;
144 |     overflow: visible;
145 | }
146 | 
147 | .nbtools-footer {
148 |     position: relative;
149 |     left: -10px;
150 |     top: 10px;
151 |     width: 100%;
152 |     background-color: var(--jp-layout-color2);
153 |     padding: 10px;
154 |     min-height: 18px;
155 |     box-sizing: content-box;
156 | }
157 | 
158 | .nbtools-dropdown {
159 |     position: relative;
160 |     display: inline-flex;
161 |     width: 100%;
162 | }
163 | 
164 | .nbtools-dropdown input::before{
165 |     position: absolute;
166 |     content: " \f078";
167 |     top: 5px;
168 |     right: 0;
169 |     height: 20px;
170 |     width: 20px;
171 |     font-size: 14px;
172 |     font-family: "Font Awesome 5 Free";
173 |     font-weight: 900;
174 |     font-style: normal;
175 |     font-variant: normal;
176 |     text-rendering: auto;
177 |     z-index: -1;
178 | }
179 | 
180 | .nbtools-uibuilder .nbtools-file-menu {
181 |     left: 0;
182 |     right: 0;
183 |     top: 27px;
184 |     max-height: 300px;
185 |     overflow-y: auto;
186 |     overflow-x: hidden;
187 |     z-index: 7;
188 | }
189 | 
190 | .nbtools-uibuilder .nbtools-group-header {
191 |     background-color: var(--jp-layout-color4);
192 |     color: var(--jp-ui-inverse-font-color0);
193 |     font-weight: bold;
194 |     padding: 5px 5px 5px 10px;
195 |     margin: 5px 0 0 0;
196 |     min-height: 25px;
197 |     line-height: 25px;
198 |     position: relative;
199 | }
200 | 
201 | .nbtools-uibuilder div.nbtools-group {
202 |     padding: 10px 10px 10px 10px;
203 |     border: solid var(--jp-border-width) var(--jp-border-color2);
204 |     border-top: none;
205 | }
206 | 
207 | .nbtools-uibuilder div.nbtools-group + div.nbtools-input {
208 |     margin-top: 20px;
209 | }
210 | 
211 | .nbtools-uibuilder div.nbtools-group-header.nbtools-hidden {
212 |     display: none;
213 | }
214 | 
215 | .nbtools-uibuilder div.nbtools-group-header.nbtools-hidden + div.nbtools-group {
216 |     padding: 0;
217 |     border: none;
218 | }
219 | 
220 | .nbtools-uibuilder div.nbtools-group > div.nbtools-header,
221 | .nbtools-uibuilder div.nbtools-group > div.nbtools-group > div.nbtools-description {
222 |     opacity: 0.8;
223 |     font-size: 0.9em;
224 | }
225 | 
226 | .nbtools-uibuilder div.nbtools-group > div.nbtools-group > div.nbtools-header,
227 | .nbtools-uibuilder div.nbtools-group > div.nbtools-group > div.nbtools-group > div.nbtools-description {
228 |     opacity: 0.7;
229 |     font-size: 0.8em;
230 | }
231 | 
232 | .nbtools-uibuilder div.nbtools-group > div.nbtools-group > div.nbtools-group > div.nbtools-header,
233 | .nbtools-uibuilder div.nbtools-group > div.nbtools-group > div.nbtools-group > div.nbtools-group > div.nbtools-description {
234 |     opacity: 0.6;
235 |     font-size: 0.7em;
236 | }
237 | 
238 | .nbtools-uibuilder .nbtools-group > .nbtools-description {
239 |     top: -10px;
240 |     left: -11px;
241 |     padding: 10px;
242 |     width: calc(100% + 2px);
243 | }
244 | 
245 | .nbtools-uibuilder .nbtools-input select[multiple] {
246 |     width: 100%;
247 |     max-width: 100%;
248 | }
249 | 
250 | .nbtools-advanced {
251 |     display: none;
252 | }
253 | 
254 | .nbtools-advanced.nbtools-advanced-show {
255 |     display: block;
256 | }
257 | 
258 | .nbtools-busy {
259 |     position: absolute;
260 |     right: 0;
261 |     left: 0;
262 |     top: 0;
263 |     bottom: 0;
264 |     background-color: rgba(0, 0, 0, 0.1);
265 |     z-index: 3;
266 |     font-size: 100px;
267 |     overflow: hidden;
268 |     display: none;
269 | }
270 | 
271 | .nbtools-busy > div {
272 |     position: relative;
273 |     width: 100%;
274 |     height: 100%;
275 | }
276 | 
277 | .nbtools-busy > div > i {
278 |     top: 20px;
279 |     left: 50%;
280 |     transform: translate(-50%, -50%);
281 |     position: absolute;
282 |     margin: 0;
283 |     margin-right: -50%;
284 | }
285 | 
286 | .nbtools-dialog {
287 |     display: block;
288 |     position: absolute;
289 |     top: 0;
290 |     bottom: 0;
291 |     left: 0;
292 |     right: 0;
293 |     background-color: var(--jp-ui-inverse-font-color3);
294 |     z-index: 2;
295 | }
296 | 
297 | .nbtools-dialog > .nbtools-panel {
298 |     height: auto;
299 |     width: 50%;
300 |     position: absolute;
301 |     top: 50%;
302 |     left: 50%;
303 |     transform: translate(-50%,-50%);
304 | }
305 | 
306 | .nbtools-dialog > .nbtools-panel > .nbtools-body {
307 |     height: calc(100% - 60px);
308 | }
309 | 
310 | .nbtools-dialog > .nbtools-panel > .nbtools-body > p {
311 |     height: calc(100% - 30px);
312 |     max-height: 300px;
313 |     overflow-x: hidden;
314 |     overflow-y: auto;
315 | }
316 | 
317 | .nbtools-panel-cancel {
318 |     border: 1px solid var(--jp-border-color1);
319 |     padding: 6px 12px;
320 |     font-size: 0.9em;
321 |     cursor: pointer;
322 | }


--------------------------------------------------------------------------------
/dev.Dockerfile:
--------------------------------------------------------------------------------
  1 | 
  2 | ###################################################################################
  3 | ##  NOTE                                                                         ##
  4 | ##  This Dockerfile mimics a development install. The Dockerfile that mimics a   ##
  5 | ##  pip install is now the default Dockerfile. This prevents an issue where the  ##
  6 | ##  dev Dockerfile runs out of memory when transpiling JS on Binder.             ##
  7 | ##  RUN: docker build -f dev.Dockerfile.db -t g2nb/lab .                         ##
  8 | ###################################################################################
  9 | 
 10 | #FROM g2nb/lab:24.08.2 AS lab
 11 | #
 12 | #RUN pip uninstall galahad -y && rm -r galahad
 13 | #RUN pip uninstall nbtools -y && rm -r nbtools
 14 | #
 15 | #RUN git clone https://github.com/g2nb/nbtools.git && cd nbtools && pip install . && echo 'Take 2'
 16 | #
 17 | #RUN git clone https://github.com/g2nb/galahad.git && \
 18 | #    cd galahad && \
 19 | #    pip install . && echo 'Take 3'
 20 | 
 21 | # Pull the latest known good scipy notebook image from the official Jupyter stacks
 22 | FROM jupyter/scipy-notebook:2023-04-10 AS lab
 23 | 
 24 | MAINTAINER Thorin Tabor 
 25 | EXPOSE 8888
 26 | 
 27 | #############################################
 28 | ##  ROOT                                   ##
 29 | ##      Install npm                        ##
 30 | #############################################
 31 | 
 32 | USER root
 33 | 
 34 | RUN apt-get update && apt-get install -y npm
 35 | 
 36 | #############################################
 37 | ##  $NB_USER                               ##
 38 | ##      Install python libraries           ##
 39 | #############################################
 40 | 
 41 | USER $NB_USER
 42 | 
 43 | RUN conda install -c conda-forge beautifulsoup4 blas bokeh cloudpickle dask dill h5py hdf5 jedi jinja2 libblas libcurl \
 44 |         matplotlib nodejs numba numexpr numpy pandas patsy pickleshare pillow pycurl requests scikit-image scikit-learn \
 45 |         scipy seaborn sqlalchemy sqlite statsmodels sympy traitlets vincent jupyter-archive jupyterlab-git && \
 46 |         conda install plotly openpyxl sphinx && \
 47 |         npm install -g yarn
 48 | RUN pip install --no-cache-dir plotnine bioblend py4cytoscape ndex2 qgrid ipycytoscape firecloud globus-jupyterlab boto3==1.16.30 \
 49 |     vitessce[all]
 50 | RUN pip install --no-cache-dir langchain-core langchain-community langchain langchain_chroma chroma bs4 pypdf unstructured pdfkit \
 51 |     fastembed langchain-openai langchain_experimental
 52 | # CUT (FOR NOW): conda install... voila
 53 | 
 54 | #############################################
 55 | ##  $NB_USER                               ##
 56 | ##      Install other labextensions        ##
 57 | #############################################
 58 | 
 59 | RUN jupyter labextension install jupyterlab-plotly --no-build && \
 60 |     printf '\nc.VoilaConfiguration.enable_nbextensions = True' >> /etc/jupyter/jupyter_notebook_config.py
 61 | 
 62 | #############################################
 63 | ##  $NB_USER                               ##
 64 | ##      Clone & install ipyuploads repo    ##
 65 | #############################################
 66 | 
 67 | RUN git clone https://github.com/g2nb/ipyuploads.git && \
 68 |     cd ipyuploads && pip install . && echo 'version 24.10 update'
 69 | 
 70 | #############################################
 71 | ##  $NB_USER                               ##
 72 | ##      Clone the nbtools repo             ##
 73 | #############################################
 74 | 
 75 | RUN git clone https://github.com/g2nb/nbtools.git && cd nbtools && pip install .
 76 | 
 77 | #############################################
 78 | ##  $NB_USER                               ##
 79 | ##      Clone and install genepattern      ##
 80 | #############################################
 81 | 
 82 | RUN git clone https://github.com/genepattern/genepattern-notebook.git && \
 83 |     cd genepattern-notebook && \
 84 |     pip install .
 85 | 
 86 | #############################################
 87 | ##  $NB_USER                               ##
 88 | ##      Clone and install jupyter-wysiwyg  ##
 89 | #############################################
 90 | 
 91 | RUN pip install jupyter-wysiwyg
 92 | #RUN git clone https://github.com/g2nb/jupyter-wysiwyg.git && \
 93 | #    cd jupyter-wysiwyg && \
 94 | #    pip install .
 95 | 
 96 | #############################################
 97 | ##  $NB_USER                               ##
 98 | ##      Install igv-jupyter                ##
 99 | #############################################
100 | 
101 | RUN git clone https://github.com/g2nb/igv-jupyter.git && \
102 |     cd igv-jupyter && \
103 |     pip install .
104 | 
105 | #############################################
106 | ##  $NB_USER                               ##
107 | ##      Install GalaxyLab                  ##
108 | #############################################
109 | 
110 | #RUN git clone -b build_function https://github.com/jaidevjoshi83/bioblend.git && \
111 | #    cd bioblend && pip install . && \
112 | #    git clone https://github.com/tmtabor/GiN.git && \
113 | #    cd GiN && npm install @g2nb/nbtools && pip install . && \
114 | #    jupyter nbextension install --py --symlink --overwrite --sys-prefix GiN && \
115 | #    jupyter nbextension enable --py --sys-prefix GiN
116 | RUN git clone -b build_function https://github.com/jaidevjoshi83/bioblend.git && \
117 |     cd bioblend && pip install . && pip install galaxy-gin==0.1.0a9
118 | 
119 | #############################################
120 | ##  $NB_USER                               ##
121 | ##      Install nvm and nodejs 18          ##
122 | #############################################
123 | 
124 | ENV NVM_DIR /home/jovyan/.nvm
125 | ENV NODE_VERSION 18.20.4
126 | 
127 | RUN wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \
128 |     && . $NVM_DIR/nvm.sh \
129 |     && nvm install $NODE_VERSION \
130 |     && nvm alias default $NODE_VERSION \
131 |     && nvm use default
132 | 
133 | ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
134 | ENV PATH      $NVM_DIR/v$NODE_VERSION/bin:$PATH
135 | 
136 | #############################################
137 | ##  $NB_USER                               ##
138 | ##      Install CyJupyter and CyWidget     ##
139 | #############################################
140 | 
141 | RUN git clone https://github.com/idekerlab/cy-jupyterlab.git && \
142 |     cd cy-jupyterlab && \
143 |     . $NVM_DIR/nvm.sh && \
144 |     nvm use $NODE_VERSION && \
145 |     npm install && \
146 |     npm run build && \
147 |     jupyter labextension install --debug .
148 | 
149 | RUN git clone https://github.com/g2nb/cywidget.git && \
150 |     cd cywidget && \
151 |     pip install .
152 | 
153 | #############################################
154 | ##  $NB_USER                               ##
155 | ##      Install galahad                    ##
156 | #############################################
157 | 
158 | RUN git clone https://github.com/g2nb/galahad.git && \
159 |     cd galahad && \
160 |     pip install . \
161 |     && rm /opt/conda/share/jupyter/nbtools/GiN.json
162 | 
163 | #############################################
164 | ##  $NB_USER                               ##
165 | ##      Install g2nb theme                 ##
166 | #############################################
167 | 
168 | RUN git clone https://github.com/g2nb/jupyterlab-theme.git && \
169 |     cd jupyterlab-theme && \
170 |     . $NVM_DIR/nvm.sh && \
171 |     nvm use $NODE_VERSION && \
172 |     jupyter labextension install . && \
173 |     jupyter lab build && \
174 |     cd .. && cp ./nbtools/config/overrides.json /opt/conda/share/jupyter/lab/settings/overrides.json
175 | 
176 | #############################################
177 | ##  $NB_USER                               ##
178 | ##      Launch lab by default              ##
179 | #############################################
180 | 
181 | ENV JUPYTER_ENABLE_LAB="true"
182 | ENV TERM xterm
183 | 
184 | #############################################
185 | ##  ROOT                                   ##
186 | ##      Install security measures          ##
187 | #############################################
188 | 
189 | FROM lab AS secure
190 | USER root
191 | 
192 | RUN mv /usr/bin/wget /usr/bin/.drgf && \
193 | #    mv /usr/bin/curl /usr/bin/.cdfg && \
194 |     mkdir -p /tmp/..drgf/patterns
195 | 
196 | COPY GPNBAntiCryptominer/wget_and_curl/wget /usr/bin/wget
197 | #COPY GPNBAntiCryptominer/wget_and_curl/curl /usr/bin/curl
198 | COPY GPNBAntiCryptominer/wget_and_curl/encrypted_patterns.zip /tmp/..drgf/patterns/
199 | 
200 | RUN chmod a+x /usr/bin/wget && \
201 |     mkdir -p /tmp/.wg && \
202 |     chmod a+rw /tmp/.wg && \
203 |     chmod -R a+rw /tmp/..drgf/patterns
204 | 
205 | USER $NB_USER


--------------------------------------------------------------------------------
/src/toolbox.ts:
--------------------------------------------------------------------------------
  1 | import { PanelLayout, Widget } from '@lumino/widgets';
  2 | import { toggle } from "./utils";
  3 | import { ContextManager } from "./context";
  4 | import { NotebookActions, NotebookPanel } from "@jupyterlab/notebook";
  5 | 
  6 | export class ToolBrowser extends Widget {
  7 |     public search:SearchBox|null = null;
  8 |     public toolbox:Toolbox|null = null;
  9 | 
 10 |     constructor() {
 11 |         super();
 12 |         this.addClass('nbtools-browser');
 13 |         this.layout = new PanelLayout();
 14 |         this.search = new SearchBox('#nbtools-browser > .nbtools-toolbox');
 15 |         this.toolbox = new Toolbox(this.search);
 16 | 
 17 |         (this.layout as PanelLayout).addWidget(this.search);
 18 |         (this.layout as PanelLayout).addWidget(this.toolbox);
 19 |     }
 20 | }
 21 | 
 22 | export class Toolbox extends Widget {
 23 |     last_update = 0;
 24 |     update_waiting = false;
 25 |     search:SearchBox;
 26 | 
 27 |     constructor(associated_search:SearchBox) {
 28 |         super();
 29 |         this.search = associated_search;
 30 |         this.addClass('nbtools-toolbox');
 31 |         this.addClass('nbtools-wrapper');
 32 | 
 33 |         // Update the toolbox when the tool registry changes
 34 |         ContextManager.tool_registry.on_update(() => {
 35 |             // If the last update was more than 10 seconds ago, update the toolbox
 36 |             if (this.update_stale()) this.fill_toolbox();
 37 |             else this.queue_update();  // Otherwise, queue an update if not already waiting for one
 38 |         });
 39 | 
 40 |         // Fill the toolbox with the registered tools
 41 |         this.fill_toolbox();
 42 |     }
 43 | 
 44 |     update_stale() {
 45 |         return this.last_update + (3 * 1000) < Date.now();
 46 |     }
 47 | 
 48 |     queue_update() {
 49 |         // If no update is waiting, queue an update
 50 |         if (!this.update_waiting) {
 51 |             setTimeout(() => {          // When an update happens
 52 |                 this.fill_toolbox();            // Fill the toolbox
 53 |                 this.update_waiting = false;    // And mark as no update queued
 54 |             }, Math.abs(this.last_update + (3 * 1000) - Date.now()));  // Queue for 3 seconds since last update
 55 |             this.update_waiting = true;                                    // And mark as queued
 56 |         }
 57 |     }
 58 | 
 59 |     static add_tool_cell(tool:any) {
 60 |         // Check to see if nbtools needs to be imported
 61 |         const import_line = ContextManager.tool_registry.needs_import() ? 'import nbtools\n\n' : '';
 62 | 
 63 |         // Add and run a code cell with the generated tool code
 64 |         Toolbox.add_code_cell(import_line + `nbtools.tool(id='${tool.id}', origin='${tool.origin}')`);
 65 |     }
 66 | 
 67 |     static add_code_cell(code:string) {
 68 |         if (!ContextManager.notebook_tracker) return; // If no NotebookTracker, do nothing
 69 | 
 70 |         const current = ContextManager.tool_registry.current;
 71 |         if (!current || !(current instanceof NotebookPanel)) return; // If no notebook is currently selected, return
 72 | 
 73 |         let cell = ContextManager.notebook_tracker.activeCell;
 74 |         if (!cell) return; // If no cell is selected, do nothing
 75 | 
 76 |         // If the currently selected cell isn't empty, insert a new one below and select it
 77 |         const current_cell_code = cell.model.value.text.trim();
 78 |         if (!!current_cell_code) NotebookActions.insertBelow(current.content);
 79 | 
 80 |         // Fill the cell with the tool's code
 81 |         cell = ContextManager.notebook_tracker.activeCell; // The active cell may just have been updated
 82 |         if (cell) cell.model.value.text = code;
 83 | 
 84 |         // Run the cell
 85 |         return NotebookActions.run(current.content, current.context.sessionContext);
 86 |     }
 87 | 
 88 |     fill_toolbox() {
 89 |         this.last_update = Date.now();
 90 | 
 91 |         // First empty the toolbox
 92 |         this.empty_toolbox();
 93 | 
 94 |         // Get the list of tools
 95 |         const tools = ContextManager.tool_registry.list();
 96 | 
 97 |         // Organize by origin and sort
 98 |         const organized_tools = this.organize_tools(tools);
 99 |         const origins = Object.keys(organized_tools);
100 |         origins.sort((a:any, b:any) => {
101 |             const a_name = a.toLowerCase();
102 |             const b_name = b.toLowerCase();
103 |             return (a_name < b_name) ? -1 : (a_name > b_name) ? 1 : 0;
104 |         });
105 | 
106 |         // Add each origin
107 |         origins.forEach((origin) => {
108 |             const origin_box = this.add_origin(origin);
109 |             organized_tools[origin].forEach((tool:any) => {
110 |                 this.add_tool(origin_box, tool);
111 |             })
112 |         });
113 | 
114 |         // Apply search filter after refresh
115 |         this.search.filter(this.search.node.querySelector('input.nbtools-search') as HTMLInputElement);
116 |     }
117 | 
118 |     organize_tools(tool_list:Array):any {
119 |         const organized:any = {};
120 | 
121 |         // Group tools by origin
122 |         tool_list.forEach((tool) => {
123 |             if (tool.origin in organized) organized[tool.origin].push(tool);    // Add tool to origin
124 |             else organized[tool.origin] = [tool];                               // Lazily create origin
125 |         });
126 | 
127 |         // Sort the tools in each origin
128 |         Object.keys(organized).forEach((origin) => {
129 |             organized[origin].sort((a:any, b:any) => {
130 |                 const a_name = a.name.toLowerCase();
131 |                 const b_name = b.name.toLowerCase();
132 |                 return (a_name < b_name) ? -1 : (a_name > b_name) ? 1 : 0;
133 |             });
134 |         });
135 | 
136 |         // Return the organized set of notebooks
137 |         return organized
138 |     }
139 | 
140 |     empty_toolbox() {
141 |         this.node.innerHTML = '';
142 |     }
143 | 
144 |     add_origin(name:string) {
145 |         // Create the HTML DOM element
146 |         const origin_wrapper = document.createElement('div');
147 |         origin_wrapper.innerHTML = `
148 |             
149 | 150 | ${name} 151 |
152 |
    `; 153 | 154 | // Attach the expand / collapse functionality 155 | const collapse = origin_wrapper.querySelector('span.nbtools-collapse') as HTMLElement; 156 | collapse.addEventListener("click", () => this.toggle_collapse(origin_wrapper)); 157 | 158 | // Add to the toolbox 159 | this.node.append(origin_wrapper); 160 | return origin_wrapper; 161 | } 162 | 163 | add_tool(origin:HTMLElement, tool:any) { 164 | const list = origin.querySelector('ul'); 165 | const tool_wrapper = document.createElement('li'); 166 | tool_wrapper.classList.add('nbtools-tool'); 167 | tool_wrapper.setAttribute('title', 'Click to add to notebook'); 168 | tool_wrapper.innerHTML = ` 169 |
    +
    170 |
    ${tool.name}
    171 |
    ${tool.description}
    `; 172 | if (list) list.append(tool_wrapper); 173 | 174 | // Add the click event 175 | tool_wrapper.addEventListener("click", () => { 176 | Toolbox.add_tool_cell(tool); 177 | }) 178 | } 179 | 180 | toggle_collapse(origin_wrapper:HTMLElement) { 181 | const list = origin_wrapper.querySelector("ul.nbtools-origin") as HTMLElement; 182 | const collapsed = list.classList.contains('nbtools-hidden'); 183 | 184 | // Toggle the collapse button 185 | const collapse = origin_wrapper.querySelector('span.nbtools-collapse') as HTMLElement; 186 | if (collapsed) { 187 | collapse.classList.add('nbtools-expanded'); 188 | collapse.classList.remove('nbtools-collapsed'); 189 | } 190 | else { 191 | collapse.classList.remove('nbtools-expanded'); 192 | collapse.classList.add('nbtools-collapsed'); 193 | } 194 | 195 | // Hide or show widget body 196 | toggle(list); 197 | } 198 | } 199 | 200 | export class SearchBox extends Widget { 201 | panel_query:string; 202 | value:string; 203 | 204 | constructor(panel_query:string) { 205 | super(); 206 | this.panel_query = panel_query; 207 | this.value = ''; 208 | this.node.innerHTML = ` 209 |
    210 |
    211 | 212 |
    213 |
    214 | `; 215 | 216 | this.attach_events(); 217 | } 218 | 219 | attach_events() { 220 | // Attach the change event to the search box 221 | const search_box = this.node.querySelector('input.nbtools-search') as HTMLInputElement; 222 | search_box.addEventListener("keyup", () => this.filter(search_box)); 223 | } 224 | 225 | filter(search_box:HTMLInputElement) { 226 | // Update the value state 227 | this.value = search_box.value.toLowerCase().replace(/[^a-z0-9]/g, ''); 228 | 229 | // Get the panel 230 | const panel = document.querySelector(this.panel_query) as HTMLElement|null; 231 | if (!panel) return; // Do nothing if the panel is null 232 | 233 | // Show any tool that matches and hide anything else 234 | panel.querySelectorAll('li.nbtools-tool').forEach((tool:any) => { 235 | if (tool.textContent.toLowerCase().replace(/[^a-z0-9]/g, '').includes(this.value)) tool.style.display = 'block'; 236 | else tool.style.display = 'none'; 237 | }); 238 | } 239 | } -------------------------------------------------------------------------------- /src/dataregistry.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from "@lumino/widgets"; 2 | import { ContextManager } from "./context"; 3 | import { Token } from "@lumino/coreutils"; 4 | import {extract_file_name, extract_file_type} from "./utils"; 5 | 6 | export const IDataRegistry = new Token("nbtools:IDataRegistry") 7 | 8 | export interface IDataRegistry {} 9 | 10 | export class DataRegistry implements IDataRegistry { 11 | public current:Widget|null = null; // Reference to the currently selected notebook or other widget 12 | update_callbacks:Array = []; // Callbacks to execute when the cache is updated 13 | kernel_data_cache:any = {}; // Keep a cache of kernels to registered data 14 | // { 'kernel_id': { 'origin': { 'identifier': data } } } 15 | kernel_origin_cache:any = {}; // Keep a cache of kernels to registered origins 16 | // { 'kernel_id': { 'origin': {} } } 17 | 18 | /** 19 | * Initialize the DataRegistry and connect event handlers 20 | */ 21 | constructor() { 22 | // Lazily assign the data registry to the context 23 | if (!ContextManager.data_registry) ContextManager.data_registry = this; 24 | 25 | ContextManager.context().notebook_focus((current_widget:any) => { 26 | // Current notebook hasn't changed, no need to do anything, return 27 | if (this.current === current_widget) return; 28 | 29 | // Otherwise, update the current notebook reference 30 | this.current = current_widget; 31 | }); 32 | } 33 | 34 | register_all_origins(origin_list:Array) { 35 | let all_good = true; 36 | for (const origin of origin_list) { 37 | origin.skip_callbacks = true; 38 | all_good = this.register_origin(origin) && all_good; 39 | } 40 | // this.execute_callbacks(); 41 | return all_good; 42 | } 43 | 44 | register_origin(origin:any) { 45 | const kernel_id = this.current_kernel_id(); 46 | if (!kernel_id) return false; // If no kernel, do nothing 47 | 48 | // Lazily initialize dict for kernel cache 49 | let cache = this.kernel_origin_cache[kernel_id]; 50 | if (!cache) cache = this.kernel_origin_cache[kernel_id] = {}; 51 | 52 | // Add to cache, execute callbacks and return 53 | this.kernel_origin_cache[kernel_id][origin.name] = origin; 54 | if (!origin.skip_callbacks) this.execute_callbacks(); 55 | return true 56 | } 57 | 58 | /** 59 | * Register all data objects in the provided list 60 | * 61 | * @param data_list 62 | */ 63 | register_all(data_list:Array): boolean { 64 | let all_good = true; 65 | for (const data of data_list) { 66 | data.skip_callbacks = true; 67 | all_good = this.register(data) && all_good; 68 | } 69 | this.execute_callbacks(); 70 | return all_good; 71 | } 72 | 73 | /** 74 | * Register data for the sent to/come from menus 75 | * Return whether registration was successful or not 76 | * 77 | * @param origin 78 | * @param uri 79 | * @param label 80 | * @param kind 81 | * @param group 82 | * @param icon 83 | * @param data 84 | * @param widget 85 | * @param skip_callbacks 86 | */ 87 | register({origin=null, uri=null, label=null, kind=null, group=null, icon=null, data=null, widget=false, skip_callbacks=false}: 88 | {origin?:string|null, uri?: string|null, label?: string|null, kind?: string|null, group?: string|null, icon?: string|null, data?:Data|null, widget?:boolean, skip_callbacks: boolean}): boolean { 89 | // Use origin, identifier, label and kind to initialize data, if needed 90 | if (!data) data = new Data(origin, uri, label, kind, group, widget, icon); 91 | 92 | const kernel_id = this.current_kernel_id(); 93 | if (!kernel_id) return false; // If no kernel, do nothing 94 | 95 | // Lazily initialize dict for kernel cache 96 | let cache = this.kernel_data_cache[kernel_id]; 97 | if (!cache) cache = this.kernel_data_cache[kernel_id] = {}; 98 | 99 | // Lazily initialize dict for origin 100 | let origin_data = cache[data.origin]; 101 | if (!origin_data) origin_data = cache[data.origin] = {}; 102 | 103 | // Add to cache, execute callbacks and return 104 | if (!origin_data[data.uri]) origin_data[data.uri] = []; 105 | origin_data[data.uri].unshift(data); 106 | if (!skip_callbacks) this.execute_callbacks(); 107 | return true 108 | } 109 | 110 | /** 111 | * Unregister data with the given origin and identifier 112 | * Return the unregistered data object 113 | * Return null if un-registration was unsuccessful 114 | * 115 | * @param origin 116 | * @param identifier 117 | * @param data 118 | */ 119 | unregister({origin=null, uri=null, data=null}: 120 | {origin?:string|null, uri?: string|null, data?:Data|null}): Data|null { 121 | // Use origin, identifier and kind to initialize data, if needed 122 | if (!data) data = new Data(origin, uri); 123 | 124 | const kernel_id = this.current_kernel_id(); 125 | if (!kernel_id) return null; // If no kernel, do nothing 126 | 127 | // If unable to retrieve cache, return null 128 | const cache = this.kernel_data_cache[kernel_id]; 129 | if (!cache) return null; 130 | 131 | // If unable to retrieve origin, return null 132 | const origin_data = cache[data.origin]; 133 | if (!origin_data) return null; 134 | 135 | // If unable to find identifier, return null; 136 | let found = origin_data[data.uri]; 137 | if (!found || !found.length) return null; 138 | 139 | // Remove from the registry, execute callbacks and return 140 | found = origin_data[data.uri].shift(); 141 | if (!origin_data[data.uri].length) delete origin_data[data.uri]; 142 | this.execute_callbacks(); 143 | return found; 144 | } 145 | 146 | /** 147 | * Execute all registered update callbacks 148 | */ 149 | execute_callbacks() { 150 | for (const c of this.update_callbacks) c(); 151 | } 152 | 153 | /** 154 | * Attach a callback that gets executed every time the data in the registry is updated 155 | * 156 | * @param callback 157 | */ 158 | on_update(callback:Function) { 159 | this.update_callbacks.push(callback); 160 | } 161 | 162 | /** 163 | * Update the data cache for the current kernel 164 | * 165 | * @param message 166 | */ 167 | update_data(message:any) { 168 | const kernel_id = this.current_kernel_id(); 169 | if (!kernel_id) return; // Do nothing if no kernel 170 | 171 | // Parse the message 172 | const data_list = message['data']; 173 | const origins = message['origins']; 174 | 175 | // Update the origin cache 176 | this.kernel_origin_cache[kernel_id] = {}; 177 | this.register_all_origins(origins); 178 | 179 | // Update the data cache 180 | this.kernel_data_cache[kernel_id] = {}; 181 | this.register_all(data_list); 182 | } 183 | 184 | /** 185 | * List all data currently in the registry 186 | */ 187 | list() { 188 | // If no kernel, return empty map 189 | const kernel_id = this.current_kernel_id(); 190 | if (!kernel_id) return {}; 191 | 192 | // If unable to retrieve cache, return empty map 193 | const cache = this.kernel_data_cache[kernel_id]; 194 | if (!cache) return {}; 195 | 196 | // FORMAT: { 'origin': { 'identifier': [data] } } 197 | return cache; 198 | } 199 | 200 | list_origins() { 201 | // If no kernel, return empty map 202 | const kernel_id = this.current_kernel_id(); 203 | if (!kernel_id) return {}; 204 | 205 | // If unable to retrieve cache, return empty map 206 | const cache = this.kernel_origin_cache[kernel_id]; 207 | if (!cache) return {}; 208 | 209 | // FORMAT: { 'name': origin} 210 | return cache; 211 | } 212 | 213 | 214 | /** 215 | * Get all data that matches one of the specified kinds or origins 216 | * If kinds or origins is null or empty, accept all kinds or origins, respectively 217 | * 218 | * @param kinds 219 | * @param origins 220 | */ 221 | get_data({kinds=null, origins=null}: { kinds:string[]|null, origins:string[]|null }) { 222 | const kernel_id = this.current_kernel_id(); 223 | if (!kernel_id) return {}; // If no kernel, return empty 224 | 225 | // If unable to retrieve cache, return empty 226 | const cache = this.kernel_data_cache[kernel_id]; 227 | if (!cache) return {}; 228 | 229 | // Compile map of data with a matching origin and kind 230 | const matching:any = {}; 231 | for (let origin of Object.keys(cache)) { 232 | if (origins === null || origins.length === 0 || origins.includes(origin)) { 233 | const hits:any = {}; 234 | for (let data of Object.values(cache[origin]) as any) { 235 | if (data[0].kind === 'error') continue; 236 | if (kinds === null || kinds.length === 0 || kinds.includes(data[0].kind)) 237 | hits[data[0].label] = data[0].uri; 238 | } 239 | if (Object.keys(hits).length > 0) matching[origin] = hits 240 | } 241 | } 242 | 243 | return matching; 244 | } 245 | 246 | /** 247 | * Retrieve the kernel ID from the currently selected notebook 248 | * Return null if no kernel or no notebook selected 249 | */ 250 | current_kernel_id() { 251 | return ContextManager.context().kernel_id(this.current); 252 | } 253 | } 254 | 255 | export class Data { 256 | public origin: string; 257 | public uri: string; 258 | public label: string; 259 | public kind: string; 260 | public group: string; 261 | public widget: boolean; 262 | public icon: string; 263 | 264 | constructor(origin:string, uri:string, label:string|null=null, kind:string|null=null, group:string|null=null, 265 | widget:boolean=false, icon:string|null=null) { 266 | this.origin = origin; 267 | this.uri = uri; 268 | this.label = !!label ? label : extract_file_name(uri); 269 | this.kind = !!kind ? kind : extract_file_type(uri); 270 | this.group = group; 271 | this.widget = widget; 272 | this.icon = icon; 273 | } 274 | } -------------------------------------------------------------------------------- /nbtools/nbextension/static/toolbox.js: -------------------------------------------------------------------------------- 1 | define("nbtools/toolbox", ["base/js/namespace", 2 | "nbextensions/jupyter-js-widgets/extension", 3 | "jquery"], function (Jupyter, widgets, $) { 4 | 5 | /** 6 | * Initialize the Notebook Toolbox 7 | */ 8 | function init() { 9 | // Add the toolbar button 10 | const action = { 11 | icon: 'fa-th', 12 | help : 'Tools', 13 | help_index : 'zz', 14 | handler : function() { 15 | tool_dialog(); 16 | } 17 | }; 18 | const prefix = 'nbtools'; 19 | const action_name = 'toolbox'; 20 | 21 | const full_action_name = Jupyter.actions.register(action, action_name, prefix); 22 | Jupyter.toolbar.add_buttons_group([{ 23 | 'action': full_action_name, 24 | 'id': 'nbtools-toolbar', 25 | 'label': 'Tools' 26 | }]); 27 | 28 | // Add the label, if necessary (in Jupyter <= 5.0) 29 | const tool_button = $("#nbtools-toolbar"); 30 | if (tool_button.text().indexOf("Tools") < 0) { 31 | tool_button.append(" Tools"); 32 | } 33 | } 34 | 35 | function tool_button(id, name, origin, anno, desc, tags) { 36 | const tagString = tags.join(", "); 37 | return $("
    ") 38 | .addClass("well well-sm nbtools-tool") 39 | .attr("name", id) 40 | .attr("data-id", id) 41 | .attr("data-name", name) 42 | .attr("data-origin", origin) 43 | .append( 44 | $("

    ") 45 | .addClass("nbtools-name") 46 | .append(name) 47 | ) 48 | .append( 49 | $("
    ") 50 | .addClass("nbtools-anno") 51 | .append(origin + (anno ? ", " + anno : anno)) 52 | ) 53 | .append( 54 | $("") 55 | .addClass("nbtools-desc") 56 | .append(desc) 57 | ) 58 | .append( 59 | $("") 60 | .addClass("nbtools-tags") 61 | .append(tagString) 62 | ); 63 | } 64 | 65 | function tab_exists(origin, toolbox) { 66 | return get_tab(origin, toolbox).length > 0; 67 | } 68 | 69 | function dom_encode(str) { 70 | return str.replace(/\W+/g, "_"); 71 | } 72 | 73 | function add_tab(origin, toolbox) { 74 | // Check to see if the tab already exists 75 | const tab_id = "nbtools-" + dom_encode(origin); 76 | if (tab_exists(origin)) { 77 | console.log("WARNING: Attempting to add slider tab that already exists"); 78 | return; 79 | } 80 | 81 | // Add the tab in the correct order 82 | const tabs = toolbox.find(".nav-tabs > li"); 83 | let after_this = null; 84 | tabs.each(function(i, e) { 85 | const tab_name = $(e).find("a").attr("name"); 86 | if (tab_name === "All") { 87 | after_this = $(e); 88 | } 89 | else if (tab_name < origin && tab_name !== "+") { 90 | after_this = $(e); 91 | } 92 | else if (origin === "+") { 93 | after_this = $(e); 94 | } 95 | }); 96 | 97 | const new_tab = $("
  • ").append( 98 | $("") 99 | .attr("data-toggle", "tab") 100 | .attr("href", "#" + tab_id) 101 | .attr("name", origin) 102 | .text(origin) 103 | ); 104 | if (origin === "All") tabs.parent().append(new_tab); 105 | else new_tab.insertAfter(after_this); 106 | 107 | // Add the content pane 108 | const contents = toolbox.find(".tab-content"); 109 | contents.append( 110 | $("
    ") 111 | .attr("id", tab_id) 112 | .addClass("tab-pane") 113 | ); 114 | } 115 | 116 | function get_tab(origin, toolbox) { 117 | const tab_id = "nbtools-" + dom_encode(origin); 118 | return toolbox ? toolbox.find("#" + tab_id) : $("#" + tab_id); 119 | } 120 | 121 | function sort_tools(tools) { 122 | tools.sort(function(a, b) { 123 | if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; 124 | else if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; 125 | else return 0; 126 | }); 127 | } 128 | 129 | function update_toolbox(toolbox) { 130 | // Get the correct list divs 131 | const nbtools_div = toolbox.find("#nbtools-tabs"); 132 | 133 | // Do we need to refresh the cache? 134 | const refresh = true; 135 | 136 | // Refresh the cache, if necessary 137 | if (refresh) { 138 | // Empty the list divs 139 | nbtools_div.find(".tab-content").children().empty(); 140 | 141 | // Get the updated list of tools 142 | const tools = NBToolManager.list(); 143 | 144 | // Sort the tools 145 | sort_tools(tools); 146 | 147 | // Add the tools to the lists 148 | tools.forEach(function(tool) { 149 | const t_button = tool_button( 150 | tool.id, 151 | tool.name, 152 | tool.origin, 153 | tool.version ? tool.version : "", 154 | tool.description ? tool.description : "", 155 | tool.tags ? tool.tags : []); 156 | 157 | // Render the tool 158 | const click_event = function() { 159 | let cell = Jupyter.notebook.get_selected_cell(); 160 | const contents = cell.get_text().trim(); 161 | 162 | // Insert a new cell if the current one has contents 163 | if (contents !== "") { 164 | cell = Jupyter.notebook.insert_cell_below(); 165 | Jupyter.notebook.select_next(); 166 | } 167 | 168 | // Check to see if nbtools needs to be imported 169 | const import_line = NBToolManager.needs_import() ? 'import nbtools\n\n' : ''; 170 | const code = import_line + `nbtools.tool(id='${tool.id}', origin='${tool.origin}')`; 171 | 172 | cell.set_text(code); 173 | cell.execute(); 174 | 175 | // Scroll to the cell, if applicable 176 | if (cell) { 177 | $('#site').animate({ 178 | scrollTop: $(cell.element).position().top 179 | }, 500); 180 | } 181 | 182 | // Close the toolbox dialog 183 | $(".modal-dialog button.close").trigger("click"); 184 | }; 185 | 186 | // Attach the click 187 | t_button.click(click_event); 188 | 189 | // Does the origin div exist? 190 | const existing_tab = tab_exists(tool.origin, toolbox); 191 | 192 | // If it doesn't exist, create it 193 | if (!existing_tab) add_tab(tool.origin, toolbox); 194 | 195 | // Get the tab and add the tool 196 | get_tab(tool.origin, toolbox).append(t_button); 197 | 198 | // Add to the All Tools tab, if necessary 199 | if (tool.origin !== "All") { 200 | const t_button_all = t_button.clone(); 201 | t_button_all.click(click_event); 202 | get_tab("All", toolbox).append(t_button_all); 203 | } 204 | }); 205 | } 206 | } 207 | 208 | function build_toolbox() { 209 | const toolbox = $("
    ") 210 | .attr("id", "nbtools") 211 | .css("height", $(window).height() - 200) 212 | 213 | // Append the filter box 214 | .append( 215 | $("
    ") 216 | .attr("id", "nbtools-filter-box") 217 | .append( 218 | $("") 219 | .attr("id", "nbtools-filter") 220 | .attr("type", "search") 221 | .attr("placeholder", "Type to Filter") 222 | .keydown(function(event) { 223 | event.stopPropagation(); 224 | }) 225 | .keyup(function() { 226 | const search = $("#nbtools-filter").val().toLowerCase(); 227 | $.each($("#nbtools-tabs").find(".nbtools-tool"), function(index, element) { 228 | const raw = $(element).text().toLowerCase(); 229 | if (raw.indexOf(search) === -1) { 230 | $(element).hide(); 231 | } 232 | else { 233 | $(element).show(); 234 | } 235 | }) 236 | }) 237 | ) 238 | ) 239 | 240 | // Append the internal tabs 241 | .append( 242 | $("
    ") 243 | .attr("id", "nbtools-tabs") 244 | .addClass("tabbable") 245 | .append( 246 | $("
      ") 247 | .addClass("nav nav-tabs") 248 | .append( 249 | $("
    • ") 250 | .addClass("active") 251 | .append( 252 | $("") 253 | .attr("data-toggle", "tab") 254 | .attr("href", "#nbtools-All") 255 | .attr("name", "All") 256 | .text("All Tools") 257 | ) 258 | ) 259 | ) 260 | .append( 261 | $("
      ") 262 | .addClass("tab-content") 263 | .css("height", $(window).height() - 250) 264 | .append( 265 | $("
      ") 266 | .attr("id", "nbtools-All") 267 | .addClass("tab-pane active") 268 | ) 269 | ) 270 | ); 271 | 272 | update_toolbox(toolbox); 273 | return toolbox; 274 | } 275 | 276 | function tool_dialog() { 277 | const dialog = require('base/js/dialog'); 278 | dialog.modal({ 279 | notebook: Jupyter.notebook, 280 | keyboard_manager: this.keyboard_manager, 281 | title : "Select Notebook Tool", 282 | body : build_toolbox(), 283 | buttons : {} 284 | }); 285 | 286 | // Give focus to the search box 287 | setTimeout(function() { 288 | $("#nbtools-filter").focus(); 289 | }, 200); 290 | } 291 | 292 | /** 293 | * Return the toolbox's public methods 294 | */ 295 | return { 296 | init: init 297 | } 298 | }); -------------------------------------------------------------------------------- /src/uioutput.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Widget for representing Python output as an interactive interface 3 | * 4 | * @author Thorin Tabor 5 | * 6 | * Copyright 2020 Regents of the University of California and the Broad Institute 7 | */ 8 | import '../style/uioutput.css' 9 | import { ISerializers, unpack_models } from '@jupyter-widgets/base'; 10 | import { MODULE_NAME, MODULE_VERSION } from './version'; 11 | import { BaseWidgetModel, BaseWidgetView } from "./basewidget"; 12 | import { extract_file_name, extract_file_type, get_absolute_url, is_absolute_path, is_url } from './utils'; 13 | import { ContextManager } from "./context"; 14 | 15 | // noinspection JSAnnotator 16 | export class UIOutputModel extends BaseWidgetModel { 17 | static model_name = 'UIOutputModel'; 18 | static model_module = MODULE_NAME; 19 | static model_module_version = MODULE_VERSION; 20 | static view_name = 'UIOutputView'; 21 | static view_module = MODULE_NAME; 22 | static view_module_version = MODULE_VERSION; 23 | 24 | static serializers: ISerializers = { 25 | ...BaseWidgetModel.serializers, 26 | appendix: { deserialize: unpack_models } 27 | }; 28 | 29 | defaults() { 30 | return { 31 | ...super.defaults(), 32 | _model_name: UIOutputModel.model_name, 33 | _model_module: UIOutputModel.model_module, 34 | _model_module_version: UIOutputModel.model_module_version, 35 | _view_name: UIOutputModel.view_name, 36 | _view_module: UIOutputModel.view_module, 37 | _view_module_version: UIOutputModel.view_module_version, 38 | name: 'Python Results', 39 | description: '', 40 | status: '', 41 | files: [] as any, 42 | text: '', 43 | visualization: '', 44 | appendix: undefined as any, 45 | extra_file_menu_items: {}, 46 | default_file_menu_items: true, 47 | attach_file_prefixes: true 48 | }; 49 | } 50 | } 51 | 52 | // noinspection JSAnnotator 53 | export class UIOutputView extends BaseWidgetView { 54 | dom_class = 'nbtools-uioutput'; 55 | traitlets = [...super.basics(), 'status', 'files', 'text', 'visualization']; 56 | renderers:any = { 57 | "description": this.render_description, 58 | "error": this.render_error, 59 | "info": this.render_info, 60 | "files": this.render_files, 61 | "visualization": this.render_visualization 62 | }; 63 | body:string = ` 64 |
      65 |
      66 |
      67 |
      68 |
      69 |
      
       70 |         
      71 |
      `; 72 | 73 | render() { 74 | super.render(); 75 | 76 | // Add the child widgets 77 | this.attach_child_widget('.nbtools-appendix', 'appendix'); 78 | } 79 | 80 | remove() { 81 | super.remove(); 82 | } 83 | 84 | render_files(files:any[], widget:UIOutputView) { 85 | let to_return = ''; 86 | files.forEach(entry => { 87 | const path = Array.isArray(entry) && entry.length >= 1 ? entry[0] : entry; 88 | const name = Array.isArray(entry) && entry.length >= 2 ? entry[1] : extract_file_name(path); 89 | const type = Array.isArray(entry) && entry.length >= 3 ? entry[2] : extract_file_type(path) as string; 90 | const path_prefix = UIOutputView.pick_path_prefix(path, widget); 91 | to_return += `${name} `; 92 | to_return += `` 93 | }); 94 | 95 | setTimeout(() => widget.initialize_file_menus(widget), 100); 96 | return to_return; 97 | } 98 | 99 | render_visualization(visualization:string, widget:UIOutputView) { 100 | // Function for toggling pop out menu item on or off 101 | function toggle_open_visualizer(hide:boolean) { 102 | const controls = widget.element.querySelector('.nbtools-controls'); 103 | if (!controls) return; // Get the gear menu buttons at the top and protect against null 104 | 105 | // Toggle or set the Pop Out Visualizer menu option's visibility 106 | controls.querySelectorAll('.nbtools-menu > li').forEach((item:any) => { 107 | if (item.textContent.includes('Pop Out Visualizer')) { 108 | if (hide) item.style.display = 'none'; 109 | else item.style.display = 'block'; 110 | } 111 | }) 112 | } 113 | 114 | // Hide or show the open visualizer menu option, depending on whether there is a visualization 115 | if (!visualization.trim()) toggle_open_visualizer(true); 116 | else toggle_open_visualizer(false); 117 | 118 | // If URL, display an iframe 119 | if (is_url(visualization)) return ``; 120 | 121 | // Otherwise, embed visualization as HTML 122 | else return visualization; 123 | } 124 | 125 | traitlet_changed(event:any) { 126 | const widget = this; 127 | const name = typeof event === "string" ? event : Object.keys(event.changed)[0]; 128 | const elements = this.element.querySelectorAll(`[data-traitlet=${name}]`); 129 | elements.forEach(element => { 130 | // Ignore traitlets in the appendix, unless this is a subwidget in the appendix 131 | if (!this.element.closest('.nbtools-appendix') && element.closest('.nbtools-appendix')) return; 132 | 133 | if (name in this.renderers) element.innerHTML = this.renderers[name](this.model.get(name), widget); 134 | else element.innerHTML = this.model.get(name) 135 | }); 136 | } 137 | 138 | static pick_path_prefix(path:string, widget:UIOutputView) { 139 | if (!widget.model.get('attach_file_prefixes')) return ''; 140 | else if (is_url(path)) return ''; // is a URL 141 | else if (is_absolute_path(path)) return ''; // is an absolute 142 | else return 'files/' + ContextManager.context().notebook_path(); // is relative path 143 | } 144 | 145 | attach_menu_options() { 146 | // Determine if "Pop Out" has already been attached 147 | const menu_exists = !!this.element.querySelector('.nbtools-menu-popout'); 148 | 149 | // Attach the Pop Out Visualizer gear option if needed 150 | if (!menu_exists) { 151 | const visualizer_option = this.add_menu_item('Pop Out Visualizer', () => this.open_visualizer(), 'nbtools-menu-popout'); 152 | visualizer_option.style.display = this.model.get('visualization').trim() ? 'block' : 'none'; 153 | } 154 | 155 | // Call the base widget's attach_menu_options() 156 | super.attach_menu_options(); 157 | } 158 | 159 | open_visualizer() { 160 | window.open(this.model.get('visualization')); 161 | } 162 | 163 | initialize_file_menus(widget:UIOutputView) { 164 | const files = widget.el.querySelectorAll('.nbtools-file') as NodeListOf; 165 | 166 | files.forEach((link:HTMLElement) => { 167 | link.addEventListener("click", function() { 168 | widget.toggle_file_menu(link); 169 | }); 170 | }); 171 | } 172 | 173 | initialize_menu_items(link:HTMLElement) { 174 | const menu = link.nextElementSibling as HTMLUListElement; 175 | if (!menu) return; // Protect against null 176 | const type = link.getAttribute('data-type') as string; 177 | const href = link.getAttribute('href') as string; 178 | const file_name = link.textContent ? link.textContent.trim() as string : href; 179 | const widget_name = this.model.get('name'); 180 | const origin = this.model.get('origin') || ''; 181 | 182 | // Add the send to options 183 | let send_to_empty = true; 184 | this.get_input_list(type, origin).forEach(input => { 185 | send_to_empty = false; 186 | this.add_menu_item(input['name'] + ' -> ' + input['param'], () => { 187 | const form_input = input['element'].querySelector('input') as HTMLFormElement; 188 | form_input.value = href; 189 | form_input.dispatchEvent(new Event('change', { bubbles: true} )); 190 | const widget = form_input.closest('.nbtools') as HTMLElement; 191 | widget.scrollIntoView(); 192 | }, 'nbtools-menu-subitem', menu); 193 | }); 194 | 195 | // Add send to header 196 | if (!send_to_empty) 197 | this.add_menu_item('Send to...', () => {}, 'nbtools-menu-header', menu); 198 | 199 | // Add the extra menu items 200 | const menu_items = this.model.get('extra_file_menu_items'); 201 | const template_vars = { 202 | 'widget_name': widget_name, 203 | 'file_name': file_name, 204 | 'href': href, 205 | 'type': type 206 | }; 207 | Object.keys(menu_items).forEach((name) => { 208 | const item = menu_items[name] as any; 209 | 210 | // Skip if this file doesn't match any type restrictions 211 | if (item['kinds'] && Array.isArray(item['kinds']) && !item['kinds'].includes(type)) return; 212 | 213 | // Create the callback and attach the menu item 214 | const callback = this.create_menu_callback(item, template_vars); 215 | this.add_menu_item(name, callback, 'nbtools-menu-subitem', menu); 216 | }); 217 | 218 | // Add download and new tab options 219 | if (this.model.get('default_file_menu_items')) { 220 | this.add_menu_item('Copy Link', () => navigator.clipboard.writeText(get_absolute_url(link.getAttribute('href'))), '', menu); 221 | this.add_menu_item('Download', () => window.open(link.getAttribute('href') + '?download=1'), '', menu); 222 | this.add_menu_item('Open in New Tab', () => window.open(link.getAttribute('href') as string), '', menu); 223 | } 224 | } 225 | 226 | toggle_file_menu(link:HTMLElement) { 227 | const menu = link.nextElementSibling as HTMLElement; 228 | const collapsed = menu.style.display === "none"; 229 | 230 | // Build the menu lazily 231 | menu.innerHTML = ''; // Clear all existing children 232 | this.initialize_menu_items(link); 233 | 234 | // Hide or show the menu 235 | if (collapsed) menu.style.display = "block"; 236 | else menu.style.display = "none"; 237 | 238 | // Hide the menu with the next click 239 | const hide_next_click = function(event:Event) { 240 | if (link.contains(event.target as Node)) return; 241 | menu.style.display = "none"; 242 | document.removeEventListener('click', hide_next_click); 243 | }; 244 | document.addEventListener('click', hide_next_click) 245 | } 246 | 247 | get_input_list(type:string, origin:string) { 248 | // Get the notebook's parent node 249 | const notebook = this.el.closest('.jp-Notebook') as HTMLElement; 250 | 251 | // Get all possible outputs 252 | const parameters = [...notebook.querySelectorAll('.nbtools-menu-attached') as any]; 253 | 254 | // Build list of compatible inputs 255 | const compatible_inputs = [] as Array; 256 | parameters.forEach((input:HTMLElement) => { 257 | // Ignore hidden parameters 258 | if (input.offsetWidth === 0 && input.offsetHeight === 0) return; 259 | 260 | // Ignore parameters with sendto=False 261 | if (input.classList.contains('nbtools-nosendto')) return; 262 | 263 | // Ignore if this origin does not match the supported origins 264 | const origins_str = input.getAttribute('data-origins') || ''; 265 | const origins_list = origins_str.split(', ') as any; 266 | if (!origins_list.includes(origin) && origins_str !== '') return; 267 | 268 | // Ignore incompatible inputs 269 | const kinds = input.getAttribute('data-type') || ''; 270 | const param_name = input.getAttribute('data-name') || ''; 271 | const kinds_list = kinds.split(', ') as any; 272 | if (!kinds_list.includes(type) && kinds !== '') return; 273 | 274 | // Add the input to the compatible list 275 | const widget_element = input.closest('.nbtools') as HTMLElement; 276 | let name = (widget_element.querySelector('.nbtools-title') as HTMLElement).textContent; 277 | if (!name) name = "Untitled Widget"; 278 | compatible_inputs.push({ 279 | 'name': name, 280 | 'param': param_name, 281 | 'element': input 282 | }); 283 | }); 284 | 285 | return compatible_inputs; 286 | } 287 | } -------------------------------------------------------------------------------- /src/databank.ts: -------------------------------------------------------------------------------- 1 | import { PanelLayout, Widget } from '@lumino/widgets'; 2 | import { escape_quotes, toggle } from "./utils"; 3 | import { ContextManager } from "./context"; 4 | import { SearchBox, Toolbox } from "./toolbox"; 5 | 6 | export class DataBrowser extends Widget { 7 | public search:SearchBox|null = null; 8 | public databank:Databank|null = null; 9 | 10 | constructor() { 11 | super(); 12 | this.addClass('nbtools-data-browser'); 13 | this.layout = new PanelLayout(); 14 | this.search = new SearchBox('#nbtools-data-browser > .nbtools-databank'); 15 | this.databank = new Databank(this.search); 16 | 17 | (this.layout as PanelLayout).addWidget(this.search); 18 | (this.layout as PanelLayout).addWidget(this.databank); 19 | } 20 | } 21 | 22 | export class Databank extends Widget { 23 | last_update = 0; 24 | update_waiting = false; 25 | search:SearchBox; 26 | 27 | constructor(associated_search:SearchBox) { 28 | super(); 29 | this.search = associated_search; 30 | this.addClass('nbtools-databank'); 31 | this.addClass('nbtools-wrapper'); 32 | 33 | // Update the databank when the data registry changes 34 | ContextManager.data_registry.on_update(() => { 35 | // If the last update was more than 3 seconds ago, update the databank 36 | if (this.update_stale()) this.fill_databank(); 37 | else this.queue_update(); // Otherwise, queue an update if not already waiting for one 38 | }); 39 | 40 | // Fill the databank with the registered data 41 | this.fill_databank(); 42 | } 43 | 44 | update_stale() { 45 | return this.last_update + (3 * 1000) < Date.now(); 46 | } 47 | 48 | queue_update() { 49 | // If no update is waiting, queue an update 50 | if (!this.update_waiting) { 51 | setTimeout(() => { // When an update happens 52 | this.fill_databank(); // Fill the databank 53 | this.update_waiting = false; // And mark as no update queued 54 | }, Math.abs(this.last_update + (3 * 1000) - Date.now())); // Queue for 3 seconds since last update 55 | this.update_waiting = true; // And mark as queued 56 | } 57 | } 58 | 59 | fill_databank() { 60 | this.last_update = Date.now(); 61 | 62 | // Gather collapsed origins and groups 63 | const collapsed_origins = Array.from(this.node.querySelectorAll('header.nbtools-origin > span.nbtools-collapsed')) 64 | .map((n:any) => n.parentElement?.getAttribute('title')); 65 | const collapsed_groups = Array.from(this.node.querySelectorAll('div.nbtools-group > span.nbtools-collapsed')) 66 | .map((n:any) => `${n.closest('ul.nbtools-origin')?.getAttribute('title')}||${n.parentElement?.getAttribute('title')}`); 67 | 68 | // First empty the databank 69 | this.empty_databank(); 70 | 71 | // Get the list of data 72 | const data = ContextManager.data_registry.list(); 73 | const declared_origins = ContextManager.data_registry.list_origins(); 74 | 75 | // Organize by origin and sort 76 | const origins = [...new Set([...Object.keys(declared_origins), ...Object.keys(data)])]; 77 | origins.sort((a:any, b:any) => { 78 | const a_name = a.toLowerCase(); 79 | const b_name = b.toLowerCase(); 80 | return (a_name < b_name) ? -1 : (a_name > b_name) ? 1 : 0; 81 | }); 82 | 83 | // Add each origin 84 | origins.forEach((origin) => { 85 | const origin_box = this.add_origin(origin, declared_origins[origin]); 86 | const click_disabled = declared_origins[origin]?.click_disabled; 87 | if (collapsed_origins.includes(origin)) this.toggle_collapse(origin_box); // Retain collapsed origins 88 | const groups = this.origin_groups(data[origin]); 89 | Object.keys(groups).reverse().forEach((key) => { 90 | this.add_group(origin_box, key, collapsed_groups.includes(`${origin}||${key}`), groups[key].reverse(), click_disabled); 91 | }) 92 | }); 93 | 94 | // Apply search filter after refresh 95 | this.search.filter(this.search.node.querySelector('input.nbtools-search') as HTMLInputElement); 96 | } 97 | 98 | origin_groups(origin: any) { 99 | const organized:any = {}; 100 | if (!origin) return organized; 101 | 102 | // Organize data by group 103 | Object.keys(origin).forEach((uri) => { 104 | const data = origin[uri][0]; 105 | if (data.group in organized) organized[data.group].push(data); // Add data to group 106 | else organized[data.group] = [data]; // Lazily create group 107 | }); 108 | 109 | // Return the organized set of groups 110 | return organized; 111 | } 112 | 113 | empty_databank() { 114 | this.node.innerHTML = ''; 115 | } 116 | 117 | add_origin(name:string, origin_object:any) { 118 | // Create the HTML DOM element 119 | const origin_wrapper = document.createElement('div'); 120 | origin_wrapper.innerHTML = ` 121 |
      122 | 123 | ${name} 124 |
      125 |
        `; 126 | 127 | // Attach the expand / collapse functionality 128 | const collapse = origin_wrapper.querySelector('span.nbtools-collapse') as HTMLElement; 129 | collapse.addEventListener("click", () => this.toggle_collapse(origin_wrapper)); 130 | 131 | // Attach functionality from origin object, if defined 132 | if (origin_object) { 133 | const header = origin_wrapper.querySelector('header'); 134 | if (origin_object.description) header.setAttribute('title', origin_object.description); 135 | if (origin_object.click_disabled) header.setAttribute('data-click-disabled', "true"); 136 | if (origin_object.collapsed) collapse.classList.add('nbtools-collapsed'); 137 | 138 | if (origin_object.buttons) 139 | for (let button_spec of origin_object.buttons) { 140 | const button = document.createElement('button'); 141 | button.setAttribute('title', button_spec.name) 142 | button.innerHTML = ``; 143 | header.append(button); 144 | 145 | // Add options menu, if required 146 | if (button_spec.options) { 147 | button.innerHTML += ''; 148 | const menu = document.createElement('menu'); 149 | for (let o of button_spec.options) { 150 | if (typeof o === 'string') o = {'label': o, 'value': o}; 151 | menu.innerHTML += `
      • ${o.label}
      • `; 152 | } 153 | menu.addEventListener('click', event => { 154 | const value = (event.target as HTMLElement)?.closest('li')?.getAttribute('data-value'); 155 | if (value) 156 | ContextManager.tool_registry.send_command(ContextManager.tool_registry.comm, 'origin_button', 157 | { name: button_spec.name, option: value }); 158 | }); 159 | button.append(menu); 160 | } 161 | 162 | // Add button click event 163 | if (button_spec.options) 164 | button.addEventListener('click', event => { 165 | const menu = (event.target as HTMLElement)?.closest('button')?.querySelector('menu'); 166 | if (!menu) return; 167 | menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; 168 | setTimeout(() => 169 | document.body.addEventListener('click', () => menu.style.display = 'none', { once: true }), 100); 170 | }); 171 | else 172 | button.addEventListener('click', 173 | () => ContextManager.tool_registry.send_command(ContextManager.tool_registry.comm, 'origin_button', { name: button_spec.name })); 174 | } 175 | } 176 | 177 | // Add to the databank 178 | this.node.append(origin_wrapper); 179 | return origin_wrapper; 180 | } 181 | 182 | add_group(origin:HTMLElement, group_name:String, collapsed:boolean, group_data:any, click_disabled=false) { 183 | const list = origin.querySelector('ul'); 184 | if (!list) return; 185 | 186 | const group_wrapper = document.createElement('li'); 187 | group_wrapper.classList.add('nbtools-tool'); 188 | if (!click_disabled) group_wrapper.setAttribute('title', 'Click to add to notebook'); 189 | group_wrapper.innerHTML = ` 190 |
        +
        191 |
        192 | 193 | ${group_name} 194 |
        195 |
          `; 196 | if (collapsed) this.toggle_collapse(group_wrapper); // Retain collapsed groups 197 | for (const data of group_data) this.add_data(group_wrapper, data, click_disabled); 198 | 199 | // Attach the expand / collapse functionality 200 | const collapse = group_wrapper.querySelector('span.nbtools-collapse') as HTMLElement; 201 | collapse.addEventListener("click", (event) => { 202 | this.toggle_collapse(group_wrapper); 203 | event.stopPropagation(); 204 | return false; 205 | }); 206 | list.append(group_wrapper); 207 | 208 | // Add the click event 209 | if (!click_disabled) 210 | group_wrapper.addEventListener("click", () => { 211 | Databank.add_group_cell(list.getAttribute('title'), group_name, group_data); 212 | }); 213 | return group_wrapper; 214 | } 215 | 216 | add_data(origin:HTMLElement, data:any, click_disabled=false) { 217 | const group_wrapper = origin.querySelector('ul.nbtools-group'); 218 | if (!group_wrapper) return; 219 | const data_wrapper = document.createElement('a'); 220 | data_wrapper.setAttribute('href', data.uri); 221 | data_wrapper.setAttribute('title', 'Drag to add parameter or cell'); 222 | data_wrapper.classList.add('nbtools-data'); 223 | data_wrapper.innerHTML = ` ${data.label}`; 224 | group_wrapper.append(data_wrapper); 225 | 226 | // Add the click event 227 | data_wrapper.addEventListener("click", event => { 228 | if (data.widget && !click_disabled) Databank.add_data_cell(data.origin, data.uri); 229 | event.preventDefault(); 230 | event.stopPropagation(); 231 | return false; 232 | }); 233 | 234 | // Add the drag event 235 | data_wrapper.addEventListener("dragstart", event => { 236 | event.dataTransfer.setData("text/plain", data.uri); 237 | }) 238 | } 239 | 240 | static add_data_cell(origin:String, data_uri:String) { 241 | // Check to see if nbtools needs to be imported 242 | const import_line = ContextManager.tool_registry.needs_import() ? 'import nbtools\n\n' : ''; 243 | 244 | // Add and run a code cell with the generated tool code 245 | Toolbox.add_code_cell(import_line + `nbtools.data(origin='${escape_quotes(origin)}', uri='${escape_quotes(data_uri)}')`); 246 | } 247 | 248 | static add_group_cell(origin:String, group_name:String, group_data:any) { 249 | // Check to see if nbtools needs to be imported 250 | const import_line = ContextManager.tool_registry.needs_import() ? 'import nbtools\n\n' : ''; 251 | 252 | // Add and run a code cell with the generated tool code 253 | const files = group_data.map((d:any) => `'${d.uri}'`).join(", "); 254 | Toolbox.add_code_cell(import_line + `nbtools.data(origin='${escape_quotes(origin)}', group='${escape_quotes(group_name)}', uris=[${files}])`); 255 | } 256 | 257 | // TODO: Move to utils.ts and refactor so both this and toolbox.ts calls the function? 258 | toggle_collapse(origin_wrapper:HTMLElement) { 259 | const list = origin_wrapper.querySelector("ul.nbtools-origin, ul.nbtools-group") as HTMLElement; 260 | const collapsed = list.classList.contains('nbtools-hidden'); 261 | 262 | // Toggle the collapse button 263 | const collapse = origin_wrapper.querySelector('span.nbtools-collapse') as HTMLElement; 264 | if (collapsed) { 265 | collapse.classList.add('nbtools-expanded'); 266 | collapse.classList.remove('nbtools-collapsed'); 267 | } 268 | else { 269 | collapse.classList.remove('nbtools-expanded'); 270 | collapse.classList.add('nbtools-collapsed'); 271 | } 272 | 273 | // Hide or show widget body 274 | toggle(list); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /nbtools/uibuilder.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import functools 3 | import warnings 4 | 5 | from IPython.core.display import display 6 | from traitlets import Unicode, List, Bool, Dict, Instance, observe 7 | from ipywidgets import widget_serialization, Output, VBox 8 | from ._frontend import module_name, module_version 9 | from .form import InteractiveForm 10 | from .basewidget import BaseWidget 11 | from .tool_manager import ToolManager, NBTool 12 | 13 | 14 | class build_ui: 15 | """ 16 | Decorator used to display the UI Builder upon definition of a function. 17 | 18 | Example: 19 | @nbtools.build_ui 20 | def example_function(arg1, arg2): 21 | return (arg1, arg2) 22 | 23 | Example: 24 | @nbtools.build_ui(name="custom name", description="custom description") 25 | def example_function(arg1, arg2): 26 | return (arg1, arg2) 27 | """ 28 | func = None 29 | kwargs = None 30 | __widget__ = None 31 | 32 | def __init__(self, *args, **kwargs): 33 | # Display if decorator with no arguments 34 | if len(args) > 0: 35 | self.func = args[0] # Set the function 36 | self.__widget__ = UIBuilder(self.func) # Set the widget 37 | self.func.__dict__["__widget__"] = self.__widget__ # Ensure function has access to widget 38 | if self.__widget__.form.register_tool: 39 | ToolManager.instance().register(self.__widget__) 40 | 41 | # Display if defined directly in a notebook 42 | # Don't display if loading from a library 43 | if self.func.__module__ == "__main__": 44 | display(self.__widget__) 45 | else: 46 | # Save the kwargs for decorators with arguments 47 | self.kwargs = kwargs 48 | 49 | def __call__(self, *args, **kwargs): 50 | # Decorators with arguments make this call at define time, while decorators without 51 | # arguments make this call at runtime. That's the reason for this madness. 52 | 53 | # Figure out what type of call this is 54 | if self.func is None: 55 | # This is a call at define time for a decorator with arguments 56 | self.func = args[0] # Set the function 57 | self.__widget__ = UIBuilder(self.func, **self.kwargs) # Set the widget 58 | self.func.__dict__["__widget__"] = self.__widget__ # Ensure function has access to widget 59 | self.func._ipython_display_ = self._ipython_display_ # Render widget when function returned 60 | if self.__widget__.form.register_tool: 61 | ToolManager.instance().register(self.__widget__) 62 | 63 | if self.func.__module__ == "__main__": # Don't automatically display if loaded from library 64 | display(self.__widget__) # Display if defined in a notebook 65 | 66 | # Return wrapped function 67 | @functools.wraps(self.func) 68 | def decorated(*args, **kwargs): 69 | return self.func(*args, **kwargs) 70 | return decorated 71 | 72 | # This is a call at runtime for a decorator without arguments 73 | else: 74 | # Just call the function 75 | return self.func(*args, **kwargs) 76 | 77 | def _ipython_display_(self): 78 | """Display widget when returned in a notebook cell""" 79 | display(self.__widget__) 80 | 81 | 82 | class UIBuilder(VBox, NBTool): 83 | """Widget used to render Python output in a UI""" 84 | origin = None 85 | id = None 86 | name = None 87 | description = None 88 | 89 | def __init__(self, function_or_method, **kwargs): 90 | # Set the function and defaults 91 | self.function_or_method = function_or_method 92 | self._apply_defaults(function_or_method) 93 | self._apply_overrides(**kwargs) 94 | 95 | # Create the child widgets 96 | self.form = UIBuilderBase(function_or_method, _parent=self, **kwargs) 97 | self.output = self.form.output 98 | 99 | # Call the super constructor 100 | VBox.__init__(self, [self.form, self.output]) 101 | 102 | # Insert a copy of this UI Builder when added as a tool 103 | self.load = lambda **override_kwargs: UIBuilder(self.function_or_method, **{**kwargs, **override_kwargs}) 104 | 105 | # Create properties to pass through to UIBuilderBase 106 | exclude = ['keys', 'form', 'layout', 'tabbable', 'tooltip', 'comm'] 107 | self.create_properties([x for x in self.form.__dict__['_trait_values'].keys() if not x.startswith('_') and x not in exclude]) 108 | 109 | def _apply_defaults(self, function_or_method): 110 | # Set the name based on the function name 111 | self.name = function_or_method.__qualname__ 112 | self.id = function_or_method.__qualname__ 113 | 114 | # Set the description based on the docstring 115 | self.description = inspect.getdoc(function_or_method) or '' 116 | 117 | # Set the origin based on the package name or "Notebook" 118 | self.origin = 'Notebook' if function_or_method.__module__ == '__main__' else function_or_method.__module__ 119 | 120 | def _apply_overrides(self, **kwargs): 121 | # Assign keyword parameters to this object 122 | for key, value in kwargs.items(): 123 | setattr(self, key, value) 124 | 125 | def id(self): 126 | """Return the function name regardless of custom display name""" 127 | return self.function_or_method.__qualname__ 128 | 129 | def _get_property(self, name): 130 | prop = getattr(self, f"_{name}", None) 131 | if prop is not None: return prop 132 | else: 133 | if hasattr(self, 'form'): return getattr(self.form, name, None) 134 | else: return None 135 | 136 | def _set_property(self, name, value): 137 | setattr(self, f"_{name}", value) 138 | if hasattr(self, 'form'): setattr(self.form, name, value) 139 | 140 | def _create_property(self, name): 141 | setattr(self.__class__, name, property(lambda self: self._get_property(name), 142 | lambda self, value: self._set_property(name, value))) 143 | 144 | def create_properties(self, property_names): 145 | for name in property_names: self._create_property(name) 146 | 147 | 148 | class UIBuilderBase(BaseWidget): 149 | """Widget that renders a function as a UI form""" 150 | 151 | _model_name = Unicode('UIBuilderModel').tag(sync=True) 152 | _model_module = Unicode(module_name).tag(sync=True) 153 | _model_module_version = Unicode(module_version).tag(sync=True) 154 | 155 | _view_name = Unicode('UIBuilderView').tag(sync=True) 156 | _view_module = Unicode(module_name).tag(sync=True) 157 | _view_module_version = Unicode(module_version).tag(sync=True) 158 | 159 | # Declare the Traitlet values for the widget 160 | output_var = Unicode(sync=True) 161 | _parameters = List(sync=True) 162 | parameter_groups = List(sync=True) 163 | accept_origins = List(sync=True) 164 | function_import = Unicode(sync=True) # Deprecated 165 | register_tool = Bool(True, sync=True) 166 | collapse = Bool(sync=True) 167 | events = Dict(sync=True) 168 | buttons = Dict(sync=True) 169 | license = Dict(sync=True) 170 | display_header = Bool(True, sync=True) 171 | display_footer = Bool(True, sync=True) 172 | run_label = Unicode('Run', sync=True) 173 | busy = Bool(False, sync=True) 174 | form = Instance(InteractiveForm, (None, [])).tag(sync=True, **widget_serialization) 175 | output = Instance(Output, ()).tag(sync=True, **widget_serialization) 176 | 177 | # Declare other properties 178 | function_or_method = None 179 | _parent = None 180 | upload_callback = None 181 | license_callback = None 182 | 183 | def __init__(self, function_or_method, **kwargs): 184 | # Apply defaults based on function docstring/annotations 185 | self._apply_defaults(function_or_method) 186 | 187 | # Set the function and call superclass constructor 188 | self.function_or_method = function_or_method 189 | self._parent = kwargs['_parent'] if '_parent' in kwargs else None 190 | BaseWidget.__init__(self, **kwargs) 191 | 192 | # Give deprecation warnings 193 | self._deprecation_warnings(kwargs) 194 | 195 | # Force the parameters setter to be called before instantiating the form 196 | # This is a hack necessary to prevent interact from throwing an error if parameters override is given 197 | if not self.parameters: self.parameters = self.parameters 198 | 199 | # Create the form and output child widgets 200 | self.form = InteractiveForm(function_or_method, self.parameters, parent=self, upload_callback=self.upload_callback) 201 | self.output = self.form.out 202 | 203 | # Insert a copy of this UI Builder when added as a tool 204 | self.load = lambda **override_kwargs: UIBuilder(self.function_or_method, **{ **kwargs, **override_kwargs}) 205 | 206 | def _apply_defaults(self, function_or_method): 207 | # Set the name based on the function name 208 | self.name = function_or_method.__qualname__ 209 | self.id = function_or_method.__qualname__ 210 | 211 | # Set the description based on the docstring 212 | self.description = inspect.getdoc(function_or_method) or '' 213 | 214 | # Set the origin based on the package name or "Notebook" 215 | self.origin = 'Notebook' if function_or_method.__module__ == '__main__' else function_or_method.__module__ 216 | 217 | # register_tool and collapse are True by default 218 | self.register_tool = True 219 | self.collapse = True 220 | 221 | @property 222 | def parameters(self): 223 | return self._parameters 224 | 225 | @parameters.setter 226 | def parameters(self, value): 227 | # Read parameters, values and annotations from the signature 228 | sig = inspect.signature(self.function_or_method) 229 | defaults = self._param_defaults(sig) 230 | 231 | # Merge the default parameter values with the custom overrides 232 | self._parameters = self._param_customs(defaults, value) 233 | 234 | @observe('license') 235 | def execute_license_callback(self, change): 236 | new_model = change["new"] # Get the new license model being saved 237 | # If a callback is defined and the license['callback'] is True, make the callback 238 | if 'callback' in new_model and new_model['callback'] and self.license_callback: self.license_callback() 239 | 240 | @staticmethod 241 | def _param_defaults(sig): 242 | """Read params, values and annotations from the signature""" 243 | params = [] # Return a list of parameter dicts 244 | 245 | for param in sig.parameters.values(): 246 | params.append({ 247 | "name": param.name, 248 | "label": param.name, 249 | "optional": param.default != inspect.Signature.empty, 250 | "default": UIBuilderBase._safe_default(param.default), 251 | "value": UIBuilderBase._safe_default(param.default), 252 | "description": param.annotation if param.annotation != inspect.Signature.empty else '', 253 | "hide": False, 254 | "type": UIBuilderBase._guess_type(param.default), 255 | "kinds": None, 256 | "choices": UIBuilderBase._choice_defaults(param), 257 | "id": None, 258 | "events": None 259 | }) 260 | 261 | # Special case for output_var 262 | params.append({ 263 | "name": 'output_var', 264 | "label": 'output variable', 265 | "optional": True, 266 | "default": '', 267 | "value": '', 268 | "description": '', 269 | "hide": True, 270 | "type": 'text', 271 | "kinds": None, 272 | "choices": {}, 273 | "id": None, 274 | "events": None 275 | }) 276 | 277 | return params 278 | 279 | def _param_customs(self, defaults, customs): 280 | """Apply custom overrides to parameter defaults""" 281 | for param in defaults: # Iterate over parameters 282 | if param['name'] in customs: # If there are custom values 283 | for key, value in customs[param['name']].items(): 284 | if key == 'name': param['label'] = value # Override display name only 285 | else: 286 | param[key] = value 287 | 288 | return defaults 289 | 290 | @staticmethod 291 | def _safe_default(default): 292 | """If not safe to serialize in a traitlet, cast to a string""" 293 | if default == inspect.Signature.empty: return '' 294 | elif isinstance(default, (int, str, bool, float)): return default 295 | else: return str(default) 296 | 297 | @staticmethod 298 | def _guess_type(val): 299 | """Guess the input type of the parameter based off the default value, if unknown use text""" 300 | if isinstance(val, bool): return "choice" 301 | elif isinstance(val, int): return "number" 302 | elif isinstance(val, float): return "number" 303 | elif isinstance(val, str): return "text" 304 | elif hasattr(val, 'read'): return "file" 305 | else: return "text" 306 | 307 | @staticmethod 308 | def _choice_defaults(param): 309 | # Handle boolean parameters 310 | if isinstance(param.default, bool): 311 | return { 'True': True, 'False': False } 312 | # TODO: Handle enums here in the future 313 | else: 314 | return {} 315 | 316 | @staticmethod 317 | def _deprecation_warnings(kwargs): 318 | if 'function_import' in kwargs: 319 | warnings.warn(DeprecationWarning('UI Builder specifies function_import, which is deprecated')) 320 | 321 | def id(self): 322 | """Return the function name regardless of custom display name""" 323 | return self.function_or_method.__qualname__ 324 | -------------------------------------------------------------------------------- /style/icon.svg: -------------------------------------------------------------------------------- 1 | genepattern-logo --------------------------------------------------------------------------------