├── .yarnrc.yml ├── setup.cfg ├── .npmrc ├── style ├── index.js ├── tensorboard.svg ├── base.css └── tab.scss ├── .husky └── pre-commit ├── src ├── svg.d.ts ├── consts.ts ├── _typing.d.ts ├── commands.ts ├── utils │ └── copy.ts ├── biz │ ├── loading.tsx │ ├── widget.tsx │ └── tab.tsx ├── index.ts ├── manager.ts └── tensorboard.ts ├── .eslintignore ├── .prettierignore ├── images ├── tensorboard.step1.png ├── tensorboard.step2.png ├── tensorboard.step3.png ├── tensorboard.step4.png ├── tensorboard.step5.png └── tensorboard.step6.png ├── .prettierrc ├── tsconfig.eslint.json ├── pyproject.toml ├── jupyter-config ├── jupyter_lab_server_config.d │ └── jupyterlab_tensorboard_pro.json └── jupyter_notebook_server_config.d │ └── jupyterlab_tensorboard_pro.json ├── install.json ├── .gitignore ├── jupyterlab_tensorboard_pro ├── __init__.py ├── api_handlers.py ├── handlers.py └── tensorboard_manager.py ├── MANIFEST.in ├── tsconfig.json ├── webpack.config.js ├── LICENSE ├── .eslintrc.js ├── .github └── workflows │ └── build.yaml ├── setup.py ├── README.zh-cn.md ├── package.json └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | import './tab.scss'; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: string; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | 7 | jupyterlab_tensorboard_pro -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_REFRESH_INTERVAL = 120; 2 | export const DEFAULT_ENABLE_MULTI_LOG = false; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | jupyterlab_tensorboard_pro 6 | -------------------------------------------------------------------------------- /images/tensorboard.step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step1.png -------------------------------------------------------------------------------- /images/tensorboard.step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step2.png -------------------------------------------------------------------------------- /images/tensorboard.step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step3.png -------------------------------------------------------------------------------- /images/tensorboard.step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step4.png -------------------------------------------------------------------------------- /images/tensorboard.step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step5.png -------------------------------------------------------------------------------- /images/tensorboard.step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HFAiLab/jupyterlab_tensorboard_pro/HEAD/images/tensorboard.step6.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "printWidth": 100 6 | } -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "src/**/*.tsx", ".eslintrc.js", "*.js", "style/*.js"] 4 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["jupyter_packaging~=0.12.3", "jupyterlab>=4.0", "setuptools>=40.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_lab_server_config.d/jupyterlab_tensorboard_pro.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyterlab_tensorboard_pro": true 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /jupyter-config/jupyter_notebook_server_config.d/jupyterlab_tensorboard_pro.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "jupyterlab_tensorboard_pro": true 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_tensorboard_pro", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_tensorboard_pro" 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | *.bundle.* 3 | lib/ 4 | node_modules/ 5 | *.egg-info/ 6 | .ipynb_checkpoints 7 | *.tsbuildinfo 8 | .idea 9 | dist 10 | build 11 | debug.log 12 | .yarn 13 | 14 | jupyterlab_tensorboard_pro/labextension 15 | jupyterlab_tensorboard_pro/__pycache__ -------------------------------------------------------------------------------- /src/_typing.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module '*.png' { 7 | const value: any; 8 | export default value; 9 | } 10 | 11 | declare module '*jpg' { 12 | const value: any; 13 | export default value; 14 | } 15 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The command IDs used by the tensorboard plugin. 3 | */ 4 | export namespace CommandIDs { 5 | export const createNew = 'tensorboard:create-new'; 6 | 7 | export const inputDirect = 'tensorboard:choose-direct'; 8 | 9 | export const open = 'tensorboard:open'; 10 | 11 | export const openDoc = 'tensorboard:openDoc'; 12 | 13 | export const close = 'tensorboard:close'; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/copy.ts: -------------------------------------------------------------------------------- 1 | export function copyToClipboard(text: string): boolean { 2 | const input = document.createElement('textarea'); 3 | input.value = text; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore 6 | document.body.appendChild(input); 7 | input.select(); 8 | const result = document.execCommand('copy'); 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | document.body.removeChild(input); 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /jupyterlab_tensorboard_pro/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .handlers import load_jupyter_server_extension # noqa 4 | 5 | __version__ = "4.0.0" 6 | 7 | 8 | def _jupyter_nbextension_paths(): 9 | name = __name__ 10 | section = "tree" 11 | src = "static" 12 | return [dict( 13 | section=section, 14 | src=src, 15 | dest=name, 16 | require="%s/%s" % (name, section))] 17 | 18 | 19 | def _jupyter_server_extension_paths(): 20 | return [{ 21 | "module": __name__ 22 | }] 23 | -------------------------------------------------------------------------------- /src/biz/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface LoadingProps { 4 | title?: string; 5 | desc?: string; 6 | } 7 | 8 | export const Loading = (props: LoadingProps): JSX.Element => { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {props.title &&

{props.title}

} 18 | {props.desc &&

{props.desc}

} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include pyproject.toml 4 | include jupyter-config/jupyterlab_tensorboard_pro.json 5 | 6 | include package.json 7 | include install.json 8 | include ts*.json 9 | include yarn.lock 10 | 11 | graft jupyterlab_tensorboard_pro/labextension 12 | 13 | # Javascript files 14 | graft src 15 | graft style 16 | prune **/node_modules 17 | prune lib 18 | 19 | # Patterns to exclude from any directory 20 | global-exclude *~ 21 | global-exclude *.pyc 22 | global-exclude *.pyo 23 | global-exclude .git 24 | global-exclude .ipynb_checkpoints 25 | -------------------------------------------------------------------------------- /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": false, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "es2018", 21 | "types": [] 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.tsx"], 24 | "exclude": ["node_modules/**/*.d.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path'); 3 | 4 | console.info('use custom webpack config...'); 5 | 6 | module.exports = { 7 | resolve: { 8 | extensions: ['.tsx', '.ts', '.js', '.svg'], 9 | alias: { 10 | '@': path.resolve(__dirname, 'src') 11 | } 12 | }, 13 | optimization: { 14 | usedExports: true 15 | }, 16 | plugins: [], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | use: 'ts-loader', 22 | exclude: /node_modules/ 23 | }, 24 | { 25 | test: /\.s[ac]ss$/i, 26 | use: [ 27 | // 将 JS 字符串生成为 style 节点 28 | 'style-loader', 29 | // 将 CSS 转化成 CommonJS 模块 30 | 'css-loader', 31 | // 将 Sass 编译成 CSS 32 | { 33 | loader: 'sass-loader' 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 HFAiLab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true 4 | }, 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/eslint-recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:prettier/recommended' 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | project: ['tsconfig.eslint.json'], 14 | sourceType: 'module' 15 | }, 16 | plugins: ['@typescript-eslint'], 17 | rules: { 18 | '@typescript-eslint/naming-convention': [ 19 | 'error', 20 | { 21 | selector: 'interface', 22 | format: ['PascalCase'] 23 | } 24 | ], 25 | '@typescript-eslint/no-non-null-assertion': 'off', 26 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-namespace': 'off', 29 | '@typescript-eslint/no-use-before-define': 'off', 30 | '@typescript-eslint/quotes': [ 31 | 'error', 32 | 'single', 33 | { avoidEscape: true, allowTemplateLiterals: false } 34 | ], 35 | curly: ['error', 'all'], 36 | eqeqeq: 'error', 37 | 'prefer-arrow-callback': 'error' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ['dev'] 6 | pull_request: 7 | branches: ['dev'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Install node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '16.x' 19 | - name: Install Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.8' 23 | architecture: 'x64' 24 | 25 | - name: Setup pip cache 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cache/pip 29 | key: pip-3.8-${{ hashFiles('package.json') }} 30 | restore-keys: | 31 | pip-3.8- 32 | pip- 33 | 34 | - name: Get yarn cache directory path 35 | id: yarn-cache-dir-path 36 | run: echo "::set-output name=dir::$(yarn cache dir)" 37 | - name: Setup yarn cache 38 | uses: actions/cache@v2 39 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 40 | with: 41 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 42 | key: yarn-${{ hashFiles('**/yarn.lock') }} 43 | restore-keys: | 44 | yarn- 45 | 46 | - name: Install dependencies 47 | run: python -m pip install -U jupyterlab~=3.0 jupyter_packaging~=0.7.9 48 | - name: Build the extension 49 | run: | 50 | jlpm 51 | python -m pip install . 52 | 53 | jupyter labextension list 2>&1 | grep -ie "jupyterlab_tensorboard_pro.*OK" 54 | -------------------------------------------------------------------------------- /style/tensorboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 32 | 37 | 42 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------- 2 | | Variables 3 | |----------------------------------------------------------------------------*/ 4 | 5 | @import "normalize.css"; 6 | @import "@blueprintjs/core/lib/css/blueprint.css"; 7 | @import "@blueprintjs/icons/lib/css/blueprint-icons.css"; 8 | 9 | :root { 10 | --jp-Tensorboard-itemIcon: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAMAAAD3eH5ZAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAD/UExURfFlKfaELvFmKfNyK/67NvWALf68Nv69NvNxK/20NfyyNP22NfN0K/JrKvqhMv2zNf25Nf24Nf23NfeOL/yzNPyvNPJoKviWMPmeMfN1K/WBLfePL/FnKfeML/qlMvR7LPmcMfeLL/aJLvR5LPFoKfJuKvR3LP66NvywNPeNL/V/LfaILv21Nf26NfNzK/NvK/R6LPmaMfyxNPqfMvV+LfurM/iSMPmbMfJvKvmdMfumM/qiMvmZMfytNPJqKvysNPN2K/iYMPNwK/upM/JtKvJsKviVMPaHLvaGLvJpKvR8LPaKLvqkMvuqM/aFLvR4LPuoM/iTMPWDLfiRMPmYMXS0ngkAAALoSURBVHja7drnctpAFIbhFUISSKJ3MKYa0+y4xTW9937/15JkJhlTjhrSrHRmvuf/as6L0YLFCgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBJ6njenqspzgnPrsrGX9Zpi2tCrmnc6+dYNthVY5WpMmxQLWPdMsOuYVwzNj3ei2t3mQwaV43BJPDCS2NbJ5aEeuX/+9qcjQOtfFIkIkrvY2g4MVcmOBsFWbowKO/kNyj62gRpJcDaPBlxLr1B0zdG0C/8LzbJiJrshuvy1gzlA9+rD8mIkuyIJjFE3/dqnYwoSm7IUEPoD/wut8iIguSIDjlFxe/yfXL5vuSI21BTZLLhXoOILMO8Hxwa/L8bI0LfmUdhGowb2ZvT0e57pFNDgB06IlVyjmmIBl2T/nl9Rw6SD9GgSG/Q0uQkaW3XhmovKQ3eFQ4N2Uo9OQ1eFZsNerf7vP+rO4rhmY1Lg3vFVoP8+8BXg1sFnwbnCk4NThW8GuiKBDdkVVtTNFvNelVsNqTbyWnIOM2oeTRoyWvwmpJHg/ucXBrcJuXT4DwrpwZi2vy0VCx8YtXg/D2bU4OfiuQ3eFfE2KD4bfCqiLNB993gXsGlwa2CT4NzBacGIVQ6YsipQdh0xEdODUKjIxrSp88onZ8zbbFLg1DoiFO5BXvDGv2My9/JhUT8JUZTI0yDaNHLBzIbvqTDNYhUiVw/kdjQ1kM2CHFDPjKW+KzyRTF0g/ga9w9y+fANQpxvX8CU+Ny7FUWDeF3Y+g3lROIf4k0UDX9eCyvO531PyYhHga9zvPZJU5b73Y/eXj8Hv9D48n6HaF5LbcjRt8TZTtda5M1DfXnbkX1C0SHCFKzQB5Fe8op4GNGNHavvZESbVwT5r6W1xyuCPBY3Y9YgDqzknH/e3YfNzzuL30l0IebrZ5kKtuDIXt1n868ET6kf3/49tLvrCcZyF8Pu215dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcPIbNrBhOaBXucoAAAAASUVORK5CYII="); 11 | --jp-Tensorboard-icon: url('./tensorboard.svg'); 12 | } 13 | 14 | .jp-Tensorboard { 15 | min-width: 240px; 16 | min-height: 120px; 17 | padding: 8px; 18 | margin: 0; 19 | } 20 | 21 | .jp-Tensorboard-icon { 22 | background-image: var(--jp-Tensorboard-icon); 23 | } 24 | 25 | .jp-Tensorboards-itemIcon { 26 | flex: 0 0 auto; 27 | margin-right: 4px; 28 | vertical-align: baseline; 29 | background-size: 16px; 30 | background-repeat: no-repeat; 31 | background-position: center; 32 | background-image: var(--jp-Tensorboard-itemIcon); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | jupyterlab_tensorboard_pro setup 3 | """ 4 | import os 5 | import json 6 | from pathlib import Path 7 | from setuptools.command.install import install 8 | from setuptools.command.develop import develop 9 | 10 | from jupyter_packaging import ( 11 | create_cmdclass, 12 | install_npm, 13 | ensure_targets, 14 | combine_commands, 15 | skip_if_exists 16 | ) 17 | import setuptools 18 | 19 | HERE = Path(__file__).parent.resolve() 20 | 21 | # The name of the project 22 | name = "jupyterlab_tensorboard_pro" 23 | 24 | lab_path = (HERE / name / "labextension") 25 | 26 | # Representative files that should exist after a successful build 27 | jstargets = [ 28 | str(lab_path / "package.json"), 29 | ] 30 | 31 | package_data_spec = { 32 | name: ["*"], 33 | } 34 | 35 | labext_name = "jupyterlab_tensorboard_pro" 36 | 37 | data_files_spec = [ 38 | ("share/jupyter/labextensions/%s" % labext_name, str(lab_path), "**"), 39 | ("share/jupyter/labextensions/%s" % labext_name, str(HERE), "install.json"), 40 | ] 41 | 42 | cmdclass = create_cmdclass("jsdeps", 43 | package_data_spec=package_data_spec, 44 | data_files_spec=data_files_spec 45 | ) 46 | 47 | js_command = combine_commands( 48 | install_npm(HERE, build_cmd="build:prod", npm=["jlpm"]), 49 | ensure_targets(jstargets), 50 | ) 51 | 52 | is_repo = (HERE / ".git").exists() 53 | if is_repo: 54 | cmdclass["jsdeps"] = js_command 55 | else: 56 | cmdclass["jsdeps"] = skip_if_exists(jstargets, js_command) 57 | 58 | long_description = (HERE / "README.md").read_text() 59 | 60 | # Get the package info from package.json 61 | pkg_json = json.loads((HERE / "package.json").read_bytes()) 62 | 63 | setup_args = dict( 64 | name=name, 65 | version=pkg_json["version"], 66 | url=pkg_json["homepage"], 67 | author=pkg_json["author"]["name"], 68 | author_email=pkg_json["author"]["email"], 69 | description=pkg_json["description"], 70 | license=pkg_json["license"], 71 | long_description=long_description, 72 | long_description_content_type="text/markdown", 73 | cmdclass=cmdclass, 74 | data_files=[ 75 | ( 76 | "etc/jupyter/jupyter_server_config.d", 77 | ["jupyter-config/jupyter_lab_server_config.d/jupyterlab_tensorboard_pro.json"] 78 | ), 79 | ( 80 | "etc/jupyter/jupyter_notebook_config.d", 81 | ["jupyter-config/jupyter_notebook_server_config.d/jupyterlab_tensorboard_pro.json"] 82 | ), 83 | ], 84 | packages=setuptools.find_packages(), 85 | install_requires=[ 86 | "jupyterlab>=4.0", 87 | "tornado<=6.2", 88 | ], 89 | zip_safe=False, 90 | include_package_data=True, 91 | python_requires=">=3.6", 92 | platforms="Linux, Mac OS X, Windows", 93 | keywords=["Jupyter", "JupyterLab", "JupyterLab3", "Tensorboard", "Tensorflow" ], 94 | entry_points={ 95 | 'console_scripts': [ 96 | 'jupyterlab-tensorboard-pro = jupyterlab_tensorboard_pro.application:main', 97 | ], 98 | }, 99 | classifiers=[ 100 | 'Intended Audience :: Developers', 101 | 'Intended Audience :: Science/Research', 102 | "License :: OSI Approved :: BSD License", 103 | "Programming Language :: Python", 104 | "Programming Language :: Python :: 3", 105 | "Programming Language :: Python :: 3.6", 106 | "Programming Language :: Python :: 3.7", 107 | "Programming Language :: Python :: 3.8", 108 | "Programming Language :: Python :: 3.9", 109 | "Programming Language :: Python :: 3.10", 110 | "Framework :: Jupyter", 111 | ], 112 | ) 113 | 114 | 115 | if __name__ == "__main__": 116 | setuptools.setup(**setup_args) 117 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # JupyterLab-TensorBoard-Pro 2 | 3 | ![Github Actions Status](https://github.com/HFAiLab/jupyterlab_tensorboard_pro/workflows/Build/badge.svg) [![pypi](https://img.shields.io/pypi/v/jupyterlab_tensorboard_pro.svg)](https://pypi.org/project/jupyterlab-tensorboard-pro/) 4 | 5 | 一个更加完善的 TensorBoard JupyterLab 插件 6 | 7 | ![](./images/tensorboard.step4.png) 8 | 9 | ## 依赖 10 | 11 | **python >= 3.6** 12 | 13 | 请在安装本项目之前安装以下依赖: 14 | 15 | - jupyterlab 16 | - tensorflow 17 | - tensorboard 18 | 19 | ## 安装 20 | 21 | ``` 22 | pip install jupyterlab-tensorboard-pro 23 | ``` 24 | 25 | > 这是一个 jupyterlab 插件,目前已经不在支持 jupyter notebook 26 | 27 | ## 开发背景 28 | 29 | 实际上,目前社区里面已经有了[jupyterlab_tensorboard](https://github.com/chaoleili/jupyterlab_tensorboard)(前端插件)和 [jupyter_tensorboard](https://github.com/lspvic/jupyter_tensorboard)(对应的后端插件),不过两个仓库都已经很久没有更新,对于一些新的修复 PR 也没有及时合入,基于此判断项目作者已经不在积极地维护对应仓库。 30 | 31 | 同时,现有社区的 TensorBoard 插件存在一定的体验问题,比如需要同时安装两个 python 包,以及点击之后无任何响应,无法设置 TensorBoard Reload 时间等问题,交互体验不够友好,也会影响用户的 JupyterLab 使用体验。 32 | 33 | 因此本项目 fork 了社区现有项目,对逻辑进行更改,并且参考了之前一些比较有帮助但是暂时没有合入的 PR,希望能够在接下来较长的一段时间持续维护。 34 | 35 | 这个项目对接口名也进行了更改,因此可以和上述插件保持完全的独立。 36 | 37 | 特别感谢之前相关仓库的开发者们。 38 | 39 | ## 使用说明 40 | 41 | ### 创建实例 42 | 43 | #### 从 launcher 面板创建 44 | 45 | 我们可以从 Launcher 面板点击 TensorBoard 图标,首次点击会进入到一个默认的初始化面板,我们可以从该面板创建 TensorBoard 实例。非首次进入则会直接进入到第一个活跃的 TensorBoard 实例。 46 | 47 | ![](./images/tensorboard.step1.png) 48 | 49 | #### 通过快捷命令创建 50 | 51 | 我们也可以在 JupyterLab 快捷指令面板(`ctrl + shift + c` 唤起)中输入 `Open TensorBoard`。 52 | 53 | ![](./images/tensorboard.step2.png) 54 | 55 | #### 创建参数 56 | 57 | 在初始化面板中,提供了两个参数设置项目: 58 | 59 | - **Log Dir**:默认是点击 TensorBoard 时当前侧边栏的目录,也可以手动填写对应目录,这里建议目录尽可能的细化,目录内容比较少的话会提高初始化速度。 60 | - **Reload Interval**:TensorBoard 多久对对应目录进行一次重新扫描,这个选项是默认是关闭的,日常使用选择手动 Reload 即可(设置 Reload Interval 之后,TensorBoard 后端持续扫描目录会对 Jupyter 的稳定性和文件系统都产生一定的影响)。 61 | 62 | 选择好参数点击 Create TensorBoard,会同步创建 TensorBoard 实例,这个时候 jupyter 后端是**阻塞**的,请等待实例创建好之后再进行其他操作。 63 | 64 | ![](./images/tensorboard.step3.png) 65 | 66 | ### 管理实例 67 | 68 | 创建实例后,我们可以对 TensorBoard 的实例进行管理,目前依次提供了以下几个功能: 69 | 70 | - **刷新和列表切换**:可以切换成其他的实例的 TensorBoard 后端,这个时候不会销毁实例。 71 | - **独立页面中打开**:可以在以独立网页 Tab 的形式打开 TensorBoard。 72 | - **Reload**:即重新初始化 TensorBoard 后端,当文件内容有更新时,可以通过此功能载入新的内容(注:TensorBoard 内部的刷新,不会造成 Reload)。 73 | - **Destroy**:销毁实例,会连同前端面板一起关掉。 74 | - **Duplicate**:重新打开一个完全一样的前端面板,此操作会复用 TensorBoard 后端。 75 | - **New**:额外新建一个 TensorBoard 后端,注意事项可以参考上文。 76 | 77 | 另外,对于我们创建的 TensorBoard 实例,可以在 Jupyter 的 Kernel 管理面板一同管理,提供跳转至对应实例和删除等功能。 78 | 79 | ![](./images/tensorboard.step5.png) 80 | 81 | ### 使用 AWS S3 82 | 83 | > 这里假设你已经对 aws s3 有了一定的使用经验 84 | 85 | TensorBoard 支持通过 `s3://path/to/dir` 的方式传入一个 s3 的路径,这个方式在本插件内也同样支持。 86 | 87 | 不过,因为 s3 的路径通常并不是直接可以访问的,需要先通过 `aws configure` 配置一些基本信息([下载](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) aws cli),通常情况下,JupyterLab 运行所在的系统应该有以下文件: 88 | 89 | ```shell 90 | # ~/.aws/config 91 | [default] 92 | region = ap-southeast-1 93 | output = json 94 | 95 | # ~/.aws/credentials 96 | [default] 97 | aws_access_key_id = ******** 98 | aws_secret_access_key = ******** 99 | ``` 100 | 101 | 然后你需要额外安装一些依赖: 102 | 103 | ``` 104 | pip install botocore boto3 tensorflow-io 105 | ``` 106 | 107 | 之后你可以输入一个 s3 路径,然后点击 tensorboard 的刷新按钮,等待加载完成后即可展示: 108 | 109 | ![](./images/tensorboard.step6.png) 110 | 111 | > 实际上,现在 tensorboard 本身在这里的状态提示并不友好,后续我们会进一步调研有没有更好的体验的方式 112 | 113 | ## 调试 114 | 115 | 你可以通过 `jupyter-lab --debug` 开启 JupyterLab 和 TensorBoard 的调试日志。 116 | 117 | ## 本地开发 118 | 119 | ```shell 120 | jlpm install 121 | pip install jupyter_packaging 122 | jlpm run install:client 123 | jlpm run install:server 124 | # after above maybe you need create use a soft link to hot update 125 | ``` 126 | 127 | 前端部分开发: 128 | 129 | ``` 130 | jlpm run watch 131 | ``` 132 | 133 | 后端部分可以在设置软链接之后,直接修改 python 文件,重启生效。 134 | 135 | 打包: 136 | 137 | ``` 138 | python setup.py bdist_wheel --universal 139 | ``` 140 | 141 | 一般情况下提交 MR 即可,本项目的开发者可以打包发布到 pypi。 142 | -------------------------------------------------------------------------------- /jupyterlab_tensorboard_pro/api_handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | import logging 6 | 7 | from tornado import web 8 | from jupyter_server.base.handlers import APIHandler 9 | 10 | from .handlers import notebook_dir 11 | 12 | 13 | def _trim_notebook_dir(dir, enable_multi_log): 14 | if dir.startswith("s3://"): 15 | return dir 16 | if enable_multi_log: 17 | return dir 18 | if ':' not in dir and not dir.startswith("/"): 19 | return os.path.join( 20 | "", os.path.relpath(dir, notebook_dir) 21 | ) 22 | return dir 23 | 24 | 25 | class TbRootConfigHandler(APIHandler): 26 | 27 | @web.authenticated 28 | def get(self): 29 | terms = { 30 | 'notebook_dir': notebook_dir, 31 | } 32 | self.finish(json.dumps(terms)) 33 | 34 | 35 | class TbRootHandler(APIHandler): 36 | 37 | @web.authenticated 38 | def get(self): 39 | terms = [ 40 | { 41 | 'name': entry.name, 42 | 'reload_interval': entry.reload_interval, 43 | 'enable_multi_log': entry.enable_multi_log, 44 | 'logdir': _trim_notebook_dir(entry.logdir, entry.enable_multi_log), 45 | 'additional_args': entry.additional_args, 46 | } for entry in 47 | self.settings["tensorboard_manager"].values() 48 | ] 49 | self.finish(json.dumps(terms)) 50 | 51 | @web.authenticated 52 | def post(self): 53 | try: 54 | data = self.get_json_body() 55 | reload_interval = data.get("reload_interval", None) 56 | enable_multi_log = data.get("enable_multi_log", False) 57 | additional_args = data.get("additional_args", '') 58 | entry = ( 59 | self.settings["tensorboard_manager"] 60 | .new_instance(data["logdir"], reload_interval=reload_interval, enable_multi_log=enable_multi_log, additional_args=additional_args) 61 | ) 62 | self.finish(json.dumps({ 63 | 'name': entry.name, 64 | 'reload_interval': entry.reload_interval, 65 | 'enable_multi_log': entry.enable_multi_log, 66 | 'additional_args': entry.additional_args, 67 | 'logdir': _trim_notebook_dir(entry.logdir, entry.enable_multi_log), 68 | })) 69 | except SystemExit: 70 | logging.error("[Tensorboard Error] mostly parse args error") 71 | raise web.HTTPError( 72 | 500, "Tensorboard Error: mostly parse args error") 73 | except Exception as e: 74 | logging.error("[Tensorboard Error] catch exception: {e}") 75 | print('[Tensorboard Error]', e) 76 | 77 | 78 | class TbInstanceHandler(APIHandler): 79 | 80 | SUPPORTED_METHODS = ('GET', 'DELETE') 81 | 82 | @web.authenticated 83 | def get(self, name): 84 | manager = self.settings["tensorboard_manager"] 85 | if name in manager: 86 | entry = manager[name] 87 | self.finish(json.dumps({ 88 | 'name': entry.name, 89 | 'reload_interval': entry.reload_interval, 90 | 'enable_multi_log': entry.enable_multi_log, 91 | 'logdir': _trim_notebook_dir(entry.logdir, entry.enable_multi_log), 92 | })) 93 | else: 94 | raise web.HTTPError( 95 | 404, "TensorBoard instance not found: %r" % name) 96 | 97 | @web.authenticated 98 | def delete(self, name): 99 | manager = self.settings["tensorboard_manager"] 100 | if name in manager: 101 | manager.terminate(name, force=True) 102 | self.set_status(204) 103 | self.finish() 104 | else: 105 | raise web.HTTPError( 106 | 404, "TensorBoard instance not found: %r" % name) 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_tensorboard_pro", 3 | "version": "4.0.0", 4 | "description": "A JupyterLab extension for tensorboard.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "tensorboard" 10 | ], 11 | "homepage": "https://github.com/HFAiLab/jupyterlab_tensorboard_pro", 12 | "bugs": { 13 | "url": "https://github.com/HFAiLab/jupyterlab_tensorboard_pro/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/HFAiLab/jupyterlab_tensorboard_pro" 18 | }, 19 | "license": "MIT", 20 | "author": { 21 | "name": "aircloud", 22 | "email": "onlythen@yeah.net" 23 | }, 24 | "sideEffects": [ 25 | "style/*.css", 26 | "style/*.scss", 27 | "style/index.js" 28 | ], 29 | "main": "lib/index.js", 30 | "types": "lib/index.d.ts", 31 | "style": "style/index.css", 32 | "files": [ 33 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 34 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", 35 | "style/index.js" 36 | ], 37 | "scripts": { 38 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 39 | "build:prod": "jlpm run build:lib && jlpm run build:labextension", 40 | "build:labextension": "jupyter labextension build .", 41 | "build:labextension:dev": "jupyter labextension build --development True .", 42 | "build:lib": "tsc", 43 | "clean": "jlpm run clean:lib", 44 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 45 | "clean:labextension": "rimraf jupyterlab_tensorboard_pro/labextension", 46 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 47 | "eslint": "eslint . --ext .ts,.tsx --fix", 48 | "eslint:check": "eslint . --ext .ts,.tsx", 49 | "install:client": "jupyter labextension develop --overwrite .", 50 | "install:server": "jupyter server extension enable jupyterlab_tensorboard_pro", 51 | "prepare": "husky install && jlpm run clean && jlpm run build:prod", 52 | "watch": "jlpm run build:lib && run-p watch:src watch:labextension", 53 | "watch:src": "tsc -w", 54 | "watch:labextension": "jupyter labextension watch ." 55 | }, 56 | "lint-staged": { 57 | "*.{js,ts,tsx}": "eslint --fix", 58 | "package.json": "sort-package-json" 59 | }, 60 | "dependencies": { 61 | "@blueprintjs/core": "^5.3.2", 62 | "@blueprintjs/select": "^5.0.12", 63 | "@jupyterlab/application": "^4.0.6", 64 | "@jupyterlab/apputils": "^4.1.6", 65 | "@jupyterlab/coreutils": "^6.0.6", 66 | "@jupyterlab/filebrowser": "^4.0.6", 67 | "@jupyterlab/launcher": "^4.0.6", 68 | "@jupyterlab/mainmenu": "^4.0.6", 69 | "@jupyterlab/running": "^4.0.6", 70 | "@jupyterlab/services": "^7.0.6", 71 | "@jupyterlab/ui-components": "^4.0.6", 72 | "classnames": "^2.3.1", 73 | "svg-url-loader": "~6.0.0" 74 | }, 75 | "devDependencies": { 76 | "@jupyterlab/builder": "^4.0.6", 77 | "@typescript-eslint/eslint-plugin": "^6.7.3", 78 | "@typescript-eslint/parser": "^6.7.3", 79 | "css-loader": "^5.0.1", 80 | "eslint": "^7.14.0", 81 | "eslint-config-prettier": "^6.15.0", 82 | "eslint-plugin-prettier": "^3.1.4", 83 | "husky": "^8.0.1", 84 | "lint-staged": "^13.0.3", 85 | "npm-run-all": "^4.1.5", 86 | "prettier": "^2.1.1", 87 | "raw-loader": "4.0.0", 88 | "rimraf": "^3.0.2", 89 | "sass": "^1.43.2", 90 | "sass-loader": "^12.2.0", 91 | "sort-package-json": "^1.57.0", 92 | "style-loader": "~2.0.0", 93 | "ts-loader": "^9.2.6", 94 | "typescript": "^5.2.2", 95 | "webpack": "*" 96 | }, 97 | "jupyterlab": { 98 | "discovery": { 99 | "server": { 100 | "managers": [ 101 | "pip" 102 | ], 103 | "base": { 104 | "name": "jupyterlab_tensorboard_pro" 105 | } 106 | } 107 | }, 108 | "extension": true, 109 | "outputDir": "jupyterlab_tensorboard_pro/labextension", 110 | "webpackConfig": "./webpack.config.js" 111 | }, 112 | "styleModule": "style/index.js" 113 | } 114 | -------------------------------------------------------------------------------- /src/biz/widget.tsx: -------------------------------------------------------------------------------- 1 | import { JupyterFrontEnd } from '@jupyterlab/application'; 2 | import { ReactWidget } from '@jupyterlab/apputils'; 3 | import { FileBrowser } from '@jupyterlab/filebrowser'; 4 | import React from 'react'; 5 | import { Tensorboard } from '../tensorboard'; 6 | import { Message } from '@lumino/messaging'; 7 | import { TensorboardManager } from '../manager'; 8 | import { CommandIDs } from '../commands'; 9 | 10 | const TENSORBOARD_CLASS = 'jp-Tensorboard'; 11 | const TENSORBOARD_ICON_CLASS = 'jp-Tensorboards-itemIcon'; 12 | 13 | import { TensorboardTabReact } from './tab'; 14 | 15 | export interface TensorboardInvokeOptions { 16 | fileBrowser: FileBrowser; 17 | tensorboardManager: TensorboardManager; 18 | createdModelName?: string; 19 | app: JupyterFrontEnd; 20 | } 21 | 22 | /** 23 | * A Counter Lumino Widget that wraps a CounterComponent. 24 | */ 25 | export class TensorboardTabReactWidget extends ReactWidget { 26 | fileBrowser: FileBrowser; 27 | tensorboardManager: TensorboardManager; 28 | app: JupyterFrontEnd; 29 | 30 | currentTensorBoardModel: Tensorboard.IModel | null = null; 31 | createdModelName?: string; 32 | currentLogDir?: string; 33 | 34 | /** 35 | * Constructs a new CounterWidget. 36 | */ 37 | constructor(options: TensorboardInvokeOptions) { 38 | super(); 39 | this.fileBrowser = options.fileBrowser; 40 | this.tensorboardManager = options.tensorboardManager; 41 | this.createdModelName = options.createdModelName; 42 | this.app = options.app; 43 | if (!this.createdModelName) { 44 | // hint: if createdModelName exists,update later 45 | this.currentLogDir = this.getCWD(); 46 | } 47 | 48 | this.addClass('tensorboard-ng-widget'); 49 | this.addClass(TENSORBOARD_CLASS); 50 | this.title.iconClass = TENSORBOARD_ICON_CLASS; 51 | this.title.closable = true; 52 | this.title.label = 'Tensorboard'; 53 | this.title.caption = `Name: ${this.title.label}`; 54 | } 55 | 56 | /** 57 | * Dispose of the resources held by the tensorboard widget. 58 | */ 59 | dispose(): void { 60 | super.dispose(); 61 | } 62 | 63 | closeCurrent = (): void => { 64 | this.dispose(); 65 | this.close(); 66 | }; 67 | 68 | protected updateCurrentModel = (model: Tensorboard.IModel | null): void => { 69 | this.currentTensorBoardModel = model; 70 | this.currentLogDir = model?.logdir || ''; 71 | }; 72 | 73 | getCWD = (): string => { 74 | return this.fileBrowser.model.path; 75 | }; 76 | 77 | protected onCloseRequest(msg: Message): void { 78 | super.onCloseRequest(msg); 79 | this.dispose(); 80 | } 81 | 82 | protected openTensorBoard = (modelName: string, copy: boolean): void => { 83 | this.app.commands.execute(CommandIDs.open, { 84 | modelName, 85 | copy 86 | }); 87 | }; 88 | 89 | protected openDoc = (): void => { 90 | this.app.commands.execute(CommandIDs.openDoc); 91 | }; 92 | 93 | startNew = ( 94 | logdir: string, 95 | refreshInterval: number, 96 | enableMultiLog: boolean, 97 | additionalArgs: string, 98 | options?: Tensorboard.IOptions 99 | ): Promise => { 100 | this.currentLogDir = logdir; 101 | return this.tensorboardManager.startNew( 102 | logdir, 103 | refreshInterval, 104 | enableMultiLog, 105 | additionalArgs, 106 | options 107 | ); 108 | }; 109 | 110 | setWidgetName = (name: string): void => { 111 | this.title.label = name || 'Tensorboard'; 112 | this.title.caption = `Name: ${this.title.label}`; 113 | }; 114 | 115 | render(): JSX.Element { 116 | return ( 117 | 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /style/tab.scss: -------------------------------------------------------------------------------- 1 | .tensorboard-ng-creator { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: row; 5 | align-items: center; 6 | 7 | .additional-config-input { 8 | width: 120px; 9 | margin-left: 5px; 10 | &.with-content { 11 | flex: 3; 12 | } 13 | } 14 | } 15 | 16 | .tensorboard-ng-control-layout { 17 | width: 100%; 18 | height: 70px; 19 | background-color: var(--jp-cell-editor-background); 20 | display: flex; 21 | flex-direction: column; 22 | overflow: hidden; 23 | 24 | * { 25 | box-sizing: border-box; 26 | } 27 | 28 | .tensorboard-ng-control-row { 29 | padding: 5px; 30 | width: 100%; 31 | height: 35px; 32 | flex-direction: row; 33 | display: flex; 34 | overflow: hidden; 35 | align-items: center; 36 | min-width: 760px; 37 | &.hide { 38 | height: 0px; 39 | padding-top: 0; 40 | padding-bottom: 0; 41 | } 42 | &.creator { 43 | background-color: var(--jp-border-color2); 44 | } 45 | } 46 | 47 | &.hide-one { 48 | height: 40px; 49 | } 50 | } 51 | 52 | .tensorboard-ng-config { 53 | display: flex; 54 | flex-direction: row; 55 | padding-left: 5px; 56 | padding-right: 5px; 57 | .input-container { 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | label { 62 | margin-right: 5px; 63 | &.interval-switch { 64 | margin-bottom: 0; 65 | } 66 | &.multi-log-switch { 67 | margin-bottom: 0; 68 | margin-right: 15px; 69 | } 70 | } 71 | &:not(:last-child) { 72 | margin-right: 10px; 73 | } 74 | } 75 | } 76 | 77 | .tensorboard-ng-logdir { 78 | .refresh-dir-btn { 79 | margin-right: 5px; 80 | margin-left: 5px; 81 | } 82 | .multi-log-tip { 83 | margin-right: 5px; 84 | } 85 | .reload-tip, 86 | .multi-log-tip { 87 | margin-bottom: 0; 88 | margin-left: 5px; 89 | font-size: 14px; 90 | } 91 | .custom-args-tip { 92 | margin-left: 10px; 93 | color: var(--jp-ui-font-color3); 94 | max-width: 200px; 95 | display: inline-block; 96 | overflow: hidden; 97 | text-overflow: ellipsis; 98 | white-space: nowrap; 99 | } 100 | } 101 | 102 | .tensorboard-ng-widget { 103 | padding: 0; 104 | .tensorboard-ng-op-btn { 105 | &:not(:last-child) { 106 | margin-right: 10px; 107 | } 108 | } 109 | } 110 | 111 | .tensorboard-ng-expand { 112 | flex: 1; 113 | } 114 | 115 | .tensorboard-ng-iframe-container { 116 | width: 100%; 117 | height: 100%; 118 | position: relative; 119 | background: var(--jp-layout-color1); 120 | .tensorboard-ng-iframe { 121 | width: 100%; 122 | height: 100%; 123 | } 124 | .tensorboard-ng-iframe-tip { 125 | position: absolute; 126 | top: 0; 127 | bottom: 0; 128 | left: 0; 129 | right: 0; 130 | display: flex; 131 | align-items: center; 132 | flex-direction: column; 133 | background: var(--jp-layout-color1); 134 | .common-tip { 135 | width: 100%; 136 | display: flex; 137 | flex-direction: column; 138 | align-items: center; 139 | margin: auto; 140 | } 141 | 142 | p { 143 | max-width: 75%; 144 | margin: auto; 145 | font-size: var(--jp-ui-font-size3); 146 | color: var(--jp-ui-font-color2); 147 | 148 | &.error { 149 | color: var(--jp-error-color1); 150 | } 151 | 152 | &.title { 153 | margin: auto; 154 | font-size: var(--jp-ui-font-size3); 155 | color: var(--jp-ui-font-color1); 156 | margin-bottom: 10px; 157 | font-weight: bold; // 700 158 | } 159 | 160 | &.desc { 161 | margin: auto; 162 | color: var(--jp-ui-font-color2); 163 | } 164 | } 165 | } 166 | } 167 | 168 | .tensorboard-ng-main { 169 | height: 100%; 170 | } 171 | 172 | .tb-ng-model-selector { 173 | width: 200px; 174 | height: 24px; 175 | 176 | .selector-active-btn { 177 | width: 200px; 178 | .active-btn-text { 179 | width: 170px; 180 | display: inline-block; 181 | overflow: hidden; 182 | text-overflow: ellipsis; 183 | white-space: nowrap; 184 | } 185 | } 186 | } 187 | 188 | .tensorboard-ng-ops { 189 | &.create { 190 | margin-left: 10px; 191 | } 192 | } 193 | 194 | // loading 195 | .tensorboard-loading-container { 196 | margin: auto; 197 | display: flex; 198 | height: max-content; 199 | flex-direction: column; 200 | align-items: center; 201 | width: 67.8%; 202 | 203 | .lds-ring { 204 | margin: auto; 205 | display: block; 206 | position: relative; 207 | width: 80px; 208 | height: 80px; 209 | } 210 | .lds-ring div { 211 | box-sizing: border-box; 212 | display: block; 213 | position: absolute; 214 | width: 64px; 215 | height: 64px; 216 | margin: 8px; 217 | border: 8px solid #ccc; 218 | border-radius: 50%; 219 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 220 | border-color: #ccc transparent transparent transparent; 221 | } 222 | .lds-ring div:nth-child(1) { 223 | animation-delay: -0.45s; 224 | } 225 | .lds-ring div:nth-child(2) { 226 | animation-delay: -0.3s; 227 | } 228 | .lds-ring div:nth-child(3) { 229 | animation-delay: -0.15s; 230 | } 231 | @keyframes lds-ring { 232 | 0% { 233 | transform: rotate(0deg); 234 | } 235 | 100% { 236 | transform: rotate(360deg); 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /jupyterlab_tensorboard_pro/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-2019, Shengpeng Liu. All rights reserved. 3 | # Copyright (c) 2019, Alex Ford. All rights reserved. 4 | # Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved. 5 | 6 | from tornado import web 7 | from tornado.wsgi import WSGIContainer 8 | from jupyter_server.base.handlers import JupyterHandler 9 | from jupyter_server.utils import url_path_join as ujoin 10 | from jupyter_server.base.handlers import path_regex 11 | 12 | notebook_dir = None 13 | 14 | 15 | def load_jupyter_server_extension(nb_app): 16 | global notebook_dir 17 | # notebook_dir should be root_dir of contents_manager 18 | notebook_dir = nb_app.contents_manager.root_dir 19 | 20 | web_app = nb_app.web_app 21 | base_url = web_app.settings['base_url'] 22 | 23 | try: 24 | from .tensorboard_manager import manager 25 | except ImportError: 26 | nb_app.log.info("import tensorboard error, check tensorflow install") 27 | handlers = [ 28 | (ujoin( 29 | base_url, r"/tensorboard_pro.*"), 30 | TensorboardErrorHandler), 31 | ] 32 | else: 33 | web_app.settings["tensorboard_manager"] = manager 34 | from . import api_handlers 35 | 36 | handlers = [ 37 | (ujoin( 38 | base_url, r"/tensorboard_pro/(?P\w+)%s" % path_regex), 39 | TensorboardHandler), 40 | (ujoin( 41 | base_url, r"/api/tensorboard_pro"), 42 | api_handlers.TbRootHandler), 43 | (ujoin( 44 | base_url, r"/api/tensorboard_pro_static_config"), 45 | api_handlers.TbRootConfigHandler), 46 | (ujoin( 47 | base_url, r"/api/tensorboard_pro/(?P\w+)"), 48 | api_handlers.TbInstanceHandler), 49 | (ujoin( 50 | base_url, r"/font-roboto/.*"), 51 | TbFontHandler), 52 | ] 53 | 54 | web_app.add_handlers('.*$', handlers) 55 | nb_app.log.info("jupyterlab_tensorboard_pro extension loaded.") 56 | 57 | 58 | class TensorboardHandler(JupyterHandler): 59 | 60 | def _impl(self, name, path): 61 | 62 | self.request.path = path 63 | 64 | manager = self.settings["tensorboard_manager"] 65 | if name in manager: 66 | tb_app = manager[name].tb_app 67 | WSGIContainer(tb_app)(self.request) 68 | else: 69 | raise web.HTTPError(404) 70 | 71 | @web.authenticated 72 | def get(self, name, path): 73 | 74 | if path == "": 75 | uri = self.request.path + "/" 76 | if self.request.query: 77 | uri += "?" + self.request.query 78 | self.redirect(uri, permanent=True) 79 | return 80 | 81 | self._impl(name, path) 82 | 83 | @web.authenticated 84 | def post(self, name, path): 85 | 86 | if path == "": 87 | raise web.HTTPError(403) 88 | 89 | self._impl(name, path) 90 | 91 | def check_xsrf_cookie(self): 92 | """Expand XSRF check exceptions for POST requests. 93 | 94 | Provides support for TensorBoard plugins that use POST to retrieve 95 | experiment information. 96 | 97 | Expands check_xsrf_cookie exceptions, normally only applied to GET 98 | and HEAD requests, to POST requests, as TensorBoard POST endpoints 99 | do not modify state, so TensorBoard doesn't handle XSRF checks. 100 | 101 | See https://github.com/tensorflow/tensorboard/issues/4685 102 | 103 | """ 104 | 105 | # Check XSRF token 106 | try: 107 | return super(TensorboardHandler, self).check_xsrf_cookie() 108 | 109 | except web.HTTPError: 110 | # Note: GET and HEAD exceptions are already handled in 111 | # IPythonHandler.check_xsrf_cookie and will not normally throw 403 112 | 113 | # For TB POSTs, we must loosen our expectations a bit. IPythonHandler 114 | # has some existing exceptions to consider a matching Referer as 115 | # sufficient for GET and HEAD requests; we extend that here to POST 116 | 117 | if self.request.method in {"POST"}: 118 | # Consider Referer a sufficient cross-origin check, mirroring 119 | # logic in IPythonHandler.check_xsrf_cookie. 120 | if not self.check_referer(): 121 | referer = self.request.headers.get("Referer") 122 | if referer: 123 | msg = ( 124 | "Blocking Cross Origin request from {}." 125 | .format(referer) 126 | ) 127 | else: 128 | msg = "Blocking request from unknown origin" 129 | raise web.HTTPError(403, msg) 130 | else: 131 | raise 132 | 133 | 134 | class TbFontHandler(JupyterHandler): 135 | 136 | @web.authenticated 137 | def get(self): 138 | manager = self.settings["tensorboard_manager"] 139 | if "1" in manager: 140 | tb_app = manager["1"].tb_app 141 | WSGIContainer(tb_app)(self.request) 142 | else: 143 | raise web.HTTPError(404) 144 | 145 | 146 | class TensorboardErrorHandler(JupyterHandler): 147 | pass 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JupyterLab-TensorBoard-Pro 2 | 3 | ![Github Actions Status](https://github.com/HFAiLab/jupyterlab_tensorboard_pro/workflows/Build/badge.svg) [![pypi](https://img.shields.io/pypi/v/jupyterlab_tensorboard_pro.svg)](https://pypi.org/project/jupyterlab-tensorboard-pro/) 4 | 5 | [中文文档](./README.zh-cn.md) 6 | 7 | A TensorBoard JupyterLab plugin. 8 | 9 | ![](./images/tensorboard.step4.png) 10 | 11 | ## Requirements 12 | 13 | **python >= 3.6** 14 | 15 | Please install the following dependencies before installing this project: 16 | 17 | - jupyterlab 18 | - tensorflow 19 | - tensorboard 20 | 21 | ## Install 22 | 23 | **important:** 24 | 25 | * jupyterlab 4.0+: use 4+: 26 | 27 | ``` 28 | pip install jupyterlab-tensorboard-pro 29 | ``` 30 | 31 | * jupyterlab 3.x: use 3: 32 | 33 | ``` 34 | pip install jupyterlab-tensorboard-pro~=3.0 35 | ``` 36 | 37 | > only jupyterlab support, not include notebook 38 | 39 | ## Background 40 | 41 | In fact, there are already [jupyterlab_tensorboard](https://github.com/chaoleili/jupyterlab_tensorboard) (front-end plugin) and [jupyter_tensorboard](https://github.com/lspvic/jupyter_tensorboard) (back-end plugin) in the community, but both repositories have not been updated for a long time, and some new repair PRs have not been merged in time. Based on this, maybe the project author is no longer actively maintaining the corresponding repositories. 42 | 43 | At the same time, the existing community TensorBoard plugin has some experience problems, such as installing two python packages at the same time, no response for a long time after clicking `TensorBoard`, and the TensorBoard Reload time cannot be set. The interactive experience is not friendly enough, which will also affect the user's JupyterLab experience. 44 | 45 | Therefore, this project is forked from the existing projects of the community, and we made some positive changes, contained some previous PRs which are helpful but have not been merged for the time being. This repo will to be maintained for a long time in the future. 46 | 47 | This repo has also changed the api name, so it can be completely independent of the above plugins. 48 | 49 | Special thanks to the developers of the previous related repositories. 50 | 51 | ## Instructions 52 | 53 | ### Create Instance 54 | 55 | #### Create from Launcher Panel 56 | 57 | We can click the TensorBoard icon from the Launcher panel, the first click will take you to a default initialization panel from which we can create a TensorBoard instance. But if there is an active TensorBoard backend at this time, it will be opened directly. 58 | 59 | ![](./images/tensorboard.step1.png) 60 | 61 | #### Create by Shortcut Command 62 | 63 | We can also type `Open TensorBoard` in the JupyterLab shortcut panel (evoked by `ctrl + shift + c`) 64 | 65 | ![](./images/tensorboard.step2.png) 66 | 67 | #### Parameters 68 | 69 | In the initialization panel, two parameters are provided: 70 | 71 | - **Log Dir**: The default is the **relative directory** of the current sidebar when TensorBoard is clicked. You can also manually fill in the corresponding directory. It is recommended to make the directory as detailed as possible. If the directory content is small, the initialization speed will be improved. 72 | - **Reload Interval**: How often does TensorBoard backend rescan the corresponding directory. This option is set to false by default. It is recommended to disable and use manually Reload for daily use (The continuous scanning of directories by the TensorBoard backend will have some impact on Jupyter's stability and file system). 73 | 74 | Select the parameters and click Create TensorBoard, and the TensorBoard instance will be created synchronously. At this time, the jupyter backend is **blocking**, please wait for the instance to be created before performing other operations. 75 | 76 | ![](./images/tensorboard.step3.png) 77 | 78 | ### Manage Instances 79 | 80 | After the instance of TensorBoard is created, we can manage the instance. Currently, the following functions are provided: 81 | 82 | - **Refresh and list all**: TensorBoard backends can be switched to other instances (won't destroy the before) 83 | - **Open in a separate page**: You can open TensorBoard in the form of a separate web page tab. 84 | - **Reload**: Reinitialize the TensorBoard backend. When the content of the file is updated, you can load the new content through this function (Note: The refresh inside TensorBoard will not cause Reload). 85 | - **Destroy**: Destroy the instance, it will close both the backend and the frontend panel. 86 | - **Duplicate**: Open an identical frontend panel, this operation will reuse the TensorBoard backend. 87 | - **New**: Create an additional TensorBoard backend, please refer to the above for precautions. 88 | 89 | In addition, for the TensorBoard instance we created, it can be managed in the Kernel management panel of Jupyter, providing functions such as jumping to the corresponding instance and deleting. 90 | 91 | ![](./images/tensorboard.step5.png) 92 | 93 | ### Use AWS S3 94 | 95 | > It is assumed here that you have some experience with aws s3 96 | 97 | TensorBoard supports passing an s3 path via `s3://path/to/dir`, which is also supported in this plugin. 98 | 99 | However, because the s3 path is usually not directly accessible, you need to configure some basic information through `aws configure` ([download](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) aws cli), Usually, the system where JupyterLab is running should have the following files: 100 | 101 | ```shell 102 | # ~/.aws/config 103 | [default] 104 | region = ap-southeast-1 105 | output = json 106 | 107 | # ~/.aws/credentials 108 | [default] 109 | aws_access_key_id = ******** 110 | aws_secret_access_key = ******** 111 | ``` 112 | 113 | Then you need to install some additional dependencies: 114 | 115 | ``` 116 | pip install botocore boto3 tensorflow-io 117 | ``` 118 | 119 | After that, you can enter an s3 path, then click the refresh button of tensorboard, and wait the loading: 120 | 121 | ![](./images/tensorboard.step6.png) 122 | 123 | > In fact, the status prompt of tensorboard itself is not friendly now, and we will further investigate whether there is a better way to experience it later. 124 | 125 | ## Debug 126 | 127 | You can use `jupyter-lab --debug` to enable debug logging for JupyterLab and TensorBoard. 128 | 129 | ## Develop 130 | 131 | ```shell 132 | jlpm install 133 | pip install jupyter_packaging 134 | jlpm run install:client 135 | jlpm run install:server 136 | ln -s /path/to/jupyterlab_tensorboard_pro jupyterlab_tensorboard_pro 137 | # after above maybe you need create use a soft link to hot update 138 | ``` 139 | 140 | watch frontend: 141 | ``` 142 | jlpm run watch 143 | ``` 144 | 145 | build: 146 | ``` 147 | python setup.py bdist_wheel --universal 148 | ``` 149 | 150 | Under normal circumstances, you can just submit MR, the developers of this project will package and publish to pypi. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; 2 | import { 3 | ICommandPalette, 4 | WidgetTracker, 5 | IWidgetTracker, 6 | showDialog, 7 | Dialog, 8 | MainAreaWidget 9 | } from '@jupyterlab/apputils'; 10 | import { ILauncher } from '@jupyterlab/launcher'; 11 | import { IMainMenu } from '@jupyterlab/mainmenu'; 12 | import { FileBrowser, IFileBrowserFactory } from '@jupyterlab/filebrowser'; 13 | import { TensorboardManager } from './manager'; 14 | import { Tensorboard } from './tensorboard'; 15 | import { IRunningSessionManagers, IRunningSessions } from '@jupyterlab/running'; 16 | import { LabIcon } from '@jupyterlab/ui-components'; 17 | import tensorboardSvgStr from '../style/tensorboard.svg'; 18 | import { TensorboardTabReactWidget } from './biz/widget'; 19 | import { CommandIDs } from './commands'; 20 | 21 | export const tensorboardIcon = new LabIcon({ 22 | name: 'jupyterlab-tensorboard-p:tensorboard', 23 | svgstr: tensorboardSvgStr 24 | }); 25 | 26 | /** 27 | * Initialization data for the tensorboard extension. 28 | */ 29 | const extension: JupyterFrontEndPlugin>> = 30 | { 31 | id: 'tensorboard', 32 | requires: [ICommandPalette, IFileBrowserFactory], 33 | optional: [ILauncher, IMainMenu, IRunningSessionManagers], 34 | autoStart: true, 35 | activate 36 | }; 37 | 38 | export default extension; 39 | 40 | async function activate( 41 | app: JupyterFrontEnd, 42 | palette: ICommandPalette, 43 | browserFactory: IFileBrowserFactory, 44 | launcher: ILauncher | null, 45 | menu: IMainMenu | null, 46 | runningSessionManagers: IRunningSessionManagers | null 47 | ): Promise>> { 48 | console.info('activate beign test!!'); 49 | const manager = new TensorboardManager(); 50 | const namespace = 'tensorboard'; 51 | const tracker = new WidgetTracker>({ 52 | namespace 53 | }); 54 | 55 | const fileBrowser = browserFactory.createFileBrowser('tensorboard_pro'); 56 | addCommands(app, manager, tracker, fileBrowser, launcher, menu); 57 | 58 | if (runningSessionManagers) { 59 | await addRunningSessionManager(runningSessionManagers, app, manager); 60 | } 61 | 62 | palette.addItem({ command: CommandIDs.inputDirect, category: 'Tensorboard' }); 63 | 64 | return tracker; 65 | } 66 | 67 | // Running Kernels and Terminals,The coin-like tab 68 | function addRunningSessionManager( 69 | managers: IRunningSessionManagers, 70 | app: JupyterFrontEnd, 71 | manager: TensorboardManager 72 | ) { 73 | class RunningTensorboard implements IRunningSessions.IRunningItem { 74 | manager: TensorboardManager; 75 | 76 | constructor(model: Tensorboard.IModel, manager: TensorboardManager) { 77 | this._model = model; 78 | this.manager = manager; 79 | } 80 | open() { 81 | app.commands.execute(CommandIDs.open, { modelName: this._model.name }); 82 | } 83 | icon() { 84 | return tensorboardIcon; 85 | } 86 | label() { 87 | return `${this._model.name}:${this.manager.formatDir(this._model.logdir)}`; 88 | } 89 | shutdown() { 90 | app.commands.execute(CommandIDs.close, { tb: this._model }); 91 | return manager.shutdown(this._model.name); 92 | } 93 | 94 | private _model: Tensorboard.IModel; 95 | } 96 | 97 | return manager.getStaticConfigPromise.then(() => { 98 | managers.add({ 99 | name: 'Tensorboard', 100 | running: () => manager.running().map(model => new RunningTensorboard(model, manager)), 101 | shutdownAll: () => manager.shutdownAll(), 102 | refreshRunning: () => manager.refreshRunning(), 103 | runningChanged: manager.runningChanged 104 | }); 105 | }); 106 | } 107 | 108 | /** 109 | * Add the commands for the tensorboard. 110 | */ 111 | export function addCommands( 112 | app: JupyterFrontEnd, 113 | manager: TensorboardManager, 114 | tracker: WidgetTracker>, 115 | fileBrowser: FileBrowser, 116 | launcher: ILauncher | null, 117 | menu: IMainMenu | null 118 | ): void { 119 | const { commands, serviceManager } = app; 120 | 121 | commands.addCommand(CommandIDs.open, { 122 | execute: args => { 123 | // if select certain 124 | 125 | let modelName = args['modelName'] as string | undefined; 126 | const copy = args['copy']; 127 | console.info('[DEBUG] browserFactory.defaultBrowser:', fileBrowser, fileBrowser.model.path); 128 | 129 | const currentCWD = fileBrowser.model.path; 130 | 131 | let widget: MainAreaWidget | null | undefined = null; 132 | 133 | // step1: find an opened widget 134 | if (!modelName) { 135 | widget = tracker.find(widget => { 136 | return ( 137 | manager.formatDir(widget.content.currentLogDir || '') === manager.formatDir(currentCWD) 138 | ); 139 | }); 140 | } else if (!copy) { 141 | widget = tracker.find(value => { 142 | return value.content.currentTensorBoardModel?.name === modelName; 143 | }); 144 | } 145 | // default we have only one tensorboard 146 | if (widget) { 147 | app.shell.activateById(widget.id); 148 | return widget; 149 | } else { 150 | // step2: try find opened backend widgets 151 | if (!modelName) { 152 | const runningTensorboards = [...manager.running()]; 153 | // hint: Using runningTensorboards directly may cause setState to fail to respond 154 | for (const model of runningTensorboards) { 155 | if (manager.formatDir(model.logdir) === manager.formatDir(currentCWD)) { 156 | modelName = model.name; 157 | } 158 | } 159 | } 160 | 161 | const tabReact = new TensorboardTabReactWidget({ 162 | fileBrowser, 163 | tensorboardManager: manager, 164 | app, 165 | createdModelName: modelName 166 | }); 167 | const tabWidget = new MainAreaWidget({ content: tabReact }); 168 | tracker.add(tabWidget); 169 | app.shell.add(tabWidget, 'main', { 170 | mode: copy ? 'split-right' : undefined 171 | }); 172 | app.shell.activateById(tabWidget.id); 173 | return tabWidget; 174 | } 175 | } 176 | }); 177 | 178 | commands.addCommand(CommandIDs.openDoc, { 179 | execute: args => { 180 | window.open('https://github.com/HFAiLab/jupyterlab_tensorboard_pro'); 181 | } 182 | }); 183 | 184 | commands.addCommand(CommandIDs.close, { 185 | execute: args => { 186 | const model = args['tb'] as Tensorboard.IModel; 187 | tracker.forEach(widget => { 188 | if ( 189 | widget.content.currentTensorBoardModel && 190 | widget.content.currentTensorBoardModel.name === model.name 191 | ) { 192 | widget.dispose(); 193 | widget.close(); 194 | } 195 | }); 196 | } 197 | }); 198 | 199 | commands.addCommand(CommandIDs.inputDirect, { 200 | label: () => 'Open TensorBoard', 201 | execute: args => { 202 | return app.commands.execute(CommandIDs.open); 203 | } 204 | }); 205 | 206 | commands.addCommand(CommandIDs.createNew, { 207 | label: args => (args['isPalette'] ? 'New TensorBoard' : 'TensorBoard'), 208 | caption: 'Start a new tensorboard', 209 | icon: args => (args['isPalette'] ? undefined : tensorboardIcon), 210 | execute: args => { 211 | const cwd = (args['cwd'] as string) || fileBrowser.model.path; 212 | const logdir = typeof args['logdir'] === 'undefined' ? cwd : (args['logdir'] as string); 213 | return serviceManager.contents.get(logdir, { type: 'directory' }).then( 214 | dir => { 215 | // Try to open the session panel to make it easier for users to observe more active tensorboard instances 216 | try { 217 | app.shell.activateById('jp-running-sessions'); 218 | } catch (e) { 219 | // do nothing 220 | } 221 | app.commands.execute(CommandIDs.open); 222 | }, 223 | () => { 224 | // no such directory. 225 | return showDialog({ 226 | title: 'Cannot create tensorboard.', 227 | body: 'Directory not found', 228 | buttons: [Dialog.okButton()] 229 | }); 230 | } 231 | ); 232 | } 233 | }); 234 | 235 | if (launcher) { 236 | launcher.add({ 237 | command: CommandIDs.createNew, 238 | category: 'Other', 239 | rank: 2 240 | }); 241 | } 242 | 243 | if (menu) { 244 | menu.fileMenu.newMenu.addGroup( 245 | [ 246 | { 247 | command: CommandIDs.createNew 248 | } 249 | ], 250 | 30 251 | ); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /jupyterlab_tensorboard_pro/tensorboard_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-2019, Shengpeng Liu. All rights reserved. 3 | # Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved. 4 | # Copyright (c) 2022, HFAiLab. All rights reserved. 5 | 6 | import os 7 | import sys 8 | import inspect 9 | import itertools 10 | from collections import namedtuple 11 | import logging 12 | 13 | is_debug = True if '--debug' in sys.argv else False 14 | 15 | sys.argv = ["tensorboard"] 16 | 17 | from tensorboard.backend import application # noqa 18 | 19 | 20 | def get_plugins(): 21 | # Gather up core plugins as well as dynamic plugins. 22 | # The way this was done varied several times in the later 1.x series 23 | if hasattr(default, 'PLUGIN_LOADERS'): # TB 1.10 24 | return default.PLUGIN_LOADERS[:] 25 | 26 | if hasattr(default, 'get_plugins') and inspect.isfunction(default.get_plugins): # TB 1.11+ 27 | if not (hasattr(default, 'get_static_plugins') and inspect.isfunction(default.get_static_plugins)): 28 | # in TB 1.11 through 2.2, get_plugins is really just the static plugins 29 | plugins = default.get_plugins() 30 | else: 31 | # in TB 2.3 and later, get_plugins was renamed to get_static_plugins and 32 | # a new get_plugins was created that returns the static+dynamic set 33 | plugins = default.get_static_plugins() 34 | 35 | if hasattr(default, 'get_dynamic_plugins') and inspect.isfunction(default.get_dynamic_plugins): 36 | # in TB 1.14 there are also dynamic plugins that should be included 37 | plugins += default.get_dynamic_plugins() 38 | 39 | return plugins 40 | return None 41 | 42 | 43 | try: 44 | # Tensorboard 0.4.x above series 45 | from tensorboard import default 46 | 47 | if hasattr(default, 'PLUGIN_LOADERS') or hasattr(default, '_PLUGINS'): 48 | # TensorBoard 1.10 or above series 49 | from tensorboard import program 50 | 51 | def create_tb_app(logdir, reload_interval, purge_orphaned_data, enable_multi_log, additional_args): 52 | argv = [ 53 | "", 54 | "--logdir", logdir, 55 | "--reload_interval", str(reload_interval), 56 | "--purge_orphaned_data", str(purge_orphaned_data), 57 | ] 58 | 59 | # example: "--samples_per_plugin", "images=1" 60 | additional_args_arr = additional_args.split() 61 | argv += additional_args_arr 62 | 63 | if enable_multi_log: 64 | argv[1] = "--logdir_spec" 65 | 66 | tensorboard = program.TensorBoard(get_plugins()) 67 | tensorboard.configure(argv) 68 | 69 | if (hasattr(application, 'standard_tensorboard_wsgi') and inspect.isfunction(application.standard_tensorboard_wsgi)): 70 | logging.debug("TensorBoard 1.10 or above series detected") 71 | standard_tensorboard_wsgi = application.standard_tensorboard_wsgi 72 | else: 73 | logging.debug("TensorBoard 2.3 or above series detected") 74 | 75 | def standard_tensorboard_wsgi(flags, plugin_loaders, assets_zip_provider): 76 | from tensorboard.backend.event_processing import data_ingester 77 | ingester = data_ingester.LocalDataIngester(flags) 78 | ingester.start() 79 | return application.TensorBoardWSGIApp(flags, plugin_loaders, ingester.data_provider, 80 | assets_zip_provider, ingester.deprecated_multiplexer) 81 | 82 | return manager.add_instance(logdir, reload_interval, enable_multi_log, additional_args, standard_tensorboard_wsgi( 83 | tensorboard.flags, 84 | tensorboard.plugin_loaders, 85 | tensorboard.assets_zip_provider)) 86 | else: 87 | logging.debug("TensorBoard 0.4.x series detected") 88 | 89 | def create_tb_app(logdir, reload_interval, purge_orphaned_data, enable_multi_log, additional_args): 90 | return manager.add_instance(logdir, reload_interval, enable_multi_log, additional_args, application.standard_tensorboard_wsgi( 91 | logdir=logdir, reload_interval=reload_interval, 92 | purge_orphaned_data=purge_orphaned_data, 93 | plugins=default.get_plugins())) 94 | 95 | except ImportError: 96 | # Tensorboard 0.3.x series 97 | from tensorboard.plugins.audio import audio_plugin 98 | from tensorboard.plugins.core import core_plugin 99 | from tensorboard.plugins.distribution import distributions_plugin 100 | from tensorboard.plugins.graph import graphs_plugin 101 | from tensorboard.plugins.histogram import histograms_plugin 102 | from tensorboard.plugins.image import images_plugin 103 | from tensorboard.plugins.profile import profile_plugin 104 | from tensorboard.plugins.projector import projector_plugin 105 | from tensorboard.plugins.scalar import scalars_plugin 106 | from tensorboard.plugins.text import text_plugin 107 | logging.debug("Tensorboard 0.3.x series detected") 108 | 109 | _plugins = [ 110 | core_plugin.CorePlugin, 111 | scalars_plugin.ScalarsPlugin, 112 | images_plugin.ImagesPlugin, 113 | audio_plugin.AudioPlugin, 114 | graphs_plugin.GraphsPlugin, 115 | distributions_plugin.DistributionsPlugin, 116 | histograms_plugin.HistogramsPlugin, 117 | projector_plugin.ProjectorPlugin, 118 | text_plugin.TextPlugin, 119 | profile_plugin.ProfilePlugin, 120 | ] 121 | 122 | def create_tb_app(logdir, reload_interval, purge_orphaned_data, enable_multi_log, additional_args): 123 | return application.standard_tensorboard_wsgi( 124 | logdir=logdir, reload_interval=reload_interval, 125 | purge_orphaned_data=purge_orphaned_data, 126 | plugins=_plugins) 127 | 128 | 129 | from .handlers import notebook_dir # noqa 130 | 131 | TensorBoardInstance = namedtuple( 132 | 'TensorBoardInstance', ['name', 'logdir', 'reload_interval', 'enable_multi_log', 'additional_args', 'tb_app']) 133 | 134 | 135 | class TensorboardManger(dict): 136 | 137 | def __init__(self): 138 | self._logdir_dict = {} 139 | if is_debug: 140 | _logger = logging.getLogger("tensorboard") 141 | _logger.setLevel(logging.DEBUG) 142 | 143 | def _next_available_name(self): 144 | # hint: 这里实现的机制,让我们可以通过 delete + create 去模拟 reload,并且 name 保持不变 145 | for n in itertools.count(start=1): 146 | name = "%d" % n 147 | if name not in self: 148 | return name 149 | 150 | def format_multi_dir_path(self, dir): 151 | dirs = dir.split(',') 152 | 153 | def format_dir(dir): 154 | name = "" 155 | realdir = "" 156 | 157 | if ':' in dir: 158 | name, realdir = dir.split(':') 159 | else: 160 | realdir = dir 161 | 162 | if not os.path.isabs(realdir) and notebook_dir and not realdir.startswith("s3://"): 163 | realdir = os.path.join(notebook_dir, realdir) 164 | 165 | if name: 166 | return f'{name}:{realdir}' 167 | return realdir 168 | 169 | return ','.join(map(format_dir, dirs)) 170 | 171 | def new_instance(self, logdir, reload_interval, enable_multi_log, additional_args): 172 | if not enable_multi_log and not os.path.isabs(logdir) and notebook_dir and not logdir.startswith("s3://"): 173 | logdir = os.path.join(notebook_dir, logdir) 174 | 175 | if logdir not in self._logdir_dict: 176 | purge_orphaned_data = True 177 | reload_interval = 120 if reload_interval is None else reload_interval 178 | if enable_multi_log: 179 | logdir = self.format_multi_dir_path(logdir) 180 | create_tb_app( 181 | logdir=logdir, reload_interval=reload_interval, 182 | purge_orphaned_data=purge_orphaned_data, enable_multi_log=enable_multi_log, additional_args=additional_args) 183 | 184 | return self._logdir_dict[logdir] 185 | 186 | def add_instance(self, logdir, reload_interval, enable_multi_log, additional_args, tb_application): 187 | name = self._next_available_name() 188 | instance = TensorBoardInstance( 189 | name, logdir, reload_interval, enable_multi_log, additional_args, tb_application) 190 | self[name] = instance 191 | self._logdir_dict[logdir] = instance 192 | return tb_application 193 | 194 | def terminate(self, name, force=True): 195 | if name in self: 196 | instance = self[name] 197 | del self[name], self._logdir_dict[instance.logdir] 198 | else: 199 | raise Exception("There's no tensorboard instance named %s" % name) 200 | 201 | 202 | manager = TensorboardManger() 203 | -------------------------------------------------------------------------------- /src/manager.ts: -------------------------------------------------------------------------------- 1 | //Tensorboard manager 2 | import { ArrayExt } from '@lumino/algorithm'; 3 | import { Signal, ISignal } from '@lumino/signaling'; 4 | import { JSONExt } from '@lumino/coreutils'; 5 | import { Tensorboard } from './tensorboard'; 6 | import { ServerConnection } from '@jupyterlab/services'; 7 | import { DEFAULT_ENABLE_MULTI_LOG, DEFAULT_REFRESH_INTERVAL } from './consts'; 8 | 9 | /** 10 | * A tensorboard manager. 11 | */ 12 | export class TensorboardManager implements Tensorboard.IManager { 13 | getStaticConfigPromise: Promise; 14 | 15 | /** 16 | * Construct a new tensorboard manager. 17 | */ 18 | constructor(options: TensorboardManager.IOptions = {}) { 19 | this.serverSettings = options.serverSettings || ServerConnection.makeSettings(); 20 | this._readyPromise = this._refreshRunning(); 21 | this._refreshTimer = (setInterval as any)(() => { 22 | if (typeof document !== 'undefined' && document.hidden) { 23 | return; 24 | } 25 | this._refreshRunning(); 26 | }, 10000); 27 | this.getStaticConfigPromise = this._getStaticConfig(); 28 | } 29 | 30 | /** 31 | * A signal emitted when the running tensorboards change. 32 | */ 33 | get runningChanged(): ISignal { 34 | return this._runningChanged; 35 | } 36 | 37 | /** 38 | * Test whether the terminal manager is disposed. 39 | */ 40 | get isDisposed(): boolean { 41 | return this._isDisposed; 42 | } 43 | 44 | /** 45 | * The server settings of the manager. 46 | */ 47 | readonly serverSettings: ServerConnection.ISettings; 48 | 49 | /** 50 | * Dispose of the resources used by the manager. 51 | */ 52 | dispose(): void { 53 | if (this.isDisposed) { 54 | return; 55 | } 56 | this._isDisposed = true; 57 | clearInterval(this._refreshTimer); 58 | Signal.clearData(this); 59 | this._models = []; 60 | } 61 | 62 | /** 63 | * Test whether the manager is ready. 64 | */ 65 | get isReady(): boolean { 66 | return this._isReady; 67 | } 68 | 69 | /** 70 | * A promise that fulfills when the manager is ready. 71 | */ 72 | get ready(): Promise { 73 | return this._readyPromise; 74 | } 75 | 76 | /** 77 | * Create an iterator over the most recent running Tensorboards. 78 | * 79 | * @returns A new iterator over the running tensorboards. 80 | */ 81 | running(): Array { 82 | return this._models; 83 | } 84 | 85 | formatDir(dir: string): string { 86 | const pageRoot = this._statusConfig?.notebook_dir; 87 | 88 | if (dir.includes(',')) { 89 | const dirs = dir.split(','); 90 | return dirs 91 | .map(dir => { 92 | if (dir.includes(':')) { 93 | return `${dir.split(':')![0]}:${this.formatDir(dir.split(':')![1])}`; 94 | } else { 95 | return this.formatDir(dir); 96 | } 97 | }) 98 | .join(','); 99 | } 100 | 101 | if (pageRoot && dir.indexOf(pageRoot) === 0) { 102 | let replaceResult = dir.replace(pageRoot, ''); 103 | if (replaceResult === '') { 104 | replaceResult = '/'; 105 | } 106 | const formatted = `${replaceResult}`.replace(/^\//, ''); 107 | if (!formatted) { 108 | return ''; 109 | } 110 | return formatted; 111 | } 112 | return dir; 113 | } 114 | 115 | /** 116 | * Create a new tensorboard. 117 | * 118 | * @param logdir - The logdir used to create a new tensorboard. 119 | * 120 | * @param options - The options used to connect to the tensorboard. 121 | * 122 | * @returns A promise that resolves with the tensorboard instance. 123 | */ 124 | async startNew( 125 | logdir: string, 126 | refreshInterval: number = DEFAULT_REFRESH_INTERVAL, 127 | enableMultiLog: boolean = DEFAULT_ENABLE_MULTI_LOG, 128 | additionalArgs = '', 129 | options?: Tensorboard.IOptions 130 | ): Promise { 131 | const tensorboard = await Tensorboard.startNew( 132 | logdir, 133 | refreshInterval, 134 | enableMultiLog, 135 | additionalArgs, 136 | this._getOptions(options) 137 | ); 138 | this._onStarted(tensorboard); 139 | return tensorboard; 140 | } 141 | 142 | /** 143 | * Shut down a tensorboard by name. 144 | */ 145 | async shutdown(name: string): Promise { 146 | const index = ArrayExt.findFirstIndex(this._models, value => value.name === name); 147 | if (index === -1) { 148 | return; 149 | } 150 | 151 | this._models.splice(index, 1); 152 | this._runningChanged.emit(this._models.slice()); 153 | 154 | return Tensorboard.shutdown(name, this.serverSettings).then(() => { 155 | const toRemove: Tensorboard.ITensorboard[] = []; 156 | this._tensorboards.forEach(t => { 157 | if (t.name === name) { 158 | t.dispose(); 159 | toRemove.push(t); 160 | } 161 | }); 162 | toRemove.forEach(s => { 163 | this._tensorboards.delete(s); 164 | }); 165 | }); 166 | } 167 | 168 | /** 169 | * Shut down all tensorboards. 170 | * 171 | * @returns A promise that resolves when all of the tensorboards are shut down. 172 | */ 173 | shutdownAll(): Promise { 174 | const models = this._models; 175 | if (models.length > 0) { 176 | this._models = []; 177 | this._runningChanged.emit([]); 178 | } 179 | 180 | return this._refreshRunning().then(() => { 181 | return Promise.all( 182 | models.map(model => { 183 | return Tensorboard.shutdown(model.name, this.serverSettings).then(() => { 184 | const toRemove: Tensorboard.ITensorboard[] = []; 185 | this._tensorboards.forEach(t => { 186 | t.dispose(); 187 | toRemove.push(t); 188 | }); 189 | toRemove.forEach(t => { 190 | this._tensorboards.delete(t); 191 | }); 192 | }); 193 | }) 194 | ).then(() => { 195 | return undefined; 196 | }); 197 | }); 198 | } 199 | 200 | /** 201 | * Force a refresh of the running tensorboards. 202 | * 203 | * @returns A promise that with the list of running tensorboards. 204 | */ 205 | refreshRunning(): Promise { 206 | return this._refreshRunning(); 207 | } 208 | 209 | /** 210 | * Handle a tensorboard terminating. 211 | */ 212 | private _onTerminated(name: string): void { 213 | const index = ArrayExt.findFirstIndex(this._models, value => value.name === name); 214 | if (index !== -1) { 215 | this._models.splice(index, 1); 216 | this._runningChanged.emit(this._models.slice()); 217 | } 218 | } 219 | 220 | /** 221 | * Handle a tensorboard starting. 222 | */ 223 | private _onStarted(tensorboard: Tensorboard.ITensorboard): void { 224 | const name = tensorboard.name; 225 | this._tensorboards.add(tensorboard); 226 | const index = ArrayExt.findFirstIndex(this._models, value => value.name === name); 227 | if (index === -1) { 228 | this._models.push(tensorboard.model); 229 | this._runningChanged.emit(this._models.slice()); 230 | } 231 | tensorboard.terminated.connect(() => { 232 | this._onTerminated(name); 233 | }); 234 | } 235 | 236 | /** 237 | * Refresh the running tensorboards. 238 | */ 239 | private _refreshRunning(): Promise { 240 | return Tensorboard.listRunning(this.serverSettings).then(models => { 241 | this._isReady = true; 242 | if (!JSONExt.deepEqual(models, this._models)) { 243 | const names = models.map(r => r.name); 244 | const toRemove: Tensorboard.ITensorboard[] = []; 245 | this._tensorboards.forEach(t => { 246 | if (names.indexOf(t.name) === -1) { 247 | t.dispose(); 248 | toRemove.push(t); 249 | } 250 | }); 251 | toRemove.forEach(t => { 252 | this._tensorboards.delete(t); 253 | }); 254 | this._models = models.slice(); 255 | this._runningChanged.emit(models); 256 | } 257 | }); 258 | } 259 | 260 | private _getStaticConfig(): Promise { 261 | return Tensorboard.getStaticConfig(this.serverSettings).then(config => { 262 | this._statusConfig = config; 263 | }); 264 | } 265 | 266 | /** 267 | * Get a set of options to pass. 268 | */ 269 | private _getOptions(options: Tensorboard.IOptions = {}): Tensorboard.IOptions { 270 | return { ...options, serverSettings: this.serverSettings }; 271 | } 272 | 273 | private _models: Tensorboard.IModel[] = []; 274 | private _tensorboards = new Set(); 275 | private _isDisposed = false; 276 | private _isReady = false; 277 | private _readyPromise: Promise; 278 | private _refreshTimer = -1; 279 | private _runningChanged = new Signal(this); 280 | private _statusConfig: Tensorboard.StaticConfig | null = null; 281 | } 282 | /** 283 | * The namespace for TensorboardManager statics. 284 | */ 285 | export namespace TensorboardManager { 286 | /** 287 | * The options used to initialize a tensorboard manager. 288 | */ 289 | export interface IOptions { 290 | /** 291 | * The server settings used by the manager. 292 | */ 293 | serverSettings?: ServerConnection.ISettings; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/tensorboard.ts: -------------------------------------------------------------------------------- 1 | import { each, map, toArray } from '@lumino/algorithm'; 2 | import { IDisposable } from '@lumino/disposable'; 3 | import { JSONObject } from '@lumino/coreutils'; 4 | import { URLExt } from '@jupyterlab/coreutils'; 5 | import { Signal, ISignal } from '@lumino/signaling'; 6 | import { ServerConnection } from '@jupyterlab/services'; 7 | 8 | /** 9 | * The url for the tensorboard service. tensorboard 10 | * service provided by jupyter_tensorboard. 11 | * ref: https://github.com/lspvic/jupyter_tensorboard 12 | * Maybe rewrite the jupyter_tensorboard service by myself. 13 | */ 14 | const TENSORBOARD_SERVICE_URL = 'api/tensorboard_pro'; 15 | 16 | const TENSORBOARD_STATIC_CONFIG_URL = 'api/tensorboard_pro_static_config'; 17 | 18 | const TENSORBOARD_URL = 'tensorboard_pro'; 19 | 20 | /** 21 | * The namespace for Tensorboard statics. 22 | */ 23 | export namespace Tensorboard { 24 | /** 25 | * An interface for a tensorboard. 26 | */ 27 | export interface ITensorboard extends IDisposable { 28 | /** 29 | * A signal emitted when the tensorboard is shut down. 30 | */ 31 | terminated: ISignal; 32 | 33 | /** 34 | * The model associated with the tensorboard. 35 | */ 36 | readonly model: IModel; 37 | 38 | /** 39 | * Get the name of the tensorboard. 40 | */ 41 | readonly name: string; 42 | 43 | /** 44 | * The server settings for the tensorboard. 45 | */ 46 | readonly serverSettings: ServerConnection.ISettings; 47 | 48 | /** 49 | * Shut down the tensorboard. 50 | */ 51 | shutdown(): Promise; 52 | } 53 | 54 | /** 55 | * Start a new tensorboard. 56 | * 57 | * @param options - The tensorboard options to use. 58 | * 59 | * @returns A promise that resolves with the tensorboard instance. 60 | */ 61 | export function startNew( 62 | logdir: string, 63 | refreshInterval: number, 64 | enableMultiLog: boolean, 65 | additionalArgs: string, 66 | options?: IOptions 67 | ): Promise { 68 | return DefaultTensorboard.startNew( 69 | logdir, 70 | refreshInterval, 71 | enableMultiLog, 72 | additionalArgs, 73 | options 74 | ); 75 | } 76 | 77 | export function getStaticConfig(settings?: ServerConnection.ISettings): Promise { 78 | return DefaultTensorboard.getStaticConfig(settings); 79 | } 80 | 81 | /** 82 | * List the running tensorboards. 83 | * 84 | * @param settings - The server settings to use. 85 | * 86 | * @returns A promise that resolves with the list of running tensorboard models. 87 | */ 88 | export function listRunning(settings?: ServerConnection.ISettings): Promise { 89 | return DefaultTensorboard.listRunning(settings); 90 | } 91 | 92 | /** 93 | * Shut down a tensorboard by name. 94 | * 95 | * @param name - The name of the target tensorboard. 96 | * 97 | * @param settings - The server settings to use. 98 | * 99 | * @returns A promise that resolves when the tensorboard is shut down. 100 | */ 101 | export function shutdown(name: string, settings?: ServerConnection.ISettings): Promise { 102 | return DefaultTensorboard.shutdown(name, settings); 103 | } 104 | 105 | /** 106 | * Shut down all tensorboard. 107 | * 108 | * @returns A promise that resolves when all of the tensorboards are shut down. 109 | */ 110 | export function shutdownAll(settings?: ServerConnection.ISettings): Promise { 111 | return DefaultTensorboard.shutdownAll(settings); 112 | } 113 | 114 | /** 115 | * Get tensorboard's url 116 | */ 117 | export function getUrl(name: string, settings?: ServerConnection.ISettings): string { 118 | return DefaultTensorboard.getUrl(name, settings); 119 | } 120 | 121 | /** 122 | * The options for intializing a tensorboard object. 123 | */ 124 | export interface IOptions { 125 | /** 126 | * The server settings for the tensorboard. 127 | */ 128 | serverSettings?: ServerConnection.ISettings; 129 | } 130 | 131 | /** 132 | * The server model for a tensorboard. 133 | */ 134 | export interface IModel extends JSONObject { 135 | /** 136 | * The name of the tensorboard. 137 | */ 138 | readonly name: string; 139 | 140 | /** 141 | * The logdir Path of the tensorboard. 142 | */ 143 | readonly logdir: string; 144 | 145 | /** 146 | * The reload interval of the tensorboard. 147 | */ 148 | readonly reload_interval: number; 149 | 150 | /** 151 | * The last reload time of the tensorboard. 152 | */ 153 | readonly reload_time: string; 154 | 155 | /** 156 | * Whether to support multiple log parameters to be passed in, the `logdir_spec` of tensorboard will actually be used internally 157 | */ 158 | readonly enable_multi_log: boolean; 159 | 160 | /** 161 | * additional args to tensorboard 162 | */ 163 | readonly additional_args: string; 164 | } 165 | 166 | export interface StaticConfig extends JSONObject { 167 | /** 168 | * The name of the tensorboard. 169 | */ 170 | readonly notebook_dir: string; 171 | } 172 | 173 | /** 174 | * The interface for a tensorboard manager. 175 | * 176 | * The manager is respoonsible for maintaining the state of running 177 | * tensorboard. 178 | */ 179 | export interface IManager extends IDisposable { 180 | readonly serverSettings: ServerConnection.ISettings; 181 | 182 | runningChanged: ISignal; 183 | 184 | running(): Array; 185 | 186 | startNew( 187 | logdir: string, 188 | refreshInterval: number, 189 | enableMultiLog: boolean, 190 | additionalArgs: string, 191 | options?: IOptions 192 | ): Promise; 193 | 194 | shutdown(name: string): Promise; 195 | 196 | shutdownAll(): Promise; 197 | 198 | refreshRunning(): Promise; 199 | } 200 | } 201 | 202 | export class DefaultTensorboard implements Tensorboard.ITensorboard { 203 | /** 204 | * Construct a new tensorboard. 205 | */ 206 | constructor( 207 | name: string, 208 | logdir: string, 209 | lastReload: string, 210 | reloadInterval: number | undefined, 211 | enableMultiLog: boolean | undefined, 212 | additionalArgs: string | undefined, 213 | options: Tensorboard.IOptions = {} 214 | ) { 215 | this._name = name; 216 | this._logdir = logdir; 217 | this._lastReload = lastReload; 218 | this._reloadInterval = reloadInterval; 219 | this._enableMultiLog = Boolean(enableMultiLog); 220 | this._additionalArgs = additionalArgs || ''; 221 | this.serverSettings = options.serverSettings || ServerConnection.makeSettings(); 222 | this._url = Private.getTensorboardInstanceUrl(this.serverSettings.baseUrl, this._name); 223 | } 224 | 225 | /** 226 | * Get the name of the tensorboard. 227 | */ 228 | get name(): string { 229 | return this._name; 230 | } 231 | 232 | /** 233 | * Get the model for the tensorboard. 234 | */ 235 | get model(): Tensorboard.IModel { 236 | return { 237 | name: this._name, 238 | logdir: this._logdir, 239 | reload_time: this._lastReload, 240 | reload_interval: this._reloadInterval || 0, 241 | enable_multi_log: this._enableMultiLog || false, 242 | additional_args: this._additionalArgs || '' 243 | }; 244 | } 245 | 246 | /** 247 | * A signal emitted when the tensorboard is shut down. 248 | */ 249 | get terminated(): Signal { 250 | return this._terminated; 251 | } 252 | 253 | /** 254 | * Test whether the tensorbaord is disposed. 255 | */ 256 | get isDisposed(): boolean { 257 | return this._isDisposed; 258 | } 259 | 260 | /** 261 | * Dispose of the resources held by the tensorboard. 262 | */ 263 | dispose(): void { 264 | if (this._isDisposed) { 265 | return; 266 | } 267 | 268 | this.terminated.emit(void 0); 269 | this._isDisposed = true; 270 | delete Private.running[this._url]; 271 | Signal.clearData(this); 272 | } 273 | 274 | /** 275 | * The server settings for the tensorboard. 276 | */ 277 | readonly serverSettings: ServerConnection.ISettings; 278 | 279 | /** 280 | * Shut down the tensorboard. 281 | */ 282 | shutdown(): Promise { 283 | const { name, serverSettings } = this; 284 | return DefaultTensorboard.shutdown(name, serverSettings); 285 | } 286 | 287 | private _isDisposed = false; 288 | private _url: string; 289 | private _name: string; 290 | private _logdir: string; 291 | private _lastReload: string; 292 | private _reloadInterval: number | undefined; 293 | private _enableMultiLog: boolean; 294 | private _additionalArgs: string; 295 | private _terminated = new Signal(this); 296 | } 297 | 298 | /** 299 | * The static namespace for `DefaultTensorboard`. 300 | */ 301 | export namespace DefaultTensorboard { 302 | /** 303 | * Start a new tensorboard. 304 | * 305 | * @param options - The tensorboard options to use. 306 | * 307 | * @returns A promise that resolves with the tensorboard instance. 308 | */ 309 | export function startNew( 310 | logdir: string, 311 | refreshInterval: number, 312 | enableMultiLog: boolean, 313 | additionalArgs: string, 314 | options: Tensorboard.IOptions = {} 315 | ): Promise { 316 | const serverSettings = options.serverSettings || ServerConnection.makeSettings(); 317 | const url = Private.getServiceUrl(serverSettings.baseUrl); 318 | // ServerConnection won't automaticy add this header when the body in not none. 319 | const header = new Headers({ 'Content-Type': 'application/json' }); 320 | 321 | const data = JSON.stringify({ 322 | logdir: logdir, 323 | reload_interval: refreshInterval, 324 | enable_multi_log: enableMultiLog, 325 | additional_args: additionalArgs 326 | }); 327 | 328 | const init = { method: 'POST', headers: header, body: data }; 329 | 330 | return ServerConnection.makeRequest(url, init, serverSettings) 331 | .then(response => { 332 | if (response.status !== 200) { 333 | throw new ServerConnection.ResponseError(response); 334 | } 335 | return response.json(); 336 | }) 337 | .then((data: Tensorboard.IModel) => { 338 | const name = data.name; 339 | const logdir = data.logdir; 340 | const lastReload = data.reload_time; 341 | const reloadInterval = data.reload_interval; 342 | const enableMultiLog = data.enable_multi_log; 343 | const additionalArgs = data.additional_args; 344 | return new DefaultTensorboard( 345 | name, 346 | logdir, 347 | lastReload, 348 | reloadInterval, 349 | enableMultiLog, 350 | additionalArgs, 351 | { 352 | ...options, 353 | serverSettings 354 | } 355 | ); 356 | }); 357 | } 358 | 359 | export function getStaticConfig( 360 | settings?: ServerConnection.ISettings 361 | ): Promise { 362 | const statis_config_url = Private.getTensorboardStaticConfigUrl(settings!.baseUrl); 363 | return ServerConnection.makeRequest(statis_config_url, {}, settings!) 364 | .then(response => { 365 | if (response.status !== 200) { 366 | throw new ServerConnection.ResponseError(response); 367 | } 368 | return response.json(); 369 | }) 370 | .then((data: Tensorboard.StaticConfig) => { 371 | return data; 372 | }); 373 | } 374 | 375 | /** 376 | * List the running tensorboards. 377 | * 378 | * @param settings - The server settings to use. 379 | * 380 | * @returns A promise that resolves with the list of running tensorboard models. 381 | */ 382 | export function listRunning( 383 | settings?: ServerConnection.ISettings 384 | ): Promise { 385 | settings = settings || ServerConnection.makeSettings(); 386 | const service_url = Private.getServiceUrl(settings.baseUrl); 387 | const instance_url = Private.getTensorboardInstanceRootUrl(settings.baseUrl); 388 | return ServerConnection.makeRequest(service_url, {}, settings) 389 | .then(response => { 390 | if (response.status !== 200) { 391 | throw new ServerConnection.ResponseError(response); 392 | } 393 | return response.json(); 394 | }) 395 | .then((data: Tensorboard.IModel[]) => { 396 | if (!Array.isArray(data)) { 397 | throw new Error('Invalid tensorboard data'); 398 | } 399 | // Update the local data store. 400 | const urls = toArray( 401 | map(data, item => { 402 | return URLExt.join(instance_url, item.name); 403 | }) 404 | ); 405 | each(Object.keys(Private.running), runningUrl => { 406 | if (urls.indexOf(runningUrl) === -1) { 407 | const tensorboard = Private.running[runningUrl]; 408 | tensorboard.dispose(); 409 | } 410 | }); 411 | return data; 412 | }); 413 | } 414 | 415 | /** 416 | * Shut down a tensorboard by name. 417 | * 418 | * @param name - Then name of the target tensorboard. 419 | * 420 | * @param settings - The server settings to use. 421 | * 422 | * @returns A promise that resolves when the tensorboard is shut down. 423 | */ 424 | export function shutdown(name: string, settings?: ServerConnection.ISettings): Promise { 425 | settings = settings || ServerConnection.makeSettings(); 426 | const url = Private.getTensorboardUrl(settings.baseUrl, name); 427 | const init = { method: 'DELETE' }; 428 | return ServerConnection.makeRequest(url, init, settings).then(response => { 429 | if (response.status === 404) { 430 | return response.json().then(data => { 431 | Private.killTensorboard(url); 432 | }); 433 | } 434 | if (response.status !== 204) { 435 | throw new ServerConnection.ResponseError(response); 436 | } 437 | Private.killTensorboard(url); 438 | }); 439 | } 440 | 441 | /** 442 | * Shut down all tensorboards. 443 | * 444 | * @param settings - The server settings to use. 445 | * 446 | * @returns A promise that resolves when all the tensorboards are shut down. 447 | */ 448 | export function shutdownAll(settings?: ServerConnection.ISettings): Promise { 449 | settings = settings || ServerConnection.makeSettings(); 450 | return listRunning(settings).then(running => { 451 | each(running, s => { 452 | shutdown(s.name, settings); 453 | }); 454 | }); 455 | } 456 | 457 | /** 458 | * According tensorboard's name to get tensorboard's url. 459 | */ 460 | export function getUrl(name: string, settings?: ServerConnection.ISettings): string { 461 | settings = settings || ServerConnection.makeSettings(); 462 | return Private.getTensorboardInstanceUrl(settings.baseUrl, name); 463 | } 464 | } 465 | 466 | /** 467 | * A namespace for private data. 468 | */ 469 | namespace Private { 470 | /** 471 | * A mapping of running tensorboards by url. 472 | */ 473 | export const running: { [key: string]: DefaultTensorboard } = Object.create(null); 474 | 475 | /** 476 | * Get the url for a tensorboard. 477 | */ 478 | export function getTensorboardUrl(baseUrl: string, name: string): string { 479 | return URLExt.join(baseUrl, TENSORBOARD_SERVICE_URL, name); 480 | } 481 | 482 | /** 483 | * Get the url for a tensorboard. 484 | */ 485 | export function getTensorboardStaticConfigUrl(baseUrl: string): string { 486 | return URLExt.join(baseUrl, TENSORBOARD_STATIC_CONFIG_URL); 487 | } 488 | 489 | /** 490 | * Get the base url. 491 | */ 492 | export function getServiceUrl(baseUrl: string): string { 493 | return URLExt.join(baseUrl, TENSORBOARD_SERVICE_URL); 494 | } 495 | 496 | /** 497 | * Kill tensorboard by url. 498 | */ 499 | export function killTensorboard(url: string): void { 500 | // Update the local data store. 501 | if (Private.running[url]) { 502 | const tensorboard = Private.running[url]; 503 | tensorboard.dispose(); 504 | } 505 | } 506 | 507 | export function getTensorboardInstanceRootUrl(baseUrl: string): string { 508 | return URLExt.join(baseUrl, TENSORBOARD_URL); 509 | } 510 | 511 | export function getTensorboardInstanceUrl(baseUrl: string, name: string): string { 512 | return URLExt.join(baseUrl, TENSORBOARD_URL, name); 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/biz/tab.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Button } from '@blueprintjs/core/lib/esm/components/button/buttons'; 3 | import { MenuItem } from '@blueprintjs/core/lib/esm/components/menu/menuItem'; 4 | import { InputGroup } from '@blueprintjs/core/lib/esm/components/forms/inputGroup'; 5 | import { Switch } from '@blueprintjs/core/lib/esm/components/forms/controls'; 6 | import { Tag } from '@blueprintjs/core/lib/esm/components/tag/tag'; 7 | import classNames from 'classnames'; 8 | import { Select } from '@blueprintjs/select'; 9 | import { showDialog, Dialog } from '@jupyterlab/apputils'; 10 | import { Loading } from './loading'; 11 | import { Tensorboard } from '../tensorboard'; 12 | import { TensorboardManager } from '../manager'; 13 | import { DEFAULT_REFRESH_INTERVAL } from '../consts'; 14 | import { copyToClipboard } from '../utils/copy'; 15 | 16 | export interface TensorboardCreatorProps { 17 | disable: boolean; 18 | getCWD: () => string; 19 | openDoc: () => void; 20 | startTensorBoard: ( 21 | logDir: string, 22 | reloadInterval: number, 23 | enableMultiLog: boolean, 24 | additionalArgs: string 25 | ) => void; 26 | } 27 | 28 | const TensorboardCreator = (props: TensorboardCreatorProps): JSX.Element => { 29 | const [logDir, setLogDir] = useState(props.getCWD()); 30 | const [reloadInterval, setReloadInterval] = useState(DEFAULT_REFRESH_INTERVAL); 31 | const [additionalArgs, setAdditionalArgs] = useState(''); 32 | const [enableReloadInterval, setEnableReloadInterval] = useState(false); 33 | const [enableMultiLog, setEnableMultiLog] = useState(false); 34 | 35 | return ( 36 |
37 |
38 |
39 | 42 | { 48 | setLogDir(e.target.value); 49 | if (e.target.value.includes(',')) { 50 | setEnableMultiLog(true); 51 | } 52 | }} 53 | /> 54 |
55 |
56 | { 60 | setEnableMultiLog(!enableMultiLog); 61 | }} 62 | labelElement={ 63 | 64 | Multi LogDir 65 | 66 | } 67 | /> 68 | { 72 | setEnableReloadInterval(!enableReloadInterval); 73 | }} 74 | labelElement={ 75 | 76 | Reload Interval 77 | 78 | } 79 | /> 80 | {enableReloadInterval && ( 81 | { 87 | setReloadInterval(Number(e.target.value)); 88 | }} 89 | rightElement={s} 90 | type="number" 91 | /> 92 | )} 93 |
94 |
95 | { 103 | setAdditionalArgs(e.target.value); 104 | }} 105 | /> 106 |
107 | 123 |
124 |
125 | 135 |
136 | ); 137 | }; 138 | 139 | export interface TensorboardTabReactProps { 140 | setWidgetName?: (name: string) => void; 141 | createdModelName?: string; 142 | tensorboardManager: TensorboardManager; 143 | closeWidget: () => void; 144 | getCWD: () => string; 145 | openTensorBoard: (modelName: string, copy: boolean) => void; 146 | openDoc: () => void; 147 | update: () => void; 148 | updateCurrentModel: (model: Tensorboard.IModel | null) => void; 149 | startNew: ( 150 | logdir: string, 151 | refreshInterval: number, 152 | enableMultiLog: boolean, 153 | additionalArgs: string, 154 | options?: Tensorboard.IOptions 155 | ) => Promise; 156 | } 157 | 158 | const ModelSelector = Select.ofType(); 159 | 160 | const useRefState = (initValue: T): [T, { current: T }, (value: T) => void] => { 161 | const [value, setValue] = useState(initValue); 162 | const valueRef = useRef(value); 163 | const updateValue = (value: T) => { 164 | setValue(value); 165 | valueRef.current = value; 166 | }; 167 | return [value, valueRef, updateValue]; 168 | }; 169 | 170 | export const TensorboardTabReact = (props: TensorboardTabReactProps): JSX.Element => { 171 | const [ready, readyRef, updateReady] = useRefState(false); 172 | 173 | const [createPending, createPendingRef, updateCreatePending] = useRefState(false); 174 | const [reloadPending, reloadPendingRef, updateReloadPending] = useRefState(false); 175 | 176 | const [showNewRow, setShowNewRow] = useState(false); 177 | const [showListStatus, setShowListStatus] = useState(false); 178 | 179 | const [currentTensorBoard, setCurrentTensorBoard] = useState(null); 180 | const currentTensorBoardRef = useRef(currentTensorBoard); 181 | const updateCurrentTensorBoard = (model: Tensorboard.IModel | null) => { 182 | props.updateCurrentModel(model); 183 | setCurrentTensorBoard(model); 184 | currentTensorBoardRef.current = model; 185 | }; 186 | 187 | const [runningTensorBoards, setRunningTensorBoards] = useState([]); 188 | 189 | // currently inactive 190 | const [notActiveError, setNotActiveError] = useState(false); 191 | 192 | const currentLoading = reloadPending || createPending; 193 | 194 | const refreshRunning = () => { 195 | if (createPendingRef.current || reloadPendingRef.current) { 196 | return; 197 | } 198 | props.tensorboardManager.refreshRunning().then(() => { 199 | const runningTensorboards = [...props.tensorboardManager.running()]; 200 | 201 | // hint: Using runningTensorboards directly may cause setState to fail to respond 202 | const modelList = []; 203 | for (const model of runningTensorboards) { 204 | modelList.push(model); 205 | } 206 | setRunningTensorBoards(modelList); 207 | 208 | if (readyRef.current) { 209 | // 如果不是第一次了 210 | if (currentTensorBoardRef.current) { 211 | if (!modelList.find(model => model.name === currentTensorBoardRef.current!.name)) { 212 | setNotActiveError(true); 213 | } 214 | } else { 215 | // do nothing 216 | // Maybe not at the beginning, the user planned to create a new one later, and then did not create a new one. At this time, he found that there was 217 | } 218 | 219 | return; 220 | } 221 | 222 | const model = props.createdModelName 223 | ? modelList.find(model => model.name === props.createdModelName) 224 | : null; 225 | 226 | if (model) { 227 | // if createdModelName exist,maybe from Sidebar kernels tab 228 | updateCurrentTensorBoard(model); 229 | setShowListStatus(true); 230 | if (props.setWidgetName) { 231 | props.setWidgetName(`${model.name}:` + props.tensorboardManager.formatDir(model.logdir)); 232 | } 233 | } else { 234 | setShowNewRow(true); 235 | setShowListStatus(false); 236 | } 237 | updateReady(true); 238 | }); 239 | }; 240 | 241 | const startTensorBoard = ( 242 | logDir: string, 243 | reloadInterval: number, 244 | enableMultiLog: boolean, 245 | additionalArgs: string 246 | ) => { 247 | if (Number.isNaN(reloadInterval) || reloadInterval < 0) { 248 | return showDialog({ 249 | title: 'Param Check Failed', 250 | body: 'reloadInterval should > 0 when enabled', 251 | buttons: [Dialog.okButton()] 252 | }); 253 | } 254 | updateCreatePending(true); 255 | const currentName = currentTensorBoard?.name; 256 | props 257 | .startNew(logDir, reloadInterval, enableMultiLog, additionalArgs) 258 | .then(tb => { 259 | if (currentName === tb.model.name) { 260 | showDialog({ 261 | body: 'Existing tensorBoard for the logDir will be reused directly', 262 | buttons: [Dialog.okButton()] 263 | }); 264 | } 265 | if (props.setWidgetName) { 266 | props.setWidgetName( 267 | `${tb.model.name}:` + props.tensorboardManager.formatDir(tb.model.logdir) 268 | ); 269 | } 270 | updateCurrentTensorBoard(tb.model); 271 | updateCreatePending(false); 272 | refreshRunning(); 273 | 274 | setShowListStatus(true); 275 | setShowNewRow(false); 276 | }) 277 | .catch(e => { 278 | updateCreatePending(false); 279 | 280 | const getMessage = () => 281 | e.response.json().then((json: any) => { 282 | return json.message as string; 283 | }); 284 | const defaultMessage = 'Start TensorBoard internal error'; 285 | 286 | getMessage() 287 | .then((msg: string) => { 288 | showDialog({ 289 | body: msg || defaultMessage, 290 | buttons: [Dialog.okButton()] 291 | }); 292 | }) 293 | .catch(() => { 294 | showDialog({ 295 | body: defaultMessage, 296 | buttons: [Dialog.okButton()] 297 | }); 298 | }); 299 | }); 300 | }; 301 | 302 | // hint: Because we are simulating reload here, the tab component cannot listen to runningChanged 303 | const reloadTensorBoard = () => { 304 | // There was no reload in the world, so I had to stop and restart to simulate reload 305 | if (!currentTensorBoard) { 306 | return; 307 | } 308 | updateReloadPending(true); 309 | updateCurrentTensorBoard(null); 310 | const reloadInterval = 311 | typeof currentTensorBoard.reload_interval === 'number' 312 | ? currentTensorBoard.reload_interval 313 | : DEFAULT_REFRESH_INTERVAL; 314 | const currentLogDir = currentTensorBoard.logdir; 315 | const enableMultiLog = currentTensorBoard.enable_multi_log; 316 | const additionalArgs = currentTensorBoard.additional_args; 317 | 318 | const errorCallback = (e: any) => { 319 | showDialog({ 320 | title: 'TensorBoard Reload Error', 321 | body: 'The panel has been closed, you can reopen to create new', 322 | buttons: [Dialog.okButton()] 323 | }); 324 | props.closeWidget(); 325 | }; 326 | 327 | try { 328 | props.tensorboardManager 329 | .shutdown(currentTensorBoard.name) 330 | .then(res => { 331 | props.tensorboardManager 332 | .startNew(currentLogDir, reloadInterval, enableMultiLog, additionalArgs) 333 | .then(res => { 334 | refreshRunning(); 335 | updateReloadPending(false); 336 | updateCurrentTensorBoard(currentTensorBoard); 337 | }) 338 | .catch(e => { 339 | errorCallback(e); 340 | }); 341 | }) 342 | .catch(e => { 343 | errorCallback(e); 344 | }); 345 | } catch (e) { 346 | errorCallback(e); 347 | } 348 | }; 349 | 350 | const destroyTensorBoard = () => { 351 | if (!currentTensorBoard) { 352 | return; 353 | } 354 | props.tensorboardManager.shutdown(currentTensorBoard.name).then(res => { 355 | props.closeWidget(); 356 | }); 357 | }; 358 | 359 | const copyTensorBoard = () => { 360 | if (!currentTensorBoard) { 361 | return; 362 | } 363 | props.openTensorBoard(currentTensorBoard.name, true); 364 | }; 365 | 366 | const getShowName = (model: Tensorboard.IModel) => { 367 | const formattedDir = props.tensorboardManager.formatDir(model.logdir); 368 | return `${model.name} - ${formattedDir}`; 369 | }; 370 | 371 | const changeModel = (model: Tensorboard.IModel) => { 372 | if (currentTensorBoard?.name === model.name) { 373 | return; 374 | } 375 | if (props.setWidgetName) { 376 | props.setWidgetName(`${model.name}:` + props.tensorboardManager.formatDir(model.logdir)); 377 | } 378 | setCurrentTensorBoard(model); 379 | }; 380 | 381 | const toggleNewRow = () => { 382 | setShowNewRow(!showNewRow); 383 | }; 384 | 385 | const openInNewTab = () => { 386 | if (!currentTensorBoard) { 387 | return; 388 | } 389 | window.open(Tensorboard.getUrl(currentTensorBoard.name)); 390 | }; 391 | 392 | useEffect(() => { 393 | refreshRunning(); 394 | const refreshIntervalId = setInterval(refreshRunning, 30 * 1000); 395 | return () => { 396 | clearInterval(refreshIntervalId); 397 | }; 398 | }, []); 399 | 400 | const getBlankContent = () => { 401 | if (!ready) { 402 | return ; 403 | } else if (createPending) { 404 | return ( 405 | 409 | ); 410 | } else if (reloadPending) { 411 | return ( 412 | 416 | ); 417 | } else { 418 | return ( 419 |
420 |

421 | No instance for current directory yet, please create a new TensorBoard 422 |

423 |

424 | 425 | If the selected log directory has too much content, tensorboard initialization may 426 | take a long time, during which jupyter will get stuck 427 | 428 |

429 |
430 | ); 431 | } 432 | }; 433 | 434 | return ( 435 |
436 | {ready && ( 437 |
442 |
443 |
444 |
445 |
509 |
510 |
511 |
512 | 521 | 531 | 540 | 550 |
551 |
552 |
553 | 559 |
560 |
561 |
562 | )} 563 |
564 | {currentTensorBoard && ( 565 |