├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── nteract Data Explorer.ipynb ├── images ├── demo.gif └── menu-items.png ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── filebrowser-deep-copy-paste │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.spec.ts │ │ └── index.ts │ ├── style │ │ └── index.css │ └── tsconfig.json ├── filebrowser-share-file │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.spec.ts │ │ └── index.ts │ ├── style │ │ └── index.css │ └── tsconfig.json ├── nteract-data-explorer │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.tsx │ ├── style │ │ └── index.css │ └── tsconfig.json └── voila-utils │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── index.spec.ts │ └── index.ts │ ├── style │ └── index.css │ └── tsconfig.json └── scripts ├── install.sh └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | lerna-debug.log 7 | 8 | *.tsbuildinfo 9 | *.lock 10 | 11 | .idea/ 12 | .npmrc 13 | nodeenv 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: node_js 4 | node_js: 5 | - '10.15.3' 6 | 7 | notifications: 8 | email: false 9 | 10 | cache: 11 | directories: 12 | - ~/.npm 13 | - ~/.cache 14 | 15 | install: 16 | - npx lerna bootstrap 17 | 18 | script: 19 | - npx lerna run test 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Tubi 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tubi JupyterLab Extensions 2 | 3 | [![Build Status](https://travis-ci.org/Tubitv/jupyterlab-extensions.svg?branch=master)](https://travis-ci.org/Tubitv/jupyterlab-extensions) 4 | [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) 5 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 6 | 7 | ## JupyterLab Extensions 8 | 9 | |#|extension|package name| 10 | |---|---|---| 11 | |1|[nteract-data-explorer](packages/nteract-data-explorer/README.md)|@tubitv/nteract-data-explorer| 12 | |2|[filebrowser-deep-copy-paste](packages/filebrowser-deep-copy-paste/README.md)|@tubitv/filebrowser-deep-copy-paste| 13 | 14 | ### nteract Data Explorer 15 | JupyterLab wrapper of [nteract Data Explorer](https://github.com/nteract/data-explorer) from Netflix. Unlike [JupyterLab Data Explorer](https://github.com/jupyterlab/jupyterlab-data-explorer), the extension renders table and charts within the same cell as code, and doesn't require a separate tab. Also the extension solved one drawback—state saving—in [nteract Data Explorer](https://blog.nteract.io/designing-the-nteract-data-explorer-f4476d53f897). 16 | 17 | ### Copy & Paste Directories 18 | The "Copy" and "Paste" menu items can't operate on each files and directories in a folder recursively, so the extension is created for the purpose. 19 | 20 | Extensions of shareable links and copy/paste directories are as follows: 21 | 22 | ![menu items](images/menu-items.png) 23 | 24 | The following demo shows how these extensions are used in a local JupyterLab. 25 | ![demo](images/demo.gif) 26 | 27 | ## Prerequisites 28 | 29 | - Python >=3.7 30 | 31 | `conda` is the preferred Python virtual environment manager in the repo. For `conda` installation, please refer to [Miniconda](https://docs.conda.io/en/latest/miniconda.html). 32 | - Node.js >=10 33 | 34 | Download and install Node.js in this [page](https://nodejs.org/en/download/) in which process `npx` and `npm` are installed as well. 35 | 36 | ## Installation 37 | 38 | ### All-in-one installation 39 | 40 | ```bash 41 | ./scripts/install.sh 42 | ``` 43 | 44 | ### Step-by-step installation 45 | 46 | #### Install Python 47 | 48 | ```bash 49 | conda create -n jupyterlab-ext python=3.7 -y 50 | conda activate jupyterlab-ext 51 | pip install -r scripts/requirements.txt 52 | ``` 53 | 54 | [requirements.txt](./scripts/requirements.txt) contains these PyPI packages 55 | - jupyterlab>=2.0 56 | - pandas 57 | - dx 58 | 59 | #### Install extensions 60 | 61 | We use [Lerna](https://github.com/lerna/lerna) to manage all JupyterLab extensions. 62 | 63 | ```bash 64 | # Install npm package dependencies 65 | npx lerna bootstrap 66 | 67 | # Compile all packages 68 | npx lerna run build 69 | 70 | # Install extensions 71 | jupyter labextension install packages/filebrowser-deep-copy-paste 72 | jupyter labextension install packages/nteract-data-explorer 73 | 74 | # List installed extensions 75 | jupyter labextension list 76 | ``` 77 | 78 | ## Run 79 | ```bash 80 | jupyter lab 81 | ``` 82 | Please check notebooks in the [examples](./examples). 83 | 84 | ## Development 85 | Please check `README.md` of each package. 86 | -------------------------------------------------------------------------------- /examples/nteract Data Explorer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "!jupyter labextension list 2>&1 | grep OK" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from dx import load_sample, dx as display\n", 19 | "df = load_sample()" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "df" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "display(df)" 38 | ] 39 | } 40 | ], 41 | "metadata": { 42 | "kernelspec": { 43 | "display_name": "Python 3", 44 | "language": "python", 45 | "name": "python3" 46 | }, 47 | "language_info": { 48 | "codemirror_mode": { 49 | "name": "ipython", 50 | "version": 3 51 | }, 52 | "file_extension": ".py", 53 | "mimetype": "text/x-python", 54 | "name": "python", 55 | "nbconvert_exporter": "python", 56 | "pygments_lexer": "ipython3", 57 | "version": "3.7.6" 58 | } 59 | }, 60 | "nbformat": 4, 61 | "nbformat_minor": 4 62 | } 63 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/images/demo.gif -------------------------------------------------------------------------------- /images/menu-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/images/menu-items.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "command": { 4 | "bootstrap": { 5 | "hoist": true 6 | }, 7 | "run": { 8 | "stream": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-extensions", 3 | "private": true, 4 | "scripts": { 5 | "preinstall": "npx npm-force-resolutions", 6 | "prepare": "npx lerna run clean && npx lerna run build" 7 | }, 8 | "devDependencies": { 9 | "@babel/core": "^7.8.4", 10 | "@babel/preset-env": "^7.8.4", 11 | "@jupyterlab/testutils": "^1.2.2", 12 | "@types/jest": "^24.9.1", 13 | "jest": "^24.9.0", 14 | "lerna": "^3.20.2", 15 | "rimraf": "~3.0.0", 16 | "ts-jest": "^24.3.0", 17 | "typescript": "~3.7.3", 18 | "minimist": ">=1.2.3" 19 | }, 20 | "engines": { 21 | "node": ">=10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.2.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.2...@tubitv/filebrowser-deep-copy-paste@0.2.0) (2021-03-22) 7 | 8 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 9 | 10 | 11 | 12 | 13 | 14 | ## [0.1.2-alpha.2](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.0...@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.2) (2021-03-19) 15 | 16 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.2-alpha.1](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.0...@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.1) (2021-03-02) 23 | 24 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.2-alpha.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-deep-copy-paste@0.1.1...@tubitv/filebrowser-deep-copy-paste@0.1.2-alpha.0) (2021-02-27) 31 | 32 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 33 | 34 | 35 | 36 | 37 | 38 | ## [0.1.1](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-deep-copy-paste@0.1.1-alpha.2...@tubitv/filebrowser-deep-copy-paste@0.1.1) (2020-05-14) 39 | 40 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 41 | 42 | 43 | 44 | 45 | 46 | ## 0.1.1-alpha.2 (2020-05-08) 47 | 48 | **Note:** Version bump only for package @tubitv/filebrowser-deep-copy-paste 49 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/README.md: -------------------------------------------------------------------------------- 1 | # filebrowser-deep-copy-paste 2 | 3 | A JupyterLab extension to make deep copy and paste possible, which means, 4 | you can copy and paste not only notebooks, files, but also directories. 5 | 6 | ## Prerequisites 7 | 8 | * JupyterLab >=1.1.1 9 | 10 | **If you're using latest JupyterLab, you might need to upgrade `@jupyterlab/services:^4.1.0"` to a newer version.** 11 | 12 | ## Installation 13 | 14 | ```bash 15 | # Make sure the package is compiled before installation 16 | conda activate jupyterlab-ext 17 | npx lerna bootstrap 18 | npx lerna run build --scope @tubitv/filebrowser-deep-copy-paste 19 | 20 | jupyter labextension install packages/filebrowser-deep-copy-paste 21 | ``` 22 | 23 | ## Development 24 | 25 | ```bash 26 | conda activate jupyterlab-ext 27 | 28 | # Install npm package dependencies 29 | npx lerna bootstrap 30 | 31 | # Develop an extension 32 | npx lerna run watch --stream --scope @tubitv/filebrowser-deep-copy-paste 33 | jupyter labextension install packages/filebrowser-deep-copy-paste --no-build 34 | jupyter lab --watch 35 | 36 | # Publish changed extensions 37 | npx lerna publish 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/jest.config.js: -------------------------------------------------------------------------------- 1 | const func = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const local = { 4 | globals: { 'ts-jest': { tsConfig: 'tsconfig.json' } }, 5 | testRegex: `.*\.spec\.tsx?$`, 6 | transform: { 7 | '\\.(ts|tsx)?$': 'ts-jest', 8 | '\\.(js|jsx)?$': 'babel-jest', 9 | }, 10 | transformIgnorePatterns: ['/node_modules/(?!(@jupyterlab/.*)/)'] 11 | }; 12 | 13 | const upstream = func('jupyterlab_filebrowser_deep_copy_paste', __dirname); 14 | const reuseFromUpstream = [ 15 | 'moduleNameMapper', 16 | 'setupFilesAfterEnv', 17 | 'setupFiles', 18 | 'moduleFileExtensions', 19 | ]; 20 | for(option of reuseFromUpstream) { 21 | local[option] = upstream[option]; 22 | } 23 | 24 | module.exports = local; 25 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tubitv/filebrowser-deep-copy-paste", 3 | "version": "0.2.0", 4 | "description": "A JupyterLab extension to support deep copy and paste.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/Tubitv/jupyterlab-extensions", 11 | "bugs": { 12 | "url": "https://github.com/Tubitv/jupyterlab-extensions/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": "Tubi Engineering", 16 | "files": [ 17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 19 | ], 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "style": "style/index.css", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/Tubitv/jupyterlab-extensions.git" 26 | }, 27 | "scripts": { 28 | "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", 29 | "build": "tsc -b", 30 | "watch": "tsc -b -w", 31 | "test": "npx jest" 32 | }, 33 | "dependencies": { 34 | "@jupyterlab/application": "^3.0.0", 35 | "@jupyterlab/coreutils": "^5.0.0", 36 | "@jupyterlab/docmanager": "^3.0.0", 37 | "@jupyterlab/filebrowser": "^3.0.0", 38 | "@jupyterlab/services": "^6.0.0", 39 | "@lumino/algorithm": "^1.3.3" 40 | }, 41 | "sideEffects": [ 42 | "style/*.css" 43 | ], 44 | "jupyterlab": { 45 | "extension": true 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { PathExt } from '@jupyterlab/coreutils'; 2 | import { IDocumentManager } from '@jupyterlab/docmanager'; 3 | import { Contents } from '@jupyterlab/services'; 4 | 5 | import { 6 | copyDirectory, 7 | getCopiedDirectoryPath, 8 | log, 9 | } from './index'; 10 | 11 | function createContent(options: Partial = {}): Contents.IModel { 12 | const defaultDirectory = { 13 | name: 'jovyan', 14 | path: 'home/jovyan', 15 | type: 'directory' as Contents.ContentType, 16 | writable: true, 17 | created: '1581665248391', 18 | last_modified: '1581665248391', 19 | mimetype: 'text/directory', 20 | content: [] as any, 21 | format: 'file' as Contents.FileFormat, 22 | }; 23 | return { 24 | ...defaultDirectory, 25 | ...options, 26 | }; 27 | } 28 | 29 | function createManager(getMethod?: (path: string) => Partial, others?: object): IDocumentManager { 30 | return { 31 | services: { 32 | contents: { 33 | get: getMethod || (() => ({ 34 | name: 'home', 35 | content: [] as Contents.IModel[], 36 | })), 37 | }, 38 | }, 39 | ...(others || {}), 40 | } as unknown as IDocumentManager; 41 | } 42 | 43 | afterEach(() => { 44 | jest.restoreAllMocks(); 45 | }); 46 | 47 | describe('#log', () => { 48 | it('should call `console.debug` with debug message if it is available', () => { 49 | const debugSpy = jest.spyOn(console, 'debug'); 50 | const message = 'deep copy'; 51 | 52 | log(message); 53 | expect(debugSpy.mock.calls[0][1]).toBe(message); 54 | }); 55 | }); 56 | 57 | describe('#getCopiedDirectoryPath', () => { 58 | it('should use the same name if no conflicts with the target directory contents', async () => { 59 | const directory = createContent(); 60 | const toDir = 'shared'; 61 | const manager = createManager(); 62 | const result = await getCopiedDirectoryPath(directory, toDir, manager); 63 | 64 | expect(result).toBe(`${toDir}/${directory.name}`); 65 | }); 66 | 67 | it('should remove the copy suffix in the copied directory', async () => { 68 | const directory = createContent({ name: 'jovyan-Copy1' }); 69 | const toDir = 'shared'; 70 | const manager = createManager(); 71 | const result = await getCopiedDirectoryPath(directory, toDir, manager); 72 | 73 | expect(result).toBe(`${toDir}/${directory.name}`); 74 | }); 75 | 76 | it('should append suffix if there is a name conflict with the target directory contents', async () => { 77 | const directory = createContent(); 78 | const toDir = 'shared'; 79 | const manager = createManager(() => ({ 80 | name: 'home', 81 | content: [ 82 | createContent({ name: directory.name }), 83 | ] as Contents.IModel[], 84 | })); 85 | const result = await getCopiedDirectoryPath(directory, toDir, manager); 86 | 87 | expect(result).toBe(`${toDir}/${directory.name}-Copy1`); 88 | }); 89 | 90 | it('should increase suffix serial number if there is name conflict with a content with copy suffix', async () => { 91 | const directory = createContent({ name: 'jovyan' }); 92 | const toDir = 'shared'; 93 | const manager = createManager(() => ({ 94 | name: 'home', 95 | content: [ 96 | createContent({ name: `${directory.name}` }), 97 | createContent({ name: `${directory.name}-Copy1` }), 98 | ] as Contents.IModel[], 99 | })); 100 | const result = await getCopiedDirectoryPath(directory, toDir, manager); 101 | 102 | expect(result).toBe(`${toDir}/${directory.name}-Copy2`); 103 | }); 104 | 105 | it('should increase suffix serial number if there are name conflicts with multiple contents with copy suffix', async () => { 106 | const directory = createContent({ name: 'jovyan' }); 107 | const toDir = 'shared'; 108 | const manager = createManager(() => ({ 109 | name: 'home', 110 | content: [ 111 | createContent({ name: `${directory.name}` }), 112 | createContent({ name: `${directory.name}-Copy1` }), 113 | createContent({ name: `${directory.name}-Copy3` }), 114 | ] as Contents.IModel[], 115 | })); 116 | const result = await getCopiedDirectoryPath(directory, toDir, manager); 117 | 118 | expect(result).toBe(`${toDir}/${directory.name}-Copy4`); 119 | }); 120 | }); 121 | 122 | describe('#copyDirectory', () => { 123 | let directory: Contents.IModel; 124 | const toDir = 'shared'; 125 | const manager = createManager( 126 | (path: string) => { 127 | // Only `getCopiedDirectoryPath` queries with `toDir` prefix, so directly return an 128 | // empty directory in order to simulate deep copy 129 | if (path.startsWith(toDir)) { 130 | return createContent({ 131 | name: toDir, 132 | path: toDir, 133 | }); 134 | } 135 | 136 | const findContent = (contentPath: string, content: Contents.IModel): Contents.IModel | undefined => { 137 | if (content.path === contentPath) return content; 138 | let result: Contents.IModel | undefined; 139 | Array.from(content.content).some((item: any) => { 140 | result = findContent(path, item); 141 | return !!result; 142 | }); 143 | return result; 144 | }; 145 | 146 | return findContent(path, directory); 147 | }, 148 | { 149 | copy: (fromPath: string, toDir: string) => createContent({ 150 | name: PathExt.basename(fromPath), 151 | path: `${toDir}/${PathExt.basename(fromPath)}`, 152 | type: 'file', 153 | }), 154 | newUntitled: ({ path }: { path: string }) => createContent({ 155 | name: 'Untitled', 156 | path: `${path}/Untitled`, 157 | }), 158 | rename: (fromPath: string, toPath: string) => createContent({ 159 | name: PathExt.basename(toPath), 160 | path: toPath, 161 | }), 162 | }, 163 | ); 164 | 165 | it('should copy directory to new path', async () => { 166 | directory = createContent(); 167 | const result = await copyDirectory(directory, toDir, manager); 168 | expect(result).toHaveProperty('path', `${toDir}/${directory.name}`); 169 | }); 170 | 171 | it('should copy all contents of the directory to new path', async () => { 172 | directory = createContent({ 173 | name: 'jovyan', 174 | path: 'home/jovyan', 175 | content: [ 176 | createContent({ 177 | name: 'child1', 178 | path: 'home/jovyan/child1', 179 | type: 'file', 180 | }), 181 | createContent({ 182 | name: 'child2', 183 | path: 'home/jovyan/child2', 184 | content: [ 185 | createContent({ 186 | name: 'grandChild1', 187 | path: 'home/jovyan/child2/grandChild1', 188 | type: 'file', 189 | }), 190 | ], 191 | }), 192 | ], 193 | }); 194 | const spyCopy = jest.spyOn(manager, 'copy'); 195 | const spyRename = jest.spyOn(manager, 'rename'); 196 | const result = await copyDirectory(directory, toDir, manager); 197 | 198 | expect(result.path).toBe(`${toDir}/${directory.name}`); 199 | 200 | expect(spyCopy).toHaveBeenCalledTimes(2); 201 | expect(spyCopy.mock.calls[0][1]).toBe(`${toDir}/${directory.name}`); 202 | expect(spyCopy.mock.calls[1][1]).toBe(`${toDir}/${directory.name}/child2`); 203 | 204 | expect(spyRename).toHaveBeenCalledTimes(2); 205 | expect(spyRename.mock.calls[1][1]).toBe(`${toDir}/${directory.name}/child2`); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/src/index.ts: -------------------------------------------------------------------------------- 1 | import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; 2 | import { PathExt } from '@jupyterlab/coreutils'; 3 | import { IDocumentManager } from '@jupyterlab/docmanager'; 4 | import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; 5 | import { Contents } from '@jupyterlab/services'; 6 | import { each, map, toArray } from '@lumino/algorithm'; 7 | 8 | const CommandIDs = { 9 | deepCopy: 'filebrowser:deep-copy', 10 | deepCut: 'filebrowser:deep-cut', 11 | deepPaste: 'filebrowser:deep-paste' 12 | }; 13 | const selectorContent = '.jp-DirListing ul.jp-DirListing-content'; 14 | const selectorItem = '.jp-DirListing-item[data-isdir]'; 15 | const regCopySuffix = /(-Copy\d+)?$/; 16 | 17 | /** 18 | * Escape the `RegExp` special characters in `text` string 19 | * @link https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L14258-L14278 20 | */ 21 | const regRegExpChar = /[\\^$.*+?()[\]{}|]/g; 22 | const regHasRegExpChar = RegExp(regRegExpChar.source); 23 | const escapeRegExp = (text: string): string => ( 24 | (text && regHasRegExpChar.test(text)) 25 | ? text.replace(regRegExpChar, '\\$&') 26 | : text 27 | ); 28 | 29 | /** 30 | * Log debugging information 31 | */ 32 | export function log(...args: any[]): void { 33 | if (console && console.debug) { 34 | console.debug('[filebrowser-deep-copy-paste]', ...args); 35 | } 36 | } 37 | 38 | /** 39 | * Get copied directory name 40 | * If there is an exisiting directory or file having the same name, suffix it with `Copy 1`, `Copy 2`, etc 41 | */ 42 | export async function getCopiedDirectoryPath(directory: Contents.IModel, toDir: string, manager: IDocumentManager): Promise { 43 | const directoryPath = PathExt.join(toDir, directory.name); 44 | const destinationDirectory = await manager.services.contents.get(toDir, { content: true }); 45 | const existingContents = toArray(destinationDirectory.content) as Contents.IModel[]; 46 | const hasSameNameContent = !!existingContents.find((content: Contents.IModel) => content.name === directory.name); 47 | if (!hasSameNameContent) { 48 | return directoryPath; 49 | } 50 | 51 | const rawDirectoryName = directory.name.replace(regCopySuffix, ''); 52 | const regCopyName = new RegExp(`^${escapeRegExp(rawDirectoryName)}-Copy(\\d+)$`); 53 | const maxSerialNumber = existingContents.reduce((serial: number, content: Contents.IModel) => { 54 | const matches = content.name.match(regCopyName); 55 | return matches === null ? serial : Math.max(Number(matches[1]), serial); 56 | }, 0); 57 | log('getCopiedDirectoryPath', 'same directory or file exists', directoryPath, maxSerialNumber); 58 | 59 | return PathExt.join(toDir, directory.name.replace(regCopySuffix, `-Copy${maxSerialNumber + 1}`)); 60 | } 61 | 62 | /** 63 | * Copy a directory recursively 64 | */ 65 | export async function copyDirectory(directory: Contents.IModel, toDir: string, manager: IDocumentManager): Promise { 66 | const copiedDirectoryPath = await getCopiedDirectoryPath(directory, toDir, manager); 67 | const untitledDirectory = await manager.newUntitled({ path: toDir, type: 'directory' }); 68 | const copiedDirectory = await manager.rename(untitledDirectory.path, copiedDirectoryPath); 69 | log('copyDirectory', 'new directory created', copiedDirectory); 70 | 71 | await Promise.all( 72 | toArray(map(directory.content, async (item: Contents.IModel) => { 73 | log('copyDirectory', `copy ${item.type} '${item.name}' in directory '${directory.path}'`); 74 | if (item.type === 'directory') { 75 | const directoryItem = await manager.services.contents.get(item.path, { content: true }); 76 | await copyDirectory(directoryItem, copiedDirectory.path, manager); 77 | } else { 78 | await manager.copy(item.path, copiedDirectory.path); 79 | } 80 | })) 81 | ); 82 | 83 | return copiedDirectory; 84 | } 85 | 86 | /** 87 | * Plugin activate handler 88 | */ 89 | function activate( 90 | app: JupyterFrontEnd, 91 | factory: IFileBrowserFactory 92 | ): void { 93 | log('Custom plugin is activated!'); 94 | const { commands } = app; 95 | const { tracker } = factory; 96 | let clipboard: string[] = []; 97 | let isCut = false; 98 | const copySelectedItemsToClipboard = () => { 99 | const widget = tracker.currentWidget; 100 | if (!widget) { 101 | return; 102 | } 103 | 104 | clipboard.length = 0; 105 | each(widget.selectedItems(), item => { 106 | clipboard.push(item.path); 107 | }); 108 | }; 109 | 110 | commands.addCommand(CommandIDs.deepCopy, { 111 | execute: () => { 112 | copySelectedItemsToClipboard(); 113 | }, 114 | iconClass: 'jp-MaterialIcon jp-CopyIcon', 115 | label: 'Deep Copy' 116 | }); 117 | 118 | commands.addCommand(CommandIDs.deepCut, { 119 | execute: () => { 120 | isCut = true; 121 | copySelectedItemsToClipboard(); 122 | }, 123 | iconClass: 'jp-MaterialIcon jp-CutIcon', 124 | label: 'Deep Cut' 125 | }); 126 | 127 | commands.addCommand(CommandIDs.deepPaste, { 128 | execute: async () => { 129 | const widget = tracker.currentWidget; 130 | if (!widget) { 131 | return; 132 | } 133 | if (!clipboard.length) { 134 | isCut = false; 135 | return; 136 | } 137 | 138 | const { manager, path: basePath } = widget.model; 139 | log('deep paste basePath', basePath); 140 | // Use serial execution rather than parallel to avoid name conflicts in new copied files 141 | for (let i = 0; i < clipboard.length; i++) { 142 | const path = clipboard[i]; 143 | if (isCut) { 144 | const newPath = PathExt.join(basePath, PathExt.basename(path)); 145 | log('deep paste cut item', path); 146 | await manager.rename(path, newPath); 147 | break; 148 | } 149 | 150 | const item = await manager.services.contents.get(path, { content: true }); 151 | log('deep paste copied item', item); 152 | if (item.type === 'directory') { 153 | await copyDirectory(item, basePath, manager); 154 | } else { 155 | await manager.copy(path, basePath); 156 | } 157 | } 158 | 159 | clipboard.length = 0; 160 | isCut = false; 161 | }, 162 | iconClass: 'jp-MaterialIcon jp-PasteIcon', 163 | label: 'Deep Paste' 164 | }); 165 | 166 | // Tune rank parameter to put these commands as close as possible 167 | // @link https://github.com/jupyterlab/lumino/blob/master/packages/widgets/src/contextmenu.ts#L159-L171 168 | app.contextMenu.addItem({ 169 | command: CommandIDs.deepCopy, 170 | selector: selectorItem, 171 | rank: 500 172 | }); 173 | app.contextMenu.addItem({ 174 | command: CommandIDs.deepCut, 175 | selector: selectorItem, 176 | rank: 500 177 | }); 178 | app.contextMenu.addItem({ 179 | command: CommandIDs.deepPaste, 180 | selector: selectorContent, 181 | rank: 0 182 | }); 183 | } 184 | 185 | const deepCopyPaste: JupyterFrontEndPlugin = { 186 | activate: activate, 187 | id: '@tubitv/filebrowser-deep-copy-paste', 188 | requires: [IFileBrowserFactory], 189 | autoStart: true 190 | }; 191 | 192 | export default deepCopyPaste; 193 | -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/style/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/packages/filebrowser-deep-copy-paste/style/index.css -------------------------------------------------------------------------------- /packages/filebrowser-deep-copy-paste/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "target": "es2017", 21 | "types": ["jest"] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.2.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-share-file@0.1.3-alpha.0...@tubitv/filebrowser-share-file@0.2.0) (2021-03-22) 7 | 8 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 9 | 10 | 11 | 12 | 13 | 14 | ## [0.1.3-alpha.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-share-file@0.1.2...@tubitv/filebrowser-share-file@0.1.3-alpha.0) (2021-03-19) 15 | 16 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.2](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-share-file@0.1.2-alpha.2...@tubitv/filebrowser-share-file@0.1.2) (2020-05-14) 23 | 24 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 25 | 26 | 27 | 28 | 29 | 30 | ## 0.1.2-alpha.2 (2020-05-08) 31 | 32 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 33 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/README.md: -------------------------------------------------------------------------------- 1 | # filebrowser-share-file 2 | 3 | It's a replacement of [`@jupyterlab/filebrowser-extension:share-file`](https://jupyterlab.readthedocs.io/en/stable/developer/extension_points.html#copy-shareable-link). 4 | 5 | ## Prerequisites 6 | 7 | * JupyterLab >=3.0.0 8 | 9 | ## Installation 10 | 11 | ```bash 12 | # Make sure the package is compiled before installation 13 | conda activate jupyterlab-ext 14 | npx lerna bootstrap 15 | npx lerna run build --scope @tubitv/filebrowser-share-file 16 | 17 | # Disable JupyterLab share-file 18 | jupyter labextension disable @jupyterlab/filebrowser-extension:share-file 19 | jupyter labextension install packages/filebrowser-share-file 20 | ``` 21 | 22 | ## Development 23 | 24 | ```bash 25 | conda activate jupyterlab-ext 26 | 27 | # Install npm package dependencies 28 | npx lerna bootstrap 29 | 30 | # Develop an extension 31 | npx lerna run watch --stream --scope @tubitv/filebrowser-share-file 32 | jupyter labextension install packages/filebrowser-share-file --no-build 33 | jupyter lab --watch 34 | 35 | # Publish changed extensions 36 | npx lerna publish 37 | ``` 38 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/jest.config.js: -------------------------------------------------------------------------------- 1 | const func = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const local = { 4 | globals: { 'ts-jest': { tsConfig: 'tsconfig.json' } }, 5 | testRegex: `.*\.spec\.tsx?$`, 6 | transform: { 7 | '\\.(ts|tsx)?$': 'ts-jest', 8 | '\\.(js|jsx)?$': 'babel-jest', 9 | }, 10 | transformIgnorePatterns: ['/node_modules/(?!(@jupyterlab/.*)/)'] 11 | }; 12 | 13 | const upstream = func('jupyterlab_filebrowser_share_file', __dirname); 14 | const reuseFromUpstream = [ 15 | 'moduleNameMapper', 16 | 'setupFilesAfterEnv', 17 | 'setupFiles', 18 | 'moduleFileExtensions', 19 | ]; 20 | for(option of reuseFromUpstream) { 21 | local[option] = upstream[option]; 22 | } 23 | 24 | module.exports = local; 25 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tubitv/filebrowser-share-file", 3 | "version": "0.2.0", 4 | "description": "Custom filebrowser plugin that generates /user-redirect link", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/Tubitv/jupyterlab-extensions", 11 | "bugs": { 12 | "url": "https://github.com/Tubitv/jupyterlab-extensions/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": "Tubi Engineering", 16 | "files": [ 17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 19 | ], 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "style": "style/index.css", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/Tubitv/jupyterlab-extensions.git" 26 | }, 27 | "scripts": { 28 | "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", 29 | "build": "tsc -b", 30 | "watch": "tsc -b --watch", 31 | "test": "npx jest" 32 | }, 33 | "dependencies": { 34 | "@jupyterlab/application": "^3.0.0", 35 | "@jupyterlab/apputils": "^3.0.0", 36 | "@jupyterlab/coreutils": "^5.0.0", 37 | "@jupyterlab/filebrowser": "^3.0.0", 38 | "@lumino/algorithm": "^1.3.3" 39 | }, 40 | "sideEffects": [ 41 | "style/*.css" 42 | ], 43 | "jupyterlab": { 44 | "extension": true 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | transformToUserRedirectUrl, 3 | log, 4 | } from './index'; 5 | 6 | const testUrls = { 7 | original: [ 8 | "http://localhost:8000/lab/tree/path/to/notebook.ipynb", 9 | "http://localhost:8000/user/jovyan/lab/tree/path/to/notebook.ipynb", 10 | "http://localhost:8000/user/jovyan/dummy/lab/tree/path/to/notebook.ipynb", 11 | ], 12 | expected: [ 13 | "http://localhost:8000/lab/tree/path/to/notebook.ipynb", 14 | "http://localhost:8000/user-redirect/lab/tree/path/to/notebook.ipynb", 15 | "http://localhost:8000/user-redirect/dummy/lab/tree/path/to/notebook.ipynb", 16 | ] 17 | }; 18 | 19 | afterEach(() => { 20 | jest.restoreAllMocks(); 21 | }); 22 | 23 | describe('#log', () => { 24 | it('should call `console.debug` with debug message if it is available', () => { 25 | const debugSpy = jest.spyOn(console, 'debug'); 26 | const message = 'shareable link'; 27 | log(message); 28 | expect(debugSpy.mock.calls[0][1]).toBe(message); 29 | }); 30 | }); 31 | 32 | describe('#transformToUserRedirectUrl', () => { 33 | it ('should transform tree url to correct user redirect url', () => { 34 | testUrls.original.forEach((originalUrl, index) => { 35 | const transformedUrl = transformToUserRedirectUrl(originalUrl); 36 | log(transformedUrl); 37 | expect(transformedUrl).toBe(testUrls.expected[index]); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | 6 | import { 7 | Clipboard 8 | } from '@jupyterlab/apputils'; 9 | 10 | import { 11 | PageConfig, 12 | URLExt 13 | } from '@jupyterlab/coreutils'; 14 | 15 | import { 16 | IFileBrowserFactory 17 | } from '@jupyterlab/filebrowser'; 18 | 19 | import { 20 | toArray 21 | } from '@lumino/algorithm'; 22 | 23 | const shareFile: JupyterFrontEndPlugin = { 24 | activate: activateShareFile, 25 | id: '@tubitv/filebrowser-extension:share-file', 26 | requires: [IFileBrowserFactory], 27 | autoStart: true 28 | }; 29 | 30 | export default shareFile; 31 | 32 | /** 33 | * Log debugging information 34 | */ 35 | export function log(...args: any[]): void { 36 | if (console && console.debug) { 37 | console.debug('[filebrowser-share-file]', ...args); 38 | } 39 | } 40 | 41 | /** 42 | * transform tree url to `/user-redirect/` url for JupyterHub 43 | * see https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#user-redirect 44 | * @param url 45 | */ 46 | export function transformToUserRedirectUrl(url: string): string { 47 | return url.replace(/\/user\/([^\/]+)\//, "/user-redirect/"); 48 | } 49 | 50 | function activateShareFile( 51 | app: JupyterFrontEnd, 52 | factory: IFileBrowserFactory 53 | ): void { 54 | log('Custom plugin is activated!'); 55 | const { commands } = app; 56 | const { tracker } = factory; 57 | 58 | commands.addCommand("filebrowser:share-main", { 59 | execute: () => { 60 | const widget = tracker.currentWidget; 61 | if (!widget) { 62 | return; 63 | } 64 | // https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#user-redirect 65 | // Replace a URL like 66 | // https://example.com/user/someone/files/shared/README.md 67 | // to 68 | // https://example.com/user-redirect/lab/tree/shared/README.md 69 | // e.g. shared/README.md 70 | const path = encodeURI(widget.selectedItems().next().path); 71 | // e.g. https://domain/user/username/lab/tree 72 | // e.g. https://domain/user/username/servername/lab/tree 73 | const treeUrl = PageConfig.getTreeUrl(); 74 | // replace `/user/username/` to `/user-redirect/` if any 75 | // for local JupyterLab, it won't change anything, because there is no `/user/username/` part in the URL 76 | const userRedirectTreeUrl = transformToUserRedirectUrl(treeUrl); 77 | // 1. local: replace https://domain/other-part with https://domain/other-part 78 | // 2. default server: replace https://domain/user/usr/other-part with https://domain/user-redirect/other-part 79 | // 3. named server: replace https://domain/user/usr/svc/other-part with https://domain/user-redirect/svc/other-part 80 | const shareableLink = URLExt.join(userRedirectTreeUrl, path); 81 | log("Tree URL is", treeUrl); 82 | log("User Redirect Tree URL is", userRedirectTreeUrl); 83 | log("Path is", path); 84 | log("Final link is", shareableLink); 85 | 86 | Clipboard.copyToSystem(shareableLink); 87 | }, 88 | isVisible: () => 89 | tracker.currentWidget && 90 | toArray(tracker.currentWidget.selectedItems()).length === 1, 91 | iconClass: 'jp-MaterialIcon jp-LinkIcon', 92 | label: 'Copy Shareable Link' 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /packages/filebrowser-share-file/style/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/packages/filebrowser-share-file/style/index.css -------------------------------------------------------------------------------- /packages/filebrowser-share-file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "target": "es2017", 21 | "types": ["jest"] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.2.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/nteract-data-explorer@0.1.3-alpha.2...@tubitv/nteract-data-explorer@0.2.0) (2021-03-22) 7 | 8 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 9 | 10 | 11 | 12 | 13 | 14 | ## [0.1.3-alpha.2](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/nteract-data-explorer@0.1.3-alpha.0...@tubitv/nteract-data-explorer@0.1.3-alpha.2) (2021-03-19) 15 | 16 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 17 | 18 | 19 | 20 | 21 | 22 | ## [0.1.3-alpha.1](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/nteract-data-explorer@0.1.3-alpha.0...@tubitv/nteract-data-explorer@0.1.3-alpha.1) (2021-03-02) 23 | 24 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.3-alpha.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/nteract-data-explorer@0.1.2...@tubitv/nteract-data-explorer@0.1.3-alpha.0) (2021-02-27) 31 | 32 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 33 | 34 | 35 | 36 | 37 | 38 | ## [0.1.2](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/nteract-data-explorer@0.1.2-alpha.2...@tubitv/nteract-data-explorer@0.1.2) (2020-05-14) 39 | 40 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 41 | 42 | 43 | 44 | 45 | 46 | ## 0.1.2-alpha.2 (2020-05-08) 47 | 48 | **Note:** Version bump only for package @tubitv/nteract-data-explorer 49 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/README.md: -------------------------------------------------------------------------------- 1 | # nteract Data Explorer 2 | 3 | - A wrapper of [nteract Data Explorer](https://github.com/nteract/data-explorer) 4 | - A JupyterLab extension for rendering tabular-data-resource files 5 | 6 | ## Prerequisites 7 | 8 | * JupyterLab >=1.1.1 9 | 10 | ## Installation 11 | 12 | ```bash 13 | # Make sure the package is compiled before installation 14 | conda activate jupyterlab-ext 15 | npx lerna bootstrap 16 | npx lerna run build --scope @tubitv/nteract-data-explorer 17 | 18 | jupyter labextension install packages/nteract-data-explorer 19 | ``` 20 | 21 | ## Development 22 | 23 | ```bash 24 | conda activate jupyterlab-ext 25 | 26 | # Install npm package dependencies 27 | npx lerna bootstrap 28 | 29 | # Develop an extension 30 | npx lerna run watch --stream --scope @tubitv/nteract-data-explorer 31 | jupyter labextension install packages/nteract-data-explorer --no-build 32 | jupyter lab --watch 33 | 34 | # Publish changed extensions 35 | npx lerna publish 36 | ``` 37 | 38 | ## References 39 | - https://github.com/nteract/data-explorer 40 | - https://github.com/jupyterlab/jupyter-renderers 41 | - https://github.com/jupyterlab/mimerender-cookiecutter-ts 42 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tubitv/nteract-data-explorer", 3 | "version": "0.2.0", 4 | "description": "A JupyterLab extension for rendering tabular-data-resource files.", 5 | "license": "BSD-3-Clause", 6 | "author": "Tubi Engineering", 7 | "main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "style": "style/index.css", 10 | "keywords": [ 11 | "jupyter", 12 | "jupyterlab", 13 | "jupyterlab-extension" 14 | ], 15 | "files": [ 16 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 17 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 18 | ], 19 | "jupyterlab": { 20 | "mimeExtension": true 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tubitv/jupyterlab-extensions.git" 25 | }, 26 | "scripts": { 27 | "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", 28 | "build": "tsc -b", 29 | "watch": "tsc -b -w" 30 | }, 31 | "dependencies": { 32 | "@jupyterlab/rendermime-interfaces": "^3.0.0", 33 | "@lumino/messaging": "^1.4.3", 34 | "@lumino/widgets": "^1.18.0", 35 | "@nteract/data-explorer": "^8.2.9", 36 | "@types/react-dom": "^17.0.0", 37 | "styled-components": "^4.4.1" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; 2 | import { JSONObject } from '@lumino/coreutils'; 3 | import { Widget } from '@lumino/widgets'; 4 | import * as React from 'react'; 5 | import * as ReactDOM from 'react-dom'; 6 | import { Message } from '@lumino/messaging'; 7 | import DataExplorer from '@nteract/data-explorer'; 8 | import { DataProps, Metadata } from '@nteract/data-explorer/lib/utilities/types' 9 | 10 | /** 11 | * The default mime type for the extension. 12 | */ 13 | const MIME_TYPE = 'application/vnd.dataresource+json'; 14 | 15 | /** 16 | * The class name added to the extension. 17 | */ 18 | const CLASS_NAME = 'mimerenderer-tabular-data-resource'; 19 | 20 | /** 21 | * Find NotebookPanel widget instance up the widget hierarchy, it has `context` property 22 | * which could be used to save notebook 23 | * @link https://github.com/jupyterlab/jupyterlab/blob/7fb018558f9c812e3e2a3355fb6ab7c1a30486d8/packages/notebook/src/panel.ts#L177 24 | */ 25 | const findNotebookPanel = (widget: Widget): any => { 26 | let currentWidget: Widget | null = widget; 27 | while(currentWidget) { 28 | if (currentWidget.constructor && currentWidget.constructor.name === 'NotebookPanel') { 29 | return currentWidget; 30 | } 31 | currentWidget = currentWidget.parent; 32 | } 33 | return null; 34 | }; 35 | 36 | /** 37 | * A widget for rendering tabular-data-resource. 38 | */ 39 | export class NteractDataExplorerWidget extends Widget implements IRenderMime.IRenderer { 40 | /** 41 | * Construct a new output widget. 42 | */ 43 | constructor(options: IRenderMime.IRendererOptions) { 44 | super(); 45 | this._mimeType = options.mimeType; 46 | this.addClass(CLASS_NAME); 47 | } 48 | 49 | /** 50 | * Render tabular-data-resource into this widget's node. 51 | */ 52 | renderModel(model: IRenderMime.IMimeModel): Promise { 53 | // If Data Explorer is rendered, no need to re-render it, it could take care of itself 54 | if (this._hasRendered) return Promise.resolve(); 55 | this._hasRendered = true; 56 | 57 | const data = model.data[this._mimeType] as JSONObject; 58 | this.node.textContent = JSON.stringify(data); 59 | 60 | // Capture Data Explorer metadata change and save them to notebook file so we can restore it 61 | const onMetadataChange = (data: any) => { 62 | model.setData({ 63 | metadata: { 64 | ...model.metadata, 65 | dataExplorer: data, 66 | }, 67 | }); 68 | 69 | const notebookPanel = findNotebookPanel(this); 70 | if (notebookPanel) { 71 | notebookPanel.context.save().then( 72 | () => console.log('Save success.'), 73 | (reason: Error) => console.error('Save fails due to error: ', reason), 74 | ); 75 | } 76 | }; 77 | 78 | return new Promise((resolve) => { 79 | // Use an interval timer to render Data Explorer whenever the current node is rendered in the screen. 80 | // This is to avoid empty plot issue in Semiotic ResponsiveFrame 81 | // @link https://github.com/nteract/semiotic/blob/v1.20.3/src/components/ResponsiveFrame.tsx#L81-L91 82 | const timer = setInterval(() => { 83 | if (this.node.offsetWidth === 0 && this.node.offsetHeight === 0) return; 84 | 85 | clearInterval(timer); 86 | ReactDOM.render( 87 | , 94 | this.node, 95 | resolve 96 | ); 97 | }, 1000 / 60); 98 | }); 99 | } 100 | 101 | /** 102 | * Called before the widget is detached from the DOM. 103 | */ 104 | protected onBeforeDetach(msg: Message): void { 105 | // Unmount the component so it can tear down. 106 | ReactDOM.unmountComponentAtNode(this.node); 107 | } 108 | 109 | private _hasRendered = false; 110 | private _mimeType: string; 111 | } 112 | 113 | /** 114 | * A mime renderer factory for tabular-data-resource data. 115 | */ 116 | export const rendererFactory: IRenderMime.IRendererFactory = { 117 | safe: true, 118 | mimeTypes: [MIME_TYPE], 119 | createRenderer: options => new NteractDataExplorerWidget(options) 120 | }; 121 | 122 | /** 123 | * Extension definition. 124 | */ 125 | const extension: IRenderMime.IExtension = { 126 | id: 'nteract-data-explorer:plugin', 127 | rendererFactory, 128 | rank: 0, 129 | dataType: 'json', 130 | fileTypes: [ 131 | { 132 | name: 'tabular-data-resource', 133 | mimeTypes: [MIME_TYPE], 134 | // files with suffixes .tdr.json or .tdrjson will be open by nteract data explorer by default 135 | // tdr is short for tabular-data-resource 136 | extensions: ['.tdr.json', '.tdrjson'] 137 | } 138 | ], 139 | documentWidgetFactoryOptions: { 140 | name: 'nteract Data Explorer', 141 | primaryFileType: 'tabular-data-resource', 142 | fileTypes: ['tabular-data-resource'], 143 | defaultFor: ['tabular-data-resource'] 144 | } 145 | }; 146 | 147 | export default extension; 148 | -------------------------------------------------------------------------------- /packages/nteract-data-explorer/style/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/packages/nteract-data-explorer/style/index.css -------------------------------------------------------------------------------- /packages/nteract-data-explorer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "outDir": "lib", 15 | "preserveWatchOutput": true, 16 | "resolveJsonModule": true, 17 | "rootDir": "src", 18 | "sourceMap": true, 19 | "strict": true, 20 | "target": "es2017", 21 | "types": [] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/voila-utils/.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | -------------------------------------------------------------------------------- /packages/voila-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.2.0](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/voila-utils@0.1.3-alpha.1...@tubitv/voila-utils@0.2.0) (2021-03-22) 7 | 8 | **Note:** Version bump only for package @tubitv/voila-utils 9 | 10 | 11 | 12 | 13 | 14 | ## 0.1.3-alpha.1 (2021-03-19) 15 | 16 | **Note:** Version bump only for package @tubitv/voila-utils 17 | 18 | 19 | 20 | 21 | 22 | ## 0.1.3-alpha.0 (2021-03-02) 23 | 24 | **Note:** Version bump only for package @tubitv/voila-utils 25 | 26 | 27 | 28 | 29 | 30 | ## [0.1.2](https://github.com/Tubitv/jupyterlab-extensions/compare/@tubitv/filebrowser-share-file@0.1.2-alpha.2...@tubitv/filebrowser-share-file@0.1.2) (2020-05-14) 31 | 32 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 33 | 34 | 35 | 36 | 37 | 38 | ## 0.1.2-alpha.2 (2020-05-08) 39 | 40 | **Note:** Version bump only for package @tubitv/filebrowser-share-file 41 | -------------------------------------------------------------------------------- /packages/voila-utils/README.md: -------------------------------------------------------------------------------- 1 | # voila-utils 2 | 3 | Derived from [`@jupyterlab/filebrowser-extension:share-file`](https://jupyterlab.readthedocs.io/en/stable/developer/extension_points.html#copy-shareable-link). 4 | 5 | This is to add a context menu item to the file browser "Copy Shareable Dashboard Link". 6 | 7 | The link is "shareable" in that references to the Jupyterhub user and server name in the URL are replaced with "user-redirect". 8 | 9 | In order for jupyterlab to *handle* the generated Voila URL, be sure you have installed the jupyterlab voila serverextension: 10 | https://voila.readthedocs.io/en/stable/install.html 11 | 12 | ## Prerequisites 13 | 14 | * JupyterLab >=3.0.0 15 | 16 | ## Installation 17 | 18 | ```bash 19 | # Make sure the package is compiled before installation 20 | conda activate jupyterlab-ext 21 | npx lerna bootstrap 22 | npx lerna run build --scope @tubitv/voila-utils 23 | 24 | # Install extension 25 | jupyter labextension install packages/voila-utils 26 | ``` 27 | 28 | ## Development 29 | 30 | ```bash 31 | conda activate jupyterlab-ext 32 | 33 | # Install npm package dependencies 34 | npx lerna bootstrap 35 | 36 | # Develop an extension 37 | npx lerna run watch --stream --scope @tubitv/voila-utils 38 | jupyter labextension install packages/voila-utils --no-build 39 | jupyter lab --watch 40 | 41 | # Publish changed extensions 42 | npx lerna publish 43 | ``` 44 | -------------------------------------------------------------------------------- /packages/voila-utils/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /packages/voila-utils/jest.config.js: -------------------------------------------------------------------------------- 1 | const func = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const local = { 4 | globals: { 'ts-jest': { tsConfig: 'tsconfig.json' } }, 5 | testRegex: `.*\.spec\.tsx?$`, 6 | transform: { 7 | '\\.(ts|tsx)?$': 'ts-jest', 8 | '\\.(js|jsx)?$': 'babel-jest', 9 | }, 10 | transformIgnorePatterns: ['/node_modules/(?!(@jupyterlab/.*)/)'] 11 | }; 12 | 13 | const upstream = func('voila_utils', __dirname); 14 | const reuseFromUpstream = [ 15 | 'moduleNameMapper', 16 | 'setupFilesAfterEnv', 17 | 'setupFiles', 18 | 'moduleFileExtensions', 19 | ]; 20 | for(option of reuseFromUpstream) { 21 | local[option] = upstream[option]; 22 | } 23 | 24 | module.exports = local; 25 | -------------------------------------------------------------------------------- /packages/voila-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tubitv/voila-utils", 3 | "version": "0.2.0", 4 | "description": "Custom plugin with some voila utilities", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "voila" 10 | ], 11 | "homepage": "https://github.com/Tubitv/jupyterlab-extensions", 12 | "bugs": { 13 | "url": "https://github.com/Tubitv/jupyterlab-extensions/issues" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": "Tubi Engineering", 17 | "files": [ 18 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 19 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 20 | ], 21 | "main": "lib/index.js", 22 | "types": "lib/index.d.ts", 23 | "style": "style/index.css", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/Tubitv/jupyterlab-extensions.git" 27 | }, 28 | "scripts": { 29 | "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", 30 | "build": "tsc -b", 31 | "watch": "tsc -b --watch", 32 | "test": "npx jest" 33 | }, 34 | "dependencies": { 35 | "@jupyterlab/application": "^3.0.0", 36 | "@jupyterlab/apputils": "^3.0.0", 37 | "@jupyterlab/coreutils": "^5.0.0", 38 | "@jupyterlab/filebrowser": "^3.0.0", 39 | "@lumino/algorithm": "^1.3.3" 40 | }, 41 | "sideEffects": [ 42 | "style/*.css" 43 | ], 44 | "jupyterlab": { 45 | "extension": true 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/voila-utils/src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | transformToUserRedirectUrl, 3 | log, 4 | } from './index'; 5 | 6 | const testUrls = { 7 | original: [ 8 | "http://localhost:8000/lab/tree/path/to/notebook.ipynb", 9 | "http://localhost:8000/user/jovyan/lab/tree/path/to/notebook.ipynb", 10 | "http://localhost:8000/user/jovyan/dummy/lab/tree/path/to/notebook.ipynb", 11 | ], 12 | expected: [ 13 | "http://localhost:8000/lab/tree/path/to/notebook.ipynb", 14 | "http://localhost:8000/user-redirect/lab/tree/path/to/notebook.ipynb", 15 | "http://localhost:8000/user-redirect/dummy/lab/tree/path/to/notebook.ipynb", 16 | ] 17 | }; 18 | 19 | afterEach(() => { 20 | jest.restoreAllMocks(); 21 | }); 22 | 23 | describe('#log', () => { 24 | it('should call `console.debug` with debug message if it is available', () => { 25 | const debugSpy = jest.spyOn(console, 'debug'); 26 | const message = 'shareable link'; 27 | log(message); 28 | expect(debugSpy.mock.calls[0][1]).toBe(message); 29 | }); 30 | }); 31 | 32 | describe('#transformToUserRedirectUrl', () => { 33 | it ('should transform tree url to correct user redirect url', () => { 34 | testUrls.original.forEach((originalUrl, index) => { 35 | const transformedUrl = transformToUserRedirectUrl(originalUrl); 36 | log(transformedUrl); 37 | expect(transformedUrl).toBe(testUrls.expected[index]); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/voila-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | 6 | import { 7 | Clipboard 8 | } from '@jupyterlab/apputils'; 9 | 10 | import { 11 | PageConfig, 12 | URLExt 13 | } from '@jupyterlab/coreutils'; 14 | 15 | import { 16 | IFileBrowserFactory 17 | } from '@jupyterlab/filebrowser'; 18 | 19 | import { 20 | toArray 21 | } from '@lumino/algorithm'; 22 | 23 | const voilaUtils: JupyterFrontEndPlugin = { 24 | activate: activateUtils, 25 | id: '@tubitv/voila-utils', 26 | requires: [IFileBrowserFactory], 27 | autoStart: true 28 | }; 29 | 30 | export default voilaUtils; 31 | 32 | /** 33 | * Log debugging information 34 | */ 35 | export function log(...args: any[]): void { 36 | if (console && console.debug) { 37 | console.debug('[voila-utils]', ...args); 38 | } 39 | } 40 | 41 | /** 42 | * transform tree url to `/user-redirect/` url for JupyterHub 43 | * see https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#user-redirect 44 | * @param url 45 | */ 46 | export function transformToUserRedirectUrl(url: string): string { 47 | return url.replace(/\/user\/([^\/]+)\//, '/user-redirect/'); 48 | } 49 | 50 | function activateUtils( 51 | app: JupyterFrontEnd, 52 | factory: IFileBrowserFactory 53 | ): void { 54 | log('Custom plugin is activated! : voila-utils'); 55 | const { commands } = app; 56 | const { tracker } = factory; 57 | 58 | commands.addCommand("voila-utils:share-dashboard", { 59 | execute: () => { 60 | const widget = tracker.currentWidget; 61 | if (!widget) { 62 | return; 63 | } 64 | // https://jupyterhub.readthedocs.io/en/stable/reference/urls.html#user-redirect 65 | // e.g. shared/README.md 66 | const path = encodeURI(widget.selectedItems().next().path); 67 | const voilaBase = `${PageConfig.getBaseUrl()}voila/render`; 68 | 69 | // replace `/user/username/` to `/user-redirect/` if any 70 | // for local JupyterLab, it won't change anything, because there is no `/user/username/` part in the URL 71 | const userRedirectVoilaUrl = transformToUserRedirectUrl(voilaBase); 72 | const shareableLink = URLExt.join(userRedirectVoilaUrl, path); 73 | log("Base Voila URL is", voilaBase); 74 | log("User Redirect Voila URL", userRedirectVoilaUrl); 75 | log("Path is", path); 76 | log("Final link is", shareableLink); 77 | 78 | Clipboard.copyToSystem(shareableLink); 79 | }, 80 | isVisible: () => 81 | tracker.currentWidget && 82 | toArray(tracker.currentWidget.selectedItems()).length === 1, 83 | iconClass: 'jp-MaterialIcon jp-LinkIcon', 84 | label: 'Copy Shareable Dashboard Link' 85 | }); 86 | 87 | const selectorNotDir = '.jp-DirListing-item[data-isdir="false"]'; 88 | app.contextMenu.addItem({ 89 | command: "voila-utils:share-dashboard", 90 | selector: selectorNotDir, 91 | rank: 13 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /packages/voila-utils/style/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tubitv/jupyterlab-extensions/43aa84fd456f9d207bd7b654a6488dbce2aaaa82/packages/voila-utils/style/index.css -------------------------------------------------------------------------------- /packages/voila-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": false, 20 | "target": "es2017", 21 | "types": ["jest"] 22 | }, 23 | "include": ["src/*"] 24 | } 25 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 4 | 5 | GREEN='\033[0;32m' 6 | PLAIN='\033[0m' 7 | 8 | function install_conda() { 9 | CONDA_DIR=$HOME/miniconda 10 | export PATH=$CONDA_DIR/bin:$PATH 11 | which conda && conda -V && return 0 12 | 13 | case $(uname | tr '[:upper:]' '[:lower:]') in 14 | linux*) 15 | OS_NAME=Linux 16 | ;; 17 | darwin*) 18 | OS_NAME=MacOSX 19 | ;; 20 | *) 21 | OS_NAME="" 22 | echo "$uname is not supported yet" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | mkdir -p "$CONDA_DIR" "$HOME/.conda" 28 | wget -q "https://repo.continuum.io/miniconda/Miniconda3-latest-${OS_NAME}-x86_64.sh" -O miniconda.sh 29 | bash miniconda.sh -f -b -p "$CONDA_DIR" && rm miniconda.sh 30 | conda -V 31 | } 32 | 33 | NODE_VERSION=v12.16.1 34 | 35 | function install_node() { 36 | pushd $HOME 37 | 38 | version=$NODE_VERSION 39 | case $(uname | tr '[:upper:]' '[:lower:]') in 40 | linux*) 41 | OS_NAME=linux 42 | NODE_DIR="node-${version}-${OS_NAME}-x64" 43 | NODE_URL="https://nodejs.org/dist/${version}/${NODE_DIR}.tar.xz" 44 | ;; 45 | darwin*) 46 | OS_NAME=darwin 47 | NODE_DIR="node-${version}-${OS_NAME}-x64" 48 | NODE_URL="https://nodejs.org/dist/${version}/${NODE_DIR}.tar.gz" 49 | ;; 50 | *) 51 | echo "$uname is not supported yet" 52 | exit 1 53 | ;; 54 | esac 55 | 56 | export PATH=$HOME/$NODE_DIR/bin:$PATH 57 | which node && { 58 | node -v 59 | popd 60 | return 0 61 | } 62 | 63 | wget -q "$NODE_URL" 64 | NODE_FILE=$(basename "$NODE_URL") 65 | [[ "$OS_NAME" == "linux" ]] && tar xf $NODE_FILE || tar zxf $NODE_FILE 66 | rm $NODE_FILE 67 | 68 | node -v 69 | npx -v 70 | npm -v 71 | 72 | popd 73 | } 74 | 75 | # execute following commands always in the root 76 | pushd $SCRIPT_DIR/../ 77 | 78 | which conda && conda -V || install_conda 79 | which node && node -v || install_node 80 | 81 | # 1. Create Python environment with necessary PyPI dependencies 82 | conda create -n jupyterlab-ext python=3.7 -y 83 | 84 | # init conda in bash 85 | conda init bash 86 | eval "$(command conda 'shell.bash' 'hook' 2> /dev/null)" 87 | conda activate jupyterlab-ext 88 | conda info -e 89 | pip install -r $SCRIPT_DIR/requirements.txt 90 | 91 | # 2. Clean existed npm packages 92 | # Fix "sh: 1: node: Permission denied" 93 | if [[ $UID -eq 0 ]]; then 94 | npm config set user $UID 95 | npm config set unsafe-perm true 96 | fi 97 | npx lerna clean --yes 98 | rm -rf node_modules # `lerna clean` does not remove modules from the root node_modules directory 99 | 100 | # 3. Install npm package dependencies 101 | npx lerna bootstrap 102 | 103 | # 4. Compile all packages 104 | npx lerna run build 105 | 106 | # 5. Install extensions 107 | jupyter labextension install packages/filebrowser-deep-copy-paste 108 | jupyter labextension install packages/nteract-data-explorer 109 | 110 | # 6. List installed extensions 111 | jupyter labextension list 112 | 113 | echo -e "If you haven't set up your own conda and node, execute the following \ncommands to use ${GREEN}conda${PLAIN} and ${GREEN}node${PLAIN} installed by this script:" 114 | echo "export PATH=$CONDA_DIR/bin:\$PATH" 115 | echo "export PATH=$HOME/$NODE_DIR/bin:\$PATH" 116 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | jupyterlab>=3.0 2 | pandas 3 | dx 4 | --------------------------------------------------------------------------------