\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 | [](https://mybinder.org/v2/gh/g2nb/nbtools/lab?urlpath=lab)
4 | [](https://travis-ci.org/genepattern/nbtools)
5 | [](https://gpnotebook-website-docs.readthedocs.io/en/latest/)
6 | [](https://hub.docker.com/r/genepattern/lab/)
7 | [](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 |
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 |
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 |
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 |
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 += ``;
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 |
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 |
--------------------------------------------------------------------------------