├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── docs.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .pylintrc ├── .vscode └── settings.json ├── LICENSE ├── MANIFEST.in ├── README.md ├── _validate_init.py ├── dash_lumino_components └── __init__.py ├── examples ├── README.md ├── multiplage.py ├── multiplots.gif ├── multiplots.py └── requirements.txt ├── jsdoc.conf.json ├── package-lock.json ├── package.json ├── pytest.ini ├── requirements.txt ├── setup.py ├── src ├── .gitignore └── lib │ ├── component.js │ ├── components │ ├── BoxPanel.react.js │ ├── Command.react.js │ ├── DockPanel.react.js │ ├── Menu.react.js │ ├── MenuBar.react.js │ ├── Panel.react.js │ ├── Separator.react.js │ ├── SplitPanel.react.js │ ├── TabPanel.react.js │ ├── Widget.react.js │ └── css │ │ └── defaults.css │ ├── index.js │ └── registry.js ├── tests ├── __init__.py ├── requirements.txt └── test_usage.py ├── usage.py ├── webpack.config.js └── webpack.serve.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "env": { 4 | "production": { 5 | "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel"] 6 | }, 7 | "development": { 8 | "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel"] 9 | }, 10 | "test": { 11 | "plugins": ["@babel/plugin-proposal-object-rest-spread", "styled-jsx/babel-test"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.css 2 | registerServiceWorker.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "generators": true, 15 | "modules": true, 16 | "templateStrings": true, 17 | "jsx": true 18 | } 19 | }, 20 | "env": { 21 | "browser": true, 22 | "es6": true, 23 | "jasmine": true, 24 | "jest": true, 25 | "node": true 26 | }, 27 | "globals": { 28 | "jest": true 29 | }, 30 | "plugins": [ 31 | "react", 32 | "import" 33 | ], 34 | "rules": { 35 | "accessor-pairs": ["error"], 36 | "block-scoped-var": ["error"], 37 | "consistent-return": ["error"], 38 | "curly": ["error", "all"], 39 | "default-case": ["error"], 40 | "dot-location": ["off"], 41 | "dot-notation": ["error"], 42 | "eqeqeq": ["error"], 43 | "guard-for-in": ["off"], 44 | "import/named": ["off"], 45 | "import/no-duplicates": ["error"], 46 | "import/no-named-as-default": ["error"], 47 | "new-cap": ["error"], 48 | "no-alert": [1], 49 | "no-caller": ["error"], 50 | "no-case-declarations": ["error"], 51 | "no-console": ["off"], 52 | "no-div-regex": ["error"], 53 | "no-dupe-keys": ["error"], 54 | "no-else-return": ["error"], 55 | "no-empty-pattern": ["error"], 56 | "no-eq-null": ["error"], 57 | "no-eval": ["error"], 58 | "no-extend-native": ["error"], 59 | "no-extra-bind": ["error"], 60 | "no-extra-boolean-cast": ["error"], 61 | "no-inline-comments": ["error"], 62 | "no-implicit-coercion": ["error"], 63 | "no-implied-eval": ["error"], 64 | "no-inner-declarations": ["off"], 65 | "no-invalid-this": ["error"], 66 | "no-iterator": ["error"], 67 | "no-labels": ["error"], 68 | "no-lone-blocks": ["error"], 69 | "no-loop-func": ["error"], 70 | "no-multi-str": ["error"], 71 | "no-native-reassign": ["error"], 72 | "no-new": ["error"], 73 | "no-new-func": ["error"], 74 | "no-new-wrappers": ["error"], 75 | "no-param-reassign": ["error"], 76 | "no-process-env": ["warn"], 77 | "no-proto": ["error"], 78 | "no-redeclare": ["error"], 79 | "no-return-assign": ["error"], 80 | "no-script-url": ["error"], 81 | "no-self-compare": ["error"], 82 | "no-sequences": ["error"], 83 | "no-shadow": ["off"], 84 | "no-throw-literal": ["error"], 85 | "no-undefined": ["error"], 86 | "no-unused-expressions": ["error"], 87 | "no-use-before-define": ["error", "nofunc"], 88 | "no-useless-call": ["error"], 89 | "no-useless-concat": ["error"], 90 | "no-with": ["error"], 91 | "prefer-const": ["error"], 92 | "radix": ["error"], 93 | "react/jsx-no-duplicate-props": ["error"], 94 | "react/jsx-no-undef": ["error"], 95 | "react/jsx-uses-react": ["error"], 96 | "react/jsx-uses-vars": ["error"], 97 | "react/no-did-update-set-state": ["error"], 98 | "react/no-direct-mutation-state": ["error"], 99 | "react/no-is-mounted": ["error"], 100 | "react/no-unknown-property": ["error"], 101 | "react/prefer-es6-class": ["error", "always"], 102 | "react/prop-types": "error", 103 | "valid-jsdoc": ["off"], 104 | "yoda": ["error"], 105 | "spaced-comment": ["error", "always", { 106 | "block": { 107 | "exceptions": ["*"] 108 | } 109 | }], 110 | "no-unused-vars": ["error", { 111 | "args": "after-used", 112 | "argsIgnorePattern": "^_", 113 | "caughtErrorsIgnorePattern": "^e$" 114 | }], 115 | "no-magic-numbers": ["error", { 116 | "ignoreArrayIndexes": true, 117 | "ignore": [-1, 0, 1, 2, 3, 100, 10, 0.5] 118 | }], 119 | "no-underscore-dangle": ["off"] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '18.x' 19 | 20 | - name: Npm install 21 | run: npm install --force 22 | 23 | - name: Npm build docs 24 | run: npm run doc 25 | 26 | - name: deploy 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./docs 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | if: ${{ startsWith(github.event.head_commit.message, 'release') }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - id: get-version 15 | run: echo "::set-output name=version::$(echo ${{ github.head_ref }} | sed 's|prerelease/||')" 16 | - name: Use Node.js 18 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 18.x 20 | - name: Install dependencies 21 | run: npm install --force 22 | - name: Set up Python 3.11 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: 3.11 26 | - name: Install Python dependencies 27 | run: python -m pip install dash[dev] flit invoke semver termcolor 28 | - name: Build the components 29 | run: | 30 | npm run build 31 | python setup.py sdist 32 | - uses: JS-DevTools/npm-publish@v1 33 | with: 34 | token: ${{ secrets.NPM_TOKEN }} 35 | - name: Publish to PyPI 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: ${{ secrets.FLIT_USERNAME }} 39 | password: ${{ secrets.FLIT_PASSWORD }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VisualStudioCode template 3 | .vscode/* 4 | !.vscode/settings.json 5 | !.vscode/tasks.json 6 | !.vscode/launch.json 7 | !.vscode/extensions.json 8 | ### JetBrains template 9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 11 | 12 | # User-specific stuff 13 | .idea/**/workspace.xml 14 | .idea/**/tasks.xml 15 | .idea/**/usage.statistics.xml 16 | .idea/**/dictionaries 17 | .idea/**/shelf 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | ### Node template 70 | # Logs 71 | logs 72 | *.log 73 | npm-debug.log* 74 | yarn-debug.log* 75 | yarn-error.log* 76 | 77 | # Runtime data 78 | pids 79 | *.pid 80 | *.seed 81 | *.pid.lock 82 | 83 | # Directory for instrumented libs generated by jscoverage/JSCover 84 | lib-cov 85 | 86 | # Coverage directory used by tools like istanbul 87 | coverage 88 | 89 | # nyc test coverage 90 | .nyc_output 91 | 92 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 93 | .grunt 94 | 95 | # Bower dependency directory (https://bower.io/) 96 | bower_components 97 | 98 | # node-waf configuration 99 | .lock-wscript 100 | 101 | # Compiled binary addons (https://nodejs.org/api/addons.html) 102 | build/Release 103 | 104 | # Dependency directories 105 | node_modules/ 106 | jspm_packages/ 107 | 108 | # TypeScript v1 declaration files 109 | typings/ 110 | 111 | # Optional npm cache directory 112 | .npm 113 | 114 | # Optional eslint cache 115 | .eslintcache 116 | 117 | # Optional REPL history 118 | .node_repl_history 119 | 120 | # Output of 'npm pack' 121 | *.tgz 122 | 123 | # Yarn Integrity file 124 | .yarn-integrity 125 | 126 | # dotenv environment variables file 127 | .env 128 | 129 | # parcel-bundler cache (https://parceljs.org/) 130 | .cache 131 | 132 | # next.js build output 133 | .next 134 | 135 | # nuxt.js build output 136 | .nuxt 137 | 138 | # vuepress build output 139 | .vuepress/dist 140 | 141 | # Serverless directories 142 | .serverless 143 | ### Python template 144 | # Byte-compiled / optimized / DLL files 145 | __pycache__/ 146 | *.py[cod] 147 | *$py.class 148 | 149 | # C extensions 150 | *.so 151 | 152 | # Distribution / packaging 153 | .Python 154 | build/ 155 | develop-eggs/ 156 | dist/ 157 | downloads/ 158 | eggs/ 159 | .eggs/ 160 | lib64/ 161 | parts/ 162 | sdist/ 163 | var/ 164 | wheels/ 165 | *.egg-info/ 166 | .installed.cfg 167 | *.egg 168 | MANIFEST 169 | 170 | # PyInstaller 171 | # Usually these files are written by a python script from a template 172 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 173 | *.manifest 174 | *.spec 175 | 176 | # Installer logs 177 | pip-log.txt 178 | pip-delete-this-directory.txt 179 | 180 | # Unit test / coverage reports 181 | htmlcov/ 182 | .tox/ 183 | .coverage 184 | .coverage.* 185 | nosetests.xml 186 | coverage.xml 187 | *.cover 188 | .hypothesis/ 189 | .pytest_cache/ 190 | 191 | # Translations 192 | *.mo 193 | *.pot 194 | 195 | # Django stuff: 196 | local_settings.py 197 | db.sqlite3 198 | 199 | # Flask stuff: 200 | instance/ 201 | .webassets-cache 202 | 203 | # Scrapy stuff: 204 | .scrapy 205 | 206 | # Documentation 207 | docs 208 | 209 | # PyBuilder 210 | target/ 211 | 212 | # Jupyter Notebook 213 | .ipynb_checkpoints 214 | 215 | # pyenv 216 | .python-version 217 | 218 | # celery beat schedule file 219 | celerybeat-schedule 220 | 221 | # SageMath parsed files 222 | *.sage.py 223 | 224 | # Environments 225 | .venv 226 | env/ 227 | venv/ 228 | ENV/ 229 | env.bak/ 230 | venv.bak/ 231 | 232 | # Spyder project settings 233 | .spyderproject 234 | .spyproject 235 | 236 | # Rope project settings 237 | .ropeproject 238 | 239 | # mkdocs documentation 240 | /site 241 | 242 | # mypy 243 | .mypy_cache/ 244 | ### SublimeText template 245 | # Cache files for Sublime Text 246 | *.tmlanguage.cache 247 | *.tmPreferences.cache 248 | *.stTheme.cache 249 | 250 | # Workspace files are user-specific 251 | *.sublime-workspace 252 | 253 | # Project files should be checked into the repository, unless a significant 254 | # proportion of contributors will probably not be using Sublime Text 255 | # *.sublime-project 256 | 257 | # SFTP configuration file 258 | sftp-config.json 259 | 260 | # Package control specific files 261 | Package Control.last-run 262 | Package Control.ca-list 263 | Package Control.ca-bundle 264 | Package Control.system-ca-bundle 265 | Package Control.cache/ 266 | Package Control.ca-certs/ 267 | Package Control.merged-ca-bundle 268 | Package Control.user-ca-bundle 269 | oscrypto-ca-bundle.crt 270 | bh_unicode_properties.cache 271 | 272 | # Sublime-github package stores a github token in this file 273 | # https://packagecontrol.io/packages/sublime-github 274 | GitHub.sublime-settings 275 | 276 | 277 | # autogenerated by npm build process 278 | dash_lumino_components/ 279 | deps/ 280 | inst/ 281 | man/ 282 | R/ 283 | env/ 284 | Project.toml 285 | NAMESPACE 286 | DESCRIPTION 287 | .Rbuildignore 288 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Development folders and files 19 | public 20 | src 21 | scripts 22 | config 23 | .travis.yml 24 | CHANGELOG.md 25 | README.md 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "bracketSpacing": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | raw-checker-failed, 68 | bad-inline-option, 69 | locally-disabled, 70 | locally-enabled, 71 | file-ignored, 72 | suppressed-message, 73 | useless-suppression, 74 | deprecated-pragma, 75 | apply-builtin, 76 | basestring-builtin, 77 | buffer-builtin, 78 | cmp-builtin, 79 | coerce-builtin, 80 | execfile-builtin, 81 | file-builtin, 82 | long-builtin, 83 | raw_input-builtin, 84 | reduce-builtin, 85 | standarderror-builtin, 86 | unicode-builtin, 87 | xrange-builtin, 88 | coerce-method, 89 | delslice-method, 90 | getslice-method, 91 | setslice-method, 92 | no-absolute-import, 93 | old-division, 94 | dict-iter-method, 95 | dict-view-method, 96 | next-method-called, 97 | metaclass-assignment, 98 | indexing-exception, 99 | raising-string, 100 | reload-builtin, 101 | oct-method, 102 | hex-method, 103 | nonzero-method, 104 | cmp-method, 105 | input-builtin, 106 | round-builtin, 107 | intern-builtin, 108 | unichr-builtin, 109 | map-builtin-not-iterating, 110 | zip-builtin-not-iterating, 111 | range-builtin-not-iterating, 112 | filter-builtin-not-iterating, 113 | using-cmp-argument, 114 | eq-without-hash, 115 | div-method, 116 | idiv-method, 117 | rdiv-method, 118 | exception-message-attribute, 119 | invalid-str-codec, 120 | sys-max-int, 121 | bad-python3-import, 122 | deprecated-string-function, 123 | deprecated-str-translate-call, 124 | deprecated-itertools-function, 125 | deprecated-types-field, 126 | next-method-defined, 127 | dict-items-not-iterating, 128 | dict-keys-not-iterating, 129 | dict-values-not-iterating, 130 | no-member, 131 | missing-docstring, 132 | invalid-name, 133 | redefined-builtin, 134 | wrong-import-order, 135 | too-many-arguments, 136 | too-many-locals, 137 | consider-using-enumerate, 138 | len-as-condition, 139 | too-many-branches, 140 | too-many-statements, 141 | blacklisted-name, 142 | line-too-long, 143 | bare-except, 144 | duplicate-code, 145 | too-many-function-args, 146 | attribute-defined-outside-init, 147 | broad-except 148 | 149 | # Enable the message, report, category or checker with the given id(s). You can 150 | # either give multiple identifier separated by comma (,) or put this option 151 | # multiple time (only on the command line, not in the configuration file where 152 | # it should appear only once). See also the "--disable" option for examples. 153 | enable=c-extension-no-member 154 | 155 | 156 | [REPORTS] 157 | 158 | # Python expression which should return a note less than 10 (10 is the highest 159 | # note). You have access to the variables errors warning, statement which 160 | # respectively contain the number of errors / warnings messages and the total 161 | # number of statements analyzed. This is used by the global evaluation report 162 | # (RP0004). 163 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 164 | 165 | # Template used to display messages. This is a python new-style format string 166 | # used to format the message information. See doc for all details 167 | #msg-template= 168 | 169 | # Set the output format. Available formats are text, parseable, colorized, json 170 | # and msvs (visual studio).You can also give a reporter class, eg 171 | # mypackage.mymodule.MyReporterClass. 172 | output-format=text 173 | 174 | # Tells whether to display a full report or only the messages 175 | reports=no 176 | 177 | # Activate the evaluation score. 178 | score=yes 179 | 180 | 181 | [REFACTORING] 182 | 183 | # Maximum number of nested blocks for function / method body 184 | max-nested-blocks=5 185 | 186 | # Complete name of functions that never returns. When checking for 187 | # inconsistent-return-statements if a never returning function is called then 188 | # it will be considered as an explicit return statement and no message will be 189 | # printed. 190 | never-returning-functions=optparse.Values,sys.exit 191 | 192 | 193 | [BASIC] 194 | 195 | # Naming style matching correct argument names 196 | argument-naming-style=snake_case 197 | 198 | # Regular expression matching correct argument names. Overrides argument- 199 | # naming-style 200 | #argument-rgx= 201 | 202 | # Naming style matching correct attribute names 203 | attr-naming-style=snake_case 204 | 205 | # Regular expression matching correct attribute names. Overrides attr-naming- 206 | # style 207 | #attr-rgx= 208 | 209 | # Bad variable names which should always be refused, separated by a comma 210 | bad-names=foo, 211 | bar, 212 | baz, 213 | toto, 214 | tutu, 215 | tata 216 | 217 | # Naming style matching correct class attribute names 218 | class-attribute-naming-style=any 219 | 220 | # Regular expression matching correct class attribute names. Overrides class- 221 | # attribute-naming-style 222 | #class-attribute-rgx= 223 | 224 | # Naming style matching correct class names 225 | class-naming-style=PascalCase 226 | 227 | # Regular expression matching correct class names. Overrides class-naming-style 228 | #class-rgx= 229 | 230 | # Naming style matching correct constant names 231 | const-naming-style=UPPER_CASE 232 | 233 | # Regular expression matching correct constant names. Overrides const-naming- 234 | # style 235 | #const-rgx= 236 | 237 | # Minimum line length for functions/classes that require docstrings, shorter 238 | # ones are exempt. 239 | docstring-min-length=-1 240 | 241 | # Naming style matching correct function names 242 | function-naming-style=snake_case 243 | 244 | # Regular expression matching correct function names. Overrides function- 245 | # naming-style 246 | #function-rgx= 247 | 248 | # Good variable names which should always be accepted, separated by a comma 249 | good-names=i, 250 | j, 251 | k, 252 | ex, 253 | Run, 254 | _ 255 | 256 | # Include a hint for the correct naming format with invalid-name 257 | include-naming-hint=no 258 | 259 | # Naming style matching correct inline iteration names 260 | inlinevar-naming-style=any 261 | 262 | # Regular expression matching correct inline iteration names. Overrides 263 | # inlinevar-naming-style 264 | #inlinevar-rgx= 265 | 266 | # Naming style matching correct method names 267 | method-naming-style=snake_case 268 | 269 | # Regular expression matching correct method names. Overrides method-naming- 270 | # style 271 | #method-rgx= 272 | 273 | # Naming style matching correct module names 274 | module-naming-style=snake_case 275 | 276 | # Regular expression matching correct module names. Overrides module-naming- 277 | # style 278 | #module-rgx= 279 | 280 | # Colon-delimited sets of names that determine each other's naming style when 281 | # the name regexes allow several styles. 282 | name-group= 283 | 284 | # Regular expression which should only match function or class names that do 285 | # not require a docstring. 286 | no-docstring-rgx=^_ 287 | 288 | # List of decorators that produce properties, such as abc.abstractproperty. Add 289 | # to this list to register other decorators that produce valid properties. 290 | property-classes=abc.abstractproperty 291 | 292 | # Naming style matching correct variable names 293 | variable-naming-style=snake_case 294 | 295 | # Regular expression matching correct variable names. Overrides variable- 296 | # naming-style 297 | #variable-rgx= 298 | 299 | 300 | [FORMAT] 301 | 302 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 303 | expected-line-ending-format= 304 | 305 | # Regexp for a line that is allowed to be longer than the limit. 306 | ignore-long-lines=^\s*(# )??$ 307 | 308 | # Number of spaces of indent required inside a hanging or continued line. 309 | indent-after-paren=4 310 | 311 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 312 | # tab). 313 | indent-string=' ' 314 | 315 | # Maximum number of characters on a single line. 316 | max-line-length=100 317 | 318 | # Maximum number of lines in a module 319 | max-module-lines=1000 320 | 321 | # List of optional constructs for which whitespace checking is disabled. `dict- 322 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 323 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 324 | # `empty-line` allows space-only lines. 325 | no-space-check=trailing-comma, 326 | dict-separator 327 | 328 | # Allow the body of a class to be on the same line as the declaration if body 329 | # contains single statement. 330 | single-line-class-stmt=no 331 | 332 | # Allow the body of an if to be on the same line as the test if there is no 333 | # else. 334 | single-line-if-stmt=no 335 | 336 | 337 | [LOGGING] 338 | 339 | # Logging modules to check that the string format arguments are in logging 340 | # function parameter format 341 | logging-modules=logging 342 | 343 | 344 | [MISCELLANEOUS] 345 | 346 | # List of note tags to take in consideration, separated by a comma. 347 | notes=FIXME, 348 | XXX, 349 | 350 | 351 | [SIMILARITIES] 352 | 353 | # Ignore comments when computing similarities. 354 | ignore-comments=yes 355 | 356 | # Ignore docstrings when computing similarities. 357 | ignore-docstrings=yes 358 | 359 | # Ignore imports when computing similarities. 360 | ignore-imports=no 361 | 362 | # Minimum lines number of a similarity. 363 | min-similarity-lines=4 364 | 365 | 366 | [SPELLING] 367 | 368 | # Limits count of emitted suggestions for spelling mistakes 369 | max-spelling-suggestions=4 370 | 371 | # Spelling dictionary name. Available dictionaries: none. To make it working 372 | # install python-enchant package. 373 | spelling-dict= 374 | 375 | # List of comma separated words that should not be checked. 376 | spelling-ignore-words= 377 | 378 | # A path to a file that contains private dictionary; one word per line. 379 | spelling-private-dict-file= 380 | 381 | # Tells whether to store unknown words to indicated private dictionary in 382 | # --spelling-private-dict-file option instead of raising a message. 383 | spelling-store-unknown-words=no 384 | 385 | 386 | [TYPECHECK] 387 | 388 | # List of decorators that produce context managers, such as 389 | # contextlib.contextmanager. Add to this list to register other decorators that 390 | # produce valid context managers. 391 | contextmanager-decorators=contextlib.contextmanager 392 | 393 | # List of members which are set dynamically and missed by pylint inference 394 | # system, and so shouldn't trigger E1101 when accessed. Python regular 395 | # expressions are accepted. 396 | generated-members= 397 | 398 | # Tells whether missing members accessed in mixin class should be ignored. A 399 | # mixin class is detected if its name ends with "mixin" (case insensitive). 400 | ignore-mixin-members=yes 401 | 402 | # This flag controls whether pylint should warn about no-member and similar 403 | # checks whenever an opaque object is returned when inferring. The inference 404 | # can return multiple potential results while evaluating a Python object, but 405 | # some branches might not be evaluated, which results in partial inference. In 406 | # that case, it might be useful to still emit no-member and other checks for 407 | # the rest of the inferred objects. 408 | ignore-on-opaque-inference=yes 409 | 410 | # List of class names for which member attributes should not be checked (useful 411 | # for classes with dynamically set attributes). This supports the use of 412 | # qualified names. 413 | ignored-classes=optparse.Values,thread._local,_thread._local 414 | 415 | # List of module names for which member attributes should not be checked 416 | # (useful for modules/projects where namespaces are manipulated during runtime 417 | # and thus existing member attributes cannot be deduced by static analysis. It 418 | # supports qualified module names, as well as Unix pattern matching. 419 | ignored-modules= 420 | 421 | # Show a hint with possible names when a member name was not found. The aspect 422 | # of finding the hint is based on edit distance. 423 | missing-member-hint=yes 424 | 425 | # The minimum edit distance a name should have in order to be considered a 426 | # similar match for a missing member name. 427 | missing-member-hint-distance=1 428 | 429 | # The total number of similar names that should be taken in consideration when 430 | # showing a hint for a missing member. 431 | missing-member-max-choices=1 432 | 433 | 434 | [VARIABLES] 435 | 436 | # List of additional names supposed to be defined in builtins. Remember that 437 | # you should avoid to define new builtins when possible. 438 | additional-builtins= 439 | 440 | # Tells whether unused global variables should be treated as a violation. 441 | allow-global-unused-variables=yes 442 | 443 | # List of strings which can identify a callback function by name. A callback 444 | # name must start or end with one of those strings. 445 | callbacks=cb_, 446 | _cb 447 | 448 | # A regular expression matching the name of dummy variables (i.e. expectedly 449 | # not used). 450 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 451 | 452 | # Argument names that match this expression will be ignored. Default to name 453 | # with leading underscore 454 | ignored-argument-names=_.*|^ignored_|^unused_ 455 | 456 | # Tells whether we should check for unused import in __init__ files. 457 | init-import=no 458 | 459 | # List of qualified module names which can have objects that can redefine 460 | # builtins. 461 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 462 | 463 | 464 | [CLASSES] 465 | 466 | # List of method names used to declare (i.e. assign) instance attributes. 467 | defining-attr-methods=__init__, 468 | __new__, 469 | setUp 470 | 471 | # List of member names, which should be excluded from the protected access 472 | # warning. 473 | exclude-protected=_asdict, 474 | _fields, 475 | _replace, 476 | _source, 477 | _make 478 | 479 | # List of valid names for the first argument in a class method. 480 | valid-classmethod-first-arg=cls 481 | 482 | # List of valid names for the first argument in a metaclass class method. 483 | valid-metaclass-classmethod-first-arg=mcs 484 | 485 | 486 | [DESIGN] 487 | 488 | # Maximum number of arguments for function / method 489 | max-args=5 490 | 491 | # Maximum number of attributes for a class (see R0902). 492 | max-attributes=7 493 | 494 | # Maximum number of boolean expressions in a if statement 495 | max-bool-expr=5 496 | 497 | # Maximum number of branch for function / method body 498 | max-branches=12 499 | 500 | # Maximum number of locals for function / method body 501 | max-locals=15 502 | 503 | # Maximum number of parents for a class (see R0901). 504 | max-parents=7 505 | 506 | # Maximum number of public methods for a class (see R0904). 507 | max-public-methods=20 508 | 509 | # Maximum number of return / yield for function / method body 510 | max-returns=6 511 | 512 | # Maximum number of statements in function / method body 513 | max-statements=50 514 | 515 | # Minimum number of public methods for a class (see R0903). 516 | min-public-methods=2 517 | 518 | 519 | [IMPORTS] 520 | 521 | # Allow wildcard imports from modules that define __all__. 522 | allow-wildcard-with-all=no 523 | 524 | # Analyse import fallback blocks. This can be used to support both Python 2 and 525 | # 3 compatible code, which means that the block might have code that exists 526 | # only in one or another interpreter, leading to false positives when analysed. 527 | analyse-fallback-blocks=no 528 | 529 | # Deprecated modules which should not be used, separated by a comma 530 | deprecated-modules=optparse,tkinter.tix 531 | 532 | # Create a graph of external dependencies in the given file (report RP0402 must 533 | # not be disabled) 534 | ext-import-graph= 535 | 536 | # Create a graph of every (i.e. internal and external) dependencies in the 537 | # given file (report RP0402 must not be disabled) 538 | import-graph= 539 | 540 | # Create a graph of internal dependencies in the given file (report RP0402 must 541 | # not be disabled) 542 | int-import-graph= 543 | 544 | # Force import order to recognize a module as part of the standard 545 | # compatibility libraries. 546 | known-standard-library= 547 | 548 | # Force import order to recognize a module as part of a third party library. 549 | known-third-party=enchant 550 | 551 | 552 | [EXCEPTIONS] 553 | 554 | # Exceptions that will emit a warning when being caught. Defaults to 555 | # "Exception" 556 | overgeneral-exceptions=Exception 557 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 Viktor Krückl (viktor@krueckl.de) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dash_lumino_components/dash_lumino_components.min.js 2 | include dash_lumino_components/dash_lumino_components.min.js.map 3 | include dash_lumino_components/metadata.json 4 | include dash_lumino_components/package-info.json 5 | include README.md 6 | include LICENSE 7 | include package.json 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash Lumino Components 2 | ![Publish release](https://github.com/VK/dash-lumino-components/workflows/Publish%20release/badge.svg) 3 | [![PyPI](https://img.shields.io/pypi/v/dash-lumino-components?logo=pypi)](https://pypi.org/project/dash-lumino-components) 4 | [![npm](https://img.shields.io/npm/v/dash_lumino_components.svg?logo=npm)](https://www.npmjs.com/package/dash_lumino_components) 5 | [![Documentation](https://github.com/VK/dash-lumino-components/workflows/Documentation/badge.svg)](https://vk.github.io/dash-lumino-components) 6 | 7 | 8 | This package integrates [Lumino Widgets](https://github.com/jupyterlab/lumino), the basis of [JupyterLab](https://github.com/jupyterlab/jupyterlab), into [Plotly's Dash](https://github.com/plotly/dash). 9 | 10 | Create a multi-window dash app with just a few lines of code. 11 | Check out the [examples](https://github.com/VK/dash-lumino-components/tree/master/examples): 12 | ![multiplots example](https://raw.githubusercontent.com/VK/dash-lumino-components/master/examples/multiplots.gif) 13 | ```python 14 | dlc.MenuBar(menus, id="main-menu"), 15 | dlc.BoxPanel([ 16 | dlc.SplitPanel([ 17 | dlc.TabPanel([ 18 | gapminderPlotsPanel, 19 | irisPlotsPanel, 20 | tipsPlotsPanel 21 | ], id='tab-panel-left'), 22 | dlc.DockPanel([], id="dock-panel") 23 | ], id="split-panel") 24 | ], id="box-panel", addToDom=True) 25 | ``` 26 | 27 | 28 | 29 | ## Local Developement 30 | 1. Install npm packages 31 | ``` 32 | $ npm install 33 | ``` 34 | 35 | 2. Create a virtual env and activate. 36 | ``` 37 | $ virtualenv venv 38 | $ . venv/bin/activate 39 | ``` 40 | _Note: venv\Scripts\activate for windows_ 41 | 42 | 3. Install python packages required to build components. 43 | ``` 44 | $ pip install -r requirements.txt 45 | $ pip install -r tests/requirements.txt 46 | ``` 47 | 48 | 4. Build your code 49 | ``` 50 | $ npm run build 51 | ``` 52 | -------------------------------------------------------------------------------- /_validate_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | DO NOT MODIFY 3 | This file is used to validate your publish settings. 4 | """ 5 | from __future__ import print_function 6 | 7 | import os 8 | import sys 9 | import importlib 10 | 11 | 12 | components_package = 'dash_lumino_components' 13 | 14 | components_lib = importlib.import_module(components_package) 15 | 16 | missing_dist_msg = 'Warning {} was not found in `{}.__init__.{}`!!!' 17 | missing_manifest_msg = ''' 18 | Warning {} was not found in `MANIFEST.in`! 19 | It will not be included in the build! 20 | ''' 21 | 22 | with open('MANIFEST.in', 'r') as f: 23 | manifest = f.read() 24 | 25 | 26 | def check_dist(dist, filename): 27 | # Support the dev bundle. 28 | if filename.endswith('dev.js'): 29 | return True 30 | 31 | return any( 32 | filename in x 33 | for d in dist 34 | for x in ( 35 | [d.get('relative_package_path')] 36 | if not isinstance(d.get('relative_package_path'), list) 37 | else d.get('relative_package_path') 38 | ) 39 | ) 40 | 41 | 42 | def check_manifest(filename): 43 | return filename in manifest 44 | 45 | 46 | def check_file(dist, filename): 47 | if not check_dist(dist, filename): 48 | print( 49 | missing_dist_msg.format(filename, components_package, '_js_dist'), 50 | file=sys.stderr 51 | ) 52 | if not check_manifest(filename): 53 | print(missing_manifest_msg.format(filename), 54 | file=sys.stderr) 55 | 56 | 57 | for cur, _, files in os.walk(components_package): 58 | for f in files: 59 | 60 | if f.endswith('js'): 61 | # noinspection PyProtectedMember 62 | check_file(components_lib._js_dist, f) 63 | elif f.endswith('css'): 64 | # noinspection PyProtectedMember 65 | check_file(components_lib._css_dist, f) 66 | elif not f.endswith('py'): 67 | check_manifest(f) 68 | -------------------------------------------------------------------------------- /dash_lumino_components/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function as _ 2 | 3 | import os as _os 4 | import sys as _sys 5 | import json 6 | 7 | import dash as _dash 8 | 9 | # noinspection PyUnresolvedReferences 10 | from ._imports_ import * 11 | from ._imports_ import __all__ 12 | 13 | if not hasattr(_dash, 'development'): 14 | print('Dash was not successfully imported. ' 15 | 'Make sure you don\'t have a file ' 16 | 'named \n"dash.py" in your current directory.', file=_sys.stderr) 17 | _sys.exit(1) 18 | 19 | _basepath = _os.path.dirname(__file__) 20 | _filepath = _os.path.abspath(_os.path.join(_basepath, 'package-info.json')) 21 | with open(_filepath) as f: 22 | package = json.load(f) 23 | 24 | package_name = package['name'].replace(' ', '_').replace('-', '_') 25 | __version__ = package['version'] 26 | 27 | _current_path = _os.path.dirname(_os.path.abspath(__file__)) 28 | 29 | _this_module = _sys.modules[__name__] 30 | 31 | 32 | _js_dist = [ 33 | { 34 | 'relative_package_path': 'dash_lumino_components.min.js', 35 | 'external_url': 'https://unpkg.com/{0}@{2}/{1}/{1}.min.js'.format( 36 | package_name, __name__, __version__), 37 | 'namespace': package_name 38 | }, 39 | { 40 | 'relative_package_path': 'dash_lumino_components.min.js.map', 41 | 'external_url': 'https://unpkg.com/{0}@{2}/{1}/{1}.min.js.map'.format( 42 | package_name, __name__, __version__), 43 | 'namespace': package_name, 44 | 'dynamic': True 45 | } 46 | ] 47 | 48 | _css_dist = [] 49 | 50 | 51 | for _component in __all__: 52 | setattr(locals()[_component], '_js_dist', _js_dist) 53 | setattr(locals()[_component], '_css_dist', _css_dist) 54 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | 4 | ## multiplots.py 5 | A dash app with a **MenuBar**, **TabPanel** and a main **DockPanel**. 6 | All widgets in the dock can be placed as needed and are filled with dynamic plots: 7 | ![multiplots example](https://raw.githubusercontent.com/VK/dash-lumino-components/master/examples/multiplots.gif) 8 | ```python 9 | dlc.MenuBar(menus, id="main-menu"), 10 | dlc.BoxPanel([ 11 | dlc.SplitPanel([ 12 | dlc.TabPanel([ 13 | gapminderPlotsPanel, 14 | irisPlotsPanel, 15 | tipsPlotsPanel 16 | ], id='tab-panel-left'), 17 | dlc.DockPanel([], id="dock-panel") 18 | ], id="split-panel") 19 | ], id="box-panel", addToDom=True) 20 | ``` -------------------------------------------------------------------------------- /examples/multiplage.py: -------------------------------------------------------------------------------- 1 | import dash_lumino_components as dlc 2 | from dash import Dash 3 | from dash import Input, Output, ALL, html 4 | 5 | # common style 6 | external_stylesheets = [ 7 | 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css', 8 | 'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css' 9 | ] 10 | 11 | menus = [ 12 | dlc.Menu([ 13 | dlc.Command(id={"type": "changePage", "url": "/page1"}, label="Page 1", icon="fa fa-1"), 14 | dlc.Command(id={"type": "changePage", "url": "/page2"}, label="Page 2", icon="fa fa-2"), 15 | ], id="openMenu", title="Widgets")] 16 | 17 | # Create first Dash app 18 | app = Dash(__name__, url_base_pathname='/page1/', external_stylesheets=external_stylesheets) 19 | app.layout = html.Div([ 20 | dlc.MenuBar(menus, 'menuBar'), 21 | dlc.BoxPanel([ 22 | dlc.DockPanel([ 23 | dlc.Widget( 24 | html.Div([ 25 | html.H1('This is our Home page'), 26 | html.Div('This is our Home page content.'), 27 | ]), 28 | id="homeWidget1", 29 | title="Home Widget 1", 30 | closable=True 31 | ), 32 | dlc.Widget( 33 | html.Div([ 34 | html.H1('This is our Viz page'), 35 | html.Div('This is our Viz page content.'), 36 | ]), 37 | id="vizWidget1", 38 | title="Viz Widget 1", 39 | closable=True 40 | ), 41 | ], id="dockPanel"), 42 | ], 43 | id="boxPanel", addToDom=True), 44 | html.Div("dummy", id="dummy", style={"visibility": "hidden", "height": "0"}), 45 | ]) 46 | 47 | # Create second Dash app 48 | app2 = Dash(__name__, server=app.server, url_base_pathname='/page2/', external_stylesheets=external_stylesheets) 49 | app2.layout = html.Div([ 50 | dlc.MenuBar(menus, 'menuBar'), 51 | html.H1('This is our Viz page'), 52 | html.Div("dummy", id="dummy", style={"visibility": "hidden", "height": "0"}), 53 | ]) 54 | 55 | for a in [app, app2]: 56 | a.clientside_callback( 57 | """ 58 | function(buttons) { 59 | 60 | ctx = window.dash_clientside.callback_context; 61 | 62 | try { 63 | // Extract the triggered input 64 | triggered_input = JSON.parse(ctx.triggered[0].prop_id.replace(".n_called", "")).url; 65 | 66 | console.log(triggered_input); 67 | window.location.href = triggered_input; 68 | } catch (error) {} 69 | 70 | return window.dash_clientside.no_update; 71 | } 72 | """, 73 | Output('dummy', 'children'), 74 | Input({"type": "changePage", "url": ALL}, "n_called"), 75 | initial_callback=False 76 | ) 77 | 78 | # Run the Flask server 79 | if __name__ == '__main__': 80 | app.run_server(debug=True) -------------------------------------------------------------------------------- /examples/multiplots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK/dash-lumino-components/335c345b63cb9fed7ba642a81d4a3eb0cab17628/examples/multiplots.gif -------------------------------------------------------------------------------- /examples/multiplots.py: -------------------------------------------------------------------------------- 1 | import dash 2 | from dash import Input, Output, State, html, dcc 3 | import dash_lumino_components as dlc 4 | import dash_bootstrap_components as dbc 5 | import plotly.express as px 6 | import random 7 | import json 8 | 9 | df_iris = px.data.iris() 10 | df_gapminder = px.data.gapminder() 11 | df_tips = px.data.tips() 12 | 13 | # use font-awesome for icons and boostrap for main style 14 | external_stylesheets = [ 15 | 'https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css', 16 | 'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css' 17 | ] 18 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets) 19 | 20 | 21 | # create a "close all" and a "close random" menu item 22 | menus = [ 23 | dlc.Menu([ 24 | dlc.Command(id="com:closeAll", label="Close All", 25 | icon="fa fa-window-restore"), 26 | dlc.Command(id="com:closeRandom", label="Close Random", 27 | icon="fa fa-window-maximize"), 28 | ], id="openMenu", title="Widgets")] 29 | 30 | # define all plots available 31 | widgetCalls = [ 32 | # some gapminder plots 33 | { 34 | "id": "gapminder-sunburst", 35 | "df_type": "gapminder", 36 | "text": "Life Expectation", 37 | "df": df_gapminder, 38 | "dfTrafos": lambda x: x.query("year == 2007"), 39 | "plotter": px.sunburst, 40 | "plotParams": {"path": ['continent', 'country'], "values":'pop', "color":'lifeExp', "hover_data":['iso_alpha'], "title": "Life Expectation"}, 41 | "widgetParams": {"title": "Gapminder", "icon": "fa fa-globe", "caption": "Life Expectation"} 42 | }, 43 | { 44 | "id": "gapminder-treemap", 45 | "df_type": "gapminder", 46 | "text": "Life Expectation Tree", 47 | "df": df_gapminder, 48 | "dfTrafos": lambda x: x.query("year == 2007"), 49 | "plotter": px.treemap, 50 | "plotParams": {"path": [px.Constant('world'), 'continent', 'country'], "values":'pop', "color":'lifeExp', "hover_data":['iso_alpha'], "title": "Life Expectation"}, 51 | "widgetParams": {"title": "Gapminder", "icon": "fa fa-globe", "caption": "Life Expectation Tree"} 52 | }, 53 | { 54 | "id": "gapminder-choropleth", 55 | "df_type": "gapminder", 56 | "text": "Life Expectation Map", 57 | "df": df_gapminder, 58 | "dfTrafos": lambda x: x, 59 | "plotter": px.choropleth, 60 | "plotParams": {"locations": "iso_alpha", "color": 'lifeExp', "hover_name": "country", "animation_frame": "year", "range_color": [20, 80]}, 61 | "widgetParams": {"title": "Gapminder", "icon": "fa fa-globe", "caption": "Life Expectation Map"} 62 | }, 63 | 64 | # some plots based on iris data 65 | { 66 | "id": "iris-scattermatrix", 67 | "df_type": "iris", 68 | "text": "Scatter Matrix", 69 | "df": df_iris, 70 | "dfTrafos": lambda x: x, 71 | "plotter": px.scatter_matrix, 72 | "plotParams": dict(dimensions=["sepal_width", "sepal_length", "petal_width", "petal_length"], color="species"), 73 | "widgetParams": {"title": "Iris", "icon": "fa fa-leaf", "caption": "Scatter Matrix"} 74 | }, 75 | { 76 | "id": "iris-parallel_coordinates", 77 | "df_type": "iris", 78 | "text": "Parallel coordinates", 79 | "df": df_iris, 80 | "dfTrafos": lambda x: x, 81 | "plotter": px.parallel_coordinates, 82 | "plotParams": dict(color="species_id", labels={"species_id": "Species", 83 | "sepal_width": "Sepal Width", "sepal_length": "Sepal Length", 84 | "petal_width": "Petal Width", "petal_length": "Petal Length", }, 85 | color_continuous_scale=px.colors.diverging.Tealrose, color_continuous_midpoint=2), 86 | "widgetParams": {"title": "Iris", "icon": "fa fa-leaf", "caption": "Parallel coordinates"} 87 | }, 88 | { 89 | "id": "iris-density_contour", 90 | "df_type": "iris", 91 | "text": "Density contour plot", 92 | "df": df_iris, 93 | "dfTrafos": lambda x: x, 94 | "plotter": px.density_contour, 95 | "plotParams": dict(color="species", x="sepal_width", y="sepal_length", marginal_x="box", marginal_y="histogram"), 96 | "widgetParams": {"title": "Iris", "icon": "fa fa-leaf", "caption": "Density contour plot"} 97 | }, 98 | 99 | # some plots based on tip data 100 | { 101 | "id": "tips-parallel_categories", 102 | "df_type": "tips", 103 | "text": "Parallel categories", 104 | "df": df_tips, 105 | "dfTrafos": lambda x: x, 106 | "plotter": px.parallel_categories, 107 | "plotParams": dict(color="size", color_continuous_scale=px.colors.sequential.Inferno), 108 | "widgetParams": {"title": "Tips", "icon": "fa fa-money", "caption": "Parallel categories"} 109 | }, 110 | { 111 | "id": "tips-histogram", 112 | "df_type": "tips", 113 | "text": "Total bill histogram", 114 | "df": df_tips, 115 | "dfTrafos": lambda x: x, 116 | "plotter": px.histogram, 117 | "plotParams": dict(x="total_bill", y="tip", color="sex", marginal="rug", hover_data=df_tips.columns), 118 | "widgetParams": {"title": "Tips", "icon": "fa fa-money", "caption": "Total bill histogram"} 119 | }, 120 | { 121 | "id": "tips-box", 122 | "df_type": "tips", 123 | "text": "Total bill box plot", 124 | "df": df_tips, 125 | "dfTrafos": lambda x: x, 126 | "plotter": px.box, 127 | "plotParams": dict(x="day", y="total_bill", color="smoker", notched=True), 128 | "widgetParams": {"title": "Tips", "icon": "fa fa-money", "caption": "Total bill box plot"} 129 | } 130 | ] 131 | 132 | # create a function to make a boostrap button based on the plot definitions 133 | 134 | 135 | def getWidgetOpenButton(data): 136 | return dbc.Button(data["text"], style={"width": "100%"}, id=data["id"]+"-button", className="mb-1") 137 | 138 | 139 | # create the panels for the left tabbar 140 | gapminderPlotsPanel = dlc.Panel([ 141 | html.H3("Gapminder Plots"), 142 | html.P(["All plots are based on the ", html.A( 143 | "gapminder dataset", href="https://www.gapminder.org/data/"), "."]), 144 | *[getWidgetOpenButton(d) for d in widgetCalls if d["df_type"] == "gapminder"] 145 | ], id="gapminder-plots-panel", label="Gapminder", icon="fa fa-globe") 146 | 147 | irisPlotsPanel = dlc.Panel([ 148 | html.H3("Iris Plots"), 149 | html.P(["All plots are based on the ", html.A("iris dataset", 150 | href="https://en.wikipedia.org/wiki/Iris_flower_data_set"), "."]), 151 | *[getWidgetOpenButton(d) for d in widgetCalls if d["df_type"] == "iris"] 152 | ], id="iris-plots-panel", label="Iris", icon="fa fa-leaf") 153 | 154 | tipsPlotsPanel = dlc.Panel([ 155 | html.H3("Tips Plots"), 156 | html.P(["All plots are based on the ", html.A("tips dataset", 157 | href="https://vincentarelbundock.github.io/Rdatasets/doc/reshape2/tips.html"), "."]), 158 | *[getWidgetOpenButton(d) for d in widgetCalls if d["df_type"] == "tips"] 159 | ], id="tips-plots-panel", label="Tips", icon="fa fa-money") 160 | 161 | 162 | configPanel = dlc.Panel([ 163 | html.H3("Dock panel layout"), 164 | html.Pre("", id="config-json") 165 | ], id="config-panel", label="Layout", icon="fa fa-gear") 166 | 167 | # create the main layout of the app 168 | app.layout = html.Div([ 169 | dlc.MenuBar(menus, id="main-menu"), 170 | dlc.BoxPanel([ 171 | dlc.SplitPanel([ 172 | dlc.TabPanel([ 173 | gapminderPlotsPanel, 174 | irisPlotsPanel, 175 | tipsPlotsPanel, 176 | configPanel, 177 | ], 178 | id='tab-panel-left', 179 | tabPlacement="left", 180 | allowDeselect=True, 181 | currentIndex=2, 182 | width=300 183 | ), 184 | dlc.DockPanel([], id="dock-panel") 185 | ], id="split-panel")], id="box-panel", addToDom=True) 186 | ]) 187 | 188 | # a single callback creates different the different plot widgets 189 | @app.callback([ 190 | Output('dock-panel', 'children'), 191 | Output('config-json', 'children'), 192 | ], 193 | [ 194 | *[Input(w["id"]+"-button", "n_clicks") for w in widgetCalls], 195 | Input("com:closeAll", "n_called"), 196 | Input("com:closeRandom", "n_called"), 197 | Input('dock-panel', 'widgetEvent') 198 | ], 199 | [State('dock-panel', 'children')]) 200 | def handle_widget(*argv): 201 | 202 | # the last argument is the current state of the dock-panel 203 | widgets = argv[-1] 204 | 205 | # the second last is the widget event 206 | event = argv[-2] 207 | 208 | # remove all closed widgets 209 | widgets = [w for w in widgets if not( 210 | "props" in w and "deleted" in w["props"] and w["props"]["deleted"])] 211 | 212 | # get which component made the callback 213 | ctx = dash.callback_context 214 | 215 | # check which widget needs to be created 216 | matching_widget_call = [w for w in widgetCalls if ctx.triggered[0]["prop_id"].startswith(w["id"])] 217 | 218 | # create the widget 219 | if len(matching_widget_call) > 0: 220 | params = matching_widget_call[0] 221 | fig = params["plotter"](params["dfTrafos"]( 222 | params["df"]), **params["plotParams"]) 223 | new_widget = dlc.Widget( 224 | dcc.Graph(figure=fig, 225 | style={"width": "100%", "height": "100%"}), 226 | id=params["id"]+"-widget-" + str(ctx.triggered[0]["value"]), 227 | **params["widgetParams"]) 228 | widgets.append( new_widget ) 229 | 230 | # close all widgets 231 | if "prop_id" in ctx.triggered[0] and ctx.triggered[0]["prop_id"] == "com:closeAll.n_called": 232 | widgets = [] 233 | 234 | # close a random widget 235 | if "prop_id" in ctx.triggered[0] and ctx.triggered[0]["prop_id"] == "com:closeRandom.n_called" and len(widgets) > 0: 236 | del_idx = random.randint(0, len(widgets)-1) 237 | del widgets[del_idx] 238 | 239 | status = { 240 | "event": event, 241 | "open": [ 242 | {k:v for k,v in w["props"].items() if k not in ["children"]} for w in widgets if "props" in w and "id" in w["props"] 243 | ] 244 | } 245 | 246 | return widgets, json.dumps(status, indent=2) 247 | 248 | #start the app 249 | if __name__ == '__main__': 250 | app.run_server(debug=True) 251 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | dash 2 | dash-lumino-components 3 | dash-bootstrap-components -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": [ 5 | "jsdoc", 6 | "closure" 7 | ] 8 | }, 9 | "source": { 10 | "include": [ 11 | "src/lib/components/" 12 | ], 13 | "includePattern": ".+\\.js(doc|x)?$", 14 | "excludePattern": "(^|\\/|\\\\)_" 15 | }, 16 | "plugins": [ 17 | "plugins/markdown" 18 | ], 19 | "templates": { 20 | "cleverLinks": true, 21 | "monospaceLinks": true, 22 | "systemName": "Dash Lumino Components", 23 | "systemSummary": "Integrate Lumino Widgets into Plotly's Dash to build interactive web applications", 24 | "collapseSymbols": false, 25 | "showTableOfContents": false 26 | }, 27 | "opts": { 28 | "destination": "docs", 29 | "recurse": true, 30 | "readme": "README.md", 31 | "private": false, 32 | "template": "node_modules/clean-jsdoc-theme", 33 | "theme_opts": { 34 | "menu": [ 35 | { 36 | "title": "Github", 37 | "link": "https://github.com/VK/dash-lumino-components", 38 | "target": "_blank" 39 | } 40 | ] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash_lumino_components", 3 | "version": "0.0.20", 4 | "description": "Lumino (JupyterLab) components for Plotly Dash", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/VK/dash-lumino-components.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/VK/dash-lumino-components/issues" 11 | }, 12 | "homepage": "https://github.com/VK/dash-lumino-components", 13 | "main": "build/index.js", 14 | "scripts": { 15 | "start": "webpack serve --config ./webpack.serve.config.js --open", 16 | "validate-init": "python _validate_init.py", 17 | "prepublishOnly": "npm run validate-init", 18 | "build:js": "webpack --mode production", 19 | "build:py_and_r": "dash-generate-components ./src/lib/components dash_lumino_components -p package-info.json --r-prefix dlc --jl-prefix dlc", 20 | "build:py_and_r-activated": "(. venv/bin/activate || venv\\scripts\\activate && npm run build:py_and_r)", 21 | "build": "npm run build:js && npm run build:py_and_r", 22 | "build:activated": "npm run build:js && npm run build:py_and_r-activated", 23 | "doc": "jsdoc -c ./jsdoc.conf.json" 24 | }, 25 | "author": "Viktor Krückl ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@lumino/commands": "^1.11.4", 29 | "@lumino/default-theme": "^0.5.1", 30 | "@lumino/dragdrop": "^1.6.4", 31 | "@lumino/messaging": "^1.4.3", 32 | "@lumino/widgets": "^1.14.1", 33 | "es6-promise": "^4.0.5", 34 | "ramda": "^0.27.2" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.15.0", 38 | "@babel/eslint-parser": "^7.15.4", 39 | "@babel/plugin-proposal-object-rest-spread": "^7.15.0", 40 | "@babel/preset-env": "^7.15.0", 41 | "@babel/preset-react": "^7.14.5", 42 | "babel-loader": "^8.2.2", 43 | "clean-jsdoc-theme": "^4.3.0", 44 | "copyfiles": "^2.4.1", 45 | "css-loader": "^6.2.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-import": "^2.24.2", 48 | "eslint-plugin-react": "^7.24.0", 49 | "file-loader": "^6.2.0", 50 | "jsdoc": "^4.0.4", 51 | "npm": "^10.9.0", 52 | "prop-types": "^15.7.2", 53 | "react": "^17.0.2", 54 | "react-docgen": "^5.3.0", 55 | "react-dom": "^17.0.2", 56 | "style-loader": "^3.2.1", 57 | "styled-jsx": "^5.1.6", 58 | "webpack": "^5.52.0", 59 | "webpack-cli": "^4.8.0", 60 | "webpack-serve": "^3.1.0" 61 | }, 62 | "engines": { 63 | "node": ">=14.0.0", 64 | "npm": ">=7.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests/ 3 | addopts = -rsxX -vv 4 | log_format = %(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s 5 | log_cli_level = ERROR 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # dash is required to call `build:py` 2 | dash[dev]>=2.15.0 3 | dash-bootstrap-components -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from setuptools import setup 4 | 5 | 6 | with open('package.json') as f: 7 | package = json.load(f) 8 | 9 | package_name = package["name"].replace(" ", "_").replace("-", "_") 10 | 11 | with open("README.md", "r") as fh: 12 | long_description = fh.read() 13 | 14 | setup( 15 | name=package_name, 16 | url="https://github.com/VK/dash-lumino-components", 17 | version=package["version"], 18 | author=package['author'], 19 | packages=[package_name], 20 | include_package_data=True, 21 | license=package['license'], 22 | description=package.get('description', package_name), 23 | long_description= long_description, 24 | long_description_content_type="text/markdown", 25 | install_requires=["dash"], 26 | classifiers = [ 27 | 'Framework :: Dash', 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.jl -------------------------------------------------------------------------------- /src/lib/component.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { components, get_uuid, props_id} from './registry.js'; 4 | import { is } from 'ramda'; 5 | 6 | import { 7 | Widget as l_Widget 8 | } from '@lumino/widgets'; 9 | 10 | /** 11 | * A base component used for all other Dash Lumino Components 12 | */ 13 | export default class DashLuminoComponent extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | 20 | move2Dash(comp) { 21 | comp.div.appendChild(comp.lumino.node) 22 | } 23 | 24 | move2Lumino(comp) { 25 | comp.lumino.node.appendChild(comp.div.children[0]) 26 | } 27 | 28 | /** 29 | * Register a new lumino component in the regirstry 30 | * @param {*} luminoComponent 31 | * @param {*} attachToDom 32 | */ 33 | register(luminoComponent, attachToDom = false, resize = true) { 34 | this.luminoComponent = luminoComponent; 35 | 36 | // create an unique id if not available 37 | this.id = (this.props.id || this.props.id != undefined) ? props_id(this) : get_uuid(); 38 | 39 | // set the same id to the lumino component 40 | luminoComponent.id = this.id; 41 | 42 | // call the update if the window resizes 43 | if (resize) { 44 | window.addEventListener("resize", function () { 45 | luminoComponent.update(); 46 | }); 47 | } 48 | 49 | //create an entry in the registry 50 | components[this.id] = { 51 | "dash": this, 52 | "lumino": luminoComponent, 53 | "div": undefined 54 | } 55 | 56 | // add the component to the dom is usually needed for the 57 | // first DashLuminoComponent 58 | if (attachToDom) { 59 | this.attachToDom(); 60 | } 61 | 62 | return luminoComponent; 63 | } 64 | 65 | 66 | /** 67 | * Attach the lumino component to the document body 68 | */ 69 | attachToDom() { 70 | l_Widget.attach(this.luminoComponent, document.body); 71 | } 72 | 73 | /** 74 | * Wait untiul the dash lumino component is created and then apply the custom function 75 | * This is usually used to create the lumino objects hierarchy, like widgets in panels, ... 76 | * 77 | * @param {*} reactComponent 78 | * @param {*} func(target_component, child_component) 79 | */ 80 | applyAfterLuminoChildCreation(reactComponent, func) { 81 | var i = 0; 82 | 83 | let target_component = components[this.id]; 84 | 85 | function updateLoop() { 86 | setTimeout(function () { 87 | i++; 88 | let child_component = components[props_id(reactComponent.props._dashprivate_layout)]; 89 | if (target_component && child_component) { 90 | func(target_component, child_component); 91 | } else if (i < 50) { 92 | updateLoop(); 93 | } else { 94 | console.log("Warning: applyAfterCreation timed out!"); 95 | } 96 | }, 10) 97 | } 98 | 99 | updateLoop(); 100 | } 101 | 102 | 103 | 104 | /** 105 | * Wait untiul the dash html dom element is created and then apply the custom function 106 | * This is usually used to create the dash component and then move it into a lumino compoent 107 | * 108 | * @param {*} reactComponent 109 | * @param {*} containerName 110 | * @param {*} func(target_component, child_component) 111 | */ 112 | applyAfterDomCreation(reactComponent, containerName, func) { 113 | var i = 0; 114 | 115 | let target_component = components[this.id]; 116 | 117 | 118 | function updateLoop() { 119 | setTimeout(function () { 120 | i++; 121 | 122 | var dash_object = document.getElementById(containerName); 123 | 124 | if (dash_object && target_component) { 125 | func(target_component, dash_object); 126 | //components[this.props.id].div = dash_object.parentElement; 127 | //lumino_object.appendChild(dash_object); 128 | //dash_object.style = ""; 129 | //console.log("Info: created Panel " + props.id + " after " + (i * 10) + "ms"); 130 | } else if (i < 50) { 131 | updateLoop(); 132 | } else { 133 | console.log("Warning: applyAfterCreation timed out!"); 134 | } 135 | }, 10) 136 | } 137 | 138 | updateLoop(); 139 | } 140 | 141 | 142 | 143 | 144 | parseChildrenToArray() { 145 | if (this.props.children && !is(Array, this.props.children)) { 146 | // if props.children contains just one single element, it gets passed as an object 147 | // instead of an array - so we put in in a array ourselves! 148 | return [this.props.children]; 149 | } 150 | return this.props.children; 151 | } 152 | 153 | 154 | render() { 155 | return ({this.props.children}); 156 | } 157 | } 158 | 159 | DashLuminoComponent.defaultProps = { 160 | }; 161 | 162 | DashLuminoComponent.propTypes = { 163 | /** 164 | * ID of the widget 165 | */ 166 | id: PropTypes.string.isRequired, 167 | 168 | /** 169 | * Dash-assigned callback that should be called to report property changes 170 | * to Dash, to make them available for callbacks. 171 | */ 172 | setProps: PropTypes.func 173 | }; 174 | -------------------------------------------------------------------------------- /src/lib/components/BoxPanel.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | 4 | import { 5 | BoxPanel as l_BoxPanel, DockPanel, SplitPanel 6 | } from '@lumino/widgets'; 7 | 8 | /** 9 | * A panel which arranges its widgets in a single row or column. 10 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/boxpanel.html} 11 | * @hideconstructor 12 | * 13 | * @example 14 | * //Python: 15 | * import dash 16 | * import dash_lumino_components as dlc 17 | * 18 | * boxPanel = dlc.BoxPanel([ 19 | * dlc.SplitPanel([], id="split-panel") 20 | * ], id="box-panel") 21 | */ 22 | class BoxPanel extends DashLuminoComponent { 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | // register a new BoxPanel 28 | super.register(new l_BoxPanel({ 29 | alignment: props.alignment, 30 | direction: props.direction, 31 | spacing: props.spacing 32 | }), props.addToDom); 33 | 34 | // add the children of the component to the widgets of the panel 35 | if (this.props.children) { 36 | super.parseChildrenToArray().forEach(el => { 37 | super.applyAfterLuminoChildCreation(el, (target, child) => { 38 | target.lumino.addWidget(child.lumino); 39 | }); 40 | }) 41 | } 42 | 43 | } 44 | 45 | 46 | render() { 47 | return super.render(); 48 | } 49 | 50 | } 51 | 52 | 53 | BoxPanel.defaultProps = { 54 | alignment: 'start', 55 | direction: 'left-to-right', 56 | spacing: 0, 57 | addToDom: false, 58 | }; 59 | 60 | /** 61 | * @typedef 62 | * @enum {} 63 | */ 64 | BoxPanel.propTypes = { 65 | /** 66 | * ID of the widget 67 | * @type {string} 68 | */ 69 | id: PropTypes.string.isRequired, 70 | 71 | /** 72 | * the content alignment of the layout ("start" | "center" | "end" | "justify") 73 | * @type {string} 74 | */ 75 | alignment: PropTypes.string, 76 | 77 | /** 78 | * a type alias for a box layout direction ("left-to-right" | "right-to-left" | "top-to-bottom" | "bottom-to-top") 79 | * @type {string} 80 | */ 81 | direction: PropTypes.string, 82 | 83 | /** 84 | * The spacing between items in the layout 85 | * @type {number} 86 | */ 87 | spacing: PropTypes.number, 88 | 89 | /** 90 | * bool if the object has to be added to the dom directly 91 | * @type {boolean} 92 | */ 93 | addToDom: PropTypes.bool, 94 | 95 | /** 96 | * The widgets 97 | * @type {Array} 98 | */ 99 | children: PropTypes.node 100 | }; 101 | 102 | /** 103 | * @private 104 | */ 105 | export default BoxPanel; -------------------------------------------------------------------------------- /src/lib/components/Command.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Component } from 'react'; 3 | import { commands, get_id } from '../registry'; 4 | 5 | /** 6 | * A widget which displays items as a canonical menu. 7 | * @hideconstructor 8 | * 9 | * @example 10 | * //Python: 11 | * import dash 12 | * import dash_lumino_components as dlc 13 | * 14 | * command_open = dlc.Command(id="com:openwidget", label="Open", icon="fa fa-plus") 15 | */ 16 | class Command extends Component { 17 | 18 | constructor(props) { 19 | super(props); 20 | 21 | commands.addCommand(get_id(props), { 22 | label: props.label, 23 | iconClass: props.icon, 24 | execute: () => { 25 | this.props.setProps({ 26 | n_called: this.props.n_called + 1, 27 | n_called_timestamp: Date.now(), 28 | }); 29 | 30 | } 31 | }); 32 | 33 | } 34 | 35 | render() { 36 | return ""; 37 | } 38 | 39 | } 40 | 41 | Command.defaultProps = { 42 | n_called: 0, 43 | n_called_timestamp: -1, 44 | }; 45 | 46 | 47 | /** 48 | * @typedef 49 | * @enum {} 50 | */ 51 | Command.propTypes = { 52 | /** 53 | * The id of the command 54 | * @type {string} 55 | */ 56 | id: PropTypes.any, 57 | 58 | /** 59 | * The label of the command 60 | * @type {string} 61 | */ 62 | label: PropTypes.string, 63 | 64 | 65 | /** 66 | * The icon of the command (a cass class name) 67 | * @type {string} 68 | */ 69 | icon: PropTypes.string, 70 | 71 | /** 72 | * Number of times the command was called 73 | * @type {number} 74 | */ 75 | 76 | n_called: PropTypes.number, 77 | /** 78 | * Last time that command was called. 79 | * @type {number} 80 | */ 81 | n_called_timestamp: PropTypes.number, 82 | 83 | /** 84 | * Dash-assigned callback that gets fired when the value changes. 85 | * @private 86 | */ 87 | setProps: PropTypes.func, 88 | 89 | 90 | }; 91 | 92 | /** 93 | * @private 94 | */ 95 | export default Command; -------------------------------------------------------------------------------- /src/lib/components/DockPanel.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | 4 | import { 5 | DockPanel as l_DockPanel, Widget 6 | } from '@lumino/widgets'; 7 | import { components, props_id } from '../registry.js'; 8 | import { any, none } from 'ramda'; 9 | 10 | 11 | 12 | /** 13 | * A widget which provides a flexible docking area for widgets. 14 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/dockpanel.html} 15 | * @hideconstructor 16 | * 17 | * @example 18 | * //Python: 19 | * import dash 20 | * import dash_lumino_components as dlc 21 | * 22 | * dock = dlc.DockPanel([ 23 | * dlc.Widget( 24 | * "Example Content", 25 | * id="initial-widget", 26 | * title="Hallo", 27 | * icon="fa fa-folder-open", 28 | * closable=True) 29 | * ], id="dock-panel") 30 | */ 31 | class DockPanel extends DashLuminoComponent { 32 | 33 | constructor(props) { 34 | super(props); 35 | 36 | // register a new BoxPanel 37 | super.register(new l_DockPanel({ 38 | mode: props.mode, 39 | spacing: props.spacing 40 | }), props.addToDom); 41 | 42 | this.added_ids = []; 43 | this.id = this.props.id; 44 | 45 | components[this.props.id].lumino.layoutModified.connect(() => { 46 | this.updateLayout(); 47 | }, this); 48 | } 49 | 50 | 51 | 52 | /** 53 | * Handle lumnino widget events like lumino:deleted, lumino:activated 54 | * Note: There seem to be some probelms with removing dash components! 55 | * Currently only the dom elements are moved back to their initial position 56 | * and the lumino component is deleted. In the future we want to clean up 57 | * the children of the dock also here! 58 | * @param {*} msg 59 | * @ignore 60 | */ 61 | handleWidgetEvent(msg) { 62 | const widgetid = msg.srcElement.id; 63 | const widget = components[widgetid]; 64 | 65 | if (msg.type === "lumino:deleted") { 66 | super.move2Dash(widget); 67 | } 68 | 69 | const parentid = widget.lumino._parent.node.id; 70 | const that = components[parentid].dash; 71 | const { setProps } = that.props; 72 | 73 | setProps({ 74 | widgetEvent: { id: widgetid, type: msg.type, timestamp: +new Date } 75 | }); 76 | } 77 | 78 | 79 | 80 | 81 | /** 82 | * Serialize the layout without widget instances 83 | */ 84 | updateLayout() { 85 | let input = components[this.props.id].lumino.saveLayout(); 86 | 87 | let layout = JSON.parse(JSON.stringify(input, (key, value) => { 88 | // Exclude widget details from serialization 89 | if (key === 'widgets') { 90 | return value.map((e) => e.node.id); 91 | } 92 | return value; 93 | })); 94 | 95 | const { setProps } = this.props; 96 | setTimeout(() => { 97 | setProps({ 98 | layout: layout 99 | }); 100 | }, 100); 101 | } 102 | 103 | /** 104 | * Function to load the layout back in 105 | * 106 | * recursively replace the widgets in the components dictionary 107 | * @param {} newlayout 108 | */ 109 | loadLayout(newlayout) { 110 | 111 | newlayout = JSON.parse(JSON.stringify(newlayout)); 112 | 113 | function updateWidgets(layout, components) { 114 | if (!layout || typeof layout !== 'object') { 115 | return; // Check if layout is null or not an object 116 | } 117 | 118 | if (Array.isArray(layout)) { 119 | layout.forEach(item => updateWidgets(item, components)); 120 | } else { 121 | Object.keys(layout).forEach(key => { 122 | if (Array.isArray(layout[key])) { 123 | if (key === "widgets") { 124 | 125 | layout[key] = layout[key].map(widget => (components[widget] && components[widget].lumino) ? components[widget].lumino : null).filter(w => w !== null); 126 | } else { 127 | updateWidgets(layout[key], components); 128 | } 129 | } else if (typeof layout[key] === 'object') { 130 | updateWidgets(layout[key], components); 131 | } 132 | }); 133 | } 134 | } 135 | 136 | updateWidgets(newlayout, components); 137 | 138 | components[this.props.id].lumino.restoreLayout(newlayout); 139 | } 140 | 141 | 142 | componentDidUpdate(prevProps) { 143 | // Check if the layout prop has changed 144 | if (this.props.layout !== prevProps.layout) { 145 | // Update the layout 146 | this.loadLayout(this.props.layout); 147 | } 148 | } 149 | 150 | render() { 151 | 152 | // add the children of the component also to the widget list of the lumino widget 153 | if (this.props.children) { 154 | 155 | let current_ids = []; 156 | super.parseChildrenToArray().forEach(el => { 157 | 158 | 159 | // check if react element has all important entries to be a widget 160 | if (el.props && el.props._dashprivate_layout && el.props._dashprivate_layout.props) { 161 | 162 | // fill the list of current widgets 163 | current_ids.push(props_id(el.props._dashprivate_layout)); 164 | 165 | //check if the widget is not yet registered 166 | if (!this.added_ids.includes(props_id(el.props._dashprivate_layout))) { 167 | 168 | super.applyAfterLuminoChildCreation(el, (target, child) => { 169 | target.lumino.addWidget(child.lumino); 170 | child.lumino.node.addEventListener('lumino:deleted', target.dash.handleWidgetEvent); 171 | child.lumino.node.addEventListener('lumino:activated', target.dash.handleWidgetEvent); 172 | target.lumino.selectWidget(child.lumino); 173 | 174 | }); 175 | this.added_ids.push(props_id(el.props._dashprivate_layout)); 176 | 177 | const { setProps } = this.props; 178 | setTimeout(() => { 179 | setProps({ 180 | widgetEvent: { 181 | id: props_id(el.props._dashprivate_layout), 182 | type: "lumino:activated", 183 | timestamp: +new Date 184 | } 185 | }); 186 | }, 100) 187 | 188 | } 189 | } 190 | 191 | }); 192 | 193 | //check if we have widgets in the list, which need to be closed 194 | let widgets_to_delete = this.added_ids.filter(el => !current_ids.includes(el)); 195 | 196 | //dispose all the components created for the widget 197 | widgets_to_delete.forEach(el => { 198 | components[el].lumino.dispose(); 199 | delete components[el].lumino; 200 | delete components[el].dash; 201 | delete components[el].div; 202 | delete components[el]; 203 | 204 | this.added_ids = this.added_ids.filter(id => id !== el); 205 | }); 206 | 207 | 208 | } else { 209 | // if the children parameter is empty or unset, all open widgets have to be deleted 210 | this.added_ids.forEach(el => { 211 | components[el].lumino.dispose(); 212 | delete components[el].lumino; 213 | delete components[el].dash; 214 | delete components[el].div; 215 | delete components[el]; 216 | }); 217 | this.added_ids = []; 218 | } 219 | 220 | 221 | return super.render(); 222 | } 223 | 224 | } 225 | 226 | DockPanel.defaultProps = { 227 | mode: 'multiple-document', 228 | spacing: 4, 229 | addToDom: false, 230 | }; 231 | 232 | /** 233 | * @typedef 234 | * @enum {} 235 | */ 236 | DockPanel.propTypes = { 237 | /** 238 | * ID of the widget 239 | * @type {string} 240 | */ 241 | id: PropTypes.string.isRequired, 242 | 243 | /** 244 | * mode for the dock panel: ("single-document" | "multiple-document") 245 | * @type {string} 246 | */ 247 | mode: PropTypes.string, 248 | 249 | /** 250 | * The spacing between the items in the panel. 251 | * @type {number} 252 | */ 253 | spacing: PropTypes.number, 254 | 255 | /** 256 | * bool if the object has to be added to the dom directly 257 | * @type {boolean} 258 | */ 259 | addToDom: PropTypes.bool, 260 | 261 | /** 262 | * The widgets 263 | * @type {Widget[]} 264 | */ 265 | children: PropTypes.node, 266 | 267 | 268 | /** 269 | * Widget events 270 | * @type {PropTypes.any} 271 | */ 272 | widgetEvent: PropTypes.any, 273 | 274 | 275 | /** 276 | * Layout similar to DockPanel.ILayoutConfig (https://phosphorjs.github.io/phosphor/api/widgets/interfaces/docklayout.ilayoutconfig.html) 277 | * 278 | * Examples: 279 | * * {"main": {"type": "tab-area", "widgets": ["initial-widget2", "initial-widget"], "currentIndex": 1}} 280 | * * {"main": {"type": "split-area", "orientation": "horizontal", "children": [{"type": "tab-area", "widgets": ["initial-widget2"], "currentIndex": 0}, {"type": "tab-area", "widgets": ["initial-widget"], "currentIndex": 0}], "sizes": [0.5, 0.5]}} 281 | * * {"main": {"type": "split-area", "orientation": "vertical", "children": [{"type": "tab-area", "widgets": ["initial-widget2"], "currentIndex": 0}, {"type": "tab-area", "widgets": ["initial-widget"], "currentIndex": 0}], "sizes": [0.5, 0.5]}} 282 | * 283 | * Note! Use widget id in widget arrays! 284 | * 285 | * @type {PropTypes.any} 286 | */ 287 | layout: PropTypes.any, 288 | 289 | 290 | /** 291 | * Dash-assigned callback that should be called to report property changes 292 | * to Dash, to make them available for callbacks. 293 | * @private 294 | */ 295 | setProps: PropTypes.func 296 | }; 297 | 298 | /** 299 | * @private 300 | */ 301 | export default DockPanel; 302 | -------------------------------------------------------------------------------- /src/lib/components/Menu.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | import { commands, props_id } from '../registry'; 4 | 5 | import { 6 | Menu as l_Menu 7 | } from '@lumino/widgets'; 8 | 9 | /** 10 | * A widget which displays items as a canonical menu. 11 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/menu.html} 12 | * @hideconstructor 13 | * 14 | * @example 15 | * //Python: 16 | * import dash 17 | * import dash_lumino_components as dlc 18 | * 19 | 20 | * menu = dlc.Menu([ 21 | * dlc.Command(id="com:openwidget", label="Open", icon="fa fa-plus"), 22 | * dlc.Separator(), 23 | * dlc.Menu([ 24 | * dlc.Command(id="com:closeall", label="Close All", icon="fa fa-minus"), 25 | * dlc.Command(id="com:closeone", 26 | * label="Close One", icon="fa fa-minus"), 27 | * ], id="extraMenu", title="Extra") 28 | * ], id="openMenu", title="Widgets") 29 | */ 30 | class Menu extends DashLuminoComponent { 31 | 32 | constructor(props) { 33 | super(props); 34 | 35 | // register a new Menu 36 | let menu = super.register(new l_Menu({ commands })); 37 | 38 | // set the properties 39 | menu.title.label = props.title; 40 | menu.title.iconClass = props.iconClass; 41 | 42 | 43 | // handle all children 44 | if (this.props.children) { 45 | super.parseChildrenToArray().forEach(reactElement => { 46 | const el = reactElement.props._dashprivate_layout; 47 | 48 | if (el.namespace === "dash_lumino_components") { 49 | if (el.type === "Command") { 50 | menu.addItem({ command: props_id(el) }); 51 | } 52 | if (el.type === "Separator") { 53 | menu.addItem({ type: 'separator' }); 54 | } 55 | if (el.type === "Menu") { 56 | super.applyAfterLuminoChildCreation(reactElement, (target, child) => { 57 | target.lumino.addItem({ type: 'submenu', submenu: child.lumino }); 58 | }); 59 | } 60 | } 61 | }) 62 | } 63 | 64 | 65 | 66 | } 67 | 68 | 69 | render() { 70 | return super.render(); 71 | } 72 | 73 | } 74 | 75 | Menu.defaultProps = { 76 | }; 77 | 78 | /** 79 | * @typedef 80 | * @enum {} 81 | */ 82 | Menu.propTypes = { 83 | 84 | /** 85 | * The ID used to identify this component in Dash callbacks. 86 | * @type {string} 87 | */ 88 | id: PropTypes.string.isRequired, 89 | 90 | /** 91 | * The title of the menu 92 | * @type {string} 93 | */ 94 | title: PropTypes.string, 95 | 96 | /** 97 | * The icon class of the menu 98 | * @type {string} 99 | */ 100 | iconClass: PropTypes.string, 101 | 102 | /** 103 | * An array of the menu items (dlc.Command | dlc.Menu | dlc.Separator) 104 | * @type {Array} 105 | */ 106 | children: PropTypes.node, 107 | }; 108 | 109 | /** 110 | * @private 111 | */ 112 | export default Menu; -------------------------------------------------------------------------------- /src/lib/components/MenuBar.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | 4 | import { 5 | MenuBar as l_MenuBar 6 | } from '@lumino/widgets'; 7 | 8 | /** 9 | * A widget which displays menus as a canonical menu bar. 10 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/menubar.html} 11 | * @hideconstructor 12 | * 13 | * @example 14 | * //Python: 15 | * import dash 16 | * import dash_lumino_components as dlc 17 | * 18 | * menuBar = dlc.MenuBar([ 19 | * dlc.Menu([ 20 | * dlc.Command(id="com:openwidget", label="Open", icon="fa fa-plus"), 21 | * ], id="exampleMenu", title="Example") 22 | * ], 'menuBar') 23 | */ 24 | class MenuBar extends DashLuminoComponent { 25 | 26 | constructor(props) { 27 | super(props); 28 | 29 | // register a new MenuBar 30 | super.register(new l_MenuBar, true); 31 | 32 | // add the children of the component to the menus of the MenuBar 33 | if (this.props.children) { 34 | super.parseChildrenToArray().forEach(el => { 35 | super.applyAfterLuminoChildCreation(el, (target, child) => { 36 | target.lumino.addMenu(child.lumino); 37 | }); 38 | }) 39 | } 40 | 41 | } 42 | 43 | 44 | render() { 45 | return super.render(); 46 | } 47 | 48 | } 49 | 50 | MenuBar.defaultProps = { 51 | }; 52 | 53 | /** 54 | * @typedef 55 | * @enum {} 56 | */ 57 | MenuBar.propTypes = { 58 | /** 59 | * ID of the widget 60 | * @type {string} 61 | */ 62 | id: PropTypes.string.isRequired, 63 | 64 | /** 65 | * An array of the menus (dlc.Menu) 66 | * @type {Menu[]} 67 | */ 68 | children: PropTypes.node 69 | }; 70 | 71 | /** 72 | * @private 73 | */ 74 | export default MenuBar; -------------------------------------------------------------------------------- /src/lib/components/Panel.react.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import DashLuminoComponent from '../component.js' 4 | import { get_id } from '../registry.js'; 5 | 6 | import { 7 | Panel as l_Panel 8 | } from '@lumino/widgets'; 9 | 10 | /** 11 | * A simple and convenient panel widget class. 12 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/panel.html} 13 | * 14 | * This class is suitable to directly display a collection of dash widgets. 15 | * @hideconstructor 16 | * 17 | * @example 18 | * //Python: 19 | * import dash_lumino_components as dlc 20 | * import dash_html_components as html 21 | * 22 | * panelA = dlc.Panel( 23 | * id="panelA", 24 | * children=html.Div("Content"), 25 | * label="Test", 26 | * icon="fa fa-plus") 27 | * 28 | * panelB = dlc.Panel( 29 | * [ 30 | * html.Div("Content") 31 | * ], 32 | * id="panelB", 33 | * label="Test", 34 | * icon="fa fa-plus") 35 | */ 36 | class Panel extends DashLuminoComponent { 37 | 38 | constructor(props) { 39 | super(props); 40 | 41 | // register a new Panel 42 | let luminoComponent = super.register(new l_Panel(), props.addToDom); 43 | 44 | // set properties 45 | luminoComponent.title.label = props.label; 46 | luminoComponent.title.iconClass = props.icon; 47 | 48 | // the component will initially be renderd in an hidden container, 49 | // then it's moved to the right dom location of lumino 50 | this.containerName = get_id(props) + "-container"; 51 | 52 | // add the children of the component to the widgets of the panel 53 | if (this.props.children) { 54 | super.parseChildrenToArray().forEach(el => { 55 | super.applyAfterDomCreation(el, this.containerName, (target, child) => { 56 | target.div = child; 57 | try { 58 | target.lumino.node.appendChild(child.children[0]); 59 | } catch (error) { 60 | } 61 | }); 62 | }) 63 | } 64 | 65 | } 66 | 67 | 68 | render() { 69 | return ( 70 |
81 |
82 | {this.props.children} 83 |
84 |
85 | ); 86 | } 87 | 88 | } 89 | 90 | Panel.defaultProps = { 91 | addToDom: false, 92 | }; 93 | 94 | /** 95 | * @typedef 96 | * @enum {} 97 | */ 98 | Panel.propTypes = { 99 | /** 100 | * ID of the widget 101 | * @type {string} 102 | */ 103 | id: PropTypes.string.isRequired, 104 | 105 | /** 106 | * The label of the panel 107 | * @type {string} 108 | */ 109 | label: PropTypes.string, 110 | 111 | 112 | /** 113 | * The icon of the panel (a cass class name) 114 | * @type {string} 115 | */ 116 | icon: PropTypes.string, 117 | 118 | /** 119 | * bool if the object has to be added to the dom directly 120 | * @type {boolean} 121 | */ 122 | addToDom: PropTypes.bool, 123 | 124 | /** 125 | * The widgets 126 | * @type {Object | Array} 127 | */ 128 | children: PropTypes.node 129 | }; 130 | 131 | /** 132 | * @private 133 | */ 134 | export default Panel; -------------------------------------------------------------------------------- /src/lib/components/Separator.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Component } from 'react'; 3 | 4 | /** 5 | * A dummy widget to create a seperation in menus. 6 | * This is actually not a component of lumino. 7 | * @hideconstructor 8 | * 9 | * @example 10 | * //Python: 11 | * import dash 12 | * import dash_lumino_components as dlc 13 | * 14 | * menu = dlc.Menu([ 15 | * dlc.Command(id="com:openwidget", label="Open", icon="fa fa-plus"), 16 | * dlc.Separator(), 17 | * dlc.Command(id="com:closeall", label="Close All", icon="fa fa-minus") 18 | * ], id="openMenu", title="File") 19 | */ 20 | class Separator extends Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | render() { 27 | return ""; 28 | } 29 | } 30 | 31 | Separator.defaultProps = {}; 32 | 33 | /** 34 | * @typedef 35 | * @enum {} 36 | */ 37 | Separator.propTypes = { 38 | /** 39 | * The id of the separator 40 | * @type {string} 41 | */ 42 | id: PropTypes.string, 43 | }; 44 | 45 | /** 46 | * @private 47 | */ 48 | export default Separator; -------------------------------------------------------------------------------- /src/lib/components/SplitPanel.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | import { components, props_id } from '../registry.js'; 4 | 5 | import { 6 | SplitPanel as l_SplitPanel, 7 | Panel as l_Panel, 8 | DockPanel, 9 | TabPanel, 10 | BoxPanel, 11 | Panel 12 | } from '@lumino/widgets'; 13 | 14 | /** 15 | * A panel which arranges its widgets into resizable sections. 16 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/splitpanel.html} 17 | * @hideconstructor 18 | * 19 | * @example 20 | * //Python: 21 | * import dash 22 | * import dash_lumino_components as dlc 23 | * 24 | * splitPanel = dlc.SplitPanel([ 25 | * dlc.TabPanel([], id="tab-panel"), 26 | * dlc.DockPanel([], id="dock-panel") 27 | * ], id="split-panel") 28 | */ 29 | class SplitPanel extends DashLuminoComponent { 30 | 31 | constructor(props) { 32 | super(props); 33 | 34 | // register a new SplitPanel 35 | let luminoComponent = super.register(new l_SplitPanel({ 36 | alignment: props.alignment, 37 | orientation: props.orientation, 38 | spacing: props.spacing 39 | }), props.addToDom, false); 40 | 41 | // do a special resize to keep the size of the fixed 42 | // side panels at the same size 43 | let that = this; 44 | window.addEventListener("resize", function () { 45 | luminoComponent.update(); 46 | that.updateSizes(); 47 | }); 48 | 49 | // add the children of the component to the widgets of the panel 50 | if (this.props.children) { 51 | super.parseChildrenToArray().forEach(el => { 52 | super.applyAfterLuminoChildCreation(el, (target, child) => { 53 | 54 | 55 | 56 | let tabBar = child.lumino.tabBar; 57 | let stackedPanel = child.lumino.stackedPanel; 58 | let tabPlacement = child.lumino.tabPlacement; 59 | if (tabBar && stackedPanel && tabPlacement && 60 | (tabPlacement === "left" || tabPlacement === "right")) { 61 | 62 | // add ids to the special items of the TabPanel 63 | stackedPanel.id = props_id(child.dash) + "-stackedPanel"; 64 | tabBar.id = props_id(child.dash) + "-tabBar"; 65 | 66 | // add the content panel directly 67 | target.lumino.addWidget(stackedPanel); 68 | 69 | 70 | //create an extra panel for the tabbar 71 | const tabBarContainer = new l_Panel(); 72 | tabBarContainer.addClass('tabBarContainer'); 73 | tabBarContainer.addWidget(tabBar); 74 | tabBar.addClass('lm-SideBar'); 75 | tabBar.addClass('lm-mod-' + tabPlacement); 76 | 77 | //store the original component order 78 | let oldWidgets = new Array(...target.lumino.parent.widgets); 79 | 80 | //add the tabbar panel to the parent container 81 | target.lumino.parent.addWidget(tabBarContainer); 82 | 83 | //if the tabpanel is placed on the left, we can enshure this 84 | // by adding all old panels again 85 | if (tabPlacement === "left") { 86 | oldWidgets.forEach(el => { 87 | target.lumino.parent.addWidget(el); 88 | }) 89 | } 90 | 91 | } else { 92 | // in all other cases just add the lumino widget to the container 93 | target.lumino.addWidget(child.lumino); 94 | } 95 | }); 96 | }) 97 | } 98 | 99 | } 100 | 101 | 102 | /** 103 | * Redistribute the sizes if the total size chanches. Panel sizes of split panes are 104 | * taken from the width parameter, if they are not hidden. 105 | * Panels with undefined width are equally spread. 106 | * @private 107 | */ 108 | updateSizes() { 109 | 110 | let w = components[this.id].lumino.widgets.map(el => { 111 | if (el.isHidden) { 112 | return 0; 113 | } else { 114 | return components[el.id.replace("-stackedPanel", "")].dash.props.width; 115 | } 116 | }) 117 | let sum_w = sum_w = w.reduce((a, c) => Number.isFinite(c) ? a + c : a, 0); 118 | let count_undef = w.reduce((a, c) => Number.isFinite(c) ? a : a + 1, 0); 119 | let replaceSize = (components[this.id].dash.props.orientation === 'horizontal') ? (window.innerWidth - sum_w) / count_undef : (window.innerHeight - sum_w) / count_undef; 120 | replaceSize = Math.max(replaceSize, 100); 121 | let sizes = w.map(el => Number.isFinite(el) ? el : replaceSize); 122 | 123 | components[this.id].lumino.setRelativeSizes(sizes); 124 | } 125 | 126 | 127 | render() { 128 | return super.render(); 129 | } 130 | 131 | } 132 | 133 | SplitPanel.defaultProps = { 134 | alignment: 'start', 135 | orientation: 'horizontal', 136 | spacing: 0, 137 | addToDom: false, 138 | }; 139 | 140 | /** 141 | * @typedef 142 | * @enum {} 143 | */ 144 | SplitPanel.propTypes = { 145 | /** 146 | * ID of the widget 147 | * @type {string} 148 | */ 149 | id: PropTypes.string.isRequired, 150 | 151 | /** 152 | * the content alignment of the layout ("start" | "center" | "end" | "justify") 153 | * @type {string} 154 | */ 155 | alignment: PropTypes.string, 156 | 157 | /** 158 | * a type alias for a split layout orientation ("horizontal" | "vertical") 159 | * @type {string} 160 | */ 161 | orientation: PropTypes.string, 162 | 163 | /** 164 | * The spacing between items in the layout 165 | * @type {number} 166 | */ 167 | spacing: PropTypes.number, 168 | 169 | /** 170 | * bool if the object has to be added to the dom directly 171 | * @type {boolean} 172 | */ 173 | addToDom: PropTypes.bool, 174 | 175 | /** 176 | * The widgets 177 | * @type {Array} 178 | */ 179 | children: PropTypes.node 180 | }; 181 | 182 | 183 | /** 184 | * @private 185 | */ 186 | export default SplitPanel; -------------------------------------------------------------------------------- /src/lib/components/TabPanel.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import DashLuminoComponent from '../component.js' 3 | import { components } from '../registry.js'; 4 | 5 | import { 6 | TabPanel as l_TabPanel 7 | } from '@lumino/widgets'; 8 | 9 | /** 10 | * A widget which combines a TabBar and a StackedPanel. 11 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/tabpanel.html} 12 | * 13 | * This is a simple panel which handles the common case of a tab bar placed 14 | * next to a content area. The selected tab controls the widget which is 15 | * shown in the content area. 16 | * For use cases which require more control than is provided by this panel, 17 | * the TabBar widget may be used independently. 18 | * @hideconstructor 19 | * 20 | * @example 21 | * //Python: 22 | * import dash 23 | * import dash_lumino_components as dlc 24 | * import dash_html_components as html 25 | * import dash_bootstrap_components as dbc 26 | * 27 | * tabPanel = dlc.TabPanel( 28 | * [ 29 | * dlc.Panel( 30 | * html.Div([ 31 | * dbc.Button("Open Plot", 32 | * id="button2", 33 | * style={"width": "100%"}) 34 | * ]), 35 | * id="tab-panel-A" 36 | * label="Plots", 37 | * icon="fa fa-bar-chart") 38 | * ], 39 | * id='tab-panel-left') 40 | */ 41 | class TabPanel extends DashLuminoComponent { 42 | 43 | constructor(props) { 44 | super(props); 45 | 46 | // register a new TabPanel 47 | let luminoComponent = super.register(new l_TabPanel({ 48 | tabPlacement: props.tabPlacement, 49 | tabsMovable: props.tabsMovable, 50 | }), props.addToDom); 51 | 52 | // set additional properties 53 | luminoComponent.tabBar.allowDeselect = props.allowDeselect; 54 | luminoComponent.currentIndex = props.currentIndex; 55 | if (props.allowDeselect) { 56 | // if the user is allow to deselect items, panel should hide if nothing is selected 57 | // this is handeled by the _onTabIndexChanged callback 58 | luminoComponent.currentChanged.connect( 59 | this._onTabIndexChanged, 60 | this 61 | ); 62 | 63 | setTimeout(this._onTabIndexChanged, 100); 64 | } 65 | 66 | if (props.currentIndex == -1) { 67 | // set the current index if it is not -1 after the component is created 68 | function setIndex() { 69 | luminoComponent.currentIndex = -1; 70 | } 71 | setTimeout(setIndex, 10); 72 | setTimeout(setIndex, 20); 73 | setTimeout(setIndex, 50); 74 | setTimeout(setIndex, 100); 75 | } 76 | 77 | // we need to update the tabpanel width once the user resizes the window 78 | let that = this; 79 | let st_panel = luminoComponent.stackedPanel; 80 | luminoComponent.stackedPanel.onResize = (msg) => { 81 | 82 | const { setProps } = that.props; 83 | try 84 | { 85 | let rel_sizes = st_panel.parent.relativeSizes(); 86 | let idx = st_panel.parent.widgets.indexOf(st_panel); 87 | 88 | if (idx != -1) { 89 | setProps({ width: rel_sizes[idx] * window.innerWidth }); 90 | } 91 | } catch (e) { 92 | } 93 | }; 94 | 95 | 96 | 97 | //add the children of the component to the widgets of the panel 98 | if (this.props.children) { 99 | super.parseChildrenToArray().forEach(el => { 100 | super.applyAfterLuminoChildCreation(el, (target, child) => { 101 | target.lumino.addWidget(child.lumino); 102 | target.lumino.currentIndex = target.dash.props.currentIndex; 103 | }); 104 | }) 105 | } 106 | 107 | } 108 | 109 | /** 110 | * this callback hids the stackedpanel if no tab is selected 111 | * to make everything seamless, the resizing is handeled here 112 | * @private 113 | */ 114 | _onTabIndexChanged() { 115 | if (components[this.id] == null) 116 | return; 117 | 118 | let tabBar = components[this.id].lumino.tabBar; 119 | let stackedPanel = components[this.id].lumino.stackedPanel; 120 | 121 | if (tabBar != null && stackedPanel != null) { 122 | 123 | let { setProps } = components[this.id].dash.props; 124 | setProps({ currentIndex: tabBar.currentIndex }); 125 | 126 | if (tabBar.currentIndex == -1) { 127 | let sizes = stackedPanel.parent.relativeSizes(); 128 | stackedPanel.hide(); 129 | } else { 130 | stackedPanel.show(); 131 | } 132 | try { 133 | components[stackedPanel.parent.id].dash.updateSizes(); 134 | } catch (e) {} 135 | } else { 136 | return; 137 | } 138 | } 139 | 140 | 141 | render() { 142 | if (components[this.id]) { 143 | components[this.id].lumino.currentIndex = this.props.currentIndex; 144 | } 145 | return super.render(); 146 | } 147 | 148 | } 149 | 150 | TabPanel.defaultProps = { 151 | tabPlacement: 'top', 152 | tabsMovable: false, 153 | allowDeselect: false, 154 | addToDom: false, 155 | width: 250, 156 | currentIndex: -1 157 | }; 158 | 159 | /** 160 | * @typedef 161 | * @enum {} 162 | */ 163 | TabPanel.propTypes = { 164 | /** 165 | * ID of the widget 166 | * @type {string} 167 | */ 168 | id: PropTypes.string.isRequired, 169 | 170 | /** 171 | * the placement of the tab bar relative to the content. ("left" | "right" | "top" | "bottom") 172 | * @type {string} 173 | */ 174 | tabPlacement: PropTypes.string, 175 | 176 | /** 177 | * whether the tabs are movable by the user 178 | * @type {boolean} 179 | */ 180 | tabsMovable: PropTypes.bool, 181 | 182 | /** 183 | * bool if all tabs can be deselected 184 | * @type {boolean} 185 | */ 186 | allowDeselect: PropTypes.bool, 187 | 188 | /** 189 | * the default width or height of the tab panel content 190 | * @type {number} 191 | */ 192 | width: PropTypes.number, 193 | 194 | /** 195 | * bool if the object has to be added to the dom directly 196 | * @type {boolean} 197 | */ 198 | addToDom: PropTypes.bool, 199 | 200 | /** 201 | * The widgets 202 | * @type {Panel[]} 203 | */ 204 | children: PropTypes.node, 205 | 206 | /** 207 | * Get the index of the currently selected tab. It will be -1 if no tab is selected. 208 | * @type {number} 209 | */ 210 | currentIndex: PropTypes.number, 211 | 212 | /** 213 | * Dash-assigned callback that should be called to report property changes 214 | * to Dash, to make them available for callbacks. 215 | * @private 216 | */ 217 | setProps: PropTypes.func 218 | }; 219 | 220 | 221 | /** 222 | * @private 223 | */ 224 | export default TabPanel; 225 | -------------------------------------------------------------------------------- /src/lib/components/Widget.react.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import DashLuminoComponent from '../component.js' 4 | 5 | import { 6 | Widget as l_Widget 7 | } from '@lumino/widgets'; 8 | import { components, get_id } from '../registry.js'; 9 | 10 | 11 | /** 12 | * Wrap the default Lumino Widget with an event once the widget is closed 13 | * @private 14 | */ 15 | class LuminoWidget extends l_Widget { 16 | 17 | constructor(props) { 18 | super(props) 19 | } 20 | 21 | onCloseRequest(msg) { 22 | const dom_element = document.getElementById(this.id); 23 | const event = new CustomEvent('lumino:deleted', { msg: msg, id: this.id }); 24 | 25 | //Note: this might dissapear if widgets can be deleted from the DockPanel 26 | // children in the future! 27 | let { setProps } = components[this.id].dash.props; 28 | setProps({ children: [], deleted: true }); 29 | 30 | 31 | dom_element.dispatchEvent(event); 32 | super.onCloseRequest(msg); 33 | } 34 | 35 | onActivateRequest(msg) { 36 | const dom_element = document.getElementById(this.id); 37 | const event = new CustomEvent('lumino:activated', { msg: msg, id: this.id }); 38 | dom_element.dispatchEvent(event); 39 | super.onActivateRequest(msg); 40 | } 41 | 42 | 43 | }; 44 | 45 | 46 | /** 47 | * The base class of the lumino widget hierarchy. 48 | * {@link https://jupyterlab.github.io/lumino/widgets/classes/widget.html} 49 | * 50 | * This class will typically be subclassed in order to create a useful 51 | * widget. However, it can be used directly to host externally created 52 | * content. 53 | * @hideconstructor 54 | * 55 | * @example 56 | * //Python: 57 | * import dash 58 | * import dash_lumino_components as dlc 59 | * 60 | * dock = dlc.DockPanel([ 61 | * dlc.Widget( 62 | * "Content", 63 | * id="test-widget", 64 | * title="Title", 65 | * icon="fa fa-folder-open", 66 | * closable=True, 67 | * caption="Hover label of the widget" 68 | * )], 69 | * id="dock-panel") 70 | */ 71 | class Widget extends DashLuminoComponent { 72 | 73 | 74 | constructor(props) { 75 | super(props); 76 | 77 | // register a new Panel 78 | let luminoComponent = super.register(new LuminoWidget({ node: document.createElement('div') })); 79 | 80 | // set properties 81 | luminoComponent.title.label = props.title; 82 | luminoComponent.title.closable = props.closable; 83 | luminoComponent.title.caption = props.caption; 84 | luminoComponent.title.iconClass = props.icon 85 | 86 | // the component will initially be renderd in an hidden container, 87 | // then it's moved to the right dom location of lumino 88 | this.containerName = get_id(props) + "-container"; 89 | 90 | 91 | // add the children of the component to the widgets of the panel 92 | if (this.props.children) { 93 | super.parseChildrenToArray().forEach(el => { 94 | super.applyAfterDomCreation(el, this.containerName, (target, child) => { 95 | target.div = child; 96 | target.lumino.node.appendChild(child.children[0]); 97 | }); 98 | }) 99 | } 100 | 101 | } 102 | 103 | 104 | render() { 105 | return ( 106 |
117 |
118 | {this.props.children} 119 |
120 |
121 | ); 122 | } 123 | 124 | } 125 | 126 | Widget.defaultProps = { 127 | closable: true, 128 | deleted: false 129 | }; 130 | 131 | /** 132 | * @typedef 133 | * @enum {} 134 | */ 135 | Widget.propTypes = { 136 | /** 137 | * ID of the widget 138 | * @type {string} 139 | */ 140 | id: PropTypes.string.isRequired, 141 | 142 | /** 143 | * The children of this component 144 | * @type {Object | Object[]} 145 | */ 146 | children: PropTypes.node, 147 | 148 | /** 149 | * The title of the widget 150 | * @type {string} 151 | */ 152 | title: PropTypes.string.isRequired, 153 | 154 | /** 155 | * Is the widget closable 156 | * @type {boolean} 157 | */ 158 | closable: PropTypes.bool, 159 | 160 | /** 161 | * The long title of the widget 162 | * @type {string} 163 | */ 164 | caption: PropTypes.string, 165 | 166 | /** 167 | * Is the widget deleted. 168 | * Note: In the future this might dissapear and the deleted widgets are 169 | * automatically removed from the dom. 170 | * @type {boolean} 171 | */ 172 | deleted: PropTypes.bool, 173 | 174 | /** 175 | * The icon of the widget (a cass class name) 176 | * @type {string} 177 | */ 178 | icon: PropTypes.string, 179 | 180 | /** 181 | * Dash-assigned callback that should be called to report property changes 182 | * to Dash, to make them available for callbacks. 183 | * @private 184 | */ 185 | setProps: PropTypes.func 186 | }; 187 | 188 | 189 | /** 190 | * @private 191 | */ 192 | export default Widget; -------------------------------------------------------------------------------- /src/lib/components/css/defaults.css: -------------------------------------------------------------------------------- 1 | .lm-Widget { 2 | box-sizing: border-box; 3 | position: relative; 4 | overflow: hidden; 5 | cursor: default 6 | } 7 | 8 | .lm-DockPanel-handle.lm-mod-hidden,.lm-Widget.lm-mod-hidden { 9 | display: none!important 10 | } 11 | 12 | .lm-DockPanel { 13 | z-index: 0; 14 | flex: 1 1 auto 15 | } 16 | 17 | .lm-DockPanel-widget { 18 | z-index: 0 19 | } 20 | 21 | .lm-DockPanel-tabBar { 22 | z-index: 1 23 | } 24 | 25 | .lm-DockPanel-handle { 26 | z-index: 2 27 | } 28 | 29 | .lm-DockPanel-handle:after { 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | width: 100%; 34 | height: 100%; 35 | content: '' 36 | } 37 | 38 | .lm-DockPanel-handle[data-orientation=horizontal] { 39 | cursor: ew-resize 40 | } 41 | 42 | .lm-DockPanel-handle[data-orientation=vertical] { 43 | cursor: ns-resize 44 | } 45 | 46 | .lm-DockPanel-handle[data-orientation=horizontal]:after { 47 | left: 50%; 48 | min-width: 8px; 49 | transform: translateX(-50%) 50 | } 51 | 52 | .lm-DockPanel-handle[data-orientation=vertical]:after { 53 | top: 50%; 54 | min-height: 8px; 55 | transform: translateY(-50%) 56 | } 57 | 58 | .lm-DockPanel-overlay { 59 | z-index: 3; 60 | box-sizing: border-box; 61 | pointer-events: none; 62 | background: rgba(255,255,255,.6); 63 | border: 1px dashed #000; 64 | transition-property: top,left,right,bottom; 65 | transition-duration: 150ms; 66 | transition-timing-function: ease 67 | } 68 | 69 | .lm-DockPanel-overlay.lm-mod-hidden { 70 | display: none!important 71 | } 72 | 73 | .lm-TabBar { 74 | min-height: 30px; 75 | max-height: 30px 76 | } 77 | 78 | .lm-TabBar li { 79 | margin: 0 80 | } 81 | 82 | .lm-TabBar-content { 83 | min-width: 0; 84 | min-height: 0; 85 | align-items: flex-end; 86 | border-bottom: 1px solid silver 87 | } 88 | 89 | .lm-TabBar[data-placement=bottom] .lm-TabBar-content { 90 | border: unset; 91 | border-top: 1px solid silver 92 | } 93 | 94 | .lm-TabBar-tab { 95 | padding: 0 10px; 96 | background: #e5e5e5; 97 | border: 1px solid silver; 98 | border-bottom: none; 99 | font: 18px; 100 | flex: 0 1 155px; 101 | min-height: 25px; 102 | max-height: 25px; 103 | min-width: 35px; 104 | margin-left: -1px; 105 | line-height: 25px; 106 | box-shadow: 0 1px 4px 0 rgba(0,0,0,.3); 107 | border-top-left-radius: 5px; 108 | border-top-right-radius: 5px 109 | } 110 | 111 | .lm-TabBar-tabLabel .lm-TabBar-tabInput { 112 | padding: 0; 113 | border: 0; 114 | font: 18px 115 | } 116 | 117 | .lm-TabBar-tab:hover:not(.lm-mod-current) { 118 | background: #f0f0f0 119 | } 120 | 121 | .lm-TabBar-tab:first-child { 122 | margin-left: 0 123 | } 124 | 125 | .lm-TabBar-tab.lm-mod-current { 126 | min-height: 28px; 127 | max-height: 28px; 128 | background: #fff; 129 | transform: translateY(1px); 130 | border-top: 2px solid #119dff 131 | } 132 | 133 | .lm-TabPanel .lm-TabBar { 134 | margin-top: 1px 135 | } 136 | 137 | .lm-TabPanel .lm-TabBar[data-placement=bottom] { 138 | margin-top: -1px 139 | } 140 | 141 | .lm-TabPanel .lm-TabBar[data-placement=bottom] .lm-TabBar-tab { 142 | transform: translateY(-3px); 143 | border-top: unset; 144 | border-radius: 0 0 5px 5px 145 | } 146 | 147 | .lm-TabPanel .lm-TabBar[data-placement=bottom] .lm-TabBar-tab.lm-mod-current { 148 | transform: translateY(-2px); 149 | border-bottom: 2px solid #119dff; 150 | border-radius: 0 0 5px 5px 151 | } 152 | 153 | .lm-TabBar-tabCloseIcon,.lm-TabBar-tabIcon,.lm-TabBar-tabLabel { 154 | display: inline-block 155 | } 156 | 157 | .lm-TabBar-tabIcon { 158 | margin-top: 7px; 159 | padding-right: 3px 160 | } 161 | 162 | .lm-TabBar-tabCloseIcon,.lm-TabBar-tabLabel { 163 | margin-top: 2px 164 | } 165 | 166 | .lm-TabBar-tab.lm-mod-closable>.lm-TabBar-tabCloseIcon { 167 | margin-left: 4px 168 | } 169 | 170 | .lm-TabBar-tab.lm-mod-closable>.lm-TabBar-tabCloseIcon:before { 171 | content: '\f00d'; 172 | font-family: FontAwesome 173 | } 174 | 175 | .lm-TabBar-tab.lm-mod-drag-image { 176 | min-height: 26px; 177 | max-height: 26px; 178 | min-width: 155px; 179 | border: 0; 180 | box-shadow: 1px 1px 2px rgba(0,0,0,.3); 181 | transform: translateX(-40%) translateY(-58%) 182 | } 183 | 184 | .lm-Menu { 185 | z-index: 10000; 186 | position: absolute; 187 | white-space: nowrap; 188 | overflow-x: hidden; 189 | overflow-y: auto; 190 | outline: 0; 191 | -webkit-user-select: none; 192 | -moz-user-select: none; 193 | -ms-user-select: none; 194 | user-select: none 195 | } 196 | 197 | .lm-Menu-content { 198 | margin: 0; 199 | padding: 0; 200 | display: table; 201 | list-style-type: none 202 | } 203 | 204 | .lm-Menu-item { 205 | display: table-row; 206 | outline: 0; 207 | border: none; 208 | } 209 | 210 | .lm-Menu-item:active, .lm-Menu-item:focus, .lm-Menu-item:hover, .lm-Menu-item:visited { 211 | outline: 0; 212 | border: none; 213 | } 214 | 215 | .lm-Menu-item.lm-mod-collapsed,.lm-Menu-item.lm-mod-hidden { 216 | display: none!important 217 | } 218 | 219 | .lm-Menu-itemIcon,.lm-Menu-itemSubmenuIcon { 220 | display: table-cell; 221 | text-align: center 222 | } 223 | 224 | .lm-Menu-itemLabel { 225 | display: table-cell; 226 | text-align: left 227 | } 228 | 229 | .lm-Menu-itemShortcut { 230 | display: table-cell; 231 | text-align: right 232 | } 233 | 234 | .lm-Menu { 235 | padding: 3px 0; 236 | background: #fff; 237 | color: rgba(0,0,0,.87); 238 | border: 1px solid silver; 239 | font: 20px; 240 | box-shadow: 0 1px 6px rgba(0,0,0,.2) 241 | } 242 | 243 | .lm-Menu-item.lm-mod-active { 244 | background: #e5e5e5 245 | } 246 | 247 | .lm-Menu-item.lm-mod-disabled { 248 | color: rgba(0,0,0,.25) 249 | } 250 | 251 | .lm-Menu-itemIcon { 252 | width: 21px; 253 | padding: 4px 2px 254 | } 255 | 256 | .lm-Menu-itemLabel { 257 | padding: 4px 35px 4px 2px 258 | } 259 | 260 | .lm-Menu-itemMnemonic { 261 | text-decoration: underline 262 | } 263 | 264 | .lm-Menu-itemShortcut { 265 | padding: 4px 0 266 | } 267 | 268 | .lm-Menu-itemSubmenuIcon { 269 | width: 16px; 270 | padding: 4px 0 271 | } 272 | 273 | .lm-Menu-item[data-type=separator]>div { 274 | padding: 0; 275 | height: 9px 276 | } 277 | 278 | .lm-Menu-item[data-type=separator]>div::after { 279 | content: ''; 280 | display: block; 281 | position: relative; 282 | top: 4px; 283 | border-top: 1px solid #ddd 284 | } 285 | 286 | .lm-Menu-itemIcon::before,.lm-Menu-itemSubmenuIcon::before { 287 | font-family: FontAwesome 288 | } 289 | 290 | .lm-Menu-item.lm-mod-toggled>.lm-Menu-itemIcon::before { 291 | content: '\f00c' 292 | } 293 | 294 | .lm-Menu-item[data-type=submenu]>.lm-Menu-itemSubmenuIcon::before { 295 | content: '\f0da' 296 | } 297 | 298 | .lm-MenuBar { 299 | outline: 0; 300 | -webkit-user-select: none; 301 | -moz-user-select: none; 302 | -ms-user-select: none; 303 | user-select: none 304 | } 305 | 306 | .lm-MenuBar-content { 307 | margin: 0; 308 | padding: 0; 309 | display: flex; 310 | flex-direction: row; 311 | list-style-type: none 312 | } 313 | 314 | .lm-MenuBar-item { 315 | box-sizing: border-box 316 | } 317 | 318 | .lm-MenuBar-itemIcon,.lm-MenuBar-itemLabel { 319 | display: inline-block 320 | } 321 | 322 | .lm-MenuBar { 323 | padding-left: 5px; 324 | background: #fafafa; 325 | color: rgba(0,0,0,.87); 326 | border-bottom: 1px solid #ddd; 327 | font: 20px 328 | } 329 | 330 | .lm-MenuBar-menu { 331 | transform: translateY(-1px) 332 | } 333 | 334 | .lm-MenuBar-item { 335 | padding: 4px 8px; 336 | border-left: 1px solid transparent; 337 | border-right: 1px solid transparent; 338 | margin-bottom: 0; 339 | margin-top: 0 340 | } 341 | 342 | .lm-MenuBar-item.lm-mod-active { 343 | background: #e5e5e5 344 | } 345 | 346 | .lm-MenuBar.lm-mod-active .lm-MenuBar-item.lm-mod-active { 347 | z-index: 10001; 348 | background: #fff; 349 | border-left: 1px solid silver; 350 | border-right: 1px solid silver; 351 | box-shadow: 0 0 6px rgba(0,0,0,.2) 352 | } 353 | 354 | .lm-ScrollBar { 355 | display: flex; 356 | -webkit-user-select: none; 357 | -moz-user-select: none; 358 | -ms-user-select: none; 359 | user-select: none 360 | } 361 | 362 | .lm-ScrollBar[data-orientation=horizontal] { 363 | flex-direction: row 364 | } 365 | 366 | .lm-ScrollBar[data-orientation=vertical] { 367 | flex-direction: column 368 | } 369 | 370 | .lm-ScrollBar-button,.lm-ScrollBar-track { 371 | box-sizing: border-box; 372 | flex: 0 0 auto 373 | } 374 | 375 | .lm-ScrollBar-track { 376 | position: relative; 377 | overflow: hidden; 378 | flex: 1 1 auto 379 | } 380 | 381 | .lm-ScrollBar-thumb { 382 | box-sizing: border-box; 383 | position: absolute 384 | } 385 | 386 | .lm-SplitPanel-child { 387 | z-index: 0 388 | } 389 | 390 | .lm-SplitPanel-handle { 391 | z-index: 1 392 | } 393 | 394 | .lm-SplitPanel-handle.lm-mod-hidden { 395 | display: none!important 396 | } 397 | 398 | .lm-SplitPanel-handle:after { 399 | position: absolute; 400 | top: 0; 401 | left: 0; 402 | width: 100%; 403 | height: 100%; 404 | content: '' 405 | } 406 | 407 | .lm-SplitPanel[data-orientation=horizontal]>.lm-SplitPanel-handle { 408 | cursor: ew-resize 409 | } 410 | 411 | .lm-SplitPanel[data-orientation=vertical]>.lm-SplitPanel-handle { 412 | cursor: ns-resize 413 | } 414 | 415 | .lm-SplitPanel[data-orientation=horizontal]>.lm-SplitPanel-handle:after { 416 | left: 50%; 417 | min-width: 8px; 418 | transform: translateX(-50%) 419 | } 420 | 421 | .lm-SplitPanel[data-orientation=vertical]>.lm-SplitPanel-handle:after { 422 | top: 50%; 423 | min-height: 8px; 424 | transform: translateY(-50%) 425 | } 426 | 427 | .lm-TabBar { 428 | display: flex; 429 | -webkit-user-select: none; 430 | -moz-user-select: none; 431 | -ms-user-select: none; 432 | user-select: none 433 | } 434 | 435 | .lm-TabBar[data-orientation=horizontal],.lm-TabBar[data-orientation=horizontal]>.lm-TabBar-content { 436 | flex-direction: row 437 | } 438 | 439 | .lm-TabBar[data-orientation=vertical],.lm-TabBar[data-orientation=vertical]>.lm-TabBar-content { 440 | flex-direction: column 441 | } 442 | 443 | .lm-TabBar-content { 444 | margin: 0; 445 | padding: 0; 446 | display: flex; 447 | flex: 1 1 auto; 448 | list-style-type: none 449 | } 450 | 451 | .lm-TabBar-tab { 452 | display: flex; 453 | flex-direction: row; 454 | box-sizing: border-box; 455 | overflow: hidden 456 | } 457 | 458 | .lm-TabBar-tabCloseIcon,.lm-TabBar-tabIcon { 459 | flex: 0 0 auto 460 | } 461 | 462 | .lm-TabBar-tabLabel { 463 | flex: 1 1 auto; 464 | overflow: hidden; 465 | white-space: nowrap 466 | } 467 | 468 | .lm-TabBar-tabInput { 469 | user-select: all; 470 | width: 100%; 471 | box-sizing: border-box 472 | } 473 | 474 | .lm-TabBar-tab.lm-mod-hidden { 475 | display: none!important 476 | } 477 | 478 | .lm-TabBar.lm-mod-dragging .lm-TabBar-tab { 479 | position: relative 480 | } 481 | 482 | .lm-TabBar.lm-mod-dragging[data-orientation=horizontal] .lm-TabBar-tab { 483 | left: 0; 484 | transition: left 150ms ease 485 | } 486 | 487 | .lm-TabBar.lm-mod-dragging[data-orientation=vertical] .lm-TabBar-tab { 488 | top: 0; 489 | transition: top 150ms ease 490 | } 491 | 492 | .lm-TabBar.lm-mod-dragging .lm-TabBar-tab.lm-mod-dragging { 493 | transition: none 494 | } 495 | 496 | .lm-TabPanel-tabBar { 497 | z-index: 1 498 | } 499 | 500 | .lm-TabPanel .lm-TabBar-tab[data-placement=bottom]>.lm-TabBar-content { 501 | background: tomato 502 | } 503 | 504 | .lm-TabPanel-stackedPanel { 505 | z-index: 0 506 | } 507 | 508 | .lm-BoxPanel,.lm-SplitPanel { 509 | flex: 1 1 auto 510 | } 511 | 512 | .lm-DockPanel { 513 | padding: 4px 514 | } 515 | 516 | .lm-Panel,body { 517 | padding: 0; 518 | margin: 0 519 | } 520 | 521 | .lm-DockPanel-widget { 522 | box-shadow: 0 1px 4px 0 rgba(0,0,0,.3); 523 | border-bottom: 1px solid silver; 524 | border-left: 1px solid silver; 525 | border-right: 1px solid silver; 526 | } 527 | 528 | .lm-Widget { 529 | display: flex 530 | } 531 | 532 | body { 533 | position: absolute; 534 | top: 0; 535 | left: 0; 536 | right: 0; 537 | bottom: 0; 538 | overflow: hidden 539 | } 540 | 541 | .lm-content,.lm-content>div { 542 | flex: 1 1 auto; 543 | border: 1px solid silver 544 | } 545 | 546 | .lm-content,.lm-panel,body { 547 | display: flex; 548 | flex-direction: column 549 | } 550 | 551 | .lm-content,.lm-panel { 552 | min-width: 50px; 553 | min-height: 50px; 554 | height: unset; 555 | width: unset; 556 | padding: 8px 557 | } 558 | 559 | .lm-content { 560 | border-top: none; 561 | background: #fff; 562 | box-shadow: 1px 1px 2px rgba(0,0,0,.2) 563 | } 564 | 565 | .lm-panel { 566 | flex: 1 1 auto 567 | } 568 | 569 | .lm-content>div { 570 | overflow: auto; 571 | padding: 5px 572 | } 573 | 574 | .lm-DockPanel { 575 | box-shadow: inset 0 0 4px 0 rgba(0,0,0,.5) 576 | } 577 | 578 | ._dash-loading { 579 | position: relative; 580 | top: 40vh; 581 | left: 40vw; 582 | font-size: 50px; 583 | color: #888; 584 | overflow: hidden; 585 | width: 20%; 586 | text-align: center 587 | } 588 | 589 | .tabBarContainer { 590 | background: #eee; 591 | max-width: 35px; 592 | min-width: 35px; 593 | padding: 7px 0 0 594 | } 595 | 596 | .lm-SideBar { 597 | min-width: 35px; 598 | max-width: 35px; 599 | margin-left: 2px 600 | } 601 | 602 | .lm-SideBar.p-TabBar { 603 | min-height: 35px; 604 | max-height: 35px; 605 | overflow: visible; 606 | display: block; 607 | min-width: fit-content 608 | } 609 | 610 | .lm-SideBar .p-TabBar-content { 611 | margin: 0; 612 | padding: 0; 613 | display: flex; 614 | align-items: stretch; 615 | list-style-type: none; 616 | height: 33px; 617 | transform-origin: 0 0 0 618 | } 619 | 620 | .lm-SideBar .p-TabBar-tab { 621 | padding: 0 16px; 622 | margin-left: -1px; 623 | overflow: visible; 624 | min-width: fit-content; 625 | border: 1px solid #999; 626 | transform: translateY(3px) 627 | } 628 | 629 | .lm-SideBar .p-TabBar-tab.p-mod-current { 630 | min-height: 32px; 631 | max-height: 32px; 632 | border-top: 2px solid #119dff 633 | } 634 | 635 | .lm-SideBar .p-TabBar-tabIcon.lm-SideBar-tabIcon { 636 | min-width: 28px; 637 | min-height: 28px; 638 | background-size: 28px; 639 | display: inline-block; 640 | vertical-align: middle; 641 | background-repeat: no-repeat; 642 | background-position: center 643 | } 644 | 645 | .lm-SideBar .p-TabBar-tabLabel { 646 | margin-top: 3px; 647 | transform: rotate(.1deg); 648 | margin-right: 6px 649 | } 650 | 651 | .lm-SideBar .p-TabBar-tab:hover:not(.p-mod-current) { 652 | background: #eee 653 | } 654 | 655 | .lm-SideBar.p-TabBar.lm-mod-left .p-TabBar-content { 656 | flex-direction: row-reverse; 657 | transform: rotate(-90deg) translateX(-100%) 658 | } 659 | 660 | .lm-SideBar.p-TabBar.lm-mod-right .p-TabBar-content { 661 | flex-direction: row; 662 | transform: rotate(90deg) translateY(-31px) 663 | } 664 | 665 | .tabBarContainer::after { 666 | margin-top: -10px; 667 | min-height: 100vh; 668 | min-width: 100%; 669 | content: ""; 670 | border-right: 1px solid silver; 671 | border-left: 1px solid silver; 672 | background: 0 0; 673 | position: absolute 674 | } 675 | 676 | .lm-SideBar .p-TabBar-tab.p-mod-current { 677 | transform: translateY(1px); 678 | border-bottom: 1px solid #fff 679 | } 680 | 681 | .lm-SideBar .p-TabBar-tab { 682 | border-bottom: 1px solid silver; 683 | min-height: 30px; 684 | max-height: 30px 685 | } 686 | 687 | .lm-SideBar .p-TabBar-tabIcon { 688 | transform: rotate(-90deg); 689 | margin: unset 690 | } 691 | 692 | .lm-SideBar .p-TabBar-tabIcon.fa { 693 | margin: unset; 694 | transform: rotate(90deg) translate(3px,14px); 695 | margin-left: 12px 696 | } 697 | 698 | .lm-SideBar.p-TabBar.lm-mod-left .p-TabBar-tab.p-mod-current .p-TabBar-tabIcon { 699 | transform: rotate(90deg) translate(2.5px,14.5px) 700 | } 701 | 702 | .lm-SideBar.p-TabBar.lm-mod-right .p-TabBar-tabIcon.fa { 703 | transform: rotate(-90deg) translate(0,1px) 704 | } 705 | 706 | .lm-SideBar.p-TabBar.lm-mod-right .p-TabBar-tab.p-mod-current .p-TabBar-tabIcon { 707 | transform: rotate(-90deg) translate(.5px,1.5px) 708 | } 709 | 710 | .lm-SideBar .p-TabBar-tabLabel:empty { 711 | display: none 712 | } -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import Command from './components/Command.react'; 3 | import Menu from './components/Menu.react'; 4 | import MenuBar from './components/MenuBar.react'; 5 | import Separator from './components/Separator.react'; 6 | 7 | import BoxPanel from './components/BoxPanel.react'; 8 | import SplitPanel from './components/SplitPanel.react'; 9 | import TabPanel from './components/TabPanel.react'; 10 | import Panel from './components/Panel.react'; 11 | import DockPanel from './components/DockPanel.react'; 12 | 13 | import Widget from './components/Widget.react'; 14 | 15 | 16 | import './components/css/defaults.css'; 17 | 18 | 19 | export { 20 | Command, Menu, MenuBar, Separator, BoxPanel, SplitPanel, TabPanel, Panel, DockPanel, Widget 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /src/lib/registry.js: -------------------------------------------------------------------------------- 1 | import { 2 | CommandRegistry 3 | } from '@lumino/commands'; 4 | 5 | import { 6 | Panel as l_Panel, 7 | } from '@lumino/widgets'; 8 | import { string } from 'prop-types'; 9 | import { none } from 'ramda'; 10 | 11 | 12 | /** 13 | * the internal command registry of lumino 14 | */ 15 | var commands = new CommandRegistry(); 16 | 17 | 18 | /** 19 | * A dictionary of all Dash Lumino components 20 | */ 21 | var components = new Object(); 22 | 23 | /** 24 | * Lookup for the unique identifiers 25 | */ 26 | var lut = []; for (var i = 0; i < 256; i++) { lut[i] = (i < 16 ? '0' : '') + (i).toString(16); } 27 | /** 28 | * Create a new unique identifer 29 | */ 30 | function get_uuid() { 31 | var d0 = Math.random() * 0xffffffff | 0; 32 | var d1 = Math.random() * 0xffffffff | 0; 33 | var d2 = Math.random() * 0xffffffff | 0; 34 | var d3 = Math.random() * 0xffffffff | 0; 35 | return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + 36 | lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + 37 | lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] + 38 | lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff]; 39 | } 40 | 41 | 42 | function get_id(props) { 43 | if (typeof props.id === 'string') { 44 | try { 45 | const idObj = JSON.parse(props.id); 46 | if (idObj.constructor === Object) { 47 | return Object.values(idObj).join("-"); 48 | } 49 | } catch (error) { 50 | // Parsing as JSON failed, return the original string 51 | return props.id; 52 | } 53 | } else if (props.id.constructor === Object) { 54 | return Object.values(props.id).join("-"); 55 | } 56 | 57 | // If no valid ID can be extracted, return null or adjust as needed. 58 | return null; 59 | } 60 | 61 | function props_id(a) { 62 | return get_id(a.props); 63 | } 64 | 65 | export { commands, components, get_uuid, props_id, get_id }; -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VK/dash-lumino-components/335c345b63cb9fed7ba642a81d4a3eb0cab17628/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | # Packages needed to run the tests. 2 | # Switch into a virtual environment 3 | # pip install -r requirements.txt 4 | 5 | dash[dev,testing]>=1.15.0 6 | -------------------------------------------------------------------------------- /tests/test_usage.py: -------------------------------------------------------------------------------- 1 | from dash.testing.application_runners import import_app 2 | 3 | 4 | # Basic test for the component rendering. 5 | # The dash_duo pytest fixture is installed with dash (v1.0+) 6 | def test_render_component(dash_duo): 7 | # Start a dash app contained as the variable `app` in `usage.py` 8 | app = import_app('usage') 9 | dash_duo.start_server(app) 10 | 11 | # Get the generated component input with selenium 12 | # The html input will be a children of the #input dash component 13 | my_component = dash_duo.find_element('#input > input') 14 | 15 | assert 'my-value' == my_component.get_attribute('value') 16 | 17 | # Clear the input 18 | dash_duo.clear_input(my_component) 19 | 20 | # Send keys to the custom input. 21 | my_component.send_keys('Hello dash') 22 | 23 | # Wait for the text to equal, if after the timeout (default 10 seconds) 24 | # the text is not equal it will fail the test. 25 | dash_duo.wait_for_text_to_equal('#output', 'You have entered Hello dash') 26 | -------------------------------------------------------------------------------- /usage.py: -------------------------------------------------------------------------------- 1 | import dash_lumino_components as dlc 2 | import dash 3 | from dash import Input, Output, State, MATCH, ALL, html, dcc 4 | from dash.exceptions import PreventUpdate 5 | import dash_bootstrap_components as dbc 6 | import random 7 | import json 8 | 9 | 10 | external_stylesheets = [ 11 | 'http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css', 12 | 'https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css' 13 | ] 14 | 15 | app = dash.Dash(__name__, external_stylesheets=external_stylesheets) 16 | 17 | 18 | def get_test_div(widgetid): 19 | return html.Div([ 20 | html.Label('Simple Widget'), 21 | dcc.Input(id={"type": 'my-input', "widget": widgetid}, 22 | value='initial value', type='text'), 23 | html.Br(), 24 | html.Div(id={"type": 'my-output', "widget": widgetid}), 25 | ]) 26 | 27 | 28 | @app.callback( 29 | Output(component_id={'type': 'my-output', 30 | 'widget': MATCH}, component_property='children'), 31 | [Input(component_id={'type': 'my-input', 32 | 'widget': MATCH}, component_property='value')] 33 | ) 34 | def update_output_div(input_value): 35 | return 'Output: {}'.format(input_value) 36 | 37 | 38 | menus = [ 39 | dlc.Menu([ 40 | dlc.Command(id={"type": "com:widget", "type": "open"}, 41 | label="Open", icon="fa fa-plus"), 42 | dlc.Separator(), 43 | dlc.Menu([ 44 | dlc.Command(id={"type": "com:widget", "type": "closeall"}, label="Close All", 45 | icon="fa fa-minus"), 46 | dlc.Command(id={"type": "com:widget", "type": "closeone"}, label="Close One", 47 | icon="fa fa-minus"), 48 | ], id="extraMenu", title="Extra" 49 | ) 50 | ], id="openMenu", title="Widgets")] 51 | 52 | app.layout = html.Div([ 53 | dlc.MenuBar(menus, 'menuBar'), 54 | dlc.BoxPanel([ 55 | dlc.SplitPanel([ 56 | dlc.TabPanel( 57 | [ 58 | dlc.Panel(id="tab-A", children=html.Div([ 59 | dbc.Button("Open Plot", id="button2", 60 | style={"width": "100%"}), 61 | ]), label="Plots", icon="fa fa-bar-chart"), 62 | dlc.Panel(id="tab-B", children=html.Div("Dummy Panel B"), 63 | label="", icon="fa fa-plus"), 64 | dlc.Panel( 65 | id="tab-C", children=html.Div([ 66 | dbc.Button("Stacked", id="stacked-layout-btn", style={"width": "100%", "marginBottom": "1em"}), 67 | dbc.Button("Vertical", id="vertical-layout-btn", style={"width": "100%", "marginBottom": "1em"}), 68 | dbc.Button("Horizontal", id="horizontal-layout-btn", style={"width": "100%"}), 69 | 70 | html.H5("Latest Event:"), 71 | html.Div("events", id="widgetEvent-output"), 72 | 73 | html.H5("Current Layout:"), 74 | html.Div("layout", id="widgetEvent-layout") 75 | 76 | ]), label="Layout"), 77 | ], 78 | id='tab-panel-left', 79 | tabPlacement="left", 80 | allowDeselect=True), 81 | 82 | dlc.DockPanel([ 83 | dlc.Widget( 84 | "test", id="initial-widget", title="Hallo", icon="fa fa-folder-open", closable=True, caption="akjdlfjkasdlkfjsajdf" 85 | ), 86 | dlc.Widget( 87 | "test", id="initial-widget2", title="Hallo", icon="fa fa-folder-open", closable=True, caption="akjdlfjkasdlkfjsajdf" 88 | ) 89 | ], id="dock-panel", ), 90 | 91 | dlc.TabPanel( 92 | [ 93 | dlc.Panel(id="tab-D", children=html.Div([ 94 | 95 | html.Div("start", id="tab-D-output") 96 | ]), label="Plots", icon="fa fa-bar-chart") 97 | ], 98 | id='tab-panel-right', 99 | tabPlacement="right", 100 | allowDeselect=True, 101 | currentIndex=-1 102 | ) 103 | 104 | ], id="splitPanel") 105 | ], "boxPanel", addToDom=True) 106 | ]) 107 | 108 | 109 | # @app.callback(Output('tab-panel-right', 'currentIndex'), Input('button2', 'n_clicks')) 110 | # def extra_click(n_clicks): 111 | # # if n_clicks: 112 | # # return html.Div("clicked: #" + str(n_clicks), style={"background": "#f99"}) 113 | # # return "Click me" 114 | # return 0 if n_clicks else -1 115 | 116 | 117 | @app.callback(Output('tab-D-output', 'children'), Input('tab-panel-left', 'currentIndex')) 118 | def extra_click(index): 119 | # if n_clicks: 120 | # return html.Div("clicked: #" + str(n_clicks), style={"background": "#f99"}) 121 | # return "Click me" 122 | return "tabindex: " + str(index) 123 | 124 | 125 | @app.callback(Output('dock-panel', 'children'), [ 126 | Input({"type": "com:widget", "type": ALL}, "n_called") 127 | ], [State('dock-panel', 'children')]) 128 | def open_widget(menubutton, widgets): 129 | 130 | open_value = len(widgets) 131 | 132 | widgets = [w for w in widgets if not( 133 | "props" in w and "deleted" in w["props"] and w["props"]["deleted"])] 134 | 135 | ctx = dash.callback_context 136 | print("triggered", ctx.triggered) 137 | 138 | if "prop_id" in ctx.triggered[0] and ctx.triggered[0]["prop_id"] == '{"type":"open"}.n_called': 139 | if open_value is not None: 140 | new_widget = dlc.Widget(id="newWidget-"+str(open_value), title="Test Widget " + 141 | str(open_value), icon="fa fa-folder-open", children=[get_test_div("testwidget" + str(open_value))]) 142 | return [*widgets, new_widget] 143 | 144 | if "prop_id" in ctx.triggered[0] and ctx.triggered[0]["prop_id"] == '{"type":"closeall"}.n_called': 145 | return [] 146 | 147 | if "prop_id" in ctx.triggered[0] and ctx.triggered[0]["prop_id"] == '{"type":"closeone"}.n_called' and len(widgets) > 0: 148 | del_idx = random.randint(0, len(widgets)-1) 149 | print("delete at index: {}".format(del_idx)) 150 | del widgets[del_idx] 151 | print(widgets) 152 | return widgets 153 | 154 | return widgets 155 | 156 | @app.callback( 157 | Output('widgetEvent-output', 'children'), 158 | Input('dock-panel', 'widgetEvent') 159 | ) 160 | def widgetEvent(event): 161 | return json.dumps(event) 162 | 163 | 164 | @app.callback( 165 | Output('dock-panel', 'layout'), 166 | Input('stacked-layout-btn', 'n_clicks'), 167 | Input('horizontal-layout-btn', 'n_clicks'), 168 | Input('vertical-layout-btn', 'n_clicks') 169 | ) 170 | def update_layout(stacked, horizontal, vertical): 171 | 172 | ctx = dash.callback_context 173 | print("triggered", ctx.triggered) 174 | 175 | if "stacked" in ctx.triggered[0]["prop_id"]: 176 | return {"main": {"type": "tab-area", "widgets": ["initial-widget2", "initial-widget"], "currentIndex": 1}} 177 | if "horizontal" in ctx.triggered[0]["prop_id"]: 178 | return {"main": {"type": "split-area", "orientation": "horizontal", "children": [{"type": "tab-area", "widgets": ["initial-widget2"], "currentIndex": 0}, {"type": "tab-area", "widgets": ["initial-widget"], "currentIndex": 0}], "sizes": [0.5, 0.5]}} 179 | if "vertical" in ctx.triggered[0]["prop_id"]: 180 | return {"main": {"type": "split-area", "orientation": "vertical", "children": [{"type": "tab-area", "widgets": ["initial-widget2"], "currentIndex": 0}, {"type": "tab-area", "widgets": ["initial-widget"], "currentIndex": 0}], "sizes": [0.5, 0.5]}} 181 | 182 | raise PreventUpdate 183 | 184 | @app.callback( 185 | Output('widgetEvent-layout', 'children'), 186 | Input('dock-panel', 'layout') 187 | ) 188 | def widgetEvent(layout): 189 | print(layout) 190 | return json.dumps(layout) 191 | 192 | if __name__ == '__main__': 193 | app.run_server(debug=True) 194 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const packagejson = require('./package.json'); 3 | 4 | const dashLibraryName = packagejson.name.replace(/-/g, '_'); 5 | 6 | module.exports = (env, argv) => { 7 | 8 | let mode; 9 | 10 | const overrides = module.exports || {}; 11 | 12 | // if user specified mode flag take that value 13 | if (argv && argv.mode) { 14 | mode = argv.mode; 15 | } 16 | 17 | // else if configuration object is already set (module.exports) use that value 18 | else if (overrides.mode) { 19 | mode = overrides.mode; 20 | } 21 | 22 | // else take webpack default (production) 23 | else { 24 | mode = 'production'; 25 | } 26 | 27 | let filename = (overrides.output || {}).filename; 28 | if (!filename) { 29 | const modeSuffix = mode === 'development' ? 'dev' : 'min'; 30 | filename = `${dashLibraryName}.${modeSuffix}.js`; 31 | } 32 | 33 | const entry = overrides.entry || { main: './src/lib/index.js' }; 34 | 35 | const devtool = overrides.devtool || 'source-map'; 36 | 37 | const externals = ('externals' in overrides) ? overrides.externals : ({ 38 | react: 'React', 39 | 'react-dom': 'ReactDOM', 40 | 'plotly.js': 'Plotly', 41 | 'prop-types': 'PropTypes', 42 | }); 43 | 44 | return { 45 | mode, 46 | entry, 47 | output: { 48 | path: path.resolve(__dirname, dashLibraryName), 49 | filename, 50 | library: dashLibraryName, 51 | libraryTarget: 'window', 52 | }, 53 | devtool, 54 | externals, 55 | module: { 56 | rules: [ 57 | { 58 | test: /\.jsx?$/, 59 | exclude: /node_modules/, 60 | use: { 61 | loader: 'babel-loader', 62 | }, 63 | }, 64 | { 65 | test: /\.css$/, 66 | use: [ 67 | { 68 | loader: 'style-loader', 69 | options: { 70 | // Remove 'insertAt' and use a valid option if needed 71 | injectType: 'styleTag' // Example of a valid option 72 | } 73 | }, 74 | { 75 | loader: 'css-loader', 76 | }, 77 | ], 78 | }, 79 | { 80 | test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i, 81 | use: [ 82 | { 83 | loader: 'file-loader', 84 | }, 85 | ], 86 | }, 87 | ], 88 | }, 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /webpack.serve.config.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.js'); 2 | const path = require('path'); 3 | 4 | config.entry = {main: './src/demo/index.js'}; 5 | config.output = { 6 | filename: './output.js', 7 | path: path.resolve(__dirname), 8 | }; 9 | config.mode = 'development'; 10 | config.externals = undefined; // eslint-disable-line 11 | config.devtool = 'inline-source-map'; 12 | module.exports = config; 13 | --------------------------------------------------------------------------------