diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index c4814a7896..0000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "kbase-extension/static/ext_components" -} \ No newline at end of file diff --git a/.flake8 b/.flake8 index 3f096d2a98..8b18c43fc7 100644 --- a/.flake8 +++ b/.flake8 @@ -1,8 +1,10 @@ [flake8] # https://ljvmiranda921.github.io/notebook/2018/06/21/precommits-using-black-and-flake8/ ignore = E203, E266, E501, W503, F403, F401, E402, C901 -# max-line-length = 79 -# max-complexity = 15 select = B,C,E,F,W,T4,B9 exclude = - ./test/selenium_scripts/ +.git, +.github, +deployment, +docs, +node_modules diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..577bb958e6 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,23 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: no + +github_checks: + annotations: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 33b100cb59..38306098ae 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,7 +5,8 @@ * List any dependencies that are required for this change. # Jira Ticket / Issue # -e.g. https://kbase-jira.atlassian.net/browse/DATAUP-X + +Related Jira ticket: https://kbase-jira.atlassian.net/browse/DATAUP-X - [ ] Added the Jira Ticket to the title of the PR (e.g. `DATAUP-69 Adds a PR template`) # Testing Instructions @@ -22,9 +23,9 @@ e.g. https://kbase-jira.atlassian.net/browse/DATAUP-X - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published in downstream modules - [ ] (JavaScript) I have run Prettier and ESLint on changed code manually or with a git precommit hook - [ ] (Python) I have run Black and Flake8 on changed Python code manually or with a git precommit hook +- [ ] Any dependent changes have been merged and published in downstream modules # Updating Version and Release Notes (if applicable) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 81db664bd6..52c95307e6 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -17,16 +17,15 @@ jobs: auto-update-conda: true condarc-file: test/condarc.yml - - name: Use Node JS 14.x + - name: Use Node JS 16.x uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 16.x - name: Install JS dependencies run: | npm ci - npm install bower - ./node_modules/bower/bin/bower install + npm run install-npm - name: Install Narrative Application shell: bash -l {0} diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index c78c6f1299..25eace6b34 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -17,16 +17,15 @@ jobs: auto-update-conda: true condarc-file: test/condarc.yml - - name: Use Node JS 14.x - uses: actions/setup-node@v1 + - name: Use Node JS 16.x + uses: actions/setup-node@v2.1.2 with: - node-version: 14.x + node-version: 16.x - name: Install JS dependencies run: | npm ci - npm install bower - ./node_modules/bower/bin/bower install + npm run install-npm - name: Install Narrative Application shell: bash -l {0} @@ -37,25 +36,36 @@ jobs: sed -i 's/{{ if ne .Env.CONFIG_ENV "prod" }} true {{- else }} false {{- end }}/true/' src/config.json jupyter notebook --version + - name: Set up environment vars + run: | + echo "KBASE_TEST_TOKEN=${{ secrets.NARRATIVE_TEST_TOKEN }}" >> $GITHUB_ENV + - name: Run Narrative Backend Tests + id: test_backend shell: bash -l {0} run: make test-backend + continue-on-error: true - name: Run Narrative Frontend Unit Tests + id: test_frontend shell: bash -l {0} run: make test-frontend-unit - - - name: make test output available as artifact in case of failure - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: karma-result.json - path: karma-result.json + continue-on-error: true - name: Send to Codecov + id: send_to_codecov uses: codecov/codecov-action@v1.5.2 + continue-on-error: true with: file: | ./python-coverage/coverage.xml ./js-coverage/lcov/lcov.info fail_ci_if_error: true + + - name: outcome + if: steps.test_backend.outcome != 'success' || steps.test_frontend.outcome != 'success' || steps.send_to_codecov.outcome != 'success' + run: | + echo "backend tests: ${{ steps.test_backend.outcome }}" + echo "frontend tests: ${{ steps.test_frontend.outcome }}" + echo "upload coverage: ${{ steps.send_to_codecov.outcome }}" + exit 1 diff --git a/.gitignore b/.gitignore index b7d71f2f6d..10885029f9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ my_narrative_venv/ narrative_venv/ new_narrative_env_2/ js-coverage +python-coverage/ cover _attic @@ -104,4 +105,4 @@ target/ .idea # For yarn users -yarn.lock \ No newline at end of file +yarn.lock diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..442d3a5e2d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run husky_msg && npx lint-staged && npm run compile_css diff --git a/.husky/pre-push b/.husky/pre-push index 68156b4322..f172194939 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -#npm run husky_msg && npm run prettier_check -npx lint-staged \ No newline at end of file +npm run update_browserslist diff --git a/.prettierignore b/.prettierignore index e8d13fa369..bda2e6e1b0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,4 @@ kbase-extension/static/ext_modules kbase-extension/static/kbase/js/patched-components node_modules cover -venv \ No newline at end of file +venv diff --git a/.stylelintrc.css.json b/.stylelintrc.css.json new file mode 100644 index 0000000000..706cafa2e6 --- /dev/null +++ b/.stylelintrc.css.json @@ -0,0 +1,41 @@ +{ + "extends": "stylelint-config-standard", + "plugins": [ + "stylelint-no-indistinguishable-colors", + "stylelint-color-format" + ], + "rules": { + "color-named": "never", + "color-no-hex": true, + "color-format/format": { + "format": "rgb" + }, + "color-function-notation": "modern", + "declaration-block-no-redundant-longhand-properties": true, + "font-family-name-quotes": "always-where-recommended", + "font-family-no-missing-generic-family-keyword": [ + true, + { + "ignoreFontFamilies": ["FontAwesome", "Glyphicons Halflings", "kbase-icons"] + } + ], + "length-zero-no-unit": true, + "no-descending-specificity": null, + "plugin/stylelint-no-indistinguishable-colors": false, + "selector-max-compound-selectors": 5, + "selector-max-class": 5, + "selector-no-qualifying-type": [ + true, + { + "ignore": ["attribute", "class"] + } + ], + "shorthand-property-no-redundant-values": true + }, + "ignoreFiles": [ + "kbase-extension/static/ext_components/**/*.css", + "kbase-extension/static/ext_packages/**/*.css", + "kbase-extension/static/kbase/js/patched-components/**/*.css", + "docs/**/*.css" + ] +} diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml new file mode 100644 index 0000000000..0c9f35d6fd --- /dev/null +++ b/.stylelintrc.yaml @@ -0,0 +1,60 @@ +extends: +- stylelint-config-standard +- stylelint-config-sass-guidelines + +plugins: +- stylelint-color-format +- stylelint-scss +# - stylelint-no-indistinguishable-colors + +ignoreFiles: +- kbase-extension/static/**/*.{less,scss,css} + +rules: + color-named: never + color-no-hex: true + color-format/format: + format: rgb + color-function-notation: modern + declaration-block-no-redundant-longhand-properties: true + declaration-block-no-shorthand-property-overrides: true + font-family-no-missing-generic-family-keyword: + - true + - ignoreFontFamilies: + - FontAwesome + - "Glyphicons Halflings" + - kbase-icons + function-url-quotes: never + length-zero-no-unit: true + max-nesting-depth: 10 + no-descending-specificity: null + # set to true to enable + # plugin/stylelint-no-indistinguishable-colors: false + selector-class-pattern: null + selector-max-compound-selectors: 5 + selector-max-class: 5 + selector-max-id: 1 + selector-no-qualifying-type: + - true + - ignore: + - attribute + - class + shorthand-property-no-redundant-values: true + unit-allowed-list: + - [em, px, rem, "%"] + - ignoreProperties: + x: + - background-image + s: + - transition + deg: + - background + value-keyword-case: + - lower + - ignoreKeywords: [ + /^\$/ + ] + - ignoreProperties: [ + "/^font.*/", + url + ] diff --git a/Dockerfile b/Dockerfile index 8d45aab818..13e3f0de9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,11 +22,22 @@ ARG SKIP_MINIFY EXPOSE 8888 +# install NodeJS 16.x (latest LTS until ~October 2022, https://nodejs.org/en/about/releases/) +RUN \ + curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \ + sudo apt-get install -y nodejs + # install pyopenssl cryptography idna and requests is the same as installing # requests[security] RUN source activate base && \ - conda install -c conda-forge ndg-httpsclient==0.5.1 pyasn1==0.4.5 pyopenssl==19.0.0 cryptography==2.7 idna==2.8 requests==2.21.0 \ - beautifulsoup4==4.8.1 html5lib==1.0.1 + conda install -c conda-forge ndg-httpsclient==0.5.1 \ + pyasn1==0.4.5 \ + pyopenssl==19.0.0 \ + cryptography==2.7 \ + idna==2.8 \ + requests==2.21.0 \ + beautifulsoup4==4.8.1 \ + html5lib==1.0.1 # Copy in the narrative repo ADD ./ /kb/dev_container/narrative @@ -40,8 +51,7 @@ RUN \ ./src/scripts/kb-update-config -f src/config.json.templ -o /kb/deployment/ui-common/narrative_version && \ # install JS deps npm install -g grunt-cli && \ - npm install && \ - ./node_modules/.bin/bower install --allow-root --config.interactive=false && \ + npm install && npm run install-npm && \ # Compile Javascript down into an itty-bitty ball unless SKIP_MINIFY is non-empty echo Skip=$SKIP_MINIFY && \ [ -n "$SKIP_MINIFY" ] || npm run minify && \ diff --git a/Gruntfile.js b/Gruntfile.js index 1aae9ca4b5..0d1b3652dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,7 @@ const crypto = require('crypto'); module.exports = function (grunt) { 'use strict'; - grunt.loadNpmTasks('grunt-regex-replace'); + require('load-grunt-tasks')(grunt); grunt.initConfig({ // This inserts the reference to the compiled, minified JS file into page.html at @@ -23,5 +23,43 @@ module.exports = function (grunt) { ], }, }, + + // Run CSS / SCSS-related tasks + // these files are modified in place + postcss: { + // autoprefix and minify the concatenated css files + concat: { + options: { + processors: [ + // add vendor prefixes + require('autoprefixer')(), + // minify + require('cssnano')([ + 'default', + { + normalizeWhitespace: { + exclude: true, + }, + }, + ]), + ], + }, + src: ['kbase-extension/static/kbase/css/all_concat.css'], + }, + }, + + // runs the npm command to compile scss -> css and run autoprefixer on it + shell: { + compile_css: { + command: 'npm run compile_css', + }, + }, + + // watch scss files for any changes + // when they change, regenerate the compiled css files + watch: { + files: 'kbase-extension/scss/**/*.scss', + tasks: ['shell:compile_css'], + }, }); }; diff --git a/README.md b/README.md index f2f41e7037..f9563a91dd 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,17 @@ ***Table of Contents*** -- [About](#about) -- [Installation](#installation) - - [Local Installation](#local-installation) - - [Using a Conda Environment](#using-a-conda-environment) - - [Without Conda](#without-conda) -- [Architecture](#architecture) -- [Testing](#testing) -- [NPM scripts](#npm-scripts) -- [Git Hooks](#git-hooks) -- [Submitting Code](#submitting-code) +- [The KBase Narrative Interface](#the-kbase-narrative-interface) + - [About](#about) + - [Installation](#installation) + - [Local Installation](#local-installation) + - [*Using a Conda Environment*](#using-a-conda-environment) + - [*Without conda*](#without-conda) + - [Architecture](#architecture) + - [Testing](#testing) + - [NPM Scripts](#npm-scripts) + - [Git Hooks](#git-hooks) + - [Submitting code](#submitting-code) ## About @@ -37,8 +38,7 @@ Requires the following: - Python 3.6+ - Anaconda/Miniconda as an environment manager () -- Node.js (latest LTS recommended) -- Bower 1.8.8+ +- Node.js (16.x+, Latest LTS recommended) ### *Using a Conda Environment* diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c658b5b2c8..532362ec89 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,49 @@ The Narrative Interface allows users to craft KBase Narratives using a combinati This is built on the Jupyter Notebook v6.0.2 (more notes will follow). +### Version 5.0.0 +This new major version of the KBase Narrative Interface introduces a way to import data from your data staging area in large batches using a single Bulk Import cell. See more details about the new workflows here: [Bulk Import Guide](https://docs.kbase.us/data/upload-download-guide/bulk-import-guide) + +There are also a ton of other updates and changes to both the Narrative interface (in various small and not-so-small ways) and to how the Narrative software will be developed going forward. + +The following is a high level list of changes, with matching JIRA tickets where appropriate. As always, all Narrative work is done on Github and is available for view and discussion there. + +Code Changes +- Built an entirely new Bulk Import cell type that runs a batch of Import jobs all at once. +- Objects that can be imported in bulk are: + - SRA reads + - Interleaved FASTQ reads + - Non-interleaved FASTQ reads + - Read Assemblies + - GFF metagenomes + - Genbank genomes +- Redesigned the Data Import Tab + - It now suggests object upload type based on file suffix. + - Files are now selected for import by clicking a checkbox (or selecting a type), then clicking “Import Selected” + - The Import Tab file browser has been improved and remembers selections more consistently + - The Import Tab styling has been made more internally consistent. +- DATAUP-225 - Styles and color usage have been normalized in many places in the app cell and bulk import cell +- DATAUP-329 - Standardized the use of select boxes in various inputs for the app cell and bulk import cell +- Remove app info dialog option - superseded by the “Info” tab on all app cells +- DATAUP-263 - Fix bug where app cells can get stuck in the “Sending…” phase +- SAM-40 - Add Name/ID column to SampleSet viewer +- DATAUP-651 - Update Data Import tab help tour to include new features. + +Development Changes +- UIP-3 - Made the migration away from Bower to using only NPM for Javascript package management. Mostly, the same versions were kept, though some were unavoidably changed + - seiyria-bootstrap-slider 10.6.2 -> bootstrap-slider 10.6.2 (renamed on npm) + - plotly.js v1.5.1 -> plotly.js-dist-min v1.50.0 (1.5.1 unavailable) + - requirejs-plugins 1.0.3 -> 1.0.2 (which is on npm) + - requirejs-text 2.0.14 -> requirejs/text 2.0.16 (renamed on npm) + - kbase-ui-plugin-catalog 1.2.14 -> kbase-ui-plugin-catalog 2.2.5 (requires a package.json) + - Datatables got modified as it was out of date, and there were multiple versions being assembled at once. Now, there's: + - `datatables` as a package is obsolete, and supplanted by `datatables.net` (i.e., we shouldn't have both). Updated from 1.10.9 -> 1.11.3 + - supporting modules (`datatables.net-bs`, `datatables.net-buttons-bs`) are the same +DATAUP-246 - migrate from CSS to using SASS throughout the Narrative, making use of [BEM notation](http://getbem.com/introduction/) for element styling +DATAUP-62 - designed and implemented simple coding standards and git workflow for the Narrative repo +DATAUP-71 - added automated linting and code quality tools to the Narrative repo + + ### Version 4.6.0 Code changes - DATAUP-599 - Adjusted the kernel code and tests to account for a Workspace service update. diff --git a/bower.json b/bower.json deleted file mode 100644 index 96680b1d6a..0000000000 --- a/bower.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "kbase-narrative", - "version": "4.6.0", - "homepage": "https://kbase.us", - "dependencies": { - "bluebird": "3.7.2", - "seiyria-bootstrap-slider": "10.6.2", - "bowser": "1.0.0", - "corejs-typeahead": "1.0.1", - "d3": "3.5.6", - "datatables": "1.10.9", - "dropzone": "5.7.0", - "font-awesome": "4.7.0", - "google-code-prettify": "1.0.5", - "handlebars": "4.0.5", - "jquery-nearest": "1.3.1", - "jquery.tipsy": "*", - "kbase-common-js": "2.16.0", - "kbase-service-clients-js": "3.3.4", - "kbase-sdk-clients-js": "0.5.1", - "numeral": "1.5.0", - "plotly.js": "git://github.com/plotly/plotly.js/#1.5.1", - "pure-uuid": "1.4.2", - "requirejs-domready": "2.0.1", - "requirejs-json": "0.0.3", - "requirejs-plugins": "1.0.3", - "requirejs-text": "2.0.14", - "require-css": "0.1.8", - "select2": "^4.0.13", - "select2-bootstrap-theme": "0.1.0-beta.9", - "underscore": "1.8.3", - "file-saver": "1.3.4", - "SparkMD5": "spark-md5#^3.0.0", - "js-yaml": "3.3.1", - "require-yaml": "0.1.2", - "bower": "*", - "install": "1.0.4", - "kbase-ui-plugin-catalog": "1.2.14", - "bootstrap": "3.3.7", - "datatables.net-bs": "2.1.1", - "datatables.net-buttons-bs": "1.4.2" - }, - "resolutions": { - "kbase-common-js": "2.16.1", - "js-yaml": "3.3.1" - } -} diff --git a/docs/adrs/0004-bulk-import-output-cells.md b/docs/adrs/0004-bulk-import-output-cells.md new file mode 100644 index 0000000000..a8583470b2 --- /dev/null +++ b/docs/adrs/0004-bulk-import-output-cells.md @@ -0,0 +1,65 @@ +# Bulk Import Output Cells + +Date: 2021-10-05 + +One of the features of the App Cell is that it can produce Output Cells after a run. These typically contain views of the data that was transformed via the app, including new calculations being made or new data being created. However, in the Bulk Import cell, this could result in dozens or more new cells all being created at once, which could overwhelm a user or lead to other confusion from what should be an easy upload process. + +This ADR details the decision to not provide output cells for those apps run by the Bulk Import cell. + +## Author + +@briehl, @ialarmedalien + +## Status + +Accepted + +## Alternatives Considered + +* Include Output Cells when Apps finish +* Don't include Output Cells when Apps finish +* Group Output Cells in a single, tabbed Output + +## Decision Outcome and Consequences + +The Bulk Import cell will not produce new Output Cells on app completion. It will, instead, ensure that there is a report view available for each imported object. This carries the consequence that users may expect an output cell for one or two uploaded object types based on the single uploader. Extra documentation should help with that. + +Bulk Import cell importer apps that employ an output cell: + +| Importer | Output cell | +|---|---| +| Import GenBank as genome from staging | Genome viewer | +| Import GFF/FASTA as genome from staging | Genome viewer | + +Other importer apps with output cells: + +| Importer | Output cell | +|---|---| +| Load paired end reads from file | Reads viewer | +| Load single end reads from file | Reads viewer | + +Workaround: users can click on the object name in the "Objects" section of the result panel or click on the object in the data panel on the left side of the narrative. Either action will create the appropriate viewer in their narrative. + +## Pros and Cons of Alternatives + +### Include Output Cells when Apps finish + +* `+` Keeps same behavior as the current App Cell. +* `+` Shows the expected output from the newly created upload object. +* `-` Will likely create a ton of Output Cells at once. +* `-` Could clog a Narrative with lots of extra cells. +* `-` Order of outputs would be semi-random (whenever a job finishes). +* `-` Retrying jobs would further confuse the output order. + +### Don't include Output Cells when Apps finish + +* `+` Prevents spamming the Narrative with Output Cells. +* `+` Consolidates outputs under the Results tab. +* `+` Results of retried jobs will be handled more efficiently. +* `-` Users could miss their expected outputs, and might need guidance to see their data as expected. + +### Group Output Cells in a single, tabbed Output + +* `+` Maintains expected flow of outputs produced from Bulk Import apps +* `-` New, unscoped, undesigned work. +* `-` Creates output view that the user may or may not want and may be difficult to navigate, given there could be hundreds or thousands of items in the view. diff --git a/docs/deploy_narrative.md b/docs/deploy_narrative.md index 6be73dd7fb..d965afd038 100644 --- a/docs/deploy_narrative.md +++ b/docs/deploy_narrative.md @@ -6,14 +6,16 @@ This document describes how to release and deploy the Narrative Interface app on **_Table of Contents_** -- [KBase Environments](#kbase-environments) -- [Deployment flow overview](#deployment-flow-overview) -- [Deploying CI](#deploying-ci) -- [Stage a Final Release](#stage-a-final-release) -- [Deploying Next, Appdev, and Prod](#deploying-next,-appdev-&-prod) - - [Create Release Image](#create-release-image) - - [Tag & Deploy Image](#tag-&-deploy-image) -- [Image URLs](#image-urls) +- [Narrative Development & Deployment](#narrative-development--deployment) + - [KBase Environments](#kbase-environments) + - [Deployment Flow Overview](#deployment-flow-overview) + - [Deploying CI](#deploying-ci) + - [Stage a Final Release](#stage-a-final-release) + - [Deploying Next, Appdev, & Prod](#deploying-next-appdev--prod) + - [Deploying Narrative-Refactor](#deploying-narrative-refactor) + - [Create Release Image](#create-release-image) + - [Tag & Deploy Image (admins only)](#tag--deploy-image-admins-only) + - [Image URLs](#image-urls) ### KBase Environments @@ -50,7 +52,7 @@ To see your changes once the new image is created, simply open a narrative on CI ### Stage a Final Release 1. Update versions following (roughly) [semantic versioning](https://semver.org). The files to be updated are: - - `package.json`, `package-lock.json`, `bower.json`, `src/config.json`, `src/config.json.templ`, `src/biokbase/narrative/\_\_init\_\_.py` + - `package.json`, `package-lock.json`, `src/config.json`, `src/config.json.templ`, `src/biokbase/narrative/__init__.py` - Major - backward incompatible changes, like the move from Python 2 -> Python 3, or Narrative typed object changes. These are generally considered to be changes that have a strong impact on the Narrative Interface and are hard or impossible to move backward from. - Minor - new features that don't affect compatibility. - Patch - adjustments to existing features, bug fixes. diff --git a/docs/design/job_architecture.md b/docs/design/job_architecture.md index 6c9430cad9..94ae5babcc 100644 --- a/docs/design/job_architecture.md +++ b/docs/design/job_architecture.md @@ -1,52 +1,57 @@ # Job Management Architecture -The Narrative job manager is based on a data flow that operates between the browser, the IPython Kernel behind the Narrative, and the KBase Execution Engine (EE2) behind that. In general, each bit of data flows between those three stops. - -In general, there's a single point of information flow on the front end and one on the backend. +The Narrative job manager is based on a data flow that operates between the browser, the IPython Kernel behind the Narrative (also known as the narrative backend), and the KBase Execution Engine (EE2) behind that. Some frontend components access external resources directly, but job-related data flows between those three stops across a single channel from frontend and backend, and backend and EE2. # Comm channels Jupyter provides a "Comm" object that allows for custom messaging between the frontend and the kernel ([details and documentation here](https://jupyter-notebook.readthedocs.io/en/stable/comms.html)). This provides an interface for the frontend to directly request information from the kernel, and to listen to asynchronous responses. On the kernel-side, it allows one or more modules to register message handlers to process those requests out of the band of the usual kernel invocation. These are used to implement the Jupyter Notebook's ipywidgets, for example. -The Narrative Interface uses one of these channels to manage job information. These are funneled through an interface on the frontend side and a matching one in the kernel. +The Narrative Interface uses one of these channels to manage job information. These are funneled through an interface on the frontend side and a matching one in the kernel. ## Frontend Comm Channel -On the frontend, there's a `jobCommChannel.js` module that uses the MiniBus system to communicate. So, the following happens. Frontend modules use the bus system to send one of the following messages over the main channel, which then get interpreted and crafted into a message that gets passed through a kernel comm object. Most of these take one or more inputs. These are listed below the command, where applicable. +On the frontend, there's a `jobCommChannel.js` module that uses the MonoBus system to communicate. Frontend modules use the bus system to send one of the following messages over the main channel, which then get transformed into a message that gets passed through a kernel comm object. Most of these take one or more inputs. These are listed below the command, where applicable. This section is broken into two parts - kernel requests and kernel responses. Both of these are from the perspective of the frontend Javascript stack, using an AMD module and the Runtime object. The request parameters and examples are given first, then the responses below. ## Bus requests These messages are sent to the `JobCommChannel` on the front end, to get processed into messages sent to the kernel. +All the `request-job-*` requests take as arguments either a single job ID string, or an array of job IDs. + `ping-comm-channel` - sees that the comm channel is open through the websocket -`request-job-status` - gets the status for a job, one time - * `jobId` - a string, the job id - * `parentJobId` - (optional) a string, the id of the requested job's "parent" job +`request-job-status` - gets the status for a job or an array of jobs. + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs OR + * `batchId` - a batch parent (make the request for all jobs in the batch) + +`request-job-updates-start` - request the status for a job or jobs, but start an update cycle so that it's continually requested. + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs OR + * `batchId` - a batch parent (make the request for all jobs in the batch) -`request-job-update` - request the status for a job, but start an update cycle so that it's continually requested. - * `jobId` - a string, the job id - * `parentJobId` - (optional) a string, the id of the requested job's "parent" job +`request-job-updates-stop` - signal that the front end doesn't need any more updates for the specified job(s), so stop sending them for each loop cycle. Doesn't actually end the job, only requests for updates. + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs OR + * `batchId` - a batch parent (make the request for all jobs in the batch) -`request-job-completion` - signal that the front end doesn't need any more updates for that job, so stop sending them for each loop cycle. Doesn't actually end the job, only requests for updates. - * `jobId` - a string, the job id - * `parentJobId` - (optional) a string, the id of the requested job's "parent" job +`request-job-info` - request information about the job(s), specifically app id, spec, input parameters and (if finished) outputs + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs OR + * `batchId` - a batch parent (make the request for all jobs in the batch) -`request-job-info` - request information about the job, specifically app id, spec, input parameters and (if finished) outputs - * `jobId` - a string, the job id - * `parentJobId` - (optional) a string, the id of the requested job's "parent" job +`request-job-cancel` - request that the server cancel the running job(s) + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs -`request-job-cancellation` - request that the server cancel the running job. - * `jobId` - a string, the job id - * `parentJobId` - (optional) a string, the id of the requested job's "parent" job +`request-job-retry` - request that the server rerun a job or set of jobs + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs `request-job-log` - request the job logs starting at some given line. - * `jobId` - a string, the job id + * `jobId` - a string, the job id OR + * `jobIdList` - an array of job IDs * `options` - an object, with attributes: * `first_line` - the first line (0-indexed) to request * `num_lines` - the number of lines to request (will get back up to that many if there aren't more) - -`request-latest-job-log` - request the latest several job log lines - * `jobId` - a string, the job id - * `options` - an object, with attributes: - * `num_lines` - the number of lines to request (will get back up to that many if there aren't more) + * `latest` - true if requesting just the latest set of logs ### Usage Example The comm channel is used through the main Bus object that's instantiated through the global `Runtime` object. That needs to be included in the `define` statement for all AMD modules. The bus is then used with its `emit` function (you have the bus *emit* a message to its listeners), and any inputs are passed along with it. @@ -85,50 +90,31 @@ define( ## Bus responses When the kernel sends a message to the front end, the only module set up to listen to them is the `JobCommChannel` as mentioned above. This takes the responses, unpacks them, and turns them into a response message that is passed back over the bus to any frontend Javascript module that listens to them. The message types are described below, along with the content that gets sent, followed by an example of how to make use of them. -`job-status` - contains the current job state - * `jobId` - string, the job id - * `jobState` - object, describes the job state (see the **Data Structures** section below for the structure) - * `outputWidgetInfo` - object, contains the parameters to be sent to an output widget. This will be different for all widgets, depending on the App that invokes them. - -`job-deleted` - sent when a job has been deleted, but some information about it has been requested - * `jobId` - the id of the deleted job - * `via` - a string about why it's been deleted (generally "no_longer_exists") - -`job-info` - contains information about the current job - * `jobId` - string, the job id - * `jobInfo` - object, the job information object (see the **Data Structures** section below) +### Cell-related `run-status` - updates the run status of the job - this is part of the initial flow of starting a job through the AppManager. * TODO -`job-canceled` - sent when a job has been canceled in the kernel, as a response to other messages - * `jobId` - string, the job id - * `via` - string, generally "job_canceled" - -`job-logs` - sent with information about some job logs. - * `jobId` - string, the job id - * `logs` - the raw message data from the kernel. (see the **Data Structures** section below) - * `latest` - if truthy, then these are the latest logs, if falsy, then they don't have to be the latest logs. +### Job-related `job-error` - sent in response to an error that happened on job information lookup, or another error that happened while processing some other message to the JobManager. * `jobId` - string, the job id * `message` - string, some message about the error -`job-cancel-error` - a cancel request has thrown an error - * `jobId` - string, the job id - * `message` - string, a reason for the error - -`job-log-deleted` - a log request has thrown an error +`job-info` - contains information about the current job * `jobId` - string, the job id - * `message` - string, a reason for the error + * `jobInfo` - object, the job information object (see the **Data Structures** section below) -`job-status-error` - a status request as thrown an error +`job-logs` - sent with information about some job logs. * `jobId` - string, the job id - * `message` - string, a reason for the error + * `logs` - the raw message data from the kernel. (see the **Data Structures** section below) + * `latest` - if truthy, then these are the latest logs, if falsy, then they don't have to be the latest logs. + * `error` - if exists, the log request has thrown an error. The error key contains the error details. -`job-does-not-exist` - sent in response to a request for information about a job that doesn't exist. Jobs might not exist if (1) they have been previously canceled, or (2) a malformed request was sent. +`job-status` - contains the current job state * `jobId` - string, the job id - * `source` - string, the source of the message in the kernel (what service, or module, was invoked. Usually "JobManager" or "ExecutionEngine2") + * `jobState` - object, describes the job state (see the **Data Structures** section below for the structure) + * `outputWidgetInfo` - object, contains the parameters to be sent to an output widget. This will be different for all widgets, depending on the App that invokes them. ### Usage example As in the Bus requests section above, the front end response handling is done through the Runtime bus. The bus provides both an `on` and a `listen` function, examples will show how to use both. Generally, the `listen` function is more specific and binds the listener to a specific bus channel. These channels can invoke the jobId, or the cellId, to make sure that only information about specific jobs is listened for. @@ -201,45 +187,48 @@ These are organized by the `request_type` field, followed by the expected respon "data": { "request_type": "job_status", "job_id": "a_job_id", - "parent_job_id": "another_job_id" } } } ``` -`all_status` - request the status of all currently running jobs, responds with `job_status_all` +`all_status` - request the status of all currently running jobs, responds with `job_status_all` -`job_status` - request a single job status, responds with `job_status` -* `job_id` - string, -* `parent_job_id` - optional string +`job_status` - request job status, responds with `job_status` for each job +* `job_id` - string OR `job_id_list` - array of strings OR `batch_id` - string -`start_update_loop` - request starting the global job status update thread, no specific response, but generally with `job_status_all` +`job_status_batch` request job statuses, responds with `job_status` +* `job_id` - string - job_id of batch container job -`stop_update_loop` - request stopping the global job status update thread, no response +`start_job_update` - request updating job(s) during the update thread, responds with `job_status` +* `job_id` - string OR `job_id_list` - array of strings -`start_job_update` - request updating a single job during the update thread, no specific response, but generally with `job_status` -* `job_id` - string -* `parent_job_id` - optional string +`stop_job_update` - request halting update for job(s) during the update thread, no response +* `job_id` - string OR `job_id_list` - array of strings -`stop_job_update` - request halting update for a single job during the update thread, no response -* `job_id` - string -* `parent_job_id` - optional string +`start_job_update_batch` - request updating batch container and children jobs, responds with `job_status` +* `job_id` - string OR `job_id_list` - array of strings, but generally uses `job_id` -`job_info` - request general information about a job, responds with `job_info` -* `job_id` - string -* `parent_job_id` - optional string +`stop_job_update_batch` - request halting update for batch container and children jobs during the update thread, no response +* `job_id` - string OR `job_id_list` - array of strings, but generally uses `job_id` -`job_logs` - request job log information, responds with `job_logs` -* `job_id` - string -* `parent_job_id` - optional string -* `first_line` - int >= 0, -* `num_lines` - int > 0 +`job_info` - request general information about job(s), responds with `job_info` for each job +* `job_id` - string OR `job_id_list` - array of strings OR `batch_id` - string -`job_logs_latest` - request the latest set of lines from job logs, responds with `job_logs` -* `job_id` - string -* `parent_job_id` - optional string +`job_info_batch` - request general information about jobs, responds with `job_info` +* `job_id` - string - job_id of batch container job + +`job_logs` - request job log information, responds with `job_logs` for each job +* `job_id` - string OR `job_id_list` - array of strings +* `first_line` - int >= 0, ignored if `latest` is `true` * `num_lines` - int > 0 +* `latest` - boolean, `true` if requesting just the latest logs + +`cancel_job` - cancel a job or list of jobs; responds with `job_status` +* `job_id` - string OR `job_id_list` - array of strings +`retry_job` - retry a job or list of jobs, responds with `jobs_retried` and `new_job` +* `job_id` - string OR `job_id_list` - array of strings ## Messages sent from the kernel to the browser These are all caught by the `JobCommChannel` on the browser side, then parsed and sent as the bus messages described above. Like other kernel messages, they have a `msg_type` field, and a `content` field containing data meant for the frontend to use. They have a rough structure like this: @@ -263,12 +252,14 @@ a specific example: "data": { "msg_type": "job_status", "content": { - "state": { - "status": "running", - ... other state keys ... - }, - "spec": {}, - "widget_info": {} + "example_job_id": { + "state": { + "job_id": "example_job_id", + "status": "running", + ... other state keys ... + }, + "widget_info": {} + } } } } @@ -278,49 +269,40 @@ These are described below. The name (`msg_type`) is given, followed by the keys By design, these should only be seen by the `JobCommChannel` instance, then sent into bus messages that get sent on specific channels. That information is also given in each block. -### `job_does_not_exist` -This is an error message triggered when trying to get info/state/logs on a job that either doesn't exist in EE2 or that the JobManager doesn't have associated with the running narrative. - -**content** - * `job_id` - a string, the job id - * `source` - string, the source of the error - -**bus** `job-does-not-exist` - ### `job_comm_error` A general job comm error, capturing most errors that get thrown by the kernel **content** (this varies, but usually includes the below) * `request_type` - the original request message that wound up in an error - * `job_id` - string, the job id (if present) + * `job_id` - string OR `job_id_list` - array of strings, the job id(s) (if present) * `message` - string, an error message -**bus** one of `job-cancel-error`, `job-log-deleted`, `job-status-error`, `job-error` - -### `job_status_all` -The set of all job states for all running jobs, or at least the set that should be updated (those that are complete and not requested by the front end are not included - if a job is sitting in an error or finished state, it doesn't need ot have its app cell updated) - -**content** - all of the below are included, but the top-level keys are all job id strings, e.g.: -```json -{ - "job_id_1": { ...contents... }, - "job_id_2": { ...contents... } -} -``` - * `state` - the job state (see the **Data Structures** section below for details - * `widget_info` - the parameters to send to output widgets, only available for a completed job - * `owner` - string, username of user who submitted the job - -**bus** - a series of `job-status` or `job-deleted` messages +**bus** `job-error` ### `job_info` Includes information about the running job **content** +Dictionary with key(s) job ID and value dictionaries with the following structure: * `app_id` - string, the app id (format = `module_name/app_name`) * `app_name` - string, the human-readable app name * `job_id` - string, the job id * `job_params` - the unstructured set of parameters sent to the execution engine + * `batch_id` - id of batch container job + +i.e. +```json +{ + "job_id_1": { + "app_id": ..., + "app_name": ..., + "job_id": "job_id_1", + "job_params": ..., + "batch_id": "some_batch_id", + }, + "job_id_2": { ...contents... } +} +``` **bus** - `job-info` @@ -328,12 +310,44 @@ Includes information about the running job The current job state. This one is probably most common. **content** - * `state` - see **Data Structures** below for details (it's big and shouldn't be repeated all over this document) +Dictionary with key(s) job ID and value dictionaries with the following structure: + * `state` - see **Data Structures** below for details (it's big and shouldn't be repeated all over this document). Regarding error states: non-existent jobs have the status `does_not_exist`, and when the job state cannot be retrieved from EE2 the status `ee2_error` is used * `widget_info` - the parameters to send to output widgets, only available for a completed job - * `owner` - string, username of user who submitted the job + * `user` - string, username of user who submitted the job + +Sample response JSON: +```json +{ + "job_id_1": { + "state": { + "job_id": "job_id_1", + "status": "running", + ... + }, + "widget_info": null, // only available for completed jobs + }, + "job_id_2": { ...contents... } +} +``` **bus** - `job-status` +### `job_status_all` +The set of all job states for all running jobs, or at least the set that should be updated (those that are complete and not requested by the front end are not included - if a job is sitting in an error or finished state, it doesn't need ot have its app cell updated) + +**content** - all of the below are included, but the top-level keys are all job id strings, e.g.: +```json +{ + "job_id_1": { ...contents... }, + "job_id_2": { ...contents... } +} +``` + * `state` - the job state (see the **Data Structures** section below for details + * `widget_info` - the parameters to send to output widgets, only available for a completed job + * `user` - string, username of user who submitted the job + +**bus** - a series of `job-status` messages + ### `job_logs` Includes log statement information for a given job. @@ -348,9 +362,39 @@ Includes log statement information for a given job. **bus** `job-logs` +### `jobs_retried` +Sent when one or more jobs are retried + +**content** An array of objects, e.g.: +```json +[ + { + "job": {"state": {"job_id": job_id, "status": status, ...} ...}, + "retry": {"state": {"job_id": job_id, "status": status, ...} ...} + }, + { + "job": {"state": {"job_id": job_id, "status": status, ...} ...}, + "error": "..." + }, + ... + { + "job": {"state": {"job_id": job_id, "status": "does_not_exist"}}, + "error": "does_not_exist" + } +] +``` +Where the dict values corresponding to "job" or "retry" are the same data structures as for `job_status` +Outer keys: + * `job` - string, the job id of the retried job + * `retry` - string, the job id of the job that was launched + * `error` - string, appears if there was an error when trying to retry the job + ### `new_job` Sent when a new job is launched and serialized. This just triggers a save/checkpoint on the frontend - no other bus message is sent +**content** + * `job_id` - string OR `job_id_list` - array of strings + ### `run_status` Sent during the job startup process. There are a few of these containing various startup status, including errors (if they happen). @@ -376,7 +420,7 @@ All cases: **bus** `run-status` ### `result` -Sent at the end of a `AppManager.run_dynamic_service` call (of which there aren't many). +Sent at the end of a `AppManager.run_dynamic_service` call (of which there aren't many). **content** * `cell_id` - the app cell id (used for routing) @@ -432,6 +476,7 @@ These steps take place whenever the user loads a narrative, or when the kernel i ## Data Structures ### Job state +#### EE2 State In kernel, as retrieved from EE2.check_job (described by example) ```json @@ -471,14 +516,19 @@ In kernel, as retrieved from EE2.check_job } }, "job_id": "5e67d5e395d1f00a7cf4ea21", - "created": 1583863267000 + "created": 1583863267000, + "batch_id": null, + "batch_job": false, + "child_jobs": [], + "retry_ids": [], + "retry_parent": null } ``` - +#### BE Output State As sent to browser, includes cell info and run info ``` { - owner: string (username, who started the job), + user: string (username, who started the job), spec: app spec (optional) widget_info: (if not finished, None, else...) job.get_viewer_params result state: { @@ -501,6 +551,25 @@ As sent to browser, includes cell info and run info error: string, (likely a stacktrace) }, error_code: optional - int + created: 1583863267000, + batch_id: str, + batch_job: bool, + child_jobs: array, + retry_ids: array, + retry_parent: str } } ``` + +When an error occurs while preparing the job state, the output states will have the formats +```json +{ + "job_id_0": { + "state": {"job_id": "job_id_0", "status": "does_not_exist"} + }, + "job_id_1": { + "state": {"job_id": "job_id_1", "status": "ee2_error"} + }, + ... +} +``` diff --git a/docs/developer/local-docker.md b/docs/developer/local-docker.md index 9de369e389..4f414170c0 100644 --- a/docs/developer/local-docker.md +++ b/docs/developer/local-docker.md @@ -93,7 +93,7 @@ All of the testing options may be used The container can't be killed with Ctrl-C; you'll need to stop it using Docker or another tool like Kitematic. -If you need to update or change dependencies (bower.json), you'll need to rebuild the image. +If you need to update or change dependencies (package.json), you'll need to rebuild the image. ### config.json changes diff --git a/docs/developer/running_tasks.md b/docs/developer/running_tasks.md index 71cebb0e01..47f6f2732a 100644 --- a/docs/developer/running_tasks.md +++ b/docs/developer/running_tasks.md @@ -12,6 +12,48 @@ npm install to install and update the npm packages required by the narrative. If there are errors, you may need to remove the `package-lock.json` file and/or the `node_modules` directory. +### Building style files + +KBase serves concatenated, minified css files (generally named `*_concat.css`) to minimise download size and HTTP hits. These are produced from the scss source files in `kbase-extension/scss` by an npm script which triggers the sass compiler and then runs Autoprefixer on the output. + +To update the css files, run the task: + +```sh +npm run compile_css +``` + +If this is not run, edits to the scss source files will not be reflected in the css file served by the browser. + +There is also a `watch` task that will automatically generate the concatenated, minified css files when there is a change to the source files. If you plan to make changes to frontend styling, run + +```sh +grunt watch +``` + +in a terminal window to launch a watcher process that regenerates the css files when changes are made. It is recommended that you have this running in a terminal window when running the `kbase-narrative` script in another window. + +### Style file styling + +The narrative repo uses the [PostCSS](https://github.com/postcss/postcss) system for post-processing css; this includes minification and [Autoprefixer](https://github.com/postcss/autoprefixer) for adding browser prefixes. SCSS linting is provided by [Stylelint](https://stylelint.io). + +**Additions to the source files should be written without vendor prefixes as these will be added automatically.** + +### Autoprefixer + +The narrative repo uses [Autoprefixer](https://github.com/postcss/autoprefixer) to add browser prefixes to css, with the browser support list set to Autoprefixer's `default` setting. The `update_browserslist` npm script is used by Autoprefixer to pull in the latest browser configurations, and is run as a git hook. If running the script returns a message that the list of browsers has been updated, please commit the updated `package-lock.json` file. + +For more information, see the [browserslist best practices and updating sections](https://github.com/browserslist/browserslist#best-practices). + +### Stylelint + +To lint the scss files, run the command + +``` +npm run stylelint +``` + +The linter config (in `.stylelint.yaml`) includes a number of rules to ensure that the scss content is error-free and (relatively) uniform. Running it will automatically fix some stylistic issues (e.g. ordering of lines within stanzas), but others may need to be fixed manually. Please note there are some issues in the SCSS files that are more difficult to fix, due to the styling set in the Jupyter notebook css. + ### JavaScript formatting and linting The narrative repo uses [Prettier][https://github.com/prettier/prettier] for code formatting and [ESLint][https://eslint.org] for code linting. Developers can use the aliases in the `package.json` file to run these tools: diff --git a/docs/install/developer.md b/docs/install/developer.md index 876b7411fc..974ec0d0db 100644 --- a/docs/install/developer.md +++ b/docs/install/developer.md @@ -40,13 +40,9 @@ the Narrative work. SciPy: [http://sourceforge.net/projects/scipy/files/scipy/](http://sourceforge.net/projects/scipy/files/scipy/) NumPy: [http://sourceforge.net/projects/numpy/files/NumPy/](http://sourceforge.net/projects/numpy/files/NumPy/) -4. **NodeJS and Bower** +4. **NodeJS** - NodeJS (especially npm) and Bower are used to manage installation and testing of front-end JavaScript code. These are critical to have installed, since they manage JavaScript dependencies. Start with Node - instructions available here https://nodejs.org/en/download/ - - Then, use npm (comes with node) to install bower - - `> npm install -g bower` + NodeJS (especially npm) is used to manage installation and testing of front-end JavaScript code. This is critical to have installed, since it manages JavaScript dependencies. Instructions are available here https://nodejs.org/en/download/ Once this is all done successfully, you can move on to installing the Narrative itself. diff --git a/docs/install/local_install.md b/docs/install/local_install.md index 0ade67aa84..a09a9f2ebc 100644 --- a/docs/install/local_install.md +++ b/docs/install/local_install.md @@ -4,8 +4,7 @@ ### Requirements * Python > 3.6+ -* NodeJS and NPM >= v10.0.0 (available here: https://nodejs.org/en/, LTS recommended) -* Bower (install with `npm install -g bower` or instructions here https://bower.io/) +* NodeJS and NPM >= v16.0.0 (available here: https://nodejs.org/en/, LTS recommended) * Highly recommended - a Python environment manager. Conda is mostly used here, but venv, Poetry, or Pipenv should work as well. ### Install - Short version diff --git a/docs/testing/testing.md b/docs/testing/testing.md index 7da55d1d86..a83d29173e 100644 --- a/docs/testing/testing.md +++ b/docs/testing/testing.md @@ -51,10 +51,32 @@ Then, simply run (from the narrative root directory) `make test`. This calls a few subcommands, and those can be run independently for specific uses: - `make test-frontend-unit` will run only the unit tests on the frontend (i.e. those with the Karma runner) -- `make test-integration` will run the frontend integration tests that make use of webdriver.io to simulate the browser on a locally instantiated Narrative, but running against live KBase services. Note that this currently requires an authentication token. +- `make test-integration` will run the frontend integration tests that make use of webdriver.io to simulate the browser on a locally instantiated Narrative, but running against live KBase services. Note that this currently requires an authentication token. - `make test-frontend` will run both the frontend unit tests and integration tests as above. - `make test-backend` will run only the backend Python tests. +#### Frontend Unit Tests + +You can run the frontend unit tests interactively by launching a headless instance of the narrative and then starting Karma, the unit test runner. There are aliases provided to make this easier. + +To start the headless narrative: +``` +npm run headless +``` + +To run tests in Firefox: +``` +npm run test_firefox +``` + +To run tests in Chrome: +``` +npm run test_chrome +``` + +There is an additional Karma config file suitable for running tests locally, which turns off Karma's file caching so that you can run tests in Firefox or Chrome by repeatedly refreshing the browser. + + ### Add Credentials for Tests The Narrative Interface is one of the hubs of KBase - it touches several different services, all of which need real authentication. Some of those have been mocked in various tests (like running apps), but others (the Workspace service) still require a real Auth token. If you have a KBase Developer account, you can create a Developer Token in the Account tab of the main KBase interface. To create a developer token, follow the instructions in [Section 3.4 of the KBase SDK documentation](https://kbase.github.io/kb_sdk_docs/tutorial/3_initialize.html). @@ -78,7 +100,7 @@ This just needs the path to the token file, which should be kept in the `test` d #### ***Frontend Integration Tests*** There are currently two options here. -1. Set your token in the `KBASE_TEST_TOKEN` environment variable before running integration tests. +1. Set your token in the `KBASE_TEST_TOKEN` environment variable before running integration tests. 2. Use the same token file as described above. These are checked in that order. That is, if there's a `KBASE_TEST_TOKEN` variable, that gets used. Otherwise, it checks for the token file referenced in `test/testConfig.json`. If both of those are absent, a fake test token is used, which might cause failures if your tests include authenticated services. @@ -146,19 +168,13 @@ To debug using the Karma Debugger complete the following steps: - In one terminal tab enter: ``` -kbase-narrative --no-browser --NotebookApp.allow_origin="*" --ip=127.0.0.1 --port=32323 +npm run headless ``` - Open a second tab and enter: ``` -export PATH=$PATH:./node_modules/.bin/ -``` - -- In the second tab enter: - -``` -karma start test/unit/karma.conf.js --browsers=Chrome --single-run=false +npm run tdd ``` -After running the third command, a chrome browser will open. Click on the debug button. This opens a second browser window where you can inspect the page and use chrome debugger tools. +After running the second command, a Chrome browser will open. Click on the debug button. This opens a second browser window where you can inspect the page and use chrome debugger tools. diff --git a/docs/testing/unit-test-mock-library.md b/docs/testing/unit-test-mock-library.md index f4f021890d..47cacb8e7a 100644 --- a/docs/testing/unit-test-mock-library.md +++ b/docs/testing/unit-test-mock-library.md @@ -3,7 +3,7 @@ ***Table of Contents*** - [Introduction](#introduction) - - [General Usage](#general-usage) + - [General Usage](#general-usage) - Functions - [buildMockCell](#buildmockcell) - [buildMockNotebook](#buildmocknotebook) @@ -29,20 +29,20 @@ define([ ``` ## buildMockCell -`buildMockCell(cellType, kbaseCellType, data)` +`buildMockCell(cellType, kbaseCellType, data)` This function builds a mock Jupyter notebook cell, given a Jupyter notebook cell type and an optional KBase cell type. It includes a minimal structure needed to portray a cell, including minimum required DOM elements. The returned object has these attributes: * `metadata`: object, with some basic metadata, * `cell_type`: string, the given cellType, * `renderMinMax`: a no-op function, * `celltoolbar`: an object with a rebuild function, that's a no-op * `element`: a jQuery node with a single div -* `input`: a jQuery node with the DOM structure: +* `input`: a jQuery node with the DOM structure: ```
``` -* `output`: a jQuery node with the DOM structure: +* `output`: a jQuery node with the DOM structure: ```
@@ -57,7 +57,7 @@ This function builds a mock Jupyter notebook cell, given a Jupyter notebook cell **example** ```Javascript define([ - '../../../../../../narrative/nbextensions/bulkImportCell/bulkImportCell', + '/narrative/nbextensions/bulkImportCell/bulkImportCell', 'narrativeMocks' ], (BulkImportCell, Mocks) => { describe('Make a dummy cell for testing', () => { @@ -83,24 +83,24 @@ define([ ``` ## buildMockNotebook -`buildMockNotebook(options)` +`buildMockNotebook(options)` Builds a mock Jupyter notebook object with a few attributes, but mostly an empty object for modification for whatever testing purposes. One simple way to use this is to set it to the `Jupyter.notebook` global object for further usage. Be sure to clear it when you're done! -**parameters** +**parameters** There's a single `options` parameter here, which is an object with several possible attributes: * `deleteCallback` - function - called when `delete_cell` is called * `fullyLoaded` - boolean - if true, treat the notebook as fully loaded * `cells` - Array - an array of mocked cells (see [buildMockCell](#buildmockcell)) * `readOnly` - boolean - set true if the Narrative should be read-only -**example** +**example** Example taken from the bulk import cell tests. ```Javascript define([ 'jquery', - '../../../../../../narrative/nbextensions/bulkImportCell/main', - '../../../../../../narrative/nbextensions/bulkImportCell/bulkImportCell', + '/narrative/nbextensions/bulkImportCell/main', + '/narrative/nbextensions/bulkImportCell/bulkImportCell', 'base/js/namespace', 'narrativeMocks', ], ($, Main, BulkImportCell, Jupyter, Mocks) => { @@ -140,7 +140,7 @@ define([ }); ``` ## mockServiceWizardLookup -`mockServiceWizardLookup(args)` +`mockServiceWizardLookup(args)` There are a number of KBase dynamic services that get used in the Narrative (NarrativeService comes to mind as an obvious one, also several data providers). These work by having a generic Service Wizard client first query for the service's URL, then make the call to it. Which means that the service wizard needs its own special mocking function to be called first. This uses jasmine.Ajax to mock a request to the service wizard. It will return the proper dynamic service URL that will be requested by the client when the call is made. If it's to be mocked, be sure to have a mock for that endpoint as well. @@ -149,14 +149,14 @@ The basic usage is that you'll need to provide the service wizard's URL, as well Note that this requires running `jasmine.Ajax.install()` first. -**parameters** +**parameters** There's only a single parameter, `args`, that is an object. Its expected attributes are below: * `module` - string - the module to mock. This should be its registered name, e.g. `NarrativeService` * `url` - string - the fake url to return. Requests to this should be mocked, too! * `statusCode` - int, optional, default 200 - the HTTP status to return. Set to something in the 400 range to mock a user error, and in the 500 range to mock a service error. * `statusText` - optional, default 'OK' - a status text to return, if you're changing the mocked status from 200, this should get changed, too. -**example** +**example** This is a partial test set taken from the App Panel tests. ```Javascript @@ -182,8 +182,8 @@ define(['narrativeMocks', 'narrativeConfig'], (Mocks, Config) => { }); }); ``` -## mockJsonRpc1Call -`mockJsonRpc1Call(args)` +## mockJsonRpc1Call +`mockJsonRpc1Call(args)` This mocks a KBase-style JSON-RPC 1.1 request, which is most KBase service calls. They're not exactly the JSON-RPC spec, but this sticks to what KBase does. Here are some simple usages: * **simple happy response:** ```Javascript @@ -206,7 +206,7 @@ mockJsonRpc1Call({ Note that this requires `jasmine.Ajax.install()` to be run first. -**parameters** +**parameters** This takes a single object as a parameter. The allowed attributes are below: * `url` - string - the url endpoint to mock * `body` - optional - string or regex, default = empty string - something from the body to identify the request, either a string or regex. For a KBase service call, this can be a function method, for example. @@ -257,7 +257,7 @@ This takes a single object as a parameter. The allowed attributes are below: }); ``` ## mockAuthRequest -`mockAuthRequest(path, response, statusCode)` +`mockAuthRequest(path, response, statusCode)` A simple auth request mocker. This takes a path to the auth REST service, a response to return, and the status code, and builds a mock that satisfies all of that. Requires that `jasmine.Ajax.install()` has been run first. @@ -267,7 +267,7 @@ Requires that `jasmine.Ajax.install()` has been run first. * `response` - object - the response object that the test expects to see * `statusCode` - int - the HTTP status code to return -**example** +**example** This is an example from the Narrative Auth API tests, slightly tweaked. ```Javascript @@ -298,20 +298,20 @@ define([ }); ``` -## setAuthToken -`setAuthToken(token)` +## setAuthToken +`setAuthToken(token)` Sets an arbitrary string in the test browser's auth token cookie. From the tests' points of view, this appears to be a real auth token. Helpful for the cases where you want to appear logged in. -**parameters** +**parameters** * `token` - string - the dummy auth token string to set. -**example** +**example** ```Javascript // This example goes through an exercise with using an auth client, mocking an auth call, // and making sure the request sent was correct. define([ - 'narrativeMocks', - 'api/auth', + 'narrativeMocks', + 'api/auth', 'narrativeConfig' ], (Mocks, Auth, Config) => { describe('Some auth-mocking tests', () => { @@ -338,13 +338,13 @@ define([ }); ``` -## clearAuthToken -`clearAuthToken()` +## clearAuthToken +`clearAuthToken()` Clears any set auth token cookie. -**parameters** -None -**example** +**parameters** +None +**example** ```Javascript define(['narrativeMocks'], (Mocks) => { describe('Some auth-mocking tests', () => { @@ -353,7 +353,3 @@ define(['narrativeMocks'], (Mocks) => { }); }); ``` - - - - diff --git a/kbase-extension/ipython/profile_default/ipython_config.py b/kbase-extension/ipython/profile_default/ipython_config.py index 7c33690c2d..c31ae5a019 100644 --- a/kbase-extension/ipython/profile_default/ipython_config.py +++ b/kbase-extension/ipython/profile_default/ipython_config.py @@ -1,11 +1,11 @@ # Configuration file for ipython. -c = get_config() +c = get_config() # noqa: F821 c.Completer.use_jedi = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # InteractiveShellApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A Mixin for applications that start InteractiveShell instances. # @@ -33,10 +33,12 @@ # c.InteractiveShellApp.exec_PYTHONSTARTUP = True # lines of code to run at IPython startup. -c.InteractiveShellApp.exec_lines = [ 'import biokbase.narrative.magics', - 'from biokbase.narrative.services import *', - 'from biokbase.narrative.widgetmanager import WidgetManager', - 'from biokbase.narrative.jobs import *' ] +c.InteractiveShellApp.exec_lines = [ + "import biokbase.narrative.magics", + "from biokbase.narrative.services import *", + "from biokbase.narrative.widgetmanager import WidgetManager", + "from biokbase.narrative.jobs import *", +] # Enable GUI event loop integration with any of ('glut', 'gtk', 'gtk3', 'osx', # 'pyglet', 'qt', 'qt5', 'tk', 'wx'). # c.InteractiveShellApp.gui = None @@ -72,9 +74,9 @@ # A file to be run # c.InteractiveShellApp.file_to_run = '' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalIPythonApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalIPythonApp will inherit config from: BaseIPythonApplication, # Application, InteractiveShellApp @@ -177,9 +179,9 @@ # The Logging format template # c.TerminalIPythonApp.log_format = '[%(name)s]%(highlevel)s %(message)s' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalInteractiveShell configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalInteractiveShell will inherit config from: InteractiveShell @@ -340,9 +342,9 @@ # Automatically call the pdb debugger after every exception. # c.TerminalInteractiveShell.pdb = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PromptManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # This is the primary interface for producing IPython's prompts. @@ -361,9 +363,9 @@ # # c.PromptManager.color_scheme = 'Linux' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # HistoryManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A class to organize all history-related functionality in one place. @@ -402,9 +404,9 @@ # This may be necessary in some threaded environments where IPython is embedded. # c.HistoryManager.enabled = True -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. # @@ -418,9 +420,9 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PlainTextFormatter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # The default pretty-printer. # @@ -475,9 +477,9 @@ # # c.PlainTextFormatter.singleton_printers = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPCompleter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Extension of the completer class with IPython-specific features @@ -515,9 +517,9 @@ # etc., but can be unsafe because the code is actually evaluated on TAB. # c.IPCompleter.greedy = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ScriptMagics configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Magics for talking to scripts # @@ -539,9 +541,9 @@ # the right interpreter. # c.ScriptMagics.script_paths = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # StoreMagics configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Lightweight persistence for python variables. # diff --git a/kbase-extension/ipython/profile_default/ipython_kernel_config.py b/kbase-extension/ipython/profile_default/ipython_kernel_config.py index 8da23b4c3e..d1c304dd52 100644 --- a/kbase-extension/ipython/profile_default/ipython_kernel_config.py +++ b/kbase-extension/ipython/profile_default/ipython_kernel_config.py @@ -1,10 +1,10 @@ # Configuration file for ipython-kernel. -c = get_config() +c = get_config() # noqa: F821 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPKernelApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPython: an enhanced interactive Python shell. @@ -62,7 +62,7 @@ # c.IPKernelApp.log_level = 30 # lines of code to run at IPython startup. -c.IPKernelApp.exec_lines = [ 'import biokbase.narrative.magics' ] +c.IPKernelApp.exec_lines = ["import biokbase.narrative.magics"] # Path to an extra config file to load. # @@ -154,9 +154,9 @@ # set the iopub (PUB) port [default: random] # c.IPKernelApp.iopub_port = 0 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPythonKernel configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPythonKernel will inherit config from: Kernel @@ -171,9 +171,9 @@ # # c.IPythonKernel._poll_interval = 0.05 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ZMQInteractiveShell configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A subclass of InteractiveShell for ZMQ. @@ -303,9 +303,9 @@ # Automatically call the pdb debugger after every exception. # c.ZMQInteractiveShell.pdb = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. # @@ -319,9 +319,9 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Session configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Object for handling serialization and sending of messages. # diff --git a/kbase-extension/jupyter_config.py b/kbase-extension/jupyter_config.py index 621c5ee560..037499ef74 100644 --- a/kbase-extension/jupyter_config.py +++ b/kbase-extension/jupyter_config.py @@ -1,18 +1,18 @@ # Configuration file for ipython. -c = get_config() -#------------------------------------------------------------------------------ +c = get_config() # noqa: F821 +# ------------------------------------------------------------------------------ # InteractiveShellApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A Mixin for applications that start InteractiveShell instances. -# +# # Provides configurables for loading extensions and executing files as part of # configuring a Shell environment. -# +# # The following methods should be called by the :meth:`initialize` method of the # subclass: -# +# # - :meth:`init_path` # - :meth:`init_shell` (to be implemented by the subclass) # - :meth:`init_gui_pylab` @@ -45,7 +45,7 @@ # If true, IPython will populate the user namespace with numpy, pylab, etc. and # an ``import *`` is done from numpy and pylab, when using pylab mode. -# +# # When False, pylab mode should not import any names into the user namespace. # c.InteractiveShellApp.pylab_import_all = True @@ -68,9 +68,9 @@ # A file to be run # c.InteractiveShellApp.file_to_run = '' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalIPythonApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalIPythonApp will inherit config from: BaseIPythonApplication, # Application, InteractiveShellApp @@ -109,7 +109,7 @@ # c.TerminalIPythonApp.ignore_old_config = False # Path to an extra config file to load. -# +# # If specified, load this config file in addition to any other IPython config. # c.TerminalIPythonApp.extra_config_file = u'' @@ -135,7 +135,7 @@ # If true, IPython will populate the user namespace with numpy, pylab, etc. and # an ``import *`` is done from numpy and pylab, when using pylab mode. -# +# # When False, pylab mode should not import any names into the user namespace. # c.TerminalIPythonApp.pylab_import_all = True @@ -173,9 +173,9 @@ # The Logging format template # c.TerminalIPythonApp.log_format = '[%(name)s]%(highlevel)s %(message)s' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalInteractiveShell configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TerminalInteractiveShell will inherit config from: InteractiveShell @@ -191,7 +191,7 @@ # user input before code is run. # c.TerminalInteractiveShell.ast_transformers = [] -# +# # c.TerminalInteractiveShell.history_length = 10000 # Don't call post-execute functions that have failed in the past. @@ -210,13 +210,13 @@ # Autoindent IPython code entered interactively. # c.TerminalInteractiveShell.autoindent = True -# +# # c.TerminalInteractiveShell.separate_in = '\n' # Deprecated, use PromptManager.in2_template # c.TerminalInteractiveShell.prompt_in2 = ' .\\D.: ' -# +# # c.TerminalInteractiveShell.separate_out = '' # Deprecated, use PromptManager.in_template @@ -249,19 +249,19 @@ # The part of the banner to be printed before the profile # c.TerminalInteractiveShell.banner1 = 'Python 2.7.6 (default, Nov 18 2013, 15:12:51) \nType "copyright", "credits" or "license" for more information.\n\nIPython 3.2.0-dev -- An enhanced Interactive Python.\n? -> Introduction and overview of IPython\'s features.\n%quickref -> Quick reference.\nhelp -> Python\'s own help system.\nobject? -> Details about \'object\', use \'object??\' for extra details.\n' -# +# # c.TerminalInteractiveShell.readline_parse_and_bind = ['tab: complete', '"\\C-l": clear-screen', 'set show-all-if-ambiguous on', '"\\C-o": tab-insert', '"\\C-r": reverse-search-history', '"\\C-s": forward-search-history', '"\\C-p": history-search-backward', '"\\C-n": history-search-forward', '"\\e[A": history-search-backward', '"\\e[B": history-search-forward', '"\\C-k": kill-line', '"\\C-u": unix-line-discard'] # The part of the banner to be printed after the profile # c.TerminalInteractiveShell.banner2 = '' -# +# # c.TerminalInteractiveShell.separate_out2 = '' -# +# # c.TerminalInteractiveShell.wildcards_case_sensitive = True -# +# # c.TerminalInteractiveShell.debug = False # Set to confirm when you try to exit IPython with an EOF (Control-D in Unix, @@ -269,10 +269,10 @@ # direct exit without any confirmation. # c.TerminalInteractiveShell.confirm_exit = True -# +# # c.TerminalInteractiveShell.ipython_dir = '' -# +# # c.TerminalInteractiveShell.readline_remove_delims = '-/~' # Start logging to the default log file in overwrite mode. Use `logappend` to @@ -291,7 +291,7 @@ # Save multi-line entries as one entry in readline history # c.TerminalInteractiveShell.multiline_history = True -# +# # c.TerminalInteractiveShell.readline_use = True # Enable deep (recursive) reloading by default. IPython can use the deep_reload @@ -306,16 +306,16 @@ # file to **overwrite** logs to. # c.TerminalInteractiveShell.logappend = '' -# +# # c.TerminalInteractiveShell.xmode = 'Context' -# +# # c.TerminalInteractiveShell.quiet = False # Enable auto setting the terminal title. # c.TerminalInteractiveShell.term_title = False -# +# # c.TerminalInteractiveShell.object_info_string_level = 0 # Deprecated, use PromptManager.out_template @@ -336,9 +336,9 @@ # Automatically call the pdb debugger after every exception. # c.TerminalInteractiveShell.pdb = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PromptManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # This is the primary interface for producing IPython's prompts. @@ -354,12 +354,12 @@ # Input prompt. '\#' will be transformed to the prompt number # c.PromptManager.in_template = 'In [\\#]: ' -# +# # c.PromptManager.color_scheme = 'Linux' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # HistoryManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A class to organize all history-related functionality in one place. @@ -373,40 +373,40 @@ # c.HistoryManager.db_cache_size = 0 # Path to file to use for SQLite history database. -# +# # By default, IPython will put the history database in the IPython profile # directory. If you would rather share one history among profiles, you can set # this value in each, so that they are consistent. -# +# # Due to an issue with fcntl, SQLite is known to misbehave on some NFS mounts. # If you see IPython hanging, try setting this to something on a local disk, # e.g:: -# +# # ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite # c.HistoryManager.hist_file = u'' # Options for configuring the SQLite connection -# +# # These options are passed as keyword args to sqlite3.connect when establishing # database conenctions. # c.HistoryManager.connection_options = {} # enable the SQLite history -# +# # set enabled=False to disable the SQLite history, in which case there will be # no stored history, no SQLite connection, and no background saving thread. # This may be necessary in some threaded environments where IPython is embedded. # c.HistoryManager.enabled = True -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. -# +# # The profile directory is used by all IPython applications, to manage # configuration, logging and security. -# +# # This object knows how to find, create and manage these directories. This # should be used by any code that wants to handle profiles. @@ -414,17 +414,17 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PlainTextFormatter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # The default pretty-printer. -# +# # This uses :mod:`IPython.lib.pretty` to compute the format data of the object. # If the object cannot be pretty printed, :func:`repr` is used. See the # documentation of :mod:`IPython.lib.pretty` for details on how to write pretty # printers. Here is a simple example:: -# +# # def dtype_pprinter(obj, p, cycle): # if cycle: # return p.text('dtype(...)') @@ -442,105 +442,105 @@ # PlainTextFormatter will inherit config from: BaseFormatter -# +# # c.PlainTextFormatter.type_printers = {} # Truncate large collections (lists, dicts, tuples, sets) to this size. -# +# # Set to 0 to disable truncation. # c.PlainTextFormatter.max_seq_length = 1000 -# +# # c.PlainTextFormatter.float_precision = '' -# +# # c.PlainTextFormatter.verbose = False -# +# # c.PlainTextFormatter.deferred_printers = {} -# +# # c.PlainTextFormatter.newline = '\n' -# +# # c.PlainTextFormatter.max_width = 79 -# +# # c.PlainTextFormatter.pprint = True -# +# # c.PlainTextFormatter.singleton_printers = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPCompleter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Extension of the completer class with IPython-specific features # IPCompleter will inherit config from: Completer # Instruct the completer to omit private method names -# +# # Specifically, when completing on ``object.``. -# +# # When 2 [default]: all names that start with '_' will be excluded. -# +# # When 1: all 'magic' names (``__foo__``) will be excluded. -# +# # When 0: nothing will be excluded. # c.IPCompleter.omit__names = 2 # Whether to merge completion results into a single list -# +# # If False, only the completion results from the first non-empty completer will # be returned. # c.IPCompleter.merge_completions = True # Instruct the completer to use __all__ for the completion -# +# # Specifically, when completing on ``object.``. -# +# # When True: only those names in obj.__all__ will be included. -# +# # When False [default]: the __all__ attribute is ignored # c.IPCompleter.limit_to__all__ = False # Activate greedy completion -# +# # This will enable completion on elements of lists, results of function calls, # etc., but can be unsafe because the code is actually evaluated on TAB. # c.IPCompleter.greedy = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ScriptMagics configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Magics for talking to scripts -# +# # This defines a base `%%script` cell magic for running a cell with a program in # a subprocess, and registers a few top-level magics that call %%script with # common interpreters. # Extra script cell magics to define -# +# # This generates simple wrappers of `%%script foo` as `%%foo`. -# +# # If you want to add script magics that aren't on your path, specify them in # script_paths # c.ScriptMagics.script_magics = [] # Dict mapping short 'ruby' names to full paths, such as '/opt/secret/bin/ruby' -# +# # Only necessary for items in script_magics where the default path will not find # the right interpreter. # c.ScriptMagics.script_paths = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # StoreMagics configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Lightweight persistence for python variables. -# +# # Provides the %store magic. # If True, any %store-d variables will be automatically restored when IPython diff --git a/kbase-extension/jupyter_console_config.py b/kbase-extension/jupyter_console_config.py index efb363a539..2fcfa183bf 100644 --- a/kbase-extension/jupyter_console_config.py +++ b/kbase-extension/jupyter_console_config.py @@ -1,10 +1,10 @@ # Configuration file for ipython-console. -c = get_config() +c = get_config() # noqa: F821 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ZMQTerminalIPythonApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ZMQTerminalIPythonApp will inherit config from: TerminalIPythonApp, # BaseIPythonApplication, Application, InteractiveShellApp, IPythonConsoleApp, @@ -63,7 +63,7 @@ # c.ZMQTerminalIPythonApp.ignore_old_config = False # Path to an extra config file to load. -# +# # If specified, load this config file in addition to any other IPython config. # c.ZMQTerminalIPythonApp.extra_config_file = u'' @@ -90,7 +90,7 @@ # c.ZMQTerminalIPythonApp.profile = u'default' # JSON file in which to store connection info [default: kernel-.json] -# +# # This file will contain the IP, ports, and authentication key needed to connect # clients to this kernel. By default, this file will be created in the security # dir of the current profile, but can be specified by absolute path. @@ -102,7 +102,7 @@ # If true, IPython will populate the user namespace with numpy, pylab, etc. and # an ``import *`` is done from numpy and pylab, when using pylab mode. -# +# # When False, pylab mode should not import any names into the user namespace. # c.ZMQTerminalIPythonApp.pylab_import_all = True @@ -151,12 +151,12 @@ # set the iopub (PUB) port [default: random] # c.ZMQTerminalIPythonApp.iopub_port = 0 -# +# # c.ZMQTerminalIPythonApp.transport = 'tcp' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ZMQTerminalInteractiveShell configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A subclass of TerminalInteractiveShell that uses the 0MQ kernel @@ -167,7 +167,7 @@ # c.ZMQTerminalInteractiveShell.autoedit_syntax = False # Timeout for giving up on a kernel (in seconds). -# +# # On first connect and restart, the console tests whether the kernel is running # and responsive by sending kernel_info_requests. This sets the timeout in # seconds for how long the kernel can take before being presumed dead. @@ -182,7 +182,7 @@ # user input before code is run. # c.ZMQTerminalInteractiveShell.ast_transformers = [] -# +# # c.ZMQTerminalInteractiveShell.history_length = 10000 # Don't call post-execute functions that have failed in the past. @@ -213,7 +213,7 @@ # Autoindent IPython code entered interactively. # c.ZMQTerminalInteractiveShell.autoindent = True -# +# # c.ZMQTerminalInteractiveShell.separate_in = '\n' # Command to invoke an image viewer program when you are using 'stream' image @@ -225,7 +225,7 @@ # Deprecated, use PromptManager.in2_template # c.ZMQTerminalInteractiveShell.prompt_in2 = ' .\\D.: ' -# +# # c.ZMQTerminalInteractiveShell.separate_out = '' # Deprecated, use PromptManager.in_template @@ -258,7 +258,7 @@ # The part of the banner to be printed before the profile # c.ZMQTerminalInteractiveShell.banner1 = 'Python 2.7.6 (default, Nov 18 2013, 15:12:51) \nType "copyright", "credits" or "license" for more information.\n\nIPython 3.2.0-dev -- An enhanced Interactive Python.\n? -> Introduction and overview of IPython\'s features.\n%quickref -> Quick reference.\nhelp -> Python\'s own help system.\nobject? -> Details about \'object\', use \'object??\' for extra details.\n' -# +# # c.ZMQTerminalInteractiveShell.readline_parse_and_bind = ['tab: complete', '"\\C-l": clear-screen', 'set show-all-if-ambiguous on', '"\\C-o": tab-insert', '"\\C-r": reverse-search-history', '"\\C-s": forward-search-history', '"\\C-p": history-search-backward', '"\\C-n": history-search-forward', '"\\e[A": history-search-backward', '"\\e[B": history-search-forward', '"\\C-k": kill-line', '"\\C-u": unix-line-discard'] # The part of the banner to be printed after the profile @@ -266,11 +266,11 @@ # Whether to include output from clients other than this one sharing the same # kernel. -# +# # Outputs are not displayed until enter is pressed. # c.ZMQTerminalInteractiveShell.include_other_output = False -# +# # c.ZMQTerminalInteractiveShell.separate_out2 = '' # Command to invoke an image viewer program when you are using 'tempfile' image @@ -284,24 +284,24 @@ # will be used. # c.ZMQTerminalInteractiveShell.mime_preference = ['image/png', 'image/jpeg', 'image/svg+xml'] -# +# # c.ZMQTerminalInteractiveShell.wildcards_case_sensitive = True # Prefix to add to outputs coming from clients other than this one. -# +# # Only relevant if include_other_output is True. # c.ZMQTerminalInteractiveShell.other_output_prefix = '[remote] ' -# +# # c.ZMQTerminalInteractiveShell.debug = False -# +# # c.ZMQTerminalInteractiveShell.object_info_string_level = 0 -# +# # c.ZMQTerminalInteractiveShell.ipython_dir = '' -# +# # c.ZMQTerminalInteractiveShell.readline_remove_delims = '-/~' # Start logging to the default log file in overwrite mode. Use `logappend` to @@ -320,7 +320,7 @@ # Save multi-line entries as one entry in readline history # c.ZMQTerminalInteractiveShell.multiline_history = True -# +# # c.ZMQTerminalInteractiveShell.readline_use = True # Callable object called via 'callable' image handler with one argument, `data`, @@ -341,10 +341,10 @@ # file to **overwrite** logs to. # c.ZMQTerminalInteractiveShell.logappend = '' -# +# # c.ZMQTerminalInteractiveShell.xmode = 'Context' -# +# # c.ZMQTerminalInteractiveShell.quiet = False # Enable auto setting the terminal title. @@ -373,18 +373,18 @@ # Automatically call the pdb debugger after every exception. # c.ZMQTerminalInteractiveShell.pdb = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # KernelManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Manages a single kernel in a subprocess on this host. -# +# # This version starts kernels with Popen. # KernelManager will inherit config from: ConnectionFileMixin # DEPRECATED: Use kernel_name instead. -# +# # The Popen Command to launch the kernel. Override this if you have a custom # kernel. If kernel_cmd is specified in a configuration file, IPython does not # pass any arguments to the kernel, because it cannot make any assumptions about @@ -405,7 +405,7 @@ # c.KernelManager.ip = u'' # JSON file in which to store connection info [default: kernel-.json] -# +# # This file will contain the IP, ports, and authentication key needed to connect # clients to this kernel. By default, this file will be created in the security # dir of the current profile, but can be specified by absolute path. @@ -420,21 +420,21 @@ # set the shell (ROUTER) port [default: random] # c.KernelManager.shell_port = 0 -# +# # c.KernelManager.transport = 'tcp' # set the iopub (PUB) port [default: random] # c.KernelManager.iopub_port = 0 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. -# +# # The profile directory is used by all IPython applications, to manage # configuration, logging and security. -# +# # This object knows how to find, create and manage these directories. This # should be used by any code that wants to handle profiles. @@ -442,32 +442,32 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Session configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Object for handling serialization and sending of messages. -# +# # The Session object handles building messages and sending them with ZMQ sockets # or ZMQStream objects. Objects can communicate with each other over the # network via Session objects, and only need to work with the dict-based IPython # message spec. The Session will handle serialization/deserialization, security, # and metadata. -# +# # Sessions support configurable serialization via packer/unpacker traits, and # signing with HMAC digests via the key/keyfile traits. -# +# # Parameters ---------- -# +# # debug : bool # whether to trigger extra debugging statements # packer/unpacker : str : 'json', 'pickle' or import_string # importstrings for methods to serialize message parts. If just # 'json' or 'pickle', predefined JSON and pickle packers will be used. # Otherwise, the entire importstring must be used. -# +# # The functions must accept at least valid JSON input, and output *bytes*. -# +# # For example, to use msgpack: # packer = 'msgpack.packb', unpacker='msgpack.unpackb' # pack/unpack : callables @@ -498,7 +498,7 @@ # c.Session.packer = 'json' # The maximum number of digests to remember. -# +# # The digest history will be culled when it exceeds this value. # c.Session.digest_history_size = 65536 diff --git a/kbase-extension/jupyter_kernel_config.py b/kbase-extension/jupyter_kernel_config.py index 33259d8512..0fe721b38d 100644 --- a/kbase-extension/jupyter_kernel_config.py +++ b/kbase-extension/jupyter_kernel_config.py @@ -1,10 +1,10 @@ # Configuration file for ipython-kernel. -c = get_config() +c = get_config() # noqa: F821 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPKernelApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPython: an enhanced interactive Python shell. @@ -62,9 +62,11 @@ # c.IPKernelApp.log_level = 30 # lines of code to run at IPython startup. -c.InteractiveShellApp.exec_lines = [ 'import biokbase.narrative.magics', - 'from biokbase.narrative.widgetmanager import WidgetManager', - 'from biokbase.narrative.jobs import *' ] +c.InteractiveShellApp.exec_lines = [ + "import biokbase.narrative.magics", + "from biokbase.narrative.widgetmanager import WidgetManager", + "from biokbase.narrative.jobs import *", +] # Path to an extra config file to load. # @@ -156,9 +158,9 @@ # set the iopub (PUB) port [default: random] # c.IPKernelApp.iopub_port = 0 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPythonKernel configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # IPythonKernel will inherit config from: Kernel @@ -173,9 +175,9 @@ # # c.IPythonKernel._poll_interval = 0.05 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ZMQInteractiveShell configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A subclass of InteractiveShell for ZMQ. @@ -305,9 +307,9 @@ # Automatically call the pdb debugger after every exception. # c.ZMQInteractiveShell.pdb = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. # @@ -321,9 +323,9 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Session configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Object for handling serialization and sending of messages. # diff --git a/kbase-extension/jupyter_nbconvert_config.py b/kbase-extension/jupyter_nbconvert_config.py index bb5d0163ab..b93cd1412b 100644 --- a/kbase-extension/jupyter_nbconvert_config.py +++ b/kbase-extension/jupyter_nbconvert_config.py @@ -1,14 +1,14 @@ # Configuration file for ipython-nbconvert. -c = get_config() +c = get_config() # noqa: F821 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # NbConvertApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # This application is used to convert notebook files (*.ipynb) to various other # formats. -# +# # WARNING: THE COMMANDLINE INTERFACE MAY CHANGE IN FUTURE RELEASES. # NbConvertApp will inherit config from: BaseIPythonApplication, Application @@ -33,7 +33,7 @@ # c.NbConvertApp.log_level = 30 # Path to an extra config file to load. -# +# # If specified, load this config file in addition to any other IPython config. # c.NbConvertApp.extra_config_file = u'' @@ -74,12 +74,12 @@ # Whether to overwrite existing config files when copying # c.NbConvertApp.overwrite = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # NbConvertBase configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Global configurable class for shared config -# +# # Useful for display data priority that might be use by many transformers # An ordered list of preferred output type, the first encountered will usually @@ -90,15 +90,15 @@ # instead # c.NbConvertBase.default_language = 'ipython' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. -# +# # The profile directory is used by all IPython applications, to manage # configuration, logging and security. -# +# # This object knows how to find, create and manage these directories. This # should be used by any code that wants to handle profiles. @@ -106,9 +106,9 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Class containing methods that sequentially run a list of preprocessors on a # NotebookNode object and then return the modified NotebookNode object and @@ -124,9 +124,9 @@ # type. # c.Exporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # HTMLExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports a basic HTML document. This exporter assists with the export of HTML. # Inherit from it if you are writing your own HTML template and need custom @@ -135,10 +135,10 @@ # HTMLExporter will inherit config from: TemplateExporter, Exporter -# +# # c.HTMLExporter.jinja_variable_block_start = '' -# +# # c.HTMLExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -151,36 +151,36 @@ # type. # c.HTMLExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.HTMLExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.HTMLExporter.file_extension = '.txt' -# +# # c.HTMLExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.HTMLExporter.filters = {} -# +# # c.HTMLExporter.jinja_comment_block_start = '' -# +# # c.HTMLExporter.jinja_logic_block_end = '' -# +# # c.HTMLExporter.jinja_logic_block_start = '' -# +# # c.HTMLExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.HTMLExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # LatexExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports to a Latex template. Inherit from this class if your template is # LaTeX based and you need custom tranformers/filters. Inherit from it if you @@ -191,10 +191,10 @@ # LatexExporter will inherit config from: TemplateExporter, Exporter -# +# # c.LatexExporter.jinja_variable_block_start = '(((' -# +# # c.LatexExporter.jinja_variable_block_end = ')))' # formats of raw cells to be included in this Exporter's output. @@ -207,45 +207,45 @@ # type. # c.LatexExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.LatexExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.LatexExporter.file_extension = '.txt' -# +# # c.LatexExporter.jinja_comment_block_end = '=))' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.LatexExporter.filters = {} -# +# # c.LatexExporter.jinja_comment_block_start = '((=' -# +# # c.LatexExporter.jinja_logic_block_end = '*))' -# +# # c.LatexExporter.jinja_logic_block_start = '((*' -# +# # c.LatexExporter.template_extension = '.tplx' # List of preprocessors, by name or namespace, to enable. # c.LatexExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # MarkdownExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports to a markdown document (.md) # MarkdownExporter will inherit config from: TemplateExporter, Exporter -# +# # c.MarkdownExporter.jinja_variable_block_start = '' -# +# # c.MarkdownExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -258,36 +258,36 @@ # type. # c.MarkdownExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.MarkdownExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.MarkdownExporter.file_extension = '.txt' -# +# # c.MarkdownExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.MarkdownExporter.filters = {} -# +# # c.MarkdownExporter.jinja_comment_block_start = '' -# +# # c.MarkdownExporter.jinja_logic_block_end = '' -# +# # c.MarkdownExporter.jinja_logic_block_start = '' -# +# # c.MarkdownExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.MarkdownExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # NotebookExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports to an IPython notebook. @@ -306,9 +306,9 @@ # type. # c.NotebookExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PDFExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Writer designed to write to PDF files @@ -318,10 +318,10 @@ # File extensions of temp files to remove after running. # c.PDFExporter.temp_file_exts = ['.aux', '.bbl', '.blg', '.idx', '.log', '.out'] -# +# # c.PDFExporter.jinja_variable_block_start = '(((' -# +# # c.PDFExporter.jinja_logic_block_start = '((*' # Whether to display the output of latex commands. @@ -337,19 +337,19 @@ # type. # c.PDFExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.PDFExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.PDFExporter.file_extension = '.txt' -# +# # c.PDFExporter.jinja_comment_block_end = '=))' -# +# # c.PDFExporter.jinja_variable_block_end = ')))' -# +# # c.PDFExporter.template_extension = '.tplx' # List of preprocessors, by name or namespace, to enable. @@ -358,7 +358,7 @@ # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.PDFExporter.filters = {} -# +# # c.PDFExporter.jinja_comment_block_start = '((=' # Name of the template file to use @@ -367,24 +367,24 @@ # How many times latex will be called. # c.PDFExporter.latex_count = 3 -# +# # c.PDFExporter.jinja_logic_block_end = '*))' # Shell command used to compile latex. # c.PDFExporter.latex_command = [u'pdflatex', u'{filename}'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PythonExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports a Python code file. # PythonExporter will inherit config from: TemplateExporter, Exporter -# +# # c.PythonExporter.jinja_variable_block_start = '' -# +# # c.PythonExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -397,45 +397,45 @@ # type. # c.PythonExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.PythonExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.PythonExporter.file_extension = '.txt' -# +# # c.PythonExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.PythonExporter.filters = {} -# +# # c.PythonExporter.jinja_comment_block_start = '' -# +# # c.PythonExporter.jinja_logic_block_end = '' -# +# # c.PythonExporter.jinja_logic_block_start = '' -# +# # c.PythonExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.PythonExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # RSTExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports restructured text documents. # RSTExporter will inherit config from: TemplateExporter, Exporter -# +# # c.RSTExporter.jinja_variable_block_start = '' -# +# # c.RSTExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -448,46 +448,46 @@ # type. # c.RSTExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.RSTExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.RSTExporter.file_extension = '.txt' -# +# # c.RSTExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.RSTExporter.filters = {} -# +# # c.RSTExporter.jinja_comment_block_start = '' -# +# # c.RSTExporter.jinja_logic_block_end = '' -# +# # c.RSTExporter.jinja_logic_block_start = '' -# +# # c.RSTExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.RSTExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # SlidesExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports HTML slides with reveal.js # SlidesExporter will inherit config from: HTMLExporter, TemplateExporter, # Exporter -# +# # c.SlidesExporter.jinja_variable_block_start = '' -# +# # c.SlidesExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -500,36 +500,36 @@ # type. # c.SlidesExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.SlidesExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.SlidesExporter.file_extension = '.txt' -# +# # c.SlidesExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.SlidesExporter.filters = {} -# +# # c.SlidesExporter.jinja_comment_block_start = '' -# +# # c.SlidesExporter.jinja_logic_block_end = '' -# +# # c.SlidesExporter.jinja_logic_block_start = '' -# +# # c.SlidesExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.SlidesExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # TemplateExporter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Exports notebooks into other file formats. Uses Jinja 2 templating engine to # output new formats. Inherit from this class if you are creating a new @@ -537,7 +537,7 @@ # preprocessors provided by default suffice, there is no need to inherit from # this class. Instead, override the template_file and file_extension traits via # a config file. -# +# # - citation2latex - highlight2html - filter_data_type - markdown2html - # markdown2rst - get_lines - ansi2latex - strip_ansi - add_prompts - # comment_lines - ascii_only - markdown2latex - escape_latex - add_anchor - @@ -547,10 +547,10 @@ # TemplateExporter will inherit config from: Exporter -# +# # c.TemplateExporter.jinja_variable_block_start = '' -# +# # c.TemplateExporter.jinja_variable_block_end = '' # formats of raw cells to be included in this Exporter's output. @@ -563,36 +563,36 @@ # type. # c.TemplateExporter.default_preprocessors = ['IPython.nbconvert.preprocessors.ExecutePreprocessor', 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor', 'IPython.nbconvert.preprocessors.coalesce_streams', 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor', 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor', 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor', 'IPython.nbconvert.preprocessors.LatexPreprocessor', 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor', 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor'] -# +# # c.TemplateExporter.template_path = ['.'] # Extension of the file that should be written to disk # c.TemplateExporter.file_extension = '.txt' -# +# # c.TemplateExporter.jinja_comment_block_end = '' # Dictionary of filters, by name and namespace, to add to the Jinja environment. # c.TemplateExporter.filters = {} -# +# # c.TemplateExporter.jinja_comment_block_start = '' -# +# # c.TemplateExporter.jinja_logic_block_end = '' -# +# # c.TemplateExporter.jinja_logic_block_start = '' -# +# # c.TemplateExporter.template_extension = '.tpl' # List of preprocessors, by name or namespace, to enable. # c.TemplateExporter.preprocessors = [] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # CSSHTMLHeaderPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Preprocessor used to pre-process notebook for HTML output. Adds IPython # notebook front-end CSS and Pygments CSS to HTML output. @@ -607,16 +607,16 @@ # CSS highlight class identifier # c.CSSHTMLHeaderPreprocessor.highlight_class = '.highlight' -# +# # c.CSSHTMLHeaderPreprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.CSSHTMLHeaderPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ClearOutputPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Removes the output from all code cells in a notebook. @@ -626,16 +626,16 @@ # instead # c.ClearOutputPreprocessor.default_language = 'ipython' -# +# # c.ClearOutputPreprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.ClearOutputPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ConvertFiguresPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Converts all of the outputs in a notebook from one format to another. @@ -649,7 +649,7 @@ # Format the converter writes # c.ConvertFiguresPreprocessor.to_format = u'' -# +# # c.ConvertFiguresPreprocessor.enabled = False # Format the converter accepts @@ -659,9 +659,9 @@ # be used when converting discarding the others. # c.ConvertFiguresPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ExecutePreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Executes all the cells in a notebook @@ -675,7 +675,7 @@ # other cells rather than throwing an error and stopping. # c.ExecutePreprocessor.interrupt_on_timeout = False -# +# # c.ExecutePreprocessor.enabled = False # The time to wait (in seconds) for output from executions. @@ -685,9 +685,9 @@ # be used when converting discarding the others. # c.ExecutePreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ExtractOutputPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Extracts all of the outputs from the notebook file. The extracted outputs # are returned in the 'resources' dictionary. @@ -699,22 +699,22 @@ # instead # c.ExtractOutputPreprocessor.default_language = 'ipython' -# +# # c.ExtractOutputPreprocessor.output_filename_template = '{unique_key}_{cell_index}_{index}{extension}' -# +# # c.ExtractOutputPreprocessor.extract_output_types = set(['image/png', 'application/pdf', 'image/jpeg', 'image/svg+xml']) -# +# # c.ExtractOutputPreprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.ExtractOutputPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # HighlightMagicsPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Detects and tags code cells that use a different languages than Python. @@ -725,7 +725,7 @@ # language magic extension such as %%R, with a pygments lexer such as r. # c.HighlightMagicsPreprocessor.languages = {} -# +# # c.HighlightMagicsPreprocessor.enabled = False # DEPRECATED default highlight language, please use language_info metadata @@ -736,12 +736,12 @@ # be used when converting discarding the others. # c.HighlightMagicsPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # LatexPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Preprocessor for latex destined documents. -# +# # Mainly populates the `latex` key in the resources dict, adding definitions for # pygments highlight styles. @@ -751,29 +751,29 @@ # instead # c.LatexPreprocessor.default_language = 'ipython' -# +# # c.LatexPreprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.LatexPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Preprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A configurable preprocessor -# +# # Inherit from this class if you wish to have configurability for your # preprocessor. -# +# # Any configurable traitlets this class exposed will be configurable in profiles # using c.SubClassName.attribute = value -# +# # you can overwrite :meth:`preprocess_cell` to apply a transformation # independently on each cell or :meth:`preprocess` if you prefer your own logic. # See corresponding docstring for informations. -# +# # Disabled by default and can be enabled via the config by # 'c.YourPreprocessorName.enabled = True' @@ -783,22 +783,22 @@ # instead # c.Preprocessor.default_language = 'ipython' -# +# # c.Preprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.Preprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # RevealHelpPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # RevealHelpPreprocessor will inherit config from: Preprocessor, NbConvertBase # The URL prefix for reveal.js. This can be a a relative URL for a local copy of # reveal.js, or point to a CDN. -# +# # For speaker notes to work, a local reveal.js prefix must be used. # c.RevealHelpPreprocessor.url_prefix = 'reveal.js' @@ -806,16 +806,16 @@ # instead # c.RevealHelpPreprocessor.default_language = 'ipython' -# +# # c.RevealHelpPreprocessor.enabled = False # An ordered list of preferred output type, the first encountered will usually # be used when converting discarding the others. # c.RevealHelpPreprocessor.display_data_priority = ['text/html', 'application/pdf', 'text/latex', 'image/svg+xml', 'image/png', 'image/jpeg', 'text/plain'] -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # SVG2PDFPreprocessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Converts all of the outputs in a notebook from SVG to PDF. @@ -829,17 +829,17 @@ # instead # c.SVG2PDFPreprocessor.default_language = 'ipython' -# +# # c.SVG2PDFPreprocessor.enabled = False # Format the converter writes # c.SVG2PDFPreprocessor.to_format = u'' # The command to use for converting SVG to PDF -# +# # This string is a template, which will be formatted with the keys to_filename # and from_filename. -# +# # The conversion call must read the SVG from {from_flename}, and write a PDF to # {to_filename}. # c.SVG2PDFPreprocessor.command = u'' @@ -851,9 +851,9 @@ # The path to Inkscape, if necessary # c.SVG2PDFPreprocessor.inkscape = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # FilesWriter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Consumes nbconvert output and produces files. @@ -881,9 +881,9 @@ # notebook. # c.FilesWriter.relpath = '' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # StdoutWriter configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Consumes output from nbconvert export...() methods and writes to the stdout # stream. @@ -902,9 +902,9 @@ # instead # c.StdoutWriter.default_language = 'ipython' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # WriterBase configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Consumes output from nbconvert export...() methods and writes to a useful # location. @@ -923,9 +923,9 @@ # instead # c.WriterBase.default_language = 'ipython' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PostProcessorBase configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # PostProcessorBase will inherit config from: NbConvertBase @@ -937,12 +937,12 @@ # instead # c.PostProcessorBase.default_language = 'ipython' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ServePostProcessor configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Post processor designed to serve files -# +# # Proxies reveal.js requests to a CDN if no local reveal.js is present # ServePostProcessor will inherit config from: PostProcessorBase, NbConvertBase diff --git a/kbase-extension/jupyter_notebook_config.py b/kbase-extension/jupyter_notebook_config.py index aa409f3357..8e2c60a347 100644 --- a/kbase-extension/jupyter_notebook_config.py +++ b/kbase-extension/jupyter_notebook_config.py @@ -1,9 +1,9 @@ # Configuration file for ipython-notebook. -c = get_config() -#------------------------------------------------------------------------------ +c = get_config() # noqa: F821 +# ------------------------------------------------------------------------------ # NotebookApp configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # NotebookApp will inherit config from: BaseIPythonApplication, Application @@ -17,7 +17,7 @@ # c.NotebookApp.jinja_environment_options = {} # The IP address the notebook server will listen on. -c.NotebookApp.ip = 'localhost' +c.NotebookApp.ip = "localhost" # DEPRECATED use base_url # c.NotebookApp.base_project_url = '/' @@ -29,7 +29,7 @@ # Python modules to load as notebook server extensions. This is an experimental # API, and may change in future releases. # c.NotebookApp.server_extensions = [] -c.NotebookApp.server_extensions=['biokbase.narrative.handlers.narrativehandler'] +c.NotebookApp.server_extensions = ["biokbase.narrative.handlers.narrativehandler"] # The random bytes used to secure cookies. By default this is a new random # number every time you start the Notebook. Set it to a value in a config file @@ -72,7 +72,9 @@ # c.NotebookApp.allow_origin = '' # The notebook manager class to use. -c.NotebookApp.contents_manager_class = 'biokbase.narrative.contents.kbasewsmanager.KBaseWSManager' +c.NotebookApp.contents_manager_class = ( + "biokbase.narrative.contents.kbasewsmanager.KBaseWSManager" +) # default: 'IPython.html.services.contents.filemanager.FileContentsManager' # Use a regular expression for the Access-Control-Allow-Origin header @@ -90,7 +92,9 @@ # c.NotebookApp.certfile = u'' # The logout handler class to use. -c.NotebookApp.logout_handler_class = 'biokbase.narrative.handlers.authhandlers.KBaseLogoutHandler' +c.NotebookApp.logout_handler_class = ( + "biokbase.narrative.handlers.authhandlers.KBaseLogoutHandler" +) # The base URL for the notebook server. # @@ -108,8 +112,9 @@ # Supply overrides for the tornado.web.Application that the IPython notebook # uses. -c.NotebookApp.tornado_settings = { 'compress_response': True, } #'debug': True, 'log_function': debug_log } - +c.NotebookApp.tornado_settings = { + "compress_response": True, +} # 'debug': True, 'log_function': debug_log } # The directory to use for notebooks and kernels. @@ -119,7 +124,7 @@ # c.NotebookApp.kernel_manager_class = # The file where the cookie secret is stored. -c.NotebookApp.cookie_secret_file = u'/tmp/notebook_cookie' +c.NotebookApp.cookie_secret_file = u"/tmp/notebook_cookie" # Supply SSL options for the tornado HTTPServer. See the tornado docs for # details. @@ -189,10 +194,11 @@ import os + try: myfile = __file__ except NameError: - myfile = os.path.abspath(inspect.getsourcefile(lambda _: None)) + myfile = os.path.abspath(inspect.getsourcefile(lambda _: None)) # noqa: F821 myfile = os.path.dirname(myfile) @@ -202,7 +208,7 @@ # This allows adding javascript/css to be available from the notebook server # machine, or overriding individual files in the IPython -c.NotebookApp.extra_static_paths = [os.path.join(myfile, 'static')] +c.NotebookApp.extra_static_paths = [os.path.join(myfile, "static")] # The full path to a private key file for usage with SSL/TLS. # c.NotebookApp.keyfile = u'' @@ -216,7 +222,7 @@ # # Can be used to override templates from IPython.html.templates. -c.NotebookApp.extra_template_paths = [os.path.join(myfile, 'kbase_templates')] +c.NotebookApp.extra_template_paths = [os.path.join(myfile, "kbase_templates")] # The config manager class to use # c.NotebookApp.config_manager_class = @@ -229,7 +235,9 @@ # The login handler class to use. # c.NotebookApp.login_handler_class = -c.NotebookApp.login_handler_class = 'biokbase.narrative.handlers.authhandlers.KBaseLoginHandler' +c.NotebookApp.login_handler_class = ( + "biokbase.narrative.handlers.authhandlers.KBaseLoginHandler" +) # DEPRECATED, use tornado_settings # c.NotebookApp.webapp_settings = {} @@ -240,9 +248,9 @@ # variable to override it. # c.NotebookApp.browser = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # KernelManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Manages a single kernel in a subprocess on this host. # @@ -293,9 +301,9 @@ # set the iopub (PUB) port [default: random] # c.KernelManager.iopub_port = 0 -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ProfileDir configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # An object to manage the profile directory and its resources. # @@ -309,9 +317,9 @@ # `profile` option. # c.ProfileDir.location = u'' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Session configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Object for handling serialization and sending of messages. # @@ -397,9 +405,9 @@ # each message. # c.Session.metadata = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # MappingKernelManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A KernelManager that handles notebook mapping and HTTP error handling @@ -415,9 +423,9 @@ # KernelManager for customized behavior. # c.MappingKernelManager.kernel_manager_class = 'IPython.kernel.ioloop.IOLoopKernelManager' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # ContentsManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Base class for serving files and directories. # @@ -471,9 +479,9 @@ # # c.ContentsManager.checkpoints_kwargs = {} -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # FileContentsManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # FileContentsManager will inherit config from: ContentsManager @@ -536,9 +544,9 @@ # DEPRECATED, use post_save_hook # c.FileContentsManager.save_script = False -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # NotebookNotary configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # A class for computing and verifying notebook signatures. @@ -560,9 +568,9 @@ # The hashing algorithm used to sign notebooks. # c.NotebookNotary.algorithm = 'sha256' -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # KernelSpecManager configuration -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Whitelist of allowed kernel names. # diff --git a/kbase-extension/kbase_templates/403.html b/kbase-extension/kbase_templates/403.html index 815772f43a..37a4280a05 100644 --- a/kbase-extension/kbase_templates/403.html +++ b/kbase-extension/kbase_templates/403.html @@ -5,7 +5,9 @@ {% block stylesheet %} {{super()}} - + {% endblock %} {% block loading_message %} diff --git a/kbase-extension/kbase_templates/error.html b/kbase-extension/kbase_templates/error.html index aab8086eb3..bf744e3c58 100644 --- a/kbase-extension/kbase_templates/error.html +++ b/kbase-extension/kbase_templates/error.html @@ -5,7 +5,9 @@ {% block stylesheet %} {{super()}} - + {% endblock %} {% block loading_message %} diff --git a/kbase-extension/kbase_templates/generic_error.html b/kbase-extension/kbase_templates/generic_error.html index 8d40b887d2..e0e98517cc 100644 --- a/kbase-extension/kbase_templates/generic_error.html +++ b/kbase-extension/kbase_templates/generic_error.html @@ -5,7 +5,9 @@ {% block stylesheet %} {{super()}} - + {% endblock %} {% block loading_message %} diff --git a/kbase-extension/kbase_templates/loading.html b/kbase-extension/kbase_templates/loading.html index dfdabef2be..7816101d2e 100644 --- a/kbase-extension/kbase_templates/loading.html +++ b/kbase-extension/kbase_templates/loading.html @@ -1,5 +1,5 @@
-
+
Sorry, the KBase Narrative appears to be taking a long time to load. This could be due to a slow internet connection, or a problem with the Narrative itself. If this seems to be taking much longer than you expect, you can try holding the shift key and reloading this page.
diff --git a/kbase-extension/kbase_templates/notebook.html b/kbase-extension/kbase_templates/notebook.html index a2ac4d213c..13a1930060 100644 --- a/kbase-extension/kbase_templates/notebook.html +++ b/kbase-extension/kbase_templates/notebook.html @@ -14,15 +14,12 @@ - + {{super()}} - + - - - {% endblock %} {% block bodyclasses %}notebook_app {{super()}}{% endblock %} diff --git a/kbase-extension/kbase_templates/page.html b/kbase-extension/kbase_templates/page.html index 4f91b43fa2..ee7ff78436 100644 --- a/kbase-extension/kbase_templates/page.html +++ b/kbase-extension/kbase_templates/page.html @@ -13,17 +13,7 @@ {% block stylesheet %} {% endblock %} - - - - - - - - - - - + - diff --git a/kbase-extension/scss/all_concat.scss b/kbase-extension/scss/all_concat.scss new file mode 100644 index 0000000000..8465751e68 --- /dev/null +++ b/kbase-extension/scss/all_concat.scss @@ -0,0 +1,56 @@ +// SCSS file containing all css + +// Modules +@import "modules/colours"; +@import "modules/variables"; +@import "modules/fonts"; +@import "modules/typography"; +@import "modules/mixins"; + +@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins/buttons"; + +// overrides of specific jupyter notebook formatting bits +@import "partials/custom"; + +// basic formatting +@import "partials/stylesheet"; + +@import "partials/narrative"; +@import "partials/icons"; +@import "partials/landingPages"; +@import "partials/editor"; +@import "partials/notify"; +@import "partials/methodCell"; +@import "partials/bootstrapHelper"; +@import "partials/buttons"; +@import "partials/contigBrowserStyles"; +@import "partials/tour"; +@import "partials/batchMode"; +@import "partials/advancedViewCell"; +@import "partials/errorDialog"; +@import "partials/appCell"; +@import "partials/editorCell"; + +// order should not be important for the following files +@import "partials/appParams"; +@import "partials/bulkImportCell"; +@import "partials/cellToolbar"; +@import "partials/dataStaging"; +@import "partials/dropzoneDz"; +@import "partials/errorDisplay"; +@import "partials/filePath"; +@import "partials/filetypePanel"; +@import "partials/fsm"; +@import "partials/infoTab"; +@import "partials/jobAction"; +@import "partials/jobLog"; +@import "partials/jobStatus"; +@import "partials/kb-icon"; +@import "partials/loadingBlocker"; +@import "partials/narrativeOutline"; +@import "partials/rcp"; +@import "partials/reportViewer"; +@import "partials/select2"; +@import "partials/stagingTable"; +@import "partials/ui"; +@import "partials/userMenu"; diff --git a/kbase-extension/scss/modules/_colours.scss b/kbase-extension/scss/modules/_colours.scss new file mode 100755 index 0000000000..14e07fa649 --- /dev/null +++ b/kbase-extension/scss/modules/_colours.scss @@ -0,0 +1,170 @@ +/* modules/colours */ + +// KBase colours (generated from the KBase logo) +$kbase-palette: ( + + // microbe orange + // Hex: #F78E1E HSL: 31 93% 54% + "orange": rgb(247, 142, 30), + + // golden yellow + // Hex: #FFD200 HSL: 49 100% 50% + "yellow": rgb(255, 210, 0), + + // grass green + // Hex: #5E9732 HSL: 94 50% 39% + "green": rgb(94, 151, 50), + + // spring green + // Hex: #C1CD23 HSL: 64 71% 47% + "grellow": rgb(193, 205, 35), + + // freshwater_blue + // Hex: #037AC0 HSL: 202 97% 38% + "blue": rgb(3, 122, 192), + + // Hex: #72CCD2 HSL: 184 52% 64% + "ocean_blue": rgb(114, 204, 210), + + // cyanobacteria_teal + // Hex: #009688 HSL: 174 100% 29% + "teal": rgb(0, 150, 136), + + // lupine purple + // Hex: #66489D HSL: 261 37% 45% + "purple": rgb(102, 72, 157), + + // Hex: #C7DBEE HSL: 209 53% 86% + "frost_blue": rgb(199, 219, 238), + + // rainier_cherry_red + // Hex: #D2232A HSL: 358 71% 48% + "red": rgb(209, 35, 41), + + // graphite grey + // Hex: #9D9389 HSL: 30, 9%, 58% + "grey": rgb(157, 147, 137), + + "white": rgb(255, 255, 255), + "black": rgb(0, 0, 0), + "ink": rgb(23, 20, 18), + "neutral": rgb(106, 97, 88), + "silver": rgb(192, 192, 192), + + "base-lightest": rgb(242, 239, 235), + "base-lighter": rgb(222, 213, 203), + "base-light": rgb(157, 147, 137), + "base": rgb(106, 97, 88), + "base-dark": rgb(84, 76, 69), + "base-darkest": rgb(62, 56, 50), + + "primary-lightest": rgb(223, 238, 246), + "primary-lighter": rgb(204, 229, 243), + "primary-light": rgb(102, 177, 219), + "primary": rgb(2, 109, 170), + "primary-vivid": rgb(3, 81, 125), + "primary-dark": rgb(2, 62, 96), + "primary-darker": rgb(2, 41, 64), + + "secondary-lightest": rgb(229, 244, 241), + "secondary-lighter": rgb(204, 234, 231), + "secondary-light": rgb(153, 213, 207), + "secondary": rgb(0, 150, 136), + "secondary-vivid": rgb(102, 192, 183), + "secondary-dark": rgb(17, 133, 119), + "secondary-darkest": rgb(6, 86, 77), + + "accent-cool-lightest": rgb(204, 234, 236), + "accent-cool-lighter": rgb(170, 224, 228), + "accent-cool-light": rgb(142, 214, 219), + "accent-cool": rgb(71, 193, 201), + "accent-cool-dark": rgb(39, 129, 135), + "accent-cool-darker": rgb(10, 83, 88), + + "accent-warm-lightest": rgb(253, 202, 146), + "accent-warm-lighter": rgb(255, 176, 92), + "accent-warm-light": rgb(255, 161, 62), + "accent-warm": rgb(247, 142, 30), + "accent-warm-dark": rgb(192, 87, 25), + "accent-warm-darker": rgb(136, 52, 4), + + "success-lightest": rgb(193, 228, 224), + "success-lighter": rgb(153, 213, 207), + "success-light": rgb(51, 171, 160), + "success": rgb(17, 133, 119), + "success-dark": rgb(10, 98, 88), + "success-darker": rgb(0, 66, 59), + + "info-lightest": rgb(240, 244, 251), + "info-lighter": rgb(231, 239, 248), + "info-light": rgb(211, 226, 242), + "info": rgb(161, 185, 207), + "info-dark": rgb(101, 121, 140), + "info-darker": rgb(64, 79, 93), + + "warning-lightest": rgb(255, 239, 172), + "warning-lighter": rgb(255, 231, 118), + "warning-light": rgb(253, 221, 73), + "warning": rgb(255, 210, 0), + "warning-dark": rgb(185, 153, 2), + "warning-darker": rgb(143, 119, 0), + + "error-lightest": rgb(249, 218, 219), + "error-lighter": rgb(246, 211, 212), + "error-light": rgb(237, 167, 170), + "error": rgb(210, 35, 42), + "error-dark": rgb(182, 21, 28), + "error-darker": rgb(124, 14, 18), + + "disabled-lightest": rgb(247, 244, 242), + "disabled-light": rgb(242, 239, 235), + "disabled": rgb(222, 213, 203), + "disabled-dark": rgb(157, 147, 137), + + // other colours that pop up + "mid-green": rgb(75, 184, 86), + "mid-blue": rgb(33, 150, 243), + "blue-hlink": rgb(42, 100, 150), +); + +$icons: ( + // specialised use cases + "icon-app": map-get($kbase-palette, 'purple'), + "icon-generic": map-get($kbase-palette, 'silver'), + "icon-type": map-get($kbase-palette, 'black'), +); + +$kbase-palette: map-merge($kbase-palette, $icons); + +// Job status colours +$job_status_colors: ( + "created": map-get($kbase-palette, 'info'), + "estimating": map-get($kbase-palette, 'info'), + "queued": map-get($kbase-palette, 'info'), + "running": map-get($kbase-palette, 'info'), + "completed": map-get($kbase-palette, 'success-dark'), + "terminated": map-get($kbase-palette, 'warning-dark'), + // error already exists, but we need it in our job status list + "error": map-get($kbase-palette, 'error'), + "does_not_exist": map-get($kbase-palette, 'error'), +); + +$palette: map-merge($kbase-palette, $job_status_colors); + +// use a colour from the palettes above +@function use_color($key) { + @if map-has-key($kbase-palette, $key) { + @return map-get($kbase-palette, $key); + } + + @if not map-has-key($palette, $key) { + @warn 'Key `#{$key}` not found in $palette map.'; + } + + @return map-get($palette, $key); +} + +// use a colour from the palettes above with the specified opacity +@function use_rgba_color($key, $opacity) { + @return rgba(use_color($key), $opacity); +} diff --git a/kbase-extension/scss/modules/_fonts.scss b/kbase-extension/scss/modules/_fonts.scss new file mode 100755 index 0000000000..f94c124209 --- /dev/null +++ b/kbase-extension/scss/modules/_fonts.scss @@ -0,0 +1,88 @@ +// modules/fonts + +@font-face { + font-family: 'Glyphicons Halflings'; + src: url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot); + src: + url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'), + url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2) format('woff2'), + url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff) format('woff'), + url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf) format('truetype'), + url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg'); +} + +@font-face { + font-family: kbase-icons; + font-style: normal; + font-weight: 400; + src: url(../fonts/kbase-icons.eot); + src: + url(../fonts/kbase-icons.eot?#iefix) format('embedded-opentype'), + url(../fonts/kbase-icons.woff) format('woff'), + url(../fonts/kbase-icons.ttf) format('truetype'), + url(../fonts/kbase-icons.svg#kbase-icons) format('svg'); +} + +@font-face { + font-family: Oxygen; + font-style: normal; + font-weight: 400; + src: url(../fonts/Oxygen-webfont.eot); + src: url(../fonts/Oxygen-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-webfont.woff) format('woff'), url(../fonts/Oxygen-webfont.ttf) format('truetype'), url(../fonts/Oxygen-webfont.svg#OxygenRegular) format('svg'); +} + +@font-face { + font-family: Oxygen; + font-style: normal; + font-weight: 700; + src: url(../fonts/Oxygen-Bold-webfont.eot); + src: url(../fonts/Oxygen-Bold-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-Bold-webfont.woff) format('woff'), url(../fonts/Oxygen-Bold-webfont.ttf) format('truetype'), url(../fonts/Oxygen-Bold-webfont.svg#OxygenBold) format('svg'); +} + +@font-face { + font-family: Oxygen; + font-style: italic; + font-weight: 400; + src: url(../fonts/Oxygen-Italic-webfont.eot); + src: url(../fonts/Oxygen-Italic-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-Italic-webfont.woff) format('woff'), url(../fonts/Oxygen-Italic-webfont.ttf) format('truetype'), url(../fonts/Oxygen-Italic-webfont.svg#OxygenItalic) format('svg'); +} + +@font-face { + font-family: Oxygen; + font-style: italic; + font-weight: 700; + src: url(../fonts/Oxygen-BoldItalic-webfont.eot); + src: url(../fonts/Oxygen-BoldItalic-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-BoldItalic-webfont.woff) format('woff'), url(../fonts/Oxygen-BoldItalic-webfont.ttf) format('truetype'), url(../fonts/Oxygen-BoldItalic-webfont.svg#OxygenBoldItalic) format('svg'); +} + +@font-face { + font-family: OxygenMono; + font-style: normal; + font-weight: 400; + src: url(../fonts/OxygenMono-Regular-webfont.eot); + src: url(../fonts/OxygenMono-Regular-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/OxygenMono-Regular-webfont.woff) format('woff'), url(../fonts/OxygenMono-Regular-webfont.ttf) format('truetype'), url(../fonts/OxygenMono-Regular-webfont.svg#OxygenMonoRegular) format('svg'); +} + +@font-face { + font-family: OxygenRegular; + font-style: normal; + font-weight: 400; + src: url(../fonts/Oxygen-webfont.eot); + src: url(../fonts/Oxygen-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-webfont.woff) format('woff'), url(../fonts/Oxygen-webfont.ttf) format('truetype'), url(../fonts/Oxygen-webfont.svg#OxygenRegular) format('svg'); +} + +@font-face { + font-family: OxygenBold; + font-style: normal; + font-weight: 400; + src: url(../fonts/Oxygen-Bold-webfont.eot); + src: url(../fonts/Oxygen-Bold-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Oxygen-Bold-webfont.woff) format('woff'), url(../fonts/Oxygen-Bold-webfont.ttf) format('truetype'), url(../fonts/Oxygen-Bold-webfont.svg#OxygenBold) format('svg'); +} + +@font-face { + font-family: RobotoBlack; + font-style: normal; + font-weight: 400; + src: url(../fonts/Roboto-Black-webfont.eot); + src: url(../fonts/Roboto-Black-webfont.eot?#iefix) format('embedded-opentype'), url(../fonts/Roboto-Black-webfont.woff) format('woff'), url(../fonts/Roboto-Black-webfont.ttf) format('truetype'), url(../fonts/Roboto-Black-webfont.svg#RobotoBlack) format('svg'); +} diff --git a/kbase-extension/scss/modules/_mixins.scss b/kbase-extension/scss/modules/_mixins.scss new file mode 100644 index 0000000000..8869fa3f24 --- /dev/null +++ b/kbase-extension/scss/modules/_mixins.scss @@ -0,0 +1,41 @@ +// standard formatting for elements containing code +@mixin code-block { + @include fixed-width; + + background-color: use-color('base-lightest'); + border: 0; + margin: 0; + max-height: 50rem; + overflow-wrap: break-word; + overflow-y: auto; + padding: 1rem; + word-break: break-word; + word-wrap: break-word; +} + +@mixin zero-box { + border: 0; + margin: 0; + padding: 0; +} + +@mixin table-cell { + border: 0; + margin: 0; + padding: 0.8rem; +} + +@mixin ellipsis-overflow { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin default-container-padding { + padding: $padding-large-horizontal; +} + +@mixin advanced-hidden { + font-style: italic; + margin-left: 6px; +} diff --git a/kbase-extension/scss/modules/_typography.scss b/kbase-extension/scss/modules/_typography.scss new file mode 100644 index 0000000000..63a46dd968 --- /dev/null +++ b/kbase-extension/scss/modules/_typography.scss @@ -0,0 +1,69 @@ +@mixin font-h1 { + font: normal 700 42px $typeface-display; +} + +@mixin font-h2 { + font: normal 700 34px $typeface-display; +} + +@mixin font-h3 { + font: normal 400 24px $typeface-display; +} + +@mixin font-h4 { + font: normal 700 18px/30px $typeface-display; +} + +@mixin font-h5 { + font: normal 700 16px/24px $typeface-display; +} + +@mixin font-h6 { + font: normal 400 13px $typeface-display; +} + +@mixin font-body { + font: normal 400 16px/24px $typeface-page-text; +} + +@mixin font-legend-title { + font: normal 700 32px $typeface-display; +} + +@mixin fixed-width { + font-family: $typeface-fixed-width; + font-size: $font-size-base; + font-weight: 400; + line-height: $line-height-spacious; +} + +@mixin body-text { + font-family: $typeface-page-text; + font-size: $font-size-base; + font-weight: 400; + line-height: $line-height-spacious; +} + +@mixin body-text-with-margins { + @include body-text; + margin: 1rem 0; +} + +@mixin tabular-text { + font-family: $typeface-page-text; + font-size: $font-size-base; + font-weight: 400; + line-height: $line-height-base; +} + +@mixin base-text { + font-family: $typeface-page-text; + font-size: $font-size-base; + font-weight: 400; + line-height: $line-height-base; +} + +@mixin base-text-with-margins { + @include base-text; + margin: 1rem 0; +} diff --git a/kbase-extension/scss/modules/_variables.scss b/kbase-extension/scss/modules/_variables.scss new file mode 100755 index 0000000000..731a6c0ef8 --- /dev/null +++ b/kbase-extension/scss/modules/_variables.scss @@ -0,0 +1,57 @@ +// modules/variables + +// Typefaces + +$typeface-page-text: Oxygen, Arial, sans-serif; +$typeface-display: Oxygen, Arial, sans-serif; +$typeface-display-legacy: RobotoBlack, Arial, sans-serif; +$typeface-fixed-width: OxygenMono, SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; +$typeface-font-awesome: FontAwesome; +$typeface-kbase-icons: kbase-icons; +$typeface-glyphicons: "Glyphicons Halflings"; + +// These names mimic those used in bootstrap: see https://getbootstrap.com/docs/3.4/customize/ +// and https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss + +$font-size-base: 14px !default; +//** Unit-less `line-height` for use in components like buttons. +$line-height-base: 1.428571429 !default; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +$line-height-computed: floor(($font-size-base * $line-height-base)) !default; // ~20px +$line-height-px: 20px; + +$line-height-spacious: 24px; + +// Bootstrap spacing defaults +$padding-xs-vertical: 0.1rem !default; +$padding-xs-horizontal: 0.5rem !default; + +$padding-small-vertical: 0.5rem !default; +$padding-small-horizontal: 1rem !default; + +$padding-base-vertical: 0.6rem !default; +$padding-base-horizontal: 1.2rem !default; + +$padding-large-vertical: 1rem !default; +$padding-large-horizontal: 1.5rem !default; + +$border-radius-small: 3px !default; +$border-radius-base: 4px !default; +$border-radius-large: 6px !default; + +$font-size-small: ceil(($font-size-base * 0.85)) !default; // ~12px +$line-height-small: 1.5 !default; + +$font-size-large: ceil(($font-size-base * 1.25)) !default; // ~18px +$line-height-large: 1.3333333 !default; + +// Button bits +$btn-font-weight: 700; +$btn-default-color: use_color('primary'); + +$btn-border-radius-small: $border-radius-small; +$btn-border-radius-base: $border-radius-base; +$btn-border-radius-large: $border-radius-large; + +//** Disabled cursor for form controls and buttons. +$cursor-disabled: not-allowed; diff --git a/kbase-extension/static/kbase/css/advancedViewCell.css b/kbase-extension/scss/partials/_advancedViewCell.scss similarity index 100% rename from kbase-extension/static/kbase/css/advancedViewCell.css rename to kbase-extension/scss/partials/_advancedViewCell.scss diff --git a/kbase-extension/scss/partials/_appCell.scss b/kbase-extension/scss/partials/_appCell.scss new file mode 100644 index 0000000000..bb01d54bb6 --- /dev/null +++ b/kbase-extension/scss/partials/_appCell.scss @@ -0,0 +1,453 @@ +/* Wrapper class + All classes below may be wrapped in this class to avoid polluting + the Narrative styles. +*/ + +.kb-app-cell { + &__container { + align-items: stretch; + display: flex; + font-family: $typeface-page-text; + } + + &__prompt { + align-items: stretch; + display: flex; + flex-direction: column; + } + + &__body { + align-items: stretch; + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + } + + &__widget_container { + display: block; + width: 100%; + } + + &-tab-pane__container-- { + &jobStatus, + &results { + @include default-container-padding; + } + } + + .kb-app-warning { + font-family: $typeface-page-text; + text-align: center; + } + + .kb-app-parameter-panel { + border-left: 3px solid rgb(255 255 255); + } + + .kb-app-parameter-panel-hover { + border-left: 3px solid rgb(66 139 202); + } + + .kb-app-parameter-row { + border-radius: 5px; + margin: 0; + padding: 5px; + + &:hover { + background-color: rgb(0 0 0 / 0.03); + } + } + + .kb-app-parameter-row-hover { + background: rgb(249 249 249); + } + + .kb-app-parameter-row .message { + font-family: $typeface-page-text; + text-align: center; + + &.-error { + background: rgb(242 222 222); + color: rgb(244 67 54); + } + } + + .kb-app-parameter-name { + color: rgb(119 119 119); + font-family: $typeface-page-text; + padding-left: 0; + padding-right: 4px; + text-align: left; + vertical-align: bottom; + white-space: normal; + } + + .kb-app-parameter-input { + vertical-align: middle; + + select.form-control { + margin: 0; + } + + .kb-app-parameter-accepted-glyph, + .kb-app-parameter-required-glyph { + /* This scoots the icon inside */ + font-size: 15px; + margin-left: 0; + } + } + + .kb-app-parameter-required-glyph { + color: rgb(244 67 54); + } + + .kb-parameter-data-selection { + font-weight: 700; + } + + .kb-app-parameter-hint { + color: rgb(119 119 119); + margin-top: 3px; + padding-left: 7px; + text-align: left; + } + + .kb-app-parameter-accepted-glyph { + color: use_color('mid-green'); + } + + .kb-app-advanced-options-controller-inactive, + .kb-app-parameter-info, + .kb-parameter-data-row-add, + .kb-parameter-data-row-remove { + color: rgb(119 119 119); + } + + .kb-app-advanced-options-controller, + .kb-app-advanced-options-controller-inactive { + font: italic 700 13px/14px $typeface-page-text; + text-align: center; + } + + .kb-app-advanced-options-controller { + color: use-color('blue'); + cursor: pointer; + + &:hover { + color: use-color('blue-hlink'); + } + } + + .kb-app-footer { + background-color: rgb(245 245 245); + overflow: none; + padding: 10px; + width: 100%; + } + + .kb-app-subtitle { + background-color: rgb(245 245 245); + padding: 3px 5px; + } + + .advanced-parameter-showing { + display: block; + } + + .advanced-parameter-hidden { + display: none; + } + + [data-element=run-control-panel] { + width: 100%; + } + + [data-element=tab-pane] { + border-top: 2px solid use_color('mid-blue'); + + > div { + padding: 0; + } + } +} + +.kb-app-row-clip-btn-addon, +.kb-app-row-close-btn-addon { + background: transparent; + border: 0; +} + +.kb-app-row-clip-btn-addon { + height: 100%; + padding: 0 10px 0 0; + + &:active::after { + background-color: rgb(255 255 255); + border: 1px solid rgb(128 128 128); + border-radius: 3px; + content: 'copied!'; + margin-top: -5px; + padding: 7px; + position: absolute; + } +} + +.kb-input-group-wide { + width: 100%; +} + +.kb-input-row-flex { + align-items: center; + display: flex; + flex-direction: row; +} + +.cell.selected .btn-default.kb-app-row-close-btn { + color: rgb(128 128 128); +} + +.kb-app-row-clip-btn, +.kb-app-row-close-btn { + background-color: transparent; + border: 0; + color: rgb(128 128 128); + height: 100%; + margin-left: 1px; +} + +.kb-app-parameter-right-error-bar { + background: rgb(255 0 0); + height: 28px; +} + +.kb-app-field-feedback { + background: transparent; + border: 0; +} + +.kb-input-group-addon { + background: transparent; + border: 0; + padding: 0 0 0 10px; +} + +/* tweaks to elemnts that should be dimmed when the cell is not selected */ + +.unselected .kb-app-cell { + opacity: 0.5; +} + +.kb-elapsed-time { + font-family: $typeface-fixed-width; + + &.-active { + color: rgb(0 255 0); + } +} + +.kb-app-cell-info-desc { + border: 1px solid rgb(119 119 119); + margin-right: 10px; + min-height: 100px; + padding: 10px; +} + +.kb-app-cell-info { + color: rgb(119 119 119); + font: 16px $typeface-page-text; + + .header { + border-bottom: 1px solid rgb(119 119 119); + margin-bottom: 8px; + padding: 5px 0; + } + + .value { + color: rgb(0 0 0); + } +} + +/* + +Buttons + +*/ + +.btn { + &.batch-active { + background-color: use_color('base-lightest'); + border-color: rgb(173 173 173); + box-shadow: inset 0 3px 7px rgb(0 0 0 / 0.125); + + &:hover { + background-color: use_color('base-lighter'); + } + } + + &.kb-app-cell-btn { + background-color: rgb(255 255 255); + border: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + margin-bottom: 4px; + + &.btn-primary { + border: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + + &:hover { + border-bottom: 6px solid use_color('mid-blue'); + margin-bottom: 0; + } + + &.active, + &:active, + &:hover { + background-color: use_color('mid-blue'); + color: rgb(255 255 255); + } + + &.active { + border: solid use_color('mid-blue'); + border-width: 2px 2px 6px; + margin-bottom: 0; + + &:hover { + background-color: rgb(221 221 221); + border-bottom: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + margin-bottom: 4px; + } + } + } + + &.btn-danger { + border: 2px solid rgb(209 82 65); + color: rgb(209 82 65); + + &:hover { + border-bottom: 6px solid rgb(209 82 65); + } + + &.active, + &:hover { + background-color: rgb(209 82 65); + color: rgb(255 255 255); + margin-bottom: 0; + } + + &.active { + border: solid rgb(209 82 65); + border-width: 2px 2px 6px; + + &:hover { + background-color: rgb(221 221 221); + border-bottom: 2px solid rgb(209 82 65); + color: rgb(209 82 65); + margin-bottom: 4px; + } + } + } + + &.disabled { + background-color: rgb(255 255 255); + border: 2px solid rgb(136 136 136); + color: rgb(136 136 136); + margin-bottom: 4px; + + &:active, + &:hover { + background-color: rgb(255 255 255); + border: 2px solid rgb(136 136 136); + color: rgb(136 136 136); + margin-bottom: 4px; + } + } + } +} + +/* Primary Button Style */ + +/* Hovering over an active cell is different, it tries to make the tab look + disassociated but using a gray background so it doesnt look like an inactive + tab */ + +/* DANGER - for error */ + +/* DISABLED is gray border and text */ + +.kb-app-status { + &-ok { + color: use_color('mid-green'); + } + + &-warning { + color: rgb(255 165 0); + } + + &-danger, + &-error { + color: rgb(209 82 65); + } + + &-default { + color: use_color('mid-blue'); + } +} + +/* app cell panels */ + +/* TODO: higher level selector */ + +.parameter-panel .info-panel { + background: transparent; + padding-top: 4px; +} + +.tt-input, +.tt-query { + box-shadow: inset 0 1px 1px rgb(0 0 0 / 0.075); + width: 100%; +} + +.tt-hint { + color: rgb(153 153 153); +} + +.tt-menu { + background-color: rgb(255 255 255); + border: 1px solid rgb(0 0 0 / 0.2); + border-radius: 4px; + box-shadow: 0 5px 10px rgb(0 0 0 / 0.2); + + /* used to be tt-dropdown-menu in older versions */ + margin-top: 4px; + padding: 4px 0; + width: 100%; +} + +.tt-suggestion { + line-height: 24px; + padding: 3px 20px; + + &.tt-cursor, + &:hover { + background-color: rgb(0 151 207); + color: rgb(255 255 255); + } + + p { + margin: 0; + } +} + +.tt-header { + font-size: 75%; + font-style: italic; + padding-left: 5px; +} + +.kb-app-results-tab { + overflow-x: auto; + max-width: inherit; +} diff --git a/kbase-extension/scss/partials/_appParams.scss b/kbase-extension/scss/partials/_appParams.scss new file mode 100644 index 0000000000..51532ad079 --- /dev/null +++ b/kbase-extension/scss/partials/_appParams.scss @@ -0,0 +1,17 @@ +.kb-app-params { + // .kb-app-params__message--advanced-hidden + &__message--advanced-hidden { + @include advanced-hidden; + } + // .kb-app-params__toggle--advanced-hidden + // &__toggle--advanced-hidden { + // .kb-ui__button_label {} + // } + + &__fields--parameters { + // .kb-app-params__fields--parameters-hidden + &-hidden { + display: none; + } + } +} diff --git a/kbase-extension/static/kbase/css/batchMode.css b/kbase-extension/scss/partials/_batchMode.scss similarity index 74% rename from kbase-extension/static/kbase/css/batchMode.css rename to kbase-extension/scss/partials/_batchMode.scss index dddf077eb3..c0bcf6a725 100644 --- a/kbase-extension/static/kbase/css/batchMode.css +++ b/kbase-extension/scss/partials/_batchMode.scss @@ -1,14 +1,14 @@ .batch-mode-col { - padding: 5px 10px; overflow: auto; + padding: 5px 10px; } .job-info { cursor: pointer; -} -.job-info.job-selected { - background-color: #eee; + &.job-selected { + background-color: use_color('base-lightest'); + } } .batch-mode-list { diff --git a/kbase-extension/scss/partials/_bootstrapHelper.scss b/kbase-extension/scss/partials/_bootstrapHelper.scss new file mode 100644 index 0000000000..c9753e29e2 --- /dev/null +++ b/kbase-extension/scss/partials/_bootstrapHelper.scss @@ -0,0 +1,137 @@ +/* + Styles to help with bootstrap files. + Fix styles broken by other styles + Make bootstrap work better + Customize bootstrap styles +*/ + +.text-silver { + color: use_color('silver'); +} + +.modal { + .modal-title { + font: normal 700 18px/24px $typeface-page-text; + } + + // Unset the global modal title color set above for alert modals, which + // may set the color style based on the alert type. + &.kb-modal-alert .modal-title { + color: unset; + } + + input[type=checkbox] { + margin-top: 1px; + } +} + +.kb-panel { + &-light, + &-batch, + &-bulk-params, + &-params, + &-container, + &-results { + border: 0; + box-shadow: none; + + .panel-heading { + background-color: transparent; + color: use_color('base'); + font-weight: 700; + } + } +} + +.kb-panel-light { + margin-bottom: 10px; + + &.panel-danger .panel-title { + color: rgb(169, 68, 66); + } + + &.panel-success .panel-title { + color: rgb(60, 118, 61); + } + + &.panel-warning .panel-title { + color: rgb(138, 109, 59); + } + + &.panel-info .panel-title { + color: rgb(0, 0, 255); + } + + .panel-heading { + border-bottom: 0; + padding-bottom: 2px; + } + + .panel-body { + padding: 0; + } +} + +// kb-panel-params is suitable for displaying parameters; +// the header has 15px horizontal margins and 10px top margin +// the body has 10px horiz margins as each parameter panel has 5px horiz padding + +// kb-panel-batch has 15px horizontal margins on both header and body + +.kb-panel-batch, +.kb-panel-params, +.kb-panel-bulk-params, +.kb-panel-results { + .panel-heading { + border-bottom: 0; + margin: 15px 15px 5px; + padding: 0; + } + + .panel-body { + border-bottom: 0; + margin: 0 15px 5px; + padding: 0; + } +} + +.kb-panel-params { + .panel-body { + margin: 0 10px 15px; + } +} + +.panel-title-collapse-toggle { + margin-left: -15px; +} + +.kb-panel-container { + .panel-heading { + border-bottom: 2px solid use_color('base-lighter'); + padding-bottom: 2px; + } +} + +.kb-cell-error .panel-heading { + background-image: none; + padding: 5px 10px; +} + +.btn { + box-shadow: none; +} + +// The button bar in a cell widget is located at the bottom of the inputs, but +// above any outputs. +// These styles attempt to help the button toolbar, which represents the actions +// the user can take, be visually distinct from the other bits surrounding it. + +.btn-toolbar.kb-btn-toolbar-cell-widget { + background-color: use_color('base-lightest'); + margin-bottom: 20px; + padding: 6px; +} + +.dataTables_empty { + padding: 0.8rem; +} diff --git a/kbase-extension/scss/partials/_bulkImportCell.scss b/kbase-extension/scss/partials/_bulkImportCell.scss new file mode 100644 index 0000000000..7b714691bc --- /dev/null +++ b/kbase-extension/scss/partials/_bulkImportCell.scss @@ -0,0 +1,155 @@ +/* + bulk import cell +*/ + +$component-border: use-color('silver'); +$panel-separator-color: use_color('mid-blue'); + +.kb-bulk-import { + &__base-node { + // everything lives under this + } + + &__layout_container { + align-items: stretch; + display: flex; + } + + // kb-bulk-import__prompt + &__prompt { + align-items: stretch; + display: flex; + flex-direction: column; + } + + // kb-bulk-import__prompt_status + &__prompt_status {} + + // kb-bulk-import__body + &__body { + align-items: stretch; + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + } + + // kb-bulk-import__tab_pane + &__tab_pane { + align-content: stretch; + align-items: stretch; + border-top: 2px solid $panel-separator-color; + display: flex; + flex-direction: row; + } + + // kb-bulk-import__tab_pane_widget + &__tab_pane_widget { + width: 100%; + } + + // kb-bulk-import-configure, + // kb-bulk-import-info + &-configure, + &-info { + &__container { + display: flex; + } + + // kb-bulk-import-configure__panel--filetype + // kb-bulk-import-info__panel--filetype + &__panel--filetype { + background-color: use-color('base-lightest'); + width: 24rem; + } + + // kb-bulk-import-configure__panel--configure + // kb-bulk-import-info__panel--info + &__panel--configure, + &__panel--info { + flex: 1; + } + } +} + +.kb-field-widget__error_message--parameters { + border: 1px solid use_color('error'); +} + +.kb-field-cell { + // kb-field-cell__param_container + &__param_container { + margin: 3px; + } + + // kb-field-cell__input_control + &__input_control select { + margin-left: 0; + } + + // kb-field-cell__cell_label + &__cell_label { + margin-bottom: 0; + padding-top: 8px; + } + + // kb-field-cell__error_message + &__error_message { + border-color: use_color('error'); + color: use_color('error'); + + .form-control { + border-color: use_color('error'); + color: use_color('error'); + } + + // select2 is picky and needs its classes overridden. + // making a note here in case a select2 update breaks this. + .select2-selection { + border-color: use_color('error'); + } + + .select2-container--default .select2-selection--single { + .select2-selection__placeholder, + .select2-selection__rendered { + color: use_color('error'); + } + } + } + + // kb-field-cell__message_panel + &__message_panel, + &__message_panel__duplicate { + border: 1px solid $component-border; + padding: 0.5em; + + // kb-field-cell__message_panel__error + &__error { + background-color: use-color('error-lightest'); + border-color: use-color('error-light'); + color: use-color('ink'); + + &__title { + color: use-color('error'); + } + } + + // kb-field-cell__message_panel__warning + &__warning { + background-color: use-color('warning-lightest'); + border-color: use-color('warning-light'); + color: use-color('ink'); + + &__title { + color: use-color('warning-darker'); + } + } + } +} + +.kb-result-tab, +.kb-job-status-tab { + &__container { + @include default-container-padding; + } +} diff --git a/kbase-extension/scss/partials/_buttons.scss b/kbase-extension/scss/partials/_buttons.scss new file mode 100644 index 0000000000..2356075a15 --- /dev/null +++ b/kbase-extension/scss/partials/_buttons.scss @@ -0,0 +1,123 @@ +// A flat button, with no unselected background color, but with hover and click styles + +.btn.kb-flat-btn { + background-color: rgb(255 255 255); + border: 0; + color: use_color('base'); + margin: 0; + text-shadow: none !important; + + .kb-nav-btn-txt { + font-size: 13px; + margin-top: -5px; + } + + &:active, + &:hover { + background-color: use_color('base-lighter'); + } +} + +.kb-flat-btn-wrapper { + // don't add shadows here + box-shadow: none; +} + +/** + KBase button, 2021 style: no borders, rounded corners, uppercased text + + The basic template below is based heavily on the button constructor in bootstrap 3.4 + (see `bootstrap-sass` in the `node_modules` directory) + + There are two button-related mixins also available from bootstrap-sass: + + @mixin button-variant($color, $background, $border) + + to generate the various buttons states for a given colour scheme, and + + @mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) + + to generate buttons of different sizes + + see `node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins/_buttons.scss` for the source + + Example usage: + + .kb-new-thing__button { + // basic button styling + @extend %kbase-button; + + // creates variants with text colour 'primary', bg colour 'primary-lightest', and transparent borders + @include button-variant(use_color('primary'), use_color('primary-lightest'), transparent); + } + + Hover, active, focus, etc., states are all generated automatically. + +*/ + +%kbase-button { + // uses the mixin from bootstrap-sass + @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base); + + border: 1px solid transparent; + cursor: pointer; + display: inline-block; + font-family: $typeface-page-text; + font-weight: $btn-font-weight; + text-align: center; + text-transform: uppercase; + touch-action: manipulation; + user-select: none; + vertical-align: middle; + white-space: nowrap; + + &, + &:active, + &.active { + &:focus, + &.focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + } + } + + &:hover, + &:focus, + &.focus { + color: $btn-default-color; + text-decoration: none; + } + + &:active, + &.active { + background-image: none; + box-shadow: inset 0 3px 5px rgb(0 0 0 / 0.125); + outline: 0; + } + + &.disabled, + &[disabled], + fieldset[disabled] & { + box-shadow: none; + cursor: $cursor-disabled; + opacity: 0.8; + } +} + +%kbase-button-xs { + @extend %kbase-button; + + @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); +} + +%kbase-button-sm { + @extend %kbase-button; + + @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small); +} + +%kbase-button-lg { + @extend %kbase-button; + + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large); +} diff --git a/kbase-extension/scss/partials/_cellToolbar.scss b/kbase-extension/scss/partials/_cellToolbar.scss new file mode 100644 index 0000000000..6bee243e24 --- /dev/null +++ b/kbase-extension/scss/partials/_cellToolbar.scss @@ -0,0 +1,126 @@ +$default-icon-size: 56px; + +.kb-cell-toolbar { + &__button { + border: 0; + } + + // .kb-cell-toolbar__container + &__container { + align-items: flex-start; + display: flex; + height: $default-icon-size; + justify-content: space-between; + } + + // .kb-cell-toolbar__app_icon + &__app_icon { + flex: none; + font-size: 14px; + height: $default-icon-size; + line-height: $default-icon-size; + margin-right: 4px; + padding: 0; + text-align: center; + width: $default-icon-size; + } + + // .kb-cell-toolbar__title-container + &__title-container { + flex: 1 1 10rem; + text-overflow: ellipsis; + + // .cell.unselected .kb-cell-toolbar__title-container + .cell.unselected & { + opacity: 0.5; + } + } + + // .kb-cell-toolbar__title + &__title { + color: use_color('primary-vivid'); + font: normal 700 18px/20px $typeface-page-text; + height: 20px; + margin-top: 8px; + min-width: 0; + text-overflow: ellipsis; + } + + // .kb-cell-toolbar__subtitle + &__subtitle { + color: use_color('neutral'); + font: normal 400 14px/20px $typeface-page-text; + height: 20px; + min-width: 0; + text-overflow: ellipsis; + } + + // .kb-cell-toolbar__buttons-container + &__buttons-container { + flex: none; + min-width: 9.5rem; + white-space: nowrap; + + // unselected cells + // .cell.unselected .kb-cell-toolbar__buttons-container + .cell.unselected & { + opacity: 0.2; + } + } + + // .kb-cell-toolbar__icon + &__icon { + // .kb-cell-toolbar__icon--info + &--info { + color: use_color('info-dark'); + } + + // .kb-cell-toolbar__icon--minus + &--minus { + color: use_color('accent-warm'); + } + + // .kb-cell-toolbar__icon--notch-spin + &--notch-spin { + color: use_color('primary'); + } + + // .kb-cell-toolbar__icon--outdated + &--outdated { + color: use_color('accent-warm'); + } + + // .kb-cell-toolbar__icon--plus + &--plus { + color: use_color('black'); + } + + // .kb-cell-toolbar__icon--table, + // .kb-cell-toolbar__icon--terminal + &--table, + &--terminal { + color: use_color('black'); + } + + /* .kb-cell-toolbar__icon--times, + .kb-cell-toolbar__icon--triangle */ + &--times, + &--triangle { + color: use_color('red'); + } + } + + // .kb-cell-toolbar__dropdown_item + &__dropdown_item { + border: 0; + text-align: left; + width: 100%; + + // .kb-cell-toolbar__dropdown_item_icon + &_icon { + display: inline-block; + margin-right: 4px; + width: 25px; + } + } +} diff --git a/kbase-extension/static/kbase/css/contigBrowserStyles.css b/kbase-extension/scss/partials/_contigBrowserStyles.scss similarity index 72% rename from kbase-extension/static/kbase/css/contigBrowserStyles.css rename to kbase-extension/scss/partials/_contigBrowserStyles.scss index 196caa3bac..15646a2ad3 100644 --- a/kbase-extension/static/kbase/css/contigBrowserStyles.css +++ b/kbase-extension/scss/partials/_contigBrowserStyles.scss @@ -3,29 +3,32 @@ /* * Styling for the tooltip that pops up when a feature is highlighted. */ + .kbcb-tooltip { - position: absolute; - z-index: 9999999; - visibility: hidden; + background-color: rgb(34 34 34); + color: rgb(255 255 255); opacity: 0.8; - background-color: #222; - color: #fff; padding: 0.5em; + position: absolute; + visibility: hidden; + z-index: 9999999; } /** * Adjusts the font size and stroke width of the base-position axis. */ -.kbcb-widget .kbcb-axis path, -.kbcb-widget .kbcb-axis line { - fill: none; - stroke: black; - shape-rendering: crispEdges; -} -.kbcb-widget .kbcb-axis text { - font-family: sans-serif; - font-size: 11px; +.kbcb-widget .kbcb-axis { + line, + path { + fill: none; + shape-rendering: crispEdges; + stroke: rgb(0 0 0); + } + + text { + font: 11px sans-serif; + } } /** @@ -37,26 +40,28 @@ * The 'center' feature is colored after that. * And highlighted features should be colored last (not currently used). */ + .kbcb-feature { - stroke: #000; - fill: #f00; + fill: rgb(255 0 0); + stroke: rgb(0 0 0); } .kbcb-operon { - fill: #0f0; + fill: rgb(0 255 0); } .kbcb-center { - fill: #00f; + fill: rgb(0 0 255); } .kbcb-highlight { - fill: #fff; + fill: rgb(255 255 255); } /** * The below controls styling for the (optional) button panel */ + .kbcb-buttons { margin-left: auto; margin-right: auto; diff --git a/kbase-extension/scss/partials/_custom.scss b/kbase-extension/scss/partials/_custom.scss new file mode 100644 index 0000000000..a2e25eb79c --- /dev/null +++ b/kbase-extension/scss/partials/_custom.scss @@ -0,0 +1,388 @@ +/* +This file is for overriding the Jupyter CSS. Any KBase-component +CSS should go in /kbase-extension/static/kbase/css/kbaseNarrative.css + +(or the specific css file if available) +*/ +#ipython-main-app { + position: relative; +} + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: rgb(51 122 183); + color: rgb(255 255 255); + + span { + color: rgb(255 255 255) !important; + } +} + +#site { + overflow: visible; +} + +/* disable the standard notebook, bottom spacer */ + +#notebook > .end_space { + height: 0; + min-height: 0; +} + +@media (min-width: 1200px) { + #notebook-container { + width: auto; + } +} + +@media (min-width: 992px) { + #notebook-container { + width: auto; + } +} + +@media (min-width: 768px) { + #notebook-container { + width: auto; + } +} + +.celltoolbar { + background: none; + border: 0; + border-left: 1px solid transparent; + height: auto; + padding-bottom: 2px; + padding-top: 0; +} + +div { + &.input_area { + border: 1px solid rgb(206 206 206); + border-radius: 0; + padding-top: 4px; + } + + &.output_subarea { + max-width: inherit; + } + + &.text_cell_input { + border: 1px solid rgb(206 206 206); + border-radius: 0; + } + + &.cell { + border: 1px solid use_color('base-lighter'); + border-left-width: 5px; + border-radius: 0; + margin: 8px 0; + padding: 0; + + &.selected { + background: none; + border: 1px solid use_color('mid-green'); + border-left-width: 5px; + box-shadow: 0 1px 2px rgb(170 170 170), 0 5px 5px rgb(170 170 170); + margin-left: 0; + padding-left: 0; + transition: box-shadow 0.2s ease-in-out; + + &::before { + background: none; + } + + /* div.cell.selected.kb-error */ + &.kb-error { + border: 1px solid rgb(217 83 79); + border-left-width: 5px; + } + } + + &.opened { + padding-top: 0; + } + } + + &#notebook { /* FIXME Jupyter css */ + padding: 0; + } +} + +.edit_mode div.cell.selected { + background: none; + border-left: 5px solid rgb(102 187 106); + padding-left: 0; +} + +#notebook_name { + color: rgb(0 102 152); + + &:hover { + background-color: rgb(224 224 224); + cursor: pointer; + } +} + +#notebook-container { + bottom: 0; + box-shadow: none; + left: 380px; + min-width: 460px; + overflow: auto; + position: fixed; + right: 0; + top: 70px; + width: auto; + + &::after { + content: ''; + display: block; + height: 100px; + } +} + +.btn-toolbar { + margin-left: 0; + padding-left: 0; +} + +.kb-narr-side-panel .btn-group .btn { + font-size: 16px; + padding: 3px 4px; +} + +.btn-subtle { + color: rgb(136 136 136); + margin: 0; + + .fa { + color: rgb(136 136 136); + + &::before { + color: rgb(136 136 136); + } + } +} + +.notebook_app { + background-color: rgb(255 255 255); + overflow-y: hidden; + + .btn.active { + box-shadow: none; + } + + .inner_cell { + overflow: visible; + } + + .celltoolbar > .button_container { + flex: 0 auto; + + &:first-child { + flex: auto; + } + } + + .celltoolbar_container { + border-bottom: 1px solid use-color('silver'); + } + + .prompt { + color: use-color('silver'); + padding-top: 0; + width: 75px; + } + + .cell { + &.selected div.text_cell_render, + &.unselected div.text_cell_render { + border: 1px solid transparent; + } + } + + .buttons .dropdown-menu { + height: auto !important; + overflow: visible !important; + } + + .btn.disabled .fa { + color: use-color('silver'); + } +} + +/* Fooling around with the narrative cells */ + +.text_cell { + &.kb-cell .rendered_html { + table { + border: 1px solid rgb(221 221 221); + } + + ul { + margin: 0; + } + } + + &.opened.rendered { + &.kb-cell .rendered_html { + border: 0; + padding: 0; + } + + &.selected.kb-cell { + .rendered_html { + border-left: 1px solid use_color('mid-green'); + } + + &.kb-error .rendered_html { + border-left: 1px solid rgb(217 83 79); + } + } + + &.kb-cell { + padding-bottom: 0; + } + } +} + +.rendered_html { + td, + th, + tr { + text-align: inherit; + } + + table { + table-layout: inherit; + } +} + +.cell { + &.selected { + .prompt { + .method-icon { + color: rgb(103 58 183); + } + + .app-icon { + color: rgb(0 150 136); + } + + .app-output-icon, + .data-viewer-icon, + .markdown-icon, + .method-output-icon { + color: rgb(0 0 0); + } + + .error-icon { + color: rgb(255 0 0); + } + } + + .btn-default { + color: rgb(0 0 0); + } + } + + > .inner_cell > .ctb_hideshow { + display: block; + } + + &.unselected { + .btn-default { + color: use_color('base-lighter'); + } + + .app-icon, + .method-icon { + color: use_color('silver'); + } + } +} + +.kb-btn-icon { + display: inline-block; + padding: 4px; + + .fa { + opacity: 0.5; + } + + &:hover .fa { + opacity: 1; + } +} + +// For some reason Jupyter removes the bottom margin. This causes problems with +// bootstrap, since bootstrap removes the top margin! +p { + margin-bottom: 1rem; +} + +.prompt { + display: none !important; +} + +/* + * Jupyter 5.6.0 introduced a "run this cell" button that appears on mouseover. + * Cool though this is, it's a problem on app cells. + * This hunk of CSS should remove it until we can better work it in to our design. + */ + +div.run_this_cell { + display: none; + width: 0; +} + +div.code_cell { + div.input_prompt { + min-width: 0; + } +} + +.panel-title { + @include font-h4; +} + +/* collapsible Bootstrap toggle */ +[data-toggle=collapse] { + cursor: pointer; + + &::before { + color: use-color('silver'); + content: '\f078 '; + display: inline-block; + font: normal 400 90%/1 $typeface-font-awesome; + margin-left: 0; + margin-right: 0.2em; + text-align: center; + vertical-align: baseline; + width: 1.2em; + } + + &.collapsed::before { + content: '\f054 '; + margin-left: 0.1em; + margin-right: 0.1em; + } +} + +/* up/down caret expander */ +[data-toggle=vertical-collapse-after] { + cursor: pointer; + + &::after { + content: ' \f0d8'; + display: inline-block; + font: normal 400 130%/1 $typeface-font-awesome; + text-align: right; + vertical-align: baseline; + width: 1em; + } +} + +.vertical_collapse--open [data-toggle=vertical-collapse-after]::after { + content: ' \f0d7'; +} diff --git a/kbase-extension/scss/partials/_dataStaging.scss b/kbase-extension/scss/partials/_dataStaging.scss new file mode 100644 index 0000000000..50c365475f --- /dev/null +++ b/kbase-extension/scss/partials/_dataStaging.scss @@ -0,0 +1,34 @@ +.kb-data-staging { + &__breadcrumbs { + margin: 1rem 0 1.5rem; + } + // .kb-data-staging__breadcrumb .kb-pointer a + &__breadcrumb_link { + cursor: pointer; + } + + // .kb-data-staging__container + &__container { + height: 604px; + overflow-y: auto; + padding: 10px; + } + + // https://www.figma.com/file/T30UN3QhWhhV5SpXiqY1ij/KBase-component-library?node-id=1%3A4 + // .kb-data-staging__button + &__button { + @extend %kbase-button; + + @include button-variant(use_color('primary'), use_color('primary-lightest'), transparent); + + letter-spacing: 0.7px; + margin-left: 1rem; + position: relative; + top: -3px; + } + + // .kb-data-staging__title + &__title { + font: normal 700 24px/30px $typeface-page-text; + } +} diff --git a/kbase-extension/scss/partials/_dropzoneDz.scss b/kbase-extension/scss/partials/_dropzoneDz.scss new file mode 100644 index 0000000000..656cc2624d --- /dev/null +++ b/kbase-extension/scss/partials/_dropzoneDz.scss @@ -0,0 +1,104 @@ +.dz-clear-all__button { + @extend %kbase-button; + + @include button-variant(use_color('primary'), use_color('primary-lightest'), transparent); +} + +.dz-file { + border: 1px solid use_color('base-lightest'); + font: 16px $typeface-page-text; + margin: 2px 0; + padding: 5px 0; + width: 100%; + + // .dz-file__progress__bar + &__progress__bar { + margin-bottom: inherit; + margin-left: 5px; + } + + // .dz-file__upload-progress + &__upload-progress { + display: inline; + } + + // .dz-file__row + &__row { + align-items: center; + display: flex; + } + + // .dz-file__name + &__name { + line-height: 20px; + white-space: nowrap; + } + + // .dz-file__msg, + // .dz-file__name + &__msg, + &__name { + overflow: hidden; + text-overflow: ellipsis; + } + + // .dz-file__msg + &__msg { + font-size: 14px; + } + + // .dz-file__msg__success + &__msg__success { + color: rgb(75 175 79); + display: none; + } + + // .dz-file__status + &__status { + align-items: center; + display: none; + font-weight: 700; + } + + // .dz-file__status__icon + &__status__icon { + margin: 0 4px 0 8px; + } + + // .dz-file__status__success + &__status__success { + color: rgb(75 175 79); + } + + // .dz-file__status__error + &__status__error { + color: rgb(223 0 2); + } +} + +.kb-dropzone { + border: 2px dashed use_color('mid-blue') !important; + margin-bottom: 5px; + max-height: 150px; + overflow-y: auto; + + &-progress__header { + border-bottom: 1px solid use_color('base-lightest'); + display: none; + margin-bottom: 5px; + } + + .dz-message { + color: rgb(0 0 0); + font: normal 400 24px/28px $typeface-page-text; + margin: 2em 4.5em; + mix-blend-mode: normal; + text-align: center; + } + + // .kb-dropzone__message--upload + &__message--upload { + font-family: $typeface-page-text; + font-weight: 700; + } +} diff --git a/kbase-extension/scss/partials/_editor.scss b/kbase-extension/scss/partials/_editor.scss new file mode 100644 index 0000000000..a45074a7e2 --- /dev/null +++ b/kbase-extension/scss/partials/_editor.scss @@ -0,0 +1,103 @@ +/** + * CSS related to editing tools. This currently includes media and model editing + */ + +.modal-body .dataTables_processing { + background: rgb(232 249 255); + border: 1px solid rgb(170 170 170); + opacity: 0.8; +} + +.kb-editor { + margin: 0 10px; + + .tab-content { + margin: 5px 0 0; + } + + .controls { + margin: 5px 0; + + .btn { + margin: 0 10px; + } + } + + .btn-primary { + background-color: rgb(51 122 183) !important; + border-color: rgb(46 109 164) !important; + } + + .btn-danger { + background-color: rgb(217 83 79) !important; + border-color: rgb(212 63 58) !important; + } + + .btn i { + color: rgb(255 255 255); + } + + table { + border: 1px solid rgb(187 187 187); + } +} + +/** + * Override KBase stylings that override jupyter styling. + * Should be removed after Bill's refactoring. + */ + +/* override button staylings that were removed */ + +/* override icon stylings in button that were removed */ + +/* override table border styling */ + +/** + * General editor styling + */ + +.kb-editor-table { + width: 100% !important; + + /* override datatables hackory */ + + > tbody > tr { + > td:first-child { + width: 1%; + + /* checkboxes are expected on editable tables */ + + .fa-square-o { + color: use_color('base'); + font-size: 1.4em; + } + + &:hover { + cursor: pointer; + + .fa-square-o { + color: rgb(0 0 0); + } + } + } + + &.row-select .fa-square-o::before { + color: rgb(68 68 68); + content: '\f046'; + } + + > td.editable { + position: relative; + + &:hover::after { + bottom: 0; + color: rgb(136 136 136); + content: '\f044'; + font-family: $typeface-font-awesome; + position: absolute; + right: 2px; + } + } + } +} diff --git a/kbase-extension/scss/partials/_editorCell.scss b/kbase-extension/scss/partials/_editorCell.scss new file mode 100644 index 0000000000..dbf598aff1 --- /dev/null +++ b/kbase-extension/scss/partials/_editorCell.scss @@ -0,0 +1,265 @@ +/* Wrapper class + All classes below may be wrapped in this class to avoid polluting + the Narrative styles. +*/ + +.kb-editor-cell { + .kb-app-warning { + font-family: $typeface-page-text; + text-align: center; + } + + .kb-app-parameter-panel { + border-left: 3px solid rgb(255 255 255); + } + + .kb-app-parameter-panel-hover { + border-left: 3px solid rgb(66 139 202); + } + + .kb-app-parameter-row { + margin: 0; + } + + .kb-app-parameter-row-hover { + background: rgb(249 249 249); + } + + .kb-app-parameter-row .message { + font-family: $typeface-page-text; + text-align: center; + + &.-error { + background: rgb(242 222 222); + color: rgb(244 67 54); + } + } + + .kb-app-parameter-name { + color: rgb(119 119 119); + font-family: $typeface-page-text; + padding-left: 0; + padding-right: 4px; + text-align: left; + vertical-align: bottom; + white-space: normal; + } + + .kb-app-parameter-input { + vertical-align: middle; + + select.form-control { + margin: 0; + } + + .kb-app-parameter-accepted-glyph, + .kb-app-parameter-required-glyph { + /* This scoots the icon inside */ + font-size: 15px; + margin-left: 0; + } + } + + .kb-app-parameter-required-glyph { + color: rgb(244 67 54); + } + + .kb-parameter-data-selection { + font-weight: 700; + } + + .kb-app-parameter-hint { + color: rgb(119 119 119); + margin-top: 3px; + padding-left: 7px; + text-align: left; + } + + .kb-app-parameter-accepted-glyph { + color: use_color('mid-green'); + } + + .kb-app-advanced-options-controller-inactive, + .kb-app-parameter-info, + .kb-parameter-data-row-add, + .kb-parameter-data-row-remove { + color: rgb(119 119 119); + } + + .kb-app-advanced-options-controller, + .kb-app-advanced-options-controller-inactive { + font: italic 700 13px/14px $typeface-page-text; + text-align: center; + } + + .kb-app-advanced-options-controller { + color: use-color('blue'); + cursor: pointer; + + &:hover { + color: use-color('blue-hlink'); + } + } + + .kb-app-footer { + background-color: rgb(245 245 245); + overflow: none; + padding: 10px; + width: 100%; + } + + .kb-app-subtitle { + background-color: rgb(245 245 245); + padding: 3px 5px; + } + + .advanced-parameter-showing { + display: block; + } + + .advanced-parameter-hidden { + display: none; + } + + [data-element=run-control-panel] { + width: 100%; + } + + [data-element=tab-pane] { + border-top: 2px solid use_color('mid-blue'); + + > div { + padding: 4px; + + &:empty { + padding: 0; + } + } + } + + .btn-primary.kb-btn-action { + &.-rerun { + color: use_color('mid-blue'); + } + + &.-run { + color: use_color('mid-green'); + } + } + + .btn-danger.kb-btn-action { + &.-cancel, + &.-reset { + color: rgb(209 82 65); + } + } +} + +.unselected .kb-editor-cell { + opacity: 0.5; +} + +.kb-editor-cell-info-desc { + border: 1px solid rgb(119 119 119); + margin-right: 10px; + min-height: 100px; + padding: 10px; +} + +.kb-editor-cell-info { + color: rgb(119 119 119); + font: 16px $typeface-page-text; + + .header { + border-bottom: 1px solid rgb(119 119 119); + margin-bottom: 8px; + padding: 5px 0; + } + + .value { + color: rgb(0 0 0); + } +} + +/* + +Buttons + +*/ + +.btn.kb-editor-cell-btn { + background-color: rgb(255 255 255); + border: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + margin-bottom: 4px; + + &.btn-primary { + border: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + + &:hover { + border-bottom: 6px solid use_color('mid-blue'); + margin-bottom: 0; + } + + &.active, + &:active, + &:hover { + background-color: use_color('mid-blue'); + color: rgb(255 255 255); + } + + &.active { + border: solid use_color('mid-blue'); + border-width: 2px 2px 6px; + margin-bottom: 0; + + &:hover { + background-color: rgb(221 221 221); + border-bottom: 2px solid use_color('mid-blue'); + color: use_color('mid-blue'); + margin-bottom: 4px; + } + } + } + + &.btn-danger { + border: 2px solid rgb(209 82 65); + color: rgb(209 82 65); + + &:hover { + background-color: rgb(209 82 65); + border-bottom: 6px solid rgb(209 82 65); + color: rgb(255 255 255); + margin-bottom: 0; + } + + &.active { + background-color: rgb(209 82 65); + border: solid rgb(209 82 65); + border-width: 2px 2px 6px; + color: rgb(255 255 255); + margin-bottom: 0; + + &:hover { + background-color: rgb(221 221 221); + border-bottom: 2px solid rgb(209 82 65); + color: rgb(209 82 65); + margin-bottom: 4px; + } + } + } + + &.disabled { + border: 2px solid rgb(136 136 136); + color: rgb(136 136 136); + margin-bottom: 4px; + + &:active, + &:hover { + border: 2px solid rgb(136 136 136); + color: rgb(136 136 136); + margin-bottom: 4px; + } + } +} diff --git a/kbase-extension/scss/partials/_errorDialog.scss b/kbase-extension/scss/partials/_errorDialog.scss new file mode 100644 index 0000000000..c636a54f9b --- /dev/null +++ b/kbase-extension/scss/partials/_errorDialog.scss @@ -0,0 +1,28 @@ +// Tabbed error dialog +.kb-error-dialog { + &__body .tab-content { + padding-top: 1rem; + } + + &__err { + // kb-error-dialog__err_preamble, + // kb-error-dialog__err_message + &_preamble, + &_message { + margin: 1rem 0; + } + } + + &__stacktrace { + // kb-error-dialog__stacktrace_container + &_container { + @include code-block; + } + + // kb-error-dialog__stacktrace_lines + // &_lines {} + + // kb-error-dialog__stacktrace_single_line + // &_single_line {} + } +} diff --git a/kbase-extension/scss/partials/_errorDisplay.scss b/kbase-extension/scss/partials/_errorDisplay.scss new file mode 100644 index 0000000000..53b045ac80 --- /dev/null +++ b/kbase-extension/scss/partials/_errorDisplay.scss @@ -0,0 +1,84 @@ +.kb-error-display { + // .kb-error-display__container + &__container { + @include default-container-padding; + // .kb-error-display__container.kb-log__error_container + // see _jobLog for styling as part of job status + &.kb-log__error_container { + padding: 0; + } + } + + &__summary, + &__detail_title, + &__stacktrace_title { + @include font-h4; + + margin-top: 1rem; + } + // .kb-error-display__summary + &__summary { + .kb-app-cell-tab-pane__container--error & { + margin-top: 0; + } + } + // .kb-error-display__stacktrace_code + &__stacktrace_code { + @include code-block; + } + // .kb-error-display__advice_list + &__advice_list { + list-style: none; + padding-left: 0; + } +} + +.access, +.generic { + &-error { + // .access-error__container + // .generic-error__container + &__container { + margin: 2rem; + text-align: center; + } + + // .access-error__heading, + // .generic-error__heading + &__heading { + font-size: 200%; + font-weight: 700; + line-height: 1.5; + } + + // .access-error__text, + // .generic-error__text + &__text { + font-size: 150%; + line-height: 2; + } + } +} + +.access-request { + // .access-request-form__container + &-form__container { + margin-top: 2rem; + } + + // .access-request-progress__container + &-progress__container { + display: none; + margin-top: 1rem; + } + + // .access-request-result + &-result { + font-size: 125%; + margin-top: 2rem; + } +} + +.error-dialog__body { + margin: 1rem; +} diff --git a/kbase-extension/scss/partials/_filePath.scss b/kbase-extension/scss/partials/_filePath.scss new file mode 100644 index 0000000000..a7a8b9e277 --- /dev/null +++ b/kbase-extension/scss/partials/_filePath.scss @@ -0,0 +1,75 @@ +.kb-file-path { + // .kb-file-path__list + &__list { + counter-reset: fpw-item; + list-style-position: outside; + list-style-type: none; + padding: 0; + } + + // .kb-file-path__list_item + &__list_item { + align-items: center; + display: flex; + font: normal 400 14px/18px $typeface-page-text; + padding: 0 1.5rem 0.5rem; + + &:nth-child(2n) { + background-color: use_color('base-lightest'); + } + } + + &__list_item::before { + content: counter(fpw-item) ' '; + counter-increment: fpw-item; + font-weight: 700; + padding-right: 0.5rem; + padding-top: 20px; + vertical-align: middle; + } + + // .kb-file-path__param_container + &__param_container { + display: flex; + flex: 1; + flex-wrap: wrap; + // .kb-file-path__param_container .input_group .select2-container + .input-group .select2-container { + display: table; + table-layout: fixed; + } + } + + // .kb-file-path__table_cell--file-path_id + &__row_cell--file-path_id { + flex: 1; + min-width: 30rem; + padding: 4px; + } + + // .kb-file-path__button--delete + &__button--delete { + @extend %kbase-button; + + @include button-variant(use_color('primary'), use-color('primary-lightest'), transparent); + + margin-top: 2rem; + } + + // .kb-file-path__button--add_row + &__button--add_row { + @extend %kbase-button; + + @include button-variant(use_color('primary'), use-color('primary-lightest'), transparent); + + margin: 1rem; + } + + &__button_icon--add_row { + margin-right: 0.5rem; + } + + &__params { + margin-left: 0.5rem; + } +} diff --git a/kbase-extension/scss/partials/_filetypePanel.scss b/kbase-extension/scss/partials/_filetypePanel.scss new file mode 100644 index 0000000000..85263ec64d --- /dev/null +++ b/kbase-extension/scss/partials/_filetypePanel.scss @@ -0,0 +1,80 @@ +// filetype panel stuff + +$component-border: use-color('silver'); +$filetype-highlight-color: use-color('white'); +$filetype-hover-color: use-color('base-lighter'); + +%file_type_button { + background-color: inherit; + border: 0 solid $component-border; + border-top-width: 1px; + display: flex; + flex-direction: row; + padding: 8px; + text-align: left; + width: 100%; +} + +.kb-filetype-panel{ + &__header { + @include font-h4; + + border-bottom: 1px solid $component-border; + padding: 8px; + + &_icon { + color: rgb(0 0 0); + padding: 8px 6px 8px 8px; + } + } + + &__filetype { + // .kb-filetype-panel__filetype_button + &_button { + @extend %file_type_button; + + @include base-text; + + &:hover:not(&--selected) { + @extend %file_type_button; + + background-color: $filetype-hover-color; + cursor: pointer; + } + + // .kb-filetype-panel__filetype_button--selected + &--selected { + @extend %file_type_button; + + background-color: $filetype-highlight-color; + box-shadow: 0 3px 2px -2px use-color('base'); + } + + &--selected + .kb-bulk-import__filetype-panel__filetype_button { + box-shadow: inset 0 3px 2px -2px use-color('base'); + } + } + + // .kb-filetype-panel__filetype_icon + &_icon { + padding: 2px 8px 0; + text-align: center; + width: 3rem; + + // .kb-filetype-panel__filetype_icon--complete + &--complete { + color: use_color('green'); + } + + // .kb-filetype-panel__filetype_icon--incomplete + &--incomplete { + color: use_color('red'); + } + } + + // .kb-filetype-panel__filetype_label + &_label { + flex: 1; + } + } +} diff --git a/kbase-extension/scss/partials/_fsm.scss b/kbase-extension/scss/partials/_fsm.scss new file mode 100644 index 0000000000..f64b2a6aa6 --- /dev/null +++ b/kbase-extension/scss/partials/_fsm.scss @@ -0,0 +1,12 @@ +.kb-fsm { + &__key { + margin-left: 3px; + } + + &__value { + background-color: use-color('neutral'); + color: use-color('white'); + margin: 0 3px; + padding: 2px; + } +} diff --git a/kbase-extension/scss/partials/_icons.scss b/kbase-extension/scss/partials/_icons.scss new file mode 100644 index 0000000000..d3870cfcb8 --- /dev/null +++ b/kbase-extension/scss/partials/_icons.scss @@ -0,0 +1,38 @@ +[data-icon]::before { + content: attr(data-icon); +} + +[class*=' icon-']::before, +[class^=icon-]::before, +[data-icon]::before { + font-family: $typeface-kbase-icons !important; + font-style: normal !important; + font-variant: normal !important; + font-weight: 400 !important; + speak: none; + text-transform: none !important; +} + +.icon-compare::before { + content: 'a'; +} + +.icon-genome::before { + content: 'b'; +} + +.icon-metabolism::before { + content: 'c'; +} + +.icon-metagenome::before { + content: 'd'; +} + +.icon-reads::before { + content: 'e'; +} + +.icon-tree::before { + content: 'f'; +} diff --git a/kbase-extension/scss/partials/_infoTab.scss b/kbase-extension/scss/partials/_infoTab.scss new file mode 100644 index 0000000000..643fd8dd78 --- /dev/null +++ b/kbase-extension/scss/partials/_infoTab.scss @@ -0,0 +1,42 @@ +.kb-info-tab { + // .kb-info-tab__container + &__container { + @include default-container-padding; + } + // .kb-info-tab__title + &__title { + line-height: 30px; // same as font-h4 line height + } + // .kb-info-tab__name + &__name { + @include font-h4; + } + // .kb-info-tab__version + &__version { + padding: 0 1rem; + } + // .kb-info-tab__tag + &__tag { + padding: 0.5rem; + vertical-align: middle; + } + // .kb-info-tab__authors + // .kb-info-tab__description + // .kb-info-tab__list--params + // .kb-info-tab__link--docs + &__authors, + &__description, + &__list--params, + &__link--docs { + @include base-text-with-margins; + } + // .kb-info-tab__list_title--params + &__list_title--params { + @include font-h5; + margin: 1rem 0; + } +} + +ul.kb-info-tab__list--params { + padding-left: 2rem; +} diff --git a/kbase-extension/scss/partials/_jobAction.scss b/kbase-extension/scss/partials/_jobAction.scss new file mode 100644 index 0000000000..0be26fa468 --- /dev/null +++ b/kbase-extension/scss/partials/_jobAction.scss @@ -0,0 +1,67 @@ +.kb-job-action { + &__dropdown { + &_header { + font-weight: 700; + text-transform: uppercase; + } + + // .kb-job-status__dropdown-menu + &-menu { + @include body-text; + + line-height: 2.5; + padding: 0; + + // .kb-job-status__dropdown-menu-item + &-item { + // .kb-job-status__dropdown-menu-item-link + &-link { + &--retry, + &--cancel { + @include body-text; + + background: use_color('white'); + border: 0; + border-bottom: 1px solid rgb(0 0 0 / 0.15); + line-height: 2.5; + text-align: left; + width: 100%; + + &:disabled { + color: use_color('base'); + cursor: not-allowed; + } + + &:hover, + &:active { + &:disabled { + background: use_color('base-light'); + color: use_color('white'); + } + } + } + + &--retry { + color: use_color('primary'); + + &:hover, + &:active { + background: use_color('primary'); + color: use_color('white'); + } + } + + &--cancel { + color: use_color('error-dark'); + + &:hover, + &:active { + background: use_color('error'); + color: use_color('white'); + } + } + } + } + } + } +} diff --git a/kbase-extension/scss/partials/_jobLog.scss b/kbase-extension/scss/partials/_jobLog.scss new file mode 100644 index 0000000000..ab1aee4459 --- /dev/null +++ b/kbase-extension/scss/partials/_jobLog.scss @@ -0,0 +1,139 @@ +$counter-name: code-list-counter; + +%line_container { + @include fixed-width; + + counter-reset: $counter-name; + list-style: none; + margin: 0; + padding: 0; +} + +%line_text { + @include fixed-width; + + counter-increment: $counter-name; + padding-left: 5rem; + position: relative; + white-space: normal; +} + +.kb-log { + // .kb-log__container + &__container { + @include body-text; + } + // .kb-log__dev_container + &__dev_container { + @include body-text; + + padding-bottom: 2rem; + } + // .kb-log__controls + &__controls { + margin: 2rem 0; + } + // .kb-log__content, + // .kb-log__content--expanded + &__content, + &__content--expanded { + @include zero-box; + + background-color: use-color('base-lightest'); + overflow: scroll; + transition: height 0.5s; + } + // .kb-log__content + &__content { + max-height: $line-height-spacious*12; + // .kb-log__content--expanded + &--expanded { + max-height: $line-height-spacious*30; + } + } + // .kb-log__log_line_container + &__log_line_container { + @extend %line_container; + // .kb-log__log_line_container--error + &--error { + @extend %line_container; + + background-color: use-color('error-lightest'); + } + } + // .kb-log__line_text + // .kb-log__line_text--error + &__line_text, + &__line_text--error { + @extend %line_text; + + &::before { + content: counter($counter-name); + left: 1rem; + position: absolute; + } + } + // .kb-log__line_text + &__line_text { + background-color: use-color('base-lightest'); + color: use-color('ink'); + // .kb-log__line_text--error + &--error { + background-color: use-color('error-lightest'); + color: use-color('error-darker'); + } + } + // .kb-log__logs_title + &__logs_title { + @include font-h4; + + margin-top: 1rem; + } + // .kb-log__spinner + &__spinner { + @include base-text; + @include zero-box; + + padding: 0.5rem 1rem; + } +} + +.kb-job-params { + &__params { + // .kb-job-params__params_container + &_container { + @include body-text; + } + // .kb-job-params__params_title + &_title { + @include font-h4; + + margin-top: 1rem; + } + } + // .kb-job-params__param_list + &__param_list { + @include body-text; + + list-style-type: none; + margin: 1rem 0; + padding: 0; + } +} + +.kb-job-state-viewer { + // .kb-job-state-viewer__container + &__container { + @include body-text; + } + // .kb-job-state-viewer__job_status_detail_container + &__job_status_detail_container { + div:first-of-type { + font-weight: 700; + } + } + // .kb-job-state-viewer__error_container + &__error_container.kb-error-display__container { + padding: 0; + } +} diff --git a/kbase-extension/scss/partials/_jobStatus.scss b/kbase-extension/scss/partials/_jobStatus.scss new file mode 100644 index 0000000000..9970fbc1f5 --- /dev/null +++ b/kbase-extension/scss/partials/_jobStatus.scss @@ -0,0 +1,167 @@ +$job_status_table_cols: 'action', 'import-type', 'output', 'status'; + +.kb-job-status { + box-shadow: none; + + &__table { + @include tabular-text; + + padding: 0; + table-layout: fixed; + width: 100%; + + &.dataTable { + border-collapse: collapse !important; + } + + // .kb-job-status__table_body + &_body { + border-bottom: 2px solid use-color('base-light'); + border-top: 2px solid use-color('base-light'); + + // .odd / .even are from DataTables + // .kb-job-status__table_body tr.odd, + // .kb-job-status__table_body tr.even, + // .kb-job-status__table_body .kb-job-status__row + tr.odd, + tr.even, + .kb-job-status__row { + border-bottom: 1px solid use-color('base-lightest'); + + &:hover { + background: use-color('disabled-light'); + } + } + } + + // .kb-job-status__row--selected + &__row--selected { + background: use-color('base-lightest'); + + &:hover { + background: use-color('primary-lighter'); + } + } + + &_head { + // .kb-job-status__table_head_row + &_row { + font-weight: 700; + + .sorting::after, + .sorting_asc::after, + .sorting_desc::after { + bottom: 0 !important; + display: inline-block !important; + left: 5px !important; + position: relative !important; + } + } + + // .kb-job-status__table_head_cell--${col} + &_cell { + @each $col in $job_status_table_cols { + &--#{$col} { + @include table-cell; + } + } + } + } + + // .kb-job-status__table_footer + &_footer { + @include body-text; + } + } + + &__cell { + // .kb-job-status__cell--${col} + @each $col in $job_status_table_cols { + &--#{$col} { + @include table-cell; + + overflow: hidden; + text-overflow: ellipsis; + } + } + + // .kb-job-status__cell--status + &--status { + text-transform: capitalize; + width: 10rem; + } + // .kb-job-status__cell--action + &--action { + width: 11rem; + } + // .kb-job-status_cell--import-type + &--import-type { + width: 18rem; + } + + // .kb-job-status__cell_action (action buttons) + &_action { + // .kb-job-status__cell_action--retry + // .kb-job-status__cell_action--go-to-results + &--retry, + &--go-to-results { + @extend %kbase-button; + + @include button-variant(use_color('primary'), use-color('primary-lightest'), transparent); + } + // .kb-job-status__cell_action--cell + &--cancel { + @extend %kbase-button; + + @include button-variant(use_color('error'), use_color('error-lightest'), transparent); + } + } + } + + // .kb-job-status__param_list + &__param_list { + @include body-text; + + list-style-type: none; + margin: 1rem 0; + padding: 0; + } + + // .kb-job-status__detail_container + &__detail_container { + background-color: use-color('info-lighter'); + border-bottom: 1px solid use-color('base-lightest'); + padding: 1rem; + } + + // .kb-job-status__icon--action_warning + &__icon--action_warning { + color: use_color('error'); + font-size: 1.5em; + padding: 0.5rem 1rem; + vertical-align: middle; + } + + @each $key, $value in $job_status_colors { + // .kb-job-status__icon--${status} + &__icon--#{$key} { + color: use-color($key); + margin: 4px; + } + + // .kb-job-status__summary--${status} + &__summary--#{$key} { + color: use-color($key); + font-weight: 700; + } + + // .kb-job-status__cell_summary--${status} + &__cell_summary--#{$key} { + color: use-color($key); + font-size: 16px; + font-weight: 700; + margin: 0 0.5rem; + text-transform: capitalize; + } + } +} diff --git a/kbase-extension/scss/partials/_kb-icon.scss b/kbase-extension/scss/partials/_kb-icon.scss new file mode 100644 index 0000000000..f7381415b3 --- /dev/null +++ b/kbase-extension/scss/partials/_kb-icon.scss @@ -0,0 +1,112 @@ +// large icons +$icon-size-default: 56px; +$icon-padding: 3px; +$icon-size-image: $icon-size-default - 2*$icon-padding; +$top-shift: 6px; +$left-shift: 8px; +$outline-offset: 1px; + +// see also .kb-cell-toolbar__app_icon + +.kb-icon { + // kb-icon__container + &__container { + // kb-icon__container--image + &--image { + display: block; + } + + /* kb-icon__container--data, + kb-icon__container--data-stack */ + &--data, + &--data-stack { + cursor: pointer; + } + + &--data, + &--app, + &--app-toolbar, + &--generic, + &--generic-toolbar, + &--type, + &--type-toolbar { + font-size: 2em; + } + + &--data-stack { + font-size: 1.7em; + } + } + + &__icon_background { + // .kb-icon__icon_background--app, + // .kb-icon__icon_background--app-toolbar + &--app, + &--app-toolbar { + color: use_color('icon-app'); + } + + // .kb-icon__icon_background--generic, + // .kb-icon__icon_background--generic-toolbar + &--generic, + &--generic-toolbar { + color: use_color('icon-generic'); + } + + // .kb-icon__icon_background--type, + // .kb-icon__icon_background--type-toolbar + &--type, + &--type-toolbar { + color: use_color('icon-type'); + } + } + + &__img { + &--image { + margin: $icon-padding; + max-height: $icon-size-image; + max-width: $icon-size-image; + } + } + + // For shifting over logos that are stacked on top of each other + &__outline--l1 { + margin-left: $outline-offset; + margin-top: $outline-offset; + } + + &__stack--l1 { + margin-left: $left-shift; + margin-top: $top-shift; + } + + &__outline--l2 { + margin-left: $outline-offset + $left-shift; + margin-top: $outline-offset + $top-shift; + } + + &__stack--l2 { + margin-left: 2*$left-shift; + margin-top: 2*$top-shift; + } + + &__outline--l1, + &__outline--l2 { + color: use_color('white'); + } +} + +.kb-data-list-logo { + background-color: rgb(96 125 139); + border: 0 solid rgb(85 85 85); + border-radius: 50%; + color: rgb(255 255 255); + display: inline-block; + font-size: 24px; + font-weight: 700; + height: 40px; + padding-top: 8px; + text-align: center; + text-shadow: -1px 0 rgb(119 119 119), 0 1px rgb(119 119 119), 1px 0 rgb(119 119 119), 0 -1px rgb(119 119 119); + width: 40px; +} diff --git a/kbase-extension/scss/partials/_landingPages.scss b/kbase-extension/scss/partials/_landingPages.scss new file mode 100644 index 0000000000..ef28a2097a --- /dev/null +++ b/kbase-extension/scss/partials/_landingPages.scss @@ -0,0 +1,467 @@ +.app-icon:hover { + opacity: 0.9; + text-decoration: none; +} + +.nav > li > a { + &:focus, + &:hover { + text-decoration: none; + } +} + +#kbase-search-box { + width: 300px; +} + +#signin-button { + display: inline-block; + padding: 0 15px 0 0; +} + +/* override bootstrap .wrapper */ +.wrapper { + margin-left: auto; + margin-right: auto; + width: 95%; +} + +#core-model { + overflow-x: auto; +} + +#logo { + margin: 2px 10px 0; +} + +table { + font-size: 14px !important; +} + +.media-info-modal { + width: 800px; +} + +#selected-workspace { + margin: 17px 14px 15px 0; +} + +.tab-view { + margin: 10px 0 0; +} + +/* mv */ + +.search-query { + margin: 0 0 5px; + width: 100%; +} + +.caret-up { + border-color: currentColor transparent rgb(255 255 255); + border-style: dotted solid solid; + border-width: 0 4px 4px; + content: ''; + display: inline-block; + height: 0; + margin-left: 2px; + vertical-align: middle; + width: 0; +} + +.scroll-pane { + height: 200px; + overflow-x: hi; + overflow-y: scroll; +} + +#select-box tr td { + padding: 6px; +} + +.selected-ws { + background-color: rgb(66 139 202); + color: rgb(255 255 255); + + &:hover { + /* fixme */ + background-color: rgb(66 139 202) !important; + } +} + +.side-bar-switch { + margin: 0 0 10px; + + li { + font-size: 12px; + + a { + padding: 5px 10px; + } + } +} + +.selected-obj-alert { + margin-bottom: 5px; + padding-bottom: 10px; + + /* override */ + padding-top: 10px; +} + +.heatmap-view { + overflow-x: scroll; +} + +/* ws browser */ + +.ws-selector { + margin: 10px 0 0; + + &.btn-ws-settings { + position: absolute; + right: 30px; + } +} + +.object-view { + margin: 10px 0 0; +} + +.select-ws { + .badge { + margin: 0 4px 0 0; + } + + .btn-ws-settings { + padding: 0 5px; + } +} + +.modal-alert { + margin: 10px 15px 0; + padding: 8px 15px; +} + +.modal-cover { + position: absolute; + z-index: 10; +} + +.modal-cover-table { + display: table; + height: 100%; + position: static; + width: 100%; +} + +.modal-cover-cell { + display: table-cell; + position: static; + vertical-align: middle; +} + +.modal-cover-box { + box-shadow: 7px 7px 5px rgb(136 136 136); + display: inline-block; + margin: 10px; + padding: 5px 15px; +} + +.modal-cover-content { + background: rgb(255 255 255); + border: 1px solid use_color('base'); + border-radius: 3px; + padding: 5px; +} + +.obj-table .dataTables_length label { + margin: 7px 0 0; +} + +.dataTables_info { + float: left; + margin: 0 10px 0 0; +} + +.dataTables_length { + float: left; +} + +.ellipsis { + display: inline-block; + max-width: 170px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ws-descript { + display: inline-block; + max-width: 400px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* custom checkboxes (prototyped) */ + +.ncheck { + border: 1px solid use_color('base-lighter'); + height: 15px; + margin-left: auto; + margin-right: auto; + width: 15px; + + &:hover { + border: 1px solid rgb(68 68 68); + } +} + +.ncheck-btn { + border: 1px solid rgb(136 136 136); + height: 15px; + margin: 4px 0 4px -1px; + width: 15px; + + &:hover { + border: 1px solid rgb(68 68 68); + } +} + +.ncheck-checked { + background: url(../img/checkmark.png) no-repeat -5px -4px; + background-image: image-set(url(../img/checkmark.png) 1x, url(../img/checkmark_2x.png) 2x); +} + +.ncheck-minus { + background: url(../img/checkmark-partial.png) no-repeat -5px -5px; + background-image: image-set(url(../img/checkmark-partial.png) 1x, url(../img/checkmark-partial_2x.png) 2x); +} + +.btn-select-all { + height: 34px; + margin: 0 20px 0 0; +} + +.type-filter { + margin: 0 20px 0 0; +} + +.btn-trash { + margin: 3px 20px 0 0; +} + +.btn-show-info .glyphicon { + padding: 0 0 4px; +} + +.open-obj { + margin: 7px 0 0 20px; +} + +/****************** narrative ***************/ + +#login-form { + margin: 70px 0 0; +} + +#narrative-nav, +.recent-narratives, +.recent-projects { + margin: 0; + padding: 0; +} + +.group-item-expander { + background: rgb(242 242 242); + + /* #d6eaff; */ + + &:hover { + cursor: hand; + cursor: pointer; + } +} + +.btn-new-narrative { + margin: 0 20px 0 0; +} + +.btn-delete-narrative { + cursor: hand; + cursor: pointer; + + &:hover { + opacity: 0.7; + } +} + +/* changing z index for datatable fixed header */ + +.navbar-fixed-top { + z-index: 500; +} + +.FixedHeader_Header { + background: rgb(255 255 255); + height: 44px; + margin: -6px 0 0; +} + +.narrative-sidebar li { + list-style: none; +} + +@media (min-width: 960px) { + .narrative-sidebar { + position: fixed; + width: 20%; + } +} + +.sidebar-header, +.view-header { + color: rgb(137 137 137); + font-family: $typeface-display-legacy; + font-weight: 400; + letter-spacing: 0; +} + +.sidebar-header { + font-size: 14px; +} + +.view-header { + font-size: 1.5em; + line-height: 1.75; +} + +.active .btn-narr { + background-color: rgb(187 232 249); + border: 1px solid rgb(66 139 202); + padding-left: 20px; + padding-right: 20px; + text-align: left; +} + +.btn-narr { + background-color: rgb(242 242 242); + border: 1px solid rgb(219 219 219); + color: rgb(109 168 207); + font: 14px $typeface-display-legacy; + padding-left: 20px; + padding-right: 20px; + text-align: left; +} + +.nav-sidebar { + top: 70px; + + li { + font-family: $typeface-display-legacy; + list-style: none; + margin-bottom: 10px; + text-align: left; + } + + > li > a { + padding: 7px 15px; + } +} + +.navbar-nav { + color: rgb(109 168 207); + font-family: $typeface-display-legacy; + + > { + li { + > a:hover { + background-color: rgb(255 255 255); + } + + &.active { + &:hover, + > a { + border-bottom: 4px solid rgb(66 139 202); + box-sizing: border-box; + color: use-color('blue-hlink') !important; + height: 50px; + } + } + } + + .active:hover { + color: use-color('blue-hlink'); + } + } +} + +#wrap { + height: auto; + + /* Negative indent footer by its height */ + margin: 0 auto -58px; + min-height: 100%; + + /* Pad bottom by footer height */ + padding: 0 0 80px; +} + +/* for the footer */ + +#footer { + height: 58px; + + ul { + text-align: center; + + li { + display: inline; + padding: 7px 5px 5px 1px; + + &::after { + color: rgb(187 187 187); + content: '\2022'; + font-size: 10px; + padding-left: 10px; + } + + &:last-child::after, + &:nth-last-child(2)::after { + content: ''; + } + } + } + + img { + position: relative; + top: -3px; + } + + .disclaimer { + background-color: rgb(169 68 66); + bottom: 0; + color: rgb(255 255 255); + display: block; + font-weight: 700; + position: fixed; + text-align: center; + width: 100%; + } +} + +.KBSnode { + rect { + cursor: pointer; + fill: rgb(255 255 255); + fill-opacity: 0.5; + stroke: rgb(49 130 189); + stroke-width: 1.5px; + } + + text { + font: 80% sans-serif; + pointer-events: none; + } +} diff --git a/kbase-extension/scss/partials/_loadingBlocker.scss b/kbase-extension/scss/partials/_loadingBlocker.scss new file mode 100644 index 0000000000..51a3cffd89 --- /dev/null +++ b/kbase-extension/scss/partials/_loadingBlocker.scss @@ -0,0 +1,58 @@ +/* Show this over the whole narrative UI until the + workspace becomes active. + + This animation was taken from: + http://cssdeck.com/labs/load + + It was created by José Barcelon-Godfrey + http://cssdeck.com/user/Jmbarcelonco + + -Dan Gunter, Feb 10, 2015 +*/ + +%kb-loading-blocker-text { + font-size: 16px; + line-height: 1.5; + margin-left: 30%; + text-align: left; + width: 40%; +} + +.kb-loading-blocker { + // .kb-loading-blocker__container + &__container { + background-color: rgb(255 255 255); + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 9999; + } + + // .kb-loading-blocker__header + &__header { + font-size: 24px; + font-weight: 700; + text-align: center; + } + + // .kb-loading-blocker__text + &__text { + @extend %kb-loading-blocker-text; + + // .kb-loading-blocker__text--warning + &--warning { + @extend %kb-loading-blocker-text; + + display: none; + position: absolute; + } + } + + // .kb-loading-blocker__image_container + &__image_container { + padding-top: 10%; + text-align: center; + } +} diff --git a/kbase-extension/static/kbase/css/methodCell.css b/kbase-extension/scss/partials/_methodCell.scss similarity index 72% rename from kbase-extension/static/kbase/css/methodCell.css rename to kbase-extension/scss/partials/_methodCell.scss index f5bddbd8a8..e48aaa8cd1 100644 --- a/kbase-extension/static/kbase/css/methodCell.css +++ b/kbase-extension/scss/partials/_methodCell.scss @@ -8,8 +8,8 @@ .code_cell div.input_area { display: none; -} -.code_cell div.input_area.-show { - display: block; + &.-show { + display: block; + } } diff --git a/kbase-extension/scss/partials/_narrative.scss b/kbase-extension/scss/partials/_narrative.scss new file mode 100644 index 0000000000..7a5e52e224 --- /dev/null +++ b/kbase-extension/scss/partials/_narrative.scss @@ -0,0 +1,3085 @@ +.kb-ga-seq { + font-family: $typeface-fixed-width; + max-height: 100px; + max-width: 300px; + overflow-y: auto; + word-wrap: break-word; +} + +/** + * Shim to make sure jQuery-ui dialogs are always on front of other DOM elements. + */ + +.ui-front { + z-index: 1000; +} + +.whiteout-pane { + background-color: rgb(255 255 255); + height: 100%; + left: 0; + position: absolute; + text-align: center; + top: 0; + width: 100%; + z-index: 1001; +} + +#kb-narr-name { + font: normal 700 18px/1.2 $typeface-page-text; + overflow: hidden; + padding-bottom: 7px; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover { + text-overflow: inherit; + white-space: normal; + } +} + +#save_widget { + padding: 0; +} + +#kb-narr-creator, +#save_widget { + font-family: $typeface-page-text; + font-weight: 700; +} + +.kb-narr-namestamp { + border-left: 2px solid rgb(206 206 206); + display: block; + flex: 2; + margin: 5px auto 0; + min-width: 0; + padding-left: 20px; +} + +.kb-narr-namestamp__error_container { + font-size: 200%; + line-height: 1.5; +} + +.ui-draggable.kb-data-inflight { + background-color: rgb(254 255 214) !important; + border: 2px solid rgb(128 128 128); + border-radius: 4px; + display: block; + z-index: 1000; + + &.-over { + background-color: rgb(239 252 235) !important; + border-color: rgb(0 128 0); + } +} + +.kb-data-list-drag-target { + border: 2px dashed rgb(255 165 0); + border-radius: 4px; + display: block; + height: 40px; + padding: 6px; + text-align: center; + transition: all 0.25s ease; + + &.-drag-active { + background-color: rgb(254 255 214); + } + + &.-drag-hover { + background-color: rgb(239 252 235); + border-color: rgb(0 128 0); + height: 90px; + transition: all 0.25s ease; + } +} + +#kb-notify-area { + float: left; + position: relative; +} + +.kb-navbar-buttons { + border-right: 2px solid rgb(206 206 206); + display: inline-block; + margin-right: 15px; + padding-right: 15px; +} + +header[role=banner] { + background-color: rgb(255 255 255); + border-bottom: 1px solid rgb(206 206 206); + position: absolute; + top: 0; + width: 100%; + z-index: 999; +} + +nav[role=navigation] ul { + margin: 1.2em 0 0.5em; + padding-left: 0; + + li { + display: inline; + margin: 0 0.5em; + vertical-align: middle; + + a { + background-color: rgb(92 149 49); + border: 1px solid rgb(92 149 49); + border-radius: 8px; + box-shadow: inset 0 3px 8px rgb(0 0 0 / 0.125); + color: rgb(255 255 255); + font: normal 700 1.2em $typeface-page-text; + padding: 7px 1em; + text-decoration: none; + } + } +} + +input#search_terms { /* FIXME */ + background-image: url(../images/search.png); + background-position: 100%; + background-repeat: no-repeat; + background-size: 22px; + border-color: rgb(92 149 49); + border-radius: 0 4px 4px 0; + display: inline; + font-size: 1em; + left: -0.5em; + margin-top: -3px; + padding: 0.4em 1em; + position: relative; +} + +span#searchspan { /* FIXME */ + display: inline-block; + width: 50%; +} + +@media screen and (max-width: 1170px) { + span#searchspan { + width: 50%; + } +} + +@media screen and (max-width: 980px) { + span#searchspan { + width: 40%; + } +} + +@media screen and (max-width: 768px) { + span#searchspan { + width: 30%; + } +} + +@media screen and (max-width: 590px) { + span#searchspan { + width: 60%; + } +} + +.navbar-kbase { + background-color: rgb(255 255 255); + border-bottom: 5px solid rgb(206 206 206) !important; + box-shadow: 0 1px 10px rgb(0 0 0 / 0.1); + padding: 8px; + + a:hover { + cursor: pointer; + } +} + +/* Top bar nav buttons + -------------------------- +*/ + +.kb-nav-btn { + background-color: rgb(255 255 255); + border: 0; + box-shadow: 0 0 3px rgb(206 206 206); + font-size: 24px; + margin: 0 5px 0 0; + min-width: 50px; + padding: 1px 7px; + text-shadow: none !important; + + > div { + color: use_color('base') !important; + } + + .kb-nav-btn-txt { + font-size: 13px; + margin-top: -5px; + } +} + +.navbar-right .fa::before { + /* color nav icons */ + color: use_color('mid-blue'); +} + +.kb-navbar-container { + display: flex; + justify-content: space-between; +} + +#kb-nav-menu { + box-shadow: none; +} + +.kb-nav-menu__container { + display: inline-block; +} + +.navbar-right .kb-nav-btn:hover { + background-color: rgb(245 245 245); +} + +.kb-nav-btn-upgrade { + background-color: use_color('mid-green') !important; + display: none; + + .fa::before, + > div { + color: rgb(255 255 255) !important; + } + + &:hover { + background-color: rgb(67 160 71) !important; + } + + &.warning { + background-color: rgb(244 67 54) !important; + + &:hover { + background-color: rgb(220 60 49) !important; + } + } +} + +.kb-nav-menu-icon { + display: inline-block; + margin-right: 5px; + text-align: center; + vertical-align: middle; + width: 20px; + + .fa { + font-size: 150%; + } +} + +#kb-ipy-menu { + /* restore rounded corners */ + border-radius: 4px !important; + margin-right: 0; +} + +#signin-button .btn { + border: 0; +} + +.btn-xs .glyphicon-user { + /* also resize user btn */ + padding: 14px 5px; +} + +/* -------------------------- */ + +.narrative-menu-container { + background-color: rgb(255 255 255); + border-bottom: 1px solid rgb(171 171 171); + margin-left: auto; + margin-top: -10px; + padding-bottom: 30px; + padding-top: 10px; + position: fixed; + width: 100%; + z-index: 499; +} + +p.clear { + clear: both; + height: 0; +} + +body > #header { + box-sizing: border-box; + display: block; +} + +p#site-title { /* FIXME */ + background: url(../images/kbase_logo.png) no-repeat; + background-size: 46px; + float: left; + height: 46px; + margin: 5px; + text-indent: -9999px; + width: 46px; +} + +#login-info { + font-size: 1.2em; + margin: 5px; + position: absolute; + right: 0; + top: 7px; +} + +#login-widget button { + margin-left: 5px; +} + +.search-box { + border-collapse: separate; + display: inline; + margin: -11px 0; + padding: 10px 1px; + position: relative; + width: 200px; +} + +#search-box-name { + border: 0; + border-radius: 4px; + padding: 6px 0; + + &:first-child { + border-bottom-right-radius: 0; + border-right: 0; + border-top-right-radius: 0; + } + + &:last-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } +} + +#search-input { + margin: -11px 0; +} + +#search-select { + margin: -11px 0; + width: 150px; +} + +#search-area { + color: rgb(104 104 104); + font: normal 400 18px $typeface-page-text; + margin: 0 5px; + padding: 11px; + position: relative; + + input { + padding: 11px; + } +} + +/* center pane */ + +#workspace { + bottom: 42px; + overflow: auto; + position: fixed; + top: 173px; + width: 878px; +} + +/* footer */ + +#bottom-tabs { + background-color: rgb(255 255 255); + border-top: 1px solid rgb(170 170 170); + bottom: 0; + clear: left; + margin: 0 auto; + position: fixed; + width: 1170px; + + p a { + background-color: rgb(255 255 255); + border: 1px solid rgb(1 136 65); + font-size: 14px; + padding: 14px 20px; + } +} + +/* left pane styling */ + +#left-column { + border-right: 5px solid rgb(206 206 206); + bottom: 0; + position: fixed; + top: 70px; + width: 380px; +} + +.kb-side-overlay-container { + background: rgb(255 255 255); + border: 1px solid rgb(206 206 206); + max-height: calc(100% - 73px); + overflow-y: auto; + position: fixed; + width: 730px; + z-index: 1030; +} + +.kb-side-overlay-header { + background-color: use_color('mid-blue'); + color: rgb(255 255 255); + font-size: 16px; + padding: 6px; + width: 100%; +} + +.kb-side-overlay-close { + color: rgb(255 255 255); + cursor: pointer; + font-size: 13px; + font-weight: 700; + + &:hover { + color: rgb(255 165 0); + } +} + +.kb-side-panel { + height: 100%; + overflow-y: hidden; +} + +#kb-side-toggle-in { + background-color: use_color('mid-blue'); + border-bottom: 6px solid rgb(206 206 206); + border-right: 6px solid rgb(206 206 206); + color: use_color('primary-lighter'); + cursor: pointer; + display: none; + font-size: 16px; + font-weight: 700; + padding: 12px 6px; + position: fixed; + text-align: center; + top: 70px; + user-select: none; + width: 22px; + z-index: 1; + + &:hover { + color: rgb(255 255 255); + } +} + +.kb-side-toggle { + background-color: use_color('mid-blue'); + border-bottom: 6px solid use_color('mid-blue'); + border-top: 6px solid use_color('mid-blue'); + color: use_color('primary-lighter'); + cursor: pointer; + display: inline-block; + font-size: 16px; + font-weight: 700; + padding: 6px; + text-align: center; + user-select: none; + + &:hover { + color: rgb(255 255 255); + } +} + +.kb-side-header { + background-color: use_color('mid-blue'); + border-bottom: 6px solid use_color('mid-blue'); + border-top: 6px solid use_color('mid-blue'); + color: use_color('primary-lighter'); + cursor: pointer; + display: inline-block; + font-size: 16px; + font-weight: 700; + padding: 6px; + text-align: center; + user-select: none; + + &.active, + &:hover { + border-bottom: 6px solid rgb(16 206 52); + } + + &.active { + color: rgb(255 255 255); + cursor: auto; + } +} + +.kb-overlay-active { + background-color: rgb(128 128 128); + border-bottom: 6px solid rgb(128 128 128); + border-top: 6px solid rgb(128 128 128); +} + +.kb-side-separator { + border-bottom: 5px solid rgb(206 206 206); +} + +.kb-side-tab { + display: none; + + &.active { + display: inherit; + } +} + +.kb-narr-side-panel-set, +.kb-side-tab { + height: 100%; +} + +.kb-narr-side-panel-set:last-child { + height: 50%; +} + +.kb-overlay-dimmer { + background-color: rgb(0 0 0); + bottom: 0; + opacity: 0.5; + position: fixed; + right: 0; + z-index: 10; +} + +#data-header { + background-color: rgb(0 153 153); + color: rgb(255 255 255); + font-weight: 700; + padding: 10px 0; +} + +#data-title { + text-align: center; +} + +#data-add-btn { + font-size: 0.8em; + padding: 0 0 -10px; + position: absolute; + right: 7px; + + &:hover { + color: rgb(0 0 0); + } +} + +#data-tab-nav { + background-color: rgb(0 187 187); + border-bottom: 1px solid rgb(170 170 170); + padding-top: 10px; + width: 100%; + + ul { + bottom: 0; + padding-bottom: 0; + top: 50px; + + li { + background-color: rgb(221 221 221); + border: 1px solid rgb(0 0 0); + display: inline; + padding: 4px 10px; + + a { + color: rgb(0 0 0); + + &:hover { + color: rgb(170 0 0); + text-decoration: none; + } + } + + &.selected { + background-color: rgb(255 255 255); + } + } + } +} + +#data-view-panel { + border: 1px solid rgb(170 170 170); + height: 150px; + overflow-y: auto; + padding: 10px; +} + +#data-pane { + border-bottom: 1px solid rgb(170 170 170); + height: 69%; + margin-top: 1px; + overflow: auto; + + p { + margin: 0; + + span { + border: 1px solid rgb(255 255 255); + color: rgb(255 255 255); + display: block; + margin: 0; + padding: 2px 10px; + text-align: center; + width: 123px; + + &.tab1 { + background: linear-gradient(use_color('base'), rgb(187 187 187)); + color: rgb(255 255 255); + float: right; + } + + &.tab2 { + background: linear-gradient(use_color('base-lighter'), use_color('base-light')); + } + } + } +} + +#function-pane { + height: 30%; + margin-top: 1px; + overflow: auto; +} + +.left-pane { + font-size: 14px; + position: relative; +} + +#kb-function-panel .kb-function-body { + height: 100%; + max-height: 100%; +} + +h3.pane-title { + background-color: rgb(224 224 224); + border: 1px solid use-color('silver'); + font-size: 16px; + margin: 0; + padding: 3px 5px; + position: relative; +} + +#add-link { + font-size: 0.8em; + position: absolute; + right: 7px; + top: 4px; +} + +ul.pane-list { + margin: 0; + padding: 2px; + + li { + list-style: none; + margin-left: 5px; + padding: 1px; + } +} + +/* --- KBase method list styles -- */ + +.kb-method-list-logo { + background-color: rgb(96 125 139); + border: 0 solid rgb(85 85 85); + color: rgb(255 255 255); + cursor: pointer; + display: inline-block; + font-size: 24px; + font-weight: 700; + height: 40px; + padding-top: 8px; + text-align: center; + text-shadow: -1px 0 rgb(119 119 119), 0 1px rgb(119 119 119), 1px 0 rgb(119 119 119), 0 -1px rgb(119 119 119); + width: 40px; + + &:hover { + border-width: 5px; + padding-top: 3px; + } +} + +.kb-method-list-more-div { + color: rgb(119 119 119); + font-size: 13px; + margin: 2px; + text-align: justify; + + > div:last-child { + text-align: right; + } +} + +.kb-method-search-clear { + border: 1px solid rgb(206 206 206); + border-left: 0; + cursor: pointer; +} + +/* A function in the function pane */ + +li { + &.function a { + color: rgb(80 130 50); + text-decoration: none; + + &:visited { + /* don't recolor */ + color: rgb(80 130 50); + } + } + + &.dataset a { + color: rgb(130 80 50); + text-decoration: none; + + &:visited { + /* don't recolor */ + color: rgb(130 80 50); + } + } +} + +/* dataset in data pane */ + +/* dialog box + forms styling */ + +.dialog-box { + padding: 20px; + + ul li { + border-bottom: 1px solid rgb(170 170 170); + font-size: 1.2em; + padding: 50px 5px; + + &:first-child { + padding-top: 20px; + } + + &:last-child { + border-bottom: 0; + } + } +} + +fieldset { + border: 0; + margin: 0 0 15px; + + label { + font-size: 1em; + font-weight: 700; + margin: 0 0 3px; + } + + input { + &[type=password], + &[type=text] { + width: 200px; + } + } + + select { + width: 200px; + } +} + +/* styling of cells */ + +div { + &.dataset-cell, + &.function-cell { + border: 1px solid use_color('base-lighter'); + margin: 3px auto; + padding: 7px 17px; + width: 90%; + } + + &.texttools, + &.tools { + p { + margin: 0; + } + } + + &.textarea { + border: 0; + display: none; + height: auto; + min-height: 1.2em; + padding: 0; + width: 650px; + } + + &.metadata { + display: block; + float: left; + height: 100px; + padding: 0 10px; + width: 200px; + } + + &.social { + display: none; + float: right; + padding: 0 10px; + width: 80px; + } +} + +img.dataset-cell { + margin: 3px auto; + padding: 0; +} + +.function-cell { + background-image: url(../images/gears.png); + background-position: 100%; + background-repeat: no-repeat; +} + +.dataset-cell, +.function-cell { + p { + font-size: 0.875em; + } + + h2 { + font-size: 1.1em; + margin-bottom: 0; + } +} + +.textcell { + border: 2px solid rgb(255 255 255); + color: rgb(17 17 17); + font-size: 1em; + height: 1em; + margin: 3px auto; + padding: 5px 17px; + width: 90%; + + &:hover { + border: 2px dotted use_color('base-lighter'); + } +} + +p.textcuepara { + color: use_color('base-lighter'); + display: none; + height: 1em; + margin: 0; + padding: 0; +} + +.texttools { + display: none; + + img { + vertical-align: middle; + } +} + +.tools { + display: none; + height: 1em; + margin-left: 10px; + margin-top: 0.5em; + + a { + border: 1px solid rgb(1 136 65); + font-size: 13px; + padding: 2px 4px; + } +} + +#narrative-header .tools { + display: block; + left: 350px; + position: relative; + top: -2em; + width: 30em; +} + +img.remove { + float: right; +} + +.ui-state-highlight { + background-color: fbfaed; +} + +p span.notification { + background-color: rgb(255 51 51); + border-radius: 1em; + color: rgb(255 255 255); + padding: 4px 8px; +} + +/** + * Bootstrap 3 submenu fix + * http://stackoverflow.com/questions/18023493/bootstrap-3-dropdown-sub-menu-missing + */ + +.dropdown-submenu { + position: relative; + + > .dropdown-menu { + border-radius: 0 6px 6px; + left: 100%; + margin-left: -1px; + margin-top: -6px; + top: 0; + } + + &:hover { + > .dropdown-menu { + display: block; + } + + > a::after { + border-left-color: rgb(255 255 255); + } + } + + > a::after { + border-color: transparent transparent transparent use_color('base-lighter'); + border-style: solid; + border-width: 5px 0 5px 5px; + content: ' '; + display: block; + float: right; + height: 0; + margin-right: -10px; + margin-top: 5px; + width: 0; + } + + &.pull-left { + float: none; + + > .dropdown-menu { + border-radius: 6px 0 6px 6px; + left: -100%; + margin-left: 10px; + } + } +} + +/** End Bootstrap 3 submenu fix **/ + +/* Fixes to bring toolbars up to Bootstrap 3 */ + +#maintoolbar { + background-color: rgb(249 249 249) !important; + background-image: none !important; + border: 0 !important; + box-shadow: none; + min-height: 0; + position: relative; + top: 5px; +} + +#maintoolbar-container { + margin-left: 10px; +} + +#menubar { + background-color: rgb(249 249 249); + border-bottom: 2px solid rgb(221 221 221); + padding-bottom: 5px; + position: absolute; + top: 3px; + width: 100%; + + .navbar .container { + padding-left: 0; + padding-right: 0; + } +} + +/* some more bootstrap hacks */ + +.navbar { + background-color: rgb(248 248 248); + background-image: none; + min-height: 0; +} + +#menubar .navbar-nav > li > a { + padding-bottom: 7px; + padding-top: 10px; +} + +.btn-danger, +.btn-default, +.btn-info, +.btn-primary, +.btn-success, +.btn-warning, +.panel-default > .panel-heading { + background-image: none; +} + +#menus { + padding-left: 0; +} + +#menubar .navbar { + margin-bottom: 5px; + width: 100%; +} + +div#notebook_panel { /* FIXME Jupyter css */ + box-shadow: none; +} + +.version-stamp a { + color: use-color('blue') !important; +} + +.creator-stamp, +.version-stamp a { + padding-right: 320px; + text-align: right; +} + +.creator-stamp { + color: rgb(0 102 152); + font: normal 700 120% $typeface-page-text; + height: 1em; +} + +.panel { + margin-bottom: 0; +} + +#kb-jobs-panel { + margin-bottom: 5px; +} + +.kb-narr-side-panel { + height: 100%; + margin-bottom: 5px; + + .kb-title { + color: use_color('primary-vivid'); + font: normal 700 19px $typeface-page-text; + padding: 10px 5px; + text-transform: uppercase; + } + + .btn { + border: 0; + } +} + +/* From user and job state service */ + +.kbujs-table-container { + height: auto; +} + +.kbujs-timestamp { + color: rgb(0 102 185); + cursor: pointer; +} + +.kbujs-error-cell { + color: rgb(199 37 78); + font-weight: 700; +} + +.kbujs-error:hover { + cursor: pointer; +} + +.kbujs-loading-modal { + font-size: 16px; + font-weight: 700; + text-align: center; +} + +.kbujs-refresh-btn { + position: absolute; + right: 5px; +} + +.kbujs-jobs-table { + margin-left: auto !important; + margin-right: auto !important; +} + +.kbujs-loading { + height: 100%; + text-align: center; + vertical-align: middle; +} + +.kbujs-delete-job { + cursor: pointer; +} + +.kbujs-error-traceback { + float: left; + max-height: 250px; + max-width: 516px; + overflow-x: scroll; + overflow-y: scroll; + white-space: nowrap; +} + +/* For connecting lines */ + +.kb-line { + background-color: rgb(211 211 211); + position: absolute; +} + +/* tab.js */ + +.nav-tabs .glyphicon-remove { + color: rgb(170 170 170); + margin: 0 0 0 3px; + + &:hover { + color: use_color('base'); + cursor: hand; + cursor: pointer; + } +} + +/** + ** Formerly in kbaseNarrFuncResults.css + **/ + +/* CSS for various narrative function results. + * This is just a hacky, dummy place to dump css for different + * result widgets. Something better will come along, once we burn + * this down... + */ + +/* Somehow a bunch of classes from jQuery.dataTables aren't getting added. + * Putting them here in the hopes they don't clobber anything else, the way + * the rest of dataTables might... + */ + +.paging_full_numbers { + height: 22px; + line-height: 22px; + + a { + &:active { + outline: none; + } + + &:hover { + text-decoration: none; + } + + &.paginate_active { + background-color: rgb(153 179 255); + border: 1px solid rgb(170 170 170); + border-radius: 5px; + color: rgb(51 51 51) !important; + cursor: pointer; + *cursor: hand; + margin: 0 3px; + padding: 2px 5px; + } + + &.paginate_button { + background-color: rgb(221 221 221); + border: 1px solid rgb(170 170 170); + border-radius: 5px; + color: rgb(51 51 51) !important; + cursor: pointer; + *cursor: hand; + margin: 0 3px; + padding: 2px 5px; + + &:hover { + background-color: use_color('base-lighter'); + text-decoration: none !important; + } + } + } +} + +/** End kbaseNarrFuncResults.css **/ + +/***************************************************************************** + ** Formerly in kbaseNarrFunc.css + *****************************************************************************/ + +/* + * Styles for KBase Narrative notebook page, function panel + */ + +/* Header for panel */ + +.kb-function-header { + background-color: rgb(182 233 248); + color: rgb(0 100 182); + font-weight: 700; + margin-bottom: 3px; + padding: 10px 0; + text-align: center; +} + +.kb-narr-panel-body { + height: 100%; + padding: 3px; + + > div { + height: 100%; + } +} + +.kb-narr-panel-body-wrapper { + height: 100%; + overflow-y: auto; + + > div { + height: 100%; + } +} + +.kb-narr-panel-toggle { + color: rgb(136 136 136); + cursor: pointer; + margin-right: 4px; + margin-top: -4px; + user-select: none; +} + +/* Container for func list */ + +.kb-function-body { + height: 100%; + width: 100%; + + .accordion .panel .panel-body { + padding: 0 0 0 5px; + } + + ul { + border: 1px solid rgb(221 221 221); + list-style-type: none; + margin: 0; + padding: 0; + width: 100%; + } + + li { + padding: 5px; + text-align: left; + width: 100%; + + &:nth-child(odd) { + background-color: use_color('base-lightest'); + border-bottom: 1px solid rgb(221 221 221); + } + + &:nth-child(2n) { + background-color: rgb(255 255 255); + border-bottom: 1px solid rgb(221 221 221); + } + + &:hover { + background-color: rgb(244 245 214); + cursor: pointer; + text-decoration: underline; + } + } + + a { + text-decoration: none; + + &:hover { + text-decoration: none; + } + } +} + +/* alt. rows */ + +/* Link to a function */ + +.kb-function-error { + background-color: rgb(242 222 222) !important; + border-color: rgb(238 211 215) !important; + color: rgb(185 74 72); +} + +/* Help on a function */ + +.kb-function-help { + color: rgb(86 85 158); + cursor: pointer; + float: right; + font-size: 19px; + padding: 0 3px; + + &:hover { + color: rgb(0 0 0); + } +} + +.kb-function-help-popup { + cursor: pointer; + left: 300px; + max-width: 300px; + position: absolute; + top: 0; + z-index: 99; + + h1 { + color: rgb(48 118 162); + font-size: 110%; + margin: -10px 0 0; + padding: 0; + } + + h2 { + color: use_color('base-light'); + font-size: 80%; + font-style: italic; + font-weight: 100; + margin: 10px 0 -10px; + padding-top: 5px; + text-align: right; + } + + .header { + background-color: rgb(255 255 255); + color: rgb(48 118 162); + font-size: 120%; + font-weight: 700; + margin: 0; + padding: 0; + } + + a { + font-weight: 700; + text-decoration: underline; + } + + .version { + color: use_color('base'); + font-size: 80%; + } + + .body { + margin-bottom: 10px; + margin-top: 10px; + } +} + +#kb-function-help { + cursor: pointer; + left: 300px; + position: absolute; + top: 400px; + width: 250px; + z-index: 99; + + h1 { + color: rgb(48 118 162); + font-size: 110%; + margin: -10px 0 0; + padding: 0; + } + + h2 { + color: use_color('base-light'); + font-size: 80%; + font-style: italic; + font-weight: 100; + margin: 10px 0 -10px; + padding-top: 5px; + text-align: right; + } +} + +#kb-function-error-traceback { + float: left; + max-width: 250px; + overflow-x: scroll; + white-space: nowrap; +} + +.kb-function-dim { + background-color: use_color('base-lightest') !important; + + .panel-heading { + background-color: use_color('base-lightest') !important; + } +} + +.kb-function-cat-dim { + background-color: rgb(221 221 221) !important; + border-color: rgb(221 221 221) !important; + + .panel-heading { + background-color: rgb(221 221 221) !important; + border-color: rgb(221 221 221) !important; + } +} + +.kb-function-toggle { + color: use-color('blue'); + cursor: pointer; + font-style: italic; +} + +.kb-toolbar-open { + border-bottom: 1px solid use_color('base-lighter'); + border-radius: 0; +} + +.selected .kb-toolbar-open { + background-color: rgb(223 240 216); + border-bottom: 1px solid use_color('mid-green'); + border-left: 1px solid use_color('mid-green'); +} + +.unselected .kb-toolbar-open { + background-color: none; +} + +.selected.kb-error .kb-toolbar-open { + background-color: rgb(242 222 222); + border-color: rgb(217 83 79); +} + +/* ------------------------------------------------- */ + +/* IPython cells */ + +.kb-cell-run { + opacity: 1; + z-index: auto; + + h1 { + color: use_color('primary-vivid'); + display: inline; + font: normal 400 16px/1 $typeface-page-text; + margin: 3px 13px 3px 3px; + opacity: inherit; + } + + div.kb-func-desc { + display: block; + opacity: inherit; + } + + h2 { + color: use_color('base'); + font: normal 400 15px/1.5 $typeface-page-text; + margin: 3px; + opacity: inherit; + } + + form { + input[type=submit] { + float: right; + margin-top: 5px; + opacity: inherit; + } + + .buttons { + float: right; + margin-top: 5px; + } + } + // grey out the form while running + &.running form { + opacity: 0.6; + } +} + +.kb-cell-params table { + border: 0; + margin-bottom: 10px; + margin-left: auto; + margin-right: auto; + opacity: inherit; + + > tr { + border: 0; + vertical-align: middle; + } +} + +td { + border: 0; + vertical-align: middle; +} + +.kb-cell-params { + tbody > tr > { + td, + th { + padding: 2px 4px; + } + } + + tfoot > tr > { + td, + th { + padding: 2px 4px; + } + } + + thead > tr > { + td, + th { + padding: 2px 4px; + } + } + + tr:hover { + td, + th { + background: use_color('base-lightest'); + } + } +} + +/* progress bar */ + +.kb-cell-progress { + display: none; + + &.running { + display: block; + } +} + +.kb-cell-progressbar { + height: 20px; + width: 400px; +} + +.kb-out-desc { + color: use_color('primary-vivid'); +} + +.kb-err-desc, +.kb-out-desc { + display: inline; + font: normal 400 16px/1 $typeface-page-text; + margin: 3px; + opacity: inherit; +} + +.kb-err-desc { + color: rgb(169 68 66); +} + +.kb-err-msg { + font-size: 80% !important; +} + +.kb-out-header { + background-color: rgb(221 221 221); + padding: 10px; +} + +.kb-func-timestamp { + color: rgb(85 85 85); + font: 13px $typeface-page-text; + margin-top: -3px; + padding-right: 1em; +} + +.unselected .kb-func-timestamp { + color: use-color('silver'); +} + +.kb-func-panel { + border-color: rgb(188 232 241); + border-radius: 1px; + + > .panel-heading { + background-color: rgb(217 237 247); + border-color: rgb(188 232 241); + border-radius: 1px; + color: rgb(49 112 143); + padding: 5px 10px; + } + + .panel-body { + font-family: $typeface-page-text; + padding: 0 10px; + } + + > { + .panel-heading + .panel-collapse .panel-body { + border-top-color: rgb(188 232 241); + } + + .panel-footer + .panel-collapse .panel-body { + border-bottom-color: rgb(188 232 241); + } + } +} + +/** output cell styling */ + +.kb-cell-output { + .panel { + border-radius: 0; + } + + .panel-heading { + padding: 5px 10px; + } + + /* override from some jupyter style? */ + .nav > li > a { + padding: 5px 10px; + } + + &-content { + overflow-x: auto; + padding: 5px; + } +} + +.rendered_html { + .kb-cell-output ul { + margin: 0 0 10px; + } + + :link { + text-decoration: none; + } +} + +.kb-cell-error .panel-heading { + background-image: none; + padding: 5px 10px; +} + +/*** styling for the jobs manager */ + +.kb-jobs-title { + color: rgb(24 90 133); + font: normal 700 1.2em $typeface-page-text; + + .glyphicon-info-sign { + font-size: 0.9em; + } +} + +.kb-jobs-info-table { + margin: 0 2px; + width: 95%; + + th { + color: rgb(119 119 119); + font-family: $typeface-page-text; + font-weight: 700; + max-width: 30%; + min-width: 30%; + padding-right: 5px; + vertical-align: top; + width: 30%; + } +} + +.kb-jobs-item { + border-bottom: 2px solid rgb(221 221 221); + margin-bottom: 5px; + padding-bottom: 3px; +} + +.kb-jobs-error { + background-color: rgb(242 222 222); +} + +.kb-jobs-error-btn { + font-size: 13px; +} + +.kb-jobs-item:last-child { + border-bottom: 0; +} + +.kb-jobs-error-modal { + max-height: 220px; + overflow-x: hidden; + overflow-y: auto; + width: 485px; + word-wrap: break-word; +} + +/** new method parameter styling **/ + +.kb-method-parameter-panel { + border-left: 3px solid rgb(255 255 255); +} + +.kb-method-parameter-panel-hover { + border-left: 3px solid rgb(66 139 202); +} + +.kb-method-parameter-row { + border-radius: 5px; + margin: 0; + padding: 5px; +} + +/* for some reason, the css :hover doesn't work right on this div, so we use jquery to toggle this class */ + +.kb-method-parameter-row-hover { + background: rgb(249 249 249); +} + +.kb-method-parameter-row-error { + background: rgb(242 222 222); +} + +.kb-method-parameter-error-mssg { + color: rgb(244 67 54); + font-family: $typeface-page-text; + font-size: 12px; + font-weight: 700; + padding: 5px; + text-align: center; +} + +/* not sure how to get text in these divs to valign middle... */ + +.kb-method-parameter-name { + color: rgb(119 119 119); + font-family: $typeface-page-text; + font-weight: 700; + margin-top: 3px; + padding-left: 0; + padding-right: 0; + text-align: right; + vertical-align: middle; + white-space: normal; +} + +.kb-method-parameter-input { + padding-left: 10px; + vertical-align: middle; + white-space: nowrap; + + input { + font-weight: 700; + } + + > div { + /* allow space for the required (red arrow), satisfied (green checkbox) icon */ + padding-right: 20px; + } + + .kb-method-parameter-accepted-glyph, + .kb-method-parameter-required-glyph { + font-size: 15px; + + /* This scoots the icon inside */ + margin-left: -15px; + } +} + +/* +This set of styles is a to accommodate the required/satisfied icon which appears +between the select input control and the help text to the right of it. The +problem is that within the columns used for layout the select is set to 100% width +and yet the icon is placed right next to it. The result is that the icon is shoved +to the right of the select control and into the next column, overlapping the +help text. The old method handling this was to scoot the help text far enough over +to accomodate the the icon intruding into its space. This technique makes space +in the column in which the icon lives, and does this by shrinking the select +control with padding, and then scooting the icon back into its column. +*/ + +.kb-parameter-data-selection { + font-weight: 700; +} + +.kb-method-parameter-hint { + color: rgb(119 119 119); + margin-top: 3px; + padding-left: 7px; + text-align: left; +} + +.kb-method-parameter-required-glyph { + color: rgb(244 67 54); + margin-left: 7px; +} + +.kb-method-parameter-accepted-glyph { + color: use_color('mid-green'); + margin-left: 7px; +} + +.kb-method-parameter-info { + margin-left: 7px; +} + +.kb-method-advanced-options-controller-inactive, +.kb-method-parameter-info, +.kb-parameter-data-row-add, +.kb-parameter-data-row-remove { + color: rgb(119 119 119); +} + +.kb-method-advanced-options-controller, +.kb-method-advanced-options-controller-inactive { + font: italic 700 13px/14px $typeface-page-text; + text-align: center; +} + +.kb-method-advanced-options-controller { + color: use-color('blue'); + cursor: pointer; + + &:hover { + color: use-color('blue-hlink'); + } +} + +.kb-method-footer { + background-color: rgb(245 245 245); + overflow: none; + padding: 10px; + width: 100%; +} + +.kb-method-subtitle { + background-color: rgb(245 245 245); + padding: 3px 5px; +} + +/* -- App/method panel styling -- */ + +.kb-app-panel { + border-radius: 0; +} + +.kb-app-error { + background-color: rgb(242 222 222); +} + +.kb-app-step-container { + margin-top: 6px; +} + +.kb-app-panel > .panel-footer { + border: 0; + border-radius: 0; +} + +.kb-app-panel-description { + color: use_color('primary-vivid'); + font: normal 400 16px/1 $typeface-page-text; +} + +.kb-app-step-error-mssg { + color: rgb(166 50 50); + font: 11px/14px sans-serif; + margin: 10px; + text-align: left; +} + +.kb-app-step-error-heading { + color: rgb(85 85 85); + font: normal 700 17px/1 $typeface-page-text; + margin-top: 20px; + padding-left: 5px; + text-align: left; +} + +.kb-app-step-error-main-heading { + color: rgb(85 85 85); + cursor: pointer; + font: 21px/1 $typeface-page-text; + margin-top: 20px; + padding-left: 20px; + text-align: left; +} + +.kb-app-step-error { + border: 3px solid rgb(209 72 54); + z-index: auto; +} + +.kb-app-step-running { + border: 3px solid use_color('mid-blue'); + z-index: auto; +} + +/* -- "next steps" text and links -- */ + +.kb-app-next { + border-top: 2px solid rgb(206 206 206); + + h3 { + color: rgb(136 136 136); + float: left; + font: italic 400 0.9em $typeface-page-text; + margin: 0; + padding: 0.5em 0 0.25em; + } +} + +.kb-app-next-hide { + float: right; + margin-right: 0.5em; +} + +.kb-app-next-hide, +.kb-app-next-unhide { + color: use_color('mid-blue'); + font: 0.9em $typeface-page-text; + margin-top: 0.25em; +} + +.kb-app-next div { + /* container for links */ + clear: both; +} + +/* -- END method/app styling -- */ + +@keyframes pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.7; + } + + to { + opacity: 1; + } +} + +button { + &.kb-app-run, + &.kb-method-run { + background-color: use_color('mid-blue'); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 1px use_color('base-lighter'); + color: rgb(255 255 255); + font-size: 13px; + padding: 10px 20px; + + &:hover { + background-color: rgb(30 136 229); + } + } + + &.kb-app-run { + &.kb-app-cancel { + background-color: rgb(244 67 54); + + &:hover { + background-color: rgb(229 57 53); + } + } + } +} + +/*********** + * END of kbaseNarrFunc.css + ***********/ + +/********* + * From kbaseData.css + *********/ + +/* +For data in KBase workspace +*/ + +.modal-body { + max-height: 100%; +} + +/* === Tooltips === */ + +/* see bootstrap styles, e.g. bootstrap.css, for the original definitions */ + +div[class=tooltip-inner] { + max-width: 400px; + text-align: left; + white-space: pre-wrap; +} + +.notebook_app { + .tooltip-inner { + background-color: rgb(27 105 182); + } + + .tooltip { + z-index: 2000 !important; + + &.top-left .tooltip-arrow, + &.top-right .tooltip-arrow, + &.top .tooltip-arrow { + border-top-color: rgb(27 105 182); + } + + &.right .tooltip-arrow { + border-right-color: rgb(27 105 182); + } + + &.left .tooltip-arrow { + border-left-color: rgb(27 105 182); + } + + &.bottom-left .tooltip-arrow, + &.bottom-right .tooltip-arrow, + &.bottom .tooltip-arrow { + border-bottom-color: rgb(27 105 182); + } + } +} + +.kb-data-main-panel { + height: 100%; + margin-bottom: 5px; + max-height: 425px; +} + +#data-tabs { + height: 375px !important; +} + +.kb-ws-refresh-btn { + position: absolute; + right: 5px; +} + +#kb-ws .form-control { + margin: 3px; + width: 85%; +} + +.kb-data-control { + width: 95%; +} + +/* Data table */ + +.kb-data-table { + max-height: 265px; + width: 100% !important; + + thead { + max-width: 100%; + width: 281px; + } + + tbody tr { + border-bottom: 1px solid rgb(220 220 220); + + td { + font-size: 1em; + max-height: 16px; + max-width: 100px; + padding: 5px; + } + + &:hover, + td.highlighted { + background-color: rgb(244 245 214); + cursor: pointer; + } + } +} + +.kb-data-obj-name { + display: inline-block; + max-width: 88%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kb-data-loading { + height: 100%; + text-align: center; + vertical-align: middle; + + img { + vertical-align: middle; + } +} + +#kb-ws .dataTables_filter { + display: none; +} + +/* ------------------------------------------ + * View details on KBase objects + * + * Namespace: kb-data-obj + */ + +/* Button to activate the object pane */ + +button.kb-data-obj { + float: right; +} + +/* Panel */ + +.kb-data-obj-panel { + background: linear-gradient(180deg, rgb(0 210 255), rgb(178 173 203)); + margin: 10px; + padding: 0; + + button.close { + font-size: 125%; + margin: 5px; + } +} + +.kb-data-obj-panel-info { + background: rgb(255 255 255 / 0.5); + height: 100%; + padding: 10px; +} + +.kb-data-obj-panel-graph { + background: transparent; + height: 100%; + padding: 10px; + + table { + background-color: rgb(255 255 255 / 0.9); + border: 0; + color: rgb(0 0 0 / 0.8); + width: 100%; + } + + thead tr { + background-color: rgb(230 230 230); + color: rgb(0 0 0 / 1); + } +} + +.kb-data-obj-panel-info dl { + margin: 0; + + dt { + clear: left; + color: rgb(0 0 0 / 0.5); + float: left; + text-align: right; + width: 90px; + + &::after { + content: ':'; + } + } + + dd { + margin: 0 0 0 100px; + } +} + +.kb-data-obj-panel-verlist span { + background-color: rgb(255 255 255 / 0.5); + border-radius: 2px; + color: rgb(0 0 0 / 0.5); + cursor: pointer; + margin-left: 3px; + padding: 0 5px; + + &.selected { + background-color: use_color('mid-blue'); + color: rgb(255 255 255); + cursor: default; + } +} + +.kb-data-obj-graph-ref-to { + margin-top: 8px; +} + +/* + * END: View details on KBase objects + *------------------------------------------ + */ + +/* default blue button in the new style */ + +.kb-primary-btn { + background-color: use_color('mid-blue'); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(255 255 255); + font: normal 400 14px $typeface-page-text; + margin: 3px; + padding: 10px 20px; + + &:hover { + background-color: rgb(30 136 229); + } + + &[disabled] { + background-color: use_color('primary-lighter'); + box-shadow: none; + color: rgb(255 255 255); + } +} + +/* gray button in the new style */ + +.kb-default-btn { + background-color: rgb(245 245 245); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(85 85 85); + font: normal 400 14px $typeface-page-text; + margin: 3px; + padding: 10px 20px; + + &:hover { + background-color: rgb(206 206 206); + } + + &[disabled] { + box-shadow: none; + color: rgb(170 170 170); + } +} + +.kb-btn-sm { + font-size: 13px; + padding: 5px 10px; +} + +/* card layout */ +.narrative-card-logo { + display: flex; + font-size: 14px; + margin-right: 2px; + width: 50px; +} + +.narrative-card-row-main { + align-items: center; + display: flex; + margin: 0 2px; + padding: 0 4px; + width: 98%; +} + +.narrative-data-list-subcontent { + padding-left: 10px; +} + +.narrative-card-ellipsis { + align-items: center; + display: flex; + justify-content: center; + width: 20px; +} + +.narrative-card-palette-icon { + color: rgb(136 136 136); + left: 15px; + position: relative; + top: -50px; +} + +.narrative-card-action-button-wrapper { + height: 50px; + margin: 0 10px; + width: 80px; +} + +.narrative-card-action-button { + display: flex; + height: 80%; + justify-content: center; + padding: 10px 0; + width: calc(100% - 6px); + + > span { + margin-right: 5px; + } +} + +.narrative-card-action-button-name { + display: inline-block; +} + +.narrative-data-panel-btnToolbar { + display: flex; + justify-content: center; + + > span { + color: rgb(136 136 136); + } +} + +/* data list */ + +.kb-data-list-obj-row, +.narrative-card-row { + border-left: 5px solid rgb(255 255 255); + box-shadow: 1px 1px 1px 1px rgb(255 255 255); + color: rgb(51 51 51); + transition: all 0.1s ease; +} + +.kb-function-dim .kb-data-list-obj-row { + border-left-color: use_color('base-lightest'); +} + +.kb-data-list-obj-row:hover, +.narrative-card-row:hover { + border-left: 5px solid use_color('mid-green'); + box-shadow: 1px 1px 1px 1px rgb(170 170 170); +} + +.palette { + background: rgb(245 245 245) !important; +} + +.kb-data-list-obj-row-selected { + border-left: 5px solid use_color('mid-green'); + box-shadow: 1px 1px 1px 1px rgb(170 170 170); +} + +.kb-data-list-obj-row-main { + margin: 0; + padding: 0; + width: 100%; +} + +/* again the css hover over div inherits strangly, so we use js */ + +.kb-data-list-nav-buttons { + border: 0; + box-shadow: none; + font-size: 18px; + margin: 0; + padding: 5px 8px; + text-shadow: none !important; +} + +.kb-data-list-add-data-button { + background-color: rgb(67 121 177); + border: 0; + border-radius: 50%; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(255 255 255); + cursor: pointer; + height: 40px; + text-align: center; + width: 40px; +} + +/* specialize buttons that hover over narrative */ + +#kb-add-code-cell, +#kb-add-md-cell { + background-color: use_color('mid-blue'); + border-radius: 50%; + bottom: 15px; + box-shadow: 2px 2px 1px rgb(206 206 206); + color: rgb(255 255 255); + cursor: pointer; + height: 40px; + opacity: 0.5; + padding-top: 5px; + position: fixed; + width: 40px; + z-index: 5; +} + +#kb-add-code-cell { + padding-left: 7px; + right: 75px; +} + +#kb-add-md-cell { + margin-right: 20px; + padding-left: 8px; + right: 10px; +} + +#kb-add-code-cell:hover, +#kb-add-md-cell:hover, +.kb-data-list-add-data-button:hover { + background-color: rgb(54 97 142); + cursor: pointer; + opacity: 1; +} + +.kb-data-list-add-data-text-button { + background-color: rgb(67 121 177); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(255 255 255); + font: normal 400 14px $typeface-page-text; + margin: 3px; + padding: 10px 20px; + + &:hover { + background-color: rgb(54 97 142); + } +} + +/* buttons */ + +.kb-data-list-btn { + background-color: use_color('mid-blue'); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(255 255 255); + font-size: 13px; + margin: 5px; + padding: 5px 10px; + + &:hover { + background-color: rgb(30 136 229); + } + + &[disabled] { + background-color: use_color('primary-lighter'); + box-shadow: none; + color: rgb(255 255 255); + } +} + +.kb-data-list-cancel-btn { + background-color: rgb(245 245 245); + border: 0; + border-radius: 1px; + box-shadow: 1px 1px 3px rgb(170 170 170); + color: rgb(136 136 136); + font-size: 13px; + margin: 5px; + padding: 5px 10px; + + &:hover { + background-color: rgb(206 206 206); + } +} + +.kb-data-list-row-hr { + margin: 2px auto; + width: 70%; +} + +.kb-data-list-info { + border-bottom: 1px solid rgb(224 224 224); + min-height: 60px; + padding: 6px 0; + width: 80%; +} + +.kb-data-list-name { + color: use_color('primary-vivid'); + cursor: pointer; + font: normal 700 15px $typeface-page-text; + margin: 2px; + + &:hover { + text-decoration: underline; + } +} + +.kb-data-list-version { + color: rgb(119 119 119); + cursor: default; + font-size: 12px; + font-style: italic; + white-space: nowrap; +} + +.kb-data-list-date, +.kb-data-list-type { + color: rgb(119 119 119); + cursor: default; + font-size: 13px; + margin: 2px; +} + +.kb-data-list-date { + white-space: nowrap; +} + +.kb-data-list-edit-by { + color: rgb(119 119 119); + cursor: pointer; + font-size: 13px; + margin: 2px 2px 2px 0; + white-space: nowrap; + + &:hover { + color: rgb(68 68 68); + } +} + +.kb-data-list-narinfo { + color: rgb(119 119 119); + font-size: 13px; + margin: 2px 2px 2px 10px; +} + +.kb-data-list-narrative-error { + color: rgb(244 67 54); + font-size: 13px; + margin: 2px; +} + +.kb-data-list-narrative { + color: rgb(119 119 119); + font-size: 13px; + margin: 2px 2px 2px 10px; +} + +.kb-data-list-more { + color: rgb(119 119 119); + font-size: 11px; + margin: 0 0 0 15px; + padding: 0; + white-space: nowrap; +} + +.kb-data-list-more-btn { + border: 0; + box-shadow: none; + font-size: 12px; + margin: 0; + padding: 2px 4px; + text-shadow: none !important; +} + +.kb-data-list-more-div { + align-items: center; + color: rgb(119 119 119); + display: flex; + flex-direction: column; + font-size: 13px; + margin: 2px; + + td, + th { + color: rgb(119 119 119); + font-size: 13px; + margin: 2px; + } + + tr { + color: rgb(119 119 119); + font-size: 13px; + margin: 2px; + + &:hover { + background-color: use_color('base-lightest'); + } + + &:nth-child(odd) { + background-color: rgb(245 245 245); + } + + &:hover:nth-child(odd) { + background-color: use_color('base-lightest'); + } + } + + th { + font-weight: 700; + text-align: right; + } + + td { + padding-left: 8px; + text-align: left; + word-break: break-all; + } +} + +.kb-data-list-job-name:hover { + text-decoration: none; +} + +/* Indent nested box */ + +.kb-data-list-box > .kb-data-list-box { + margin-left: 40px; +} + +/* Control buttons */ + +.kb-data-list-ctl { + span.inviso { + background-color: rgb(255 255 255); + + /* like mayo on wonderbread */ + color: rgb(255 255 255); + text-shadow: none; + } + + &[enabled] { + background-color: rgb(211 211 211); + } +} + +/* end data list */ + +/* APP CATALOG SLIDEOUT BROWSER STYLES */ + +.kbcb-browser-container { + margin: 0 auto; + + /* background: #eeeded; */ +} + +.kbcb-back-link { + padding: 0.4em; +} + +.kbcb-ctr-toolbar { + margin: 0.4em; +} + +.kbcb-main-panel-div { + margin: 0; +} + +.kbcb-loading-panel-div { + margin: 0.1em; + text-align: center; + vertical-align: middle; +} + +/* when the entire app card is a link, use this style */ + +.kbcb-app-card-link { + text-decoration: none; +} + +.kbcb-app-card-list-container { + overflow: auto; + padding: 0 0.2em 1em; +} + +.kbcb-app-card-container { + display: block; + float: left; + height: 110px; + margin: 0.4em; + position: relative; + width: 300px; +} + +.kbcb-app-card { + background: rgb(241 241 245); + border-radius: 2px; + color: use_color('base'); + height: 100%; + overflow: hidden; + text-align: center; + vertical-align: middle; + width: 100%; +} + +.kbcb-app-card-header { + max-height: 90px; + overflow: hidden; + text-align: left; +} + +.kbcb-app-card-title-panel { + padding: 10px 5px 5px 10px; +} + +.kbcb-app-card-title { + font-size: 1.1em; +} + +.kbcb-app-card-authors, +.kbcb-app-card-module { + font-size: 0.8em; +} + +.kbcb-app-card-subtitle { + font-size: 0.9em; + overflow: hidden; + padding: 10px; +} + +.kbcb-app-card-footer { + bottom: 0; + color: rgb(136 136 136); + font-size: 0.9em; + left: 0; + margin: 0 5px 5px; + position: absolute; + width: 100%; +} + +.kbcb-app-card-logo { + font-size: 14px; + margin: 0; + padding: 0; +} + +.kbcb-star-default { + color: rgb(136 136 136); +} + +.kbcb-star-favorite { + color: rgb(255 165 0); +} + +.kbcb-star-default, +.kbcb-star-favorite, +.kbcb-star-nonfavorite { + cursor: pointer; +} + +.kbcb-star-default:hover, +.kbcb-star-nonfavorite:hover { + color: rgb(255 0 0); +} + +.kbcb-star-favorite:hover { + color: rgb(255 165 0); +} + +.kbcb-run-count, +.kbcb-star-count { + display: inline-block; + margin-left: 0.3em; +} + +.kbcb-info:hover { + color: rgb(255 165 0); +} + +.kbcb-hover { + box-shadow: 0 1px 3px rgb(0 0 0 / 0.12), 0 1px 2px rgb(0 0 0 / 0.24); + transition: all 0.2s ease-in-out; + + &:hover { + box-shadow: 0 10px 20px rgb(0 0 0 / 0.19), 0 6px 6px rgb(0 0 0 / 0.23); + cursor: pointer; + } +} + +/* styles for App page */ + +.kbcb-app-page { + background: rgb(241 241 245); + border-radius: 2px; + color: use_color('base'); + text-align: center; +} + +.kbcb-app-page-header { + text-align: left; +} + +.kbcb-app-page-title-panel { + padding: 10px 5px 5px 0; +} + +.kbcb-app-page-title { + font-size: 2em; +} + +.kbcb-app-page-module { + font-size: 1.2em; +} + +.kbcb-app-page-authors { + font-size: 1em; + margin: 0.3em 0 0; +} + +.kbcb-app-page-subtitle { + font-size: 1.2em; + padding: 1.5em 2em 0; +} + +.kbcb-app-page-stats-bar { + font-size: 1.2em; + padding: 0 2em 0.8em 3em; + width: 100%; +} + +.kbcb-app-page-logo { + margin: 1em; + padding: 0; + text-align: center; +} + +/* END CATALOG SLIDEOUT BROWSER STYLES */ + +/* share panel styles */ + +.kb-share-user-permissions-dropdown { + border: 0; + box-shadow: none; + height: auto; + padding: 2px; + text-shadow: none !important; + width: auto; +} + +.kb-share-select { + width: 200px; + + .select2-selection__choice { + background-color: rgb(245 245 245) !important; + width: 100% !important; + } +} + +/* hack to make the selection fill the width */ + +.kb-nar-manager-titles { + color: rgb(119 119 119); + font: normal 700 19px $typeface-page-text; + margin: 20px 10px 10px; +} + +/* End kbaseData.css */ + +/* styling for data import overlay */ + +.kb-import-content { + display: block; + margin: 0; + position: relative; + + .btn-xs { + border: 0; + } +} + +.ftp-file-header { + margin: 1rem 0; +} + +.globus-link:hover { + text-decoration: none; +} + +.kb-import-content .upload-options button { + background: rgb(255 255 255); + border: 1px solid use-color('silver'); + border-radius: 6px; + box-sizing: border-box; + color: rgb(0 0 0); + font: normal 400 14px/18px $typeface-page-text; + height: 42px; + margin: 0 6px; + + /* identical to box height */ + text-align: center; + width: 140px; + + &:hover { + background-color: rgb(250 250 250); + border: 1px solid rgb(207 207 207); + } + + &:focus { + background-color: rgb(242 242 242); + } + + &:active { + background-color: rgb(230 230 230); + } + + &:disabled { + background-color: rgb(250 250 250); + color: rgb(146 146 146); + } +} + +.kb-overlay-footer { + bottom: 0; + height: 50px; + position: absolute; + width: 100%; + + .btn-default, + .btn-primary { + margin: 15px 15px 0 0; + } +} + +.kb-import-filter, +.kb-import-search { + margin: 10px 40px 10px 10px; +} + +.kb-import-item { + margin: 0 20px 0 5px; + padding: 20px 0 0; + + &:hover { + background-color: rgb(248 248 248); + box-shadow: 0 0 2px rgb(170 170 170); + } + + hr { + border: 2px solid rgb(234 234 234); + margin-bottom: 0; + margin-top: 15px; + width: 85%; + } + + .kb-import-checkbox { + font-size: 1.5em; + opacity: 0.4; + padding: 15px 20px; + } + + .fa-check-square-o { + opacity: 1 !important; + } +} + +.kb-import-info { + display: inline-block; + font-weight: 400; + margin: 10px 20px 0 0; + + span { + color: use_color('base-light'); + font-size: 0.9em; + font-weight: 700; + } +} + +.kb-import-status { + margin: 20px; +} + +/* Error dialog */ + +.kb-error-dialog strong { + color: rgb(0 100 0); +} + +.kb-err-text { + color: rgb(0 0 0); + font-family: $typeface-fixed-width; +} + +.kb-err-warn { + color: rgb(178 34 34); + font-style: italic; + margin-top: 0.5em; +} + +/* View-only (read-only) */ + +#kb-ro-btn { + margin-right: 20px; +} + +#kb-view-mode { + cursor: pointer; + display: inline-block; + font-size: 17px; + margin: -15px 0 0 1em; + padding: 0; +} + +#kb-view-mode-narr div { + color: use_color('mid-blue'); + display: inline-block; + font: normal 700 16px $typeface-page-text; + margin-top: -3px; + padding: 3px; + position: fixed; + text-align: left; + width: 147px; +} + +/* little space for control-close icon */ + +#kb-view-mode-narr-hide { + background-color: use_color('mid-blue'); + color: use_color('primary-lighter'); + cursor: pointer; + display: block; + float: left; + font-size: 16px; + height: 41px; + padding-left: 5px; + width: 15px; + + span { + margin-top: 10px; + } +} + +/* this is the banner for readonly mode */ + +.navbar-right div.label-warning { + font: normal 700 16px $typeface-page-text; +} + +/* Override the default max-width of the narrative box */ + +/* Styles for KNHX trees to remove its document.write nonsense that breaks asynchronous loading */ + +#popdiv { + background-color: rgb(252 252 252); + border: 1px solid use_color('base-lighter'); + font-size: 12px; + position: absolute; + visibility: hidden; + z-index: 10000; + + a.alt { + border: 0; + display: inline; + font: 12px $typeface-fixed-width; + padding-left: 9px; + } +} + +.kb-loading-spinner { + padding: $padding-large-vertical; + text-align: center; +} + +// specific font awesome size hack +.fa.fa-14-pt { + font-size: 19px; +} diff --git a/kbase-extension/scss/partials/_narrativeOutline.scss b/kbase-extension/scss/partials/_narrativeOutline.scss new file mode 100644 index 0000000000..863c0983ba --- /dev/null +++ b/kbase-extension/scss/partials/_narrativeOutline.scss @@ -0,0 +1,113 @@ +$kb-narr-outline: 2px solid use_color('base-lighter'); + +.kb-narr-outline { + padding: 0.5em 0.5em 0.5em 0.25em; + + ul { + list-style: none; + padding: 0; + position: relative; + + ul { + padding-left: 1.5em; + } + + li:not(:last-of-type)::before { + border-left: $kb-narr-outline; + content: ''; + height: calc(100% + 0.25em); + left: -0.5em; + position: absolute; + top: 0; + } + } + + li { + position: relative; + } + + &__item { + align-items: center; + cursor: pointer; + display: flex; + flex-flow: row nowrap; + justify-content: left; + left: 0; + padding: 0.1em; + padding-bottom: 0.3em; + position: relative; + top: 0; + + &::before { + background-color: transparent; + border-radius: 0.333em; + border: 1px solid transparent; + bottom: 0.2em; + content: ""; + position: absolute; + top: 0; + transition: background-color 100ms linear, border 100ms linear; + width: 100%; + z-index: -1; + } + + &::after { + border-bottom: $kb-narr-outline; + border-left: $kb-narr-outline; + bottom: calc(50% + 1px); + content: ""; + height: calc(50% - 0.25em); + left: -0.5em; + position: absolute; + width: 0.5em; + } + + &-content { + display: block; + flex-grow: 0; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-icon { + display: inline-block; + transform: scale(0.5); + transform-origin: top left; + + &-wrapper { + border-radius: 10px; + display: block; + flex-grow: 0; + flex-shrink: 0; + height: 2em; + margin-left: 0.1em; + margin-right: 0.5em; + max-width: 2em; + overflow: hidden; + } + } + + &--highlight::before { + background-color: use_color('base-lightest'); + border: 1px solid use_color('base-light'); + } + + &--highlight-selected::before { + background-color: use_rgba_color('mid-green', 0.15); + border: 1px solid use_color('mid-green'); + } + } +} + +.kb-narr-outline ul ul .kb-narr-outline__item::after { + border-bottom: $kb-narr-outline; + border-left: $kb-narr-outline; + bottom: calc(50% + 1px); + content: ''; + height: calc(50% - 0.25em); + left: -0.5em; + position: absolute; + width: 0.5em; +} diff --git a/kbase-extension/scss/partials/_notify.scss b/kbase-extension/scss/partials/_notify.scss new file mode 100644 index 0000000000..6fb84c641d --- /dev/null +++ b/kbase-extension/scss/partials/_notify.scss @@ -0,0 +1,13 @@ +.kb-notify { + background-color: rgb(0 0 0); + bottom: 10px; + color: rgb(255 255 255); + opacity: 0.8; + padding: 15px; + position: relative; + width: 300px; + + &.kb-notify-success { + color: rgb(0 204 9); + } +} diff --git a/kbase-extension/scss/partials/_rcp.scss b/kbase-extension/scss/partials/_rcp.scss new file mode 100644 index 0000000000..f817fb3a2b --- /dev/null +++ b/kbase-extension/scss/partials/_rcp.scss @@ -0,0 +1,79 @@ +// run-control-panel +$rcp-height: 50px; + +.kb-rcp { + &__layout_div { + border-top: 1px solid use-color('silver'); + display: flex; + flex-direction: row; + height: $rcp-height; + position: relative; + } + + &__toolbar { + flex: none; + height: $rcp-height; + line-height: $rcp-height; + } + + &__btn-toolbar { + display: inline-block; + line-height: $rcp-height; + padding-right: 0.5rem; + vertical-align: bottom; + } + + // .kb-rcp__action-button + &__action-button { + float: left; + margin: 6px; + width: 8rem; + + // .kb-rcp__action-button-container + &-container { + flex: none; + height: 50px; + line-height: 50px; + overflow: hidden; + } + } + + &-status { + &__container { + @include ellipsis-overflow; + + flex: 1 1 auto; + font-size: 14px; + line-height: $rcp-height; + margin: 0 1rem; + } + + &__fsm_display { + background: use-color('white'); + display: inline-block; + font: 12px/1 $typeface-page-text; + margin-top: -30px; + padding: 6px 10px; + position: absolute; + top: 0; + } + } + + // .kb-rcp__tab-button + &__tab-button { + float: left; + margin-left: 0.5rem; + } +} +// .kb-rcp__tab-button--outdated +a.kb-rcp__tab-button--outdated { + color: use-color('warning'); + float: left; + margin-left: 0.5rem; + padding: 6px 0 0; + + &:hover, + &:active { + color: use-color('warning-dark'); + } +} diff --git a/kbase-extension/scss/partials/_reportViewer.scss b/kbase-extension/scss/partials/_reportViewer.scss new file mode 100644 index 0000000000..5b5fa4ff9d --- /dev/null +++ b/kbase-extension/scss/partials/_reportViewer.scss @@ -0,0 +1,82 @@ +.kb-report-view { + &__warning { + &__count { + margin: 5px; + } + + &__container { + margin: 0 5px 5px 10px; + max-height: 100px; + overflow-y: auto; + } + + &__text { + margin: 0 5px 5px 10px; + } + } + + &__summary { + @include fixed-width; + + color: use_color('base-dark'); + height: auto; + max-height: 500px; + overflow: auto; + white-space: pre-wrap; + width: 100%; + } + + &__download-iframe { + display: none; + } + + &__download_button { + cursor: pointer; + } + + &__report_iframe { + display: block; + height: auto; + margin: 0; + padding: 0; + width: 100%; + } + + &__report_button { + margin: 4px 4px 8px 0; + } +} + +.kb-output-widget { + // .kb-output-widget__object_link + &__object_link { + cursor: pointer; + } + // .kb-output-widget__table + &__table.dataTable { + width: 100%; + } +} + +.kb-report { + &__container { + margin: -1.5rem; + } + + &__item_container { + padding: 0 1.5rem; + border-bottom: 1px solid use-color('base-lighter'); + } + + + &__item_toggle { + @include font-h4; + padding: 0.5rem 0; + margin: 0 -1rem; + + color: use-color('blue'); + &:hover { + color: use-color('blue-hlink'); + } + } +} diff --git a/kbase-extension/scss/partials/_select2.scss b/kbase-extension/scss/partials/_select2.scss new file mode 100644 index 0000000000..73ed1d166c --- /dev/null +++ b/kbase-extension/scss/partials/_select2.scss @@ -0,0 +1,37 @@ +.kb-select2 { + &-taxonomy-ref, + &-object-input { + &__item { + @include base-text; + + display: block; + height: $line-height-px; + } + } + + &-object-input { + &__object { + @include base-text; + + word-wrap: break-word; + + &_name { + font-weight: 700; + } + + &_type { + font-style: italic; + } + + &_details { + margin-left: 1rem; + } + + &_type, + &_narrative, + &_updated { + display: block; + } + } + } +} diff --git a/kbase-extension/scss/partials/_stagingTable.scss b/kbase-extension/scss/partials/_stagingTable.scss new file mode 100644 index 0000000000..28a2aab12c --- /dev/null +++ b/kbase-extension/scss/partials/_stagingTable.scss @@ -0,0 +1,310 @@ +.kb-staging-table { + table-layout: fixed; + + .kb-staging-table-body td { + vertical-align: middle; + } + + .kb-staging-table-header { + // Overriding default datatables sort icons so they come + // before the header title rather than after + .sorting::before, + .sorting_asc::before, + .sorting_desc::before { + content: '\e150'; + display: inline-block; + font-family: $typeface-glyphicons; + opacity: 0.2; + position: relative; + right: 4px; + top: 1px; + } + + .sorting_asc::before { + content: '\e155'; + } + + .sorting_desc::before { + content: '\e156'; + } + + .sorting::after, + .sorting_asc::after, + .sorting_desc::after { + display: none; + } + + .kb-staging-table-header { + // .kb-staging-table-header__checkbox + &__checkbox { + padding-right: 0; + width: 2rem; + } + + // .kb-staging-table-header__file + &__file { + padding-right: 0; + width: 4rem; + + // Special positioning for folder sort icon as there is no title + &.sorting::before, + &.sorting_asc::before, + &.sorting_desc::before { + left: 9px; + } + } + + // .kb-staging-table-header__age, + // .kb-staging-table-header__size + &__age, + &__size { + width: 7rem; + } + + // .kb-staging-table-header__import + &__import { + width: 24rem; + } + } + } + + // .kb-staging-table-body + &-body { + // .kb-staging-table-body__cell--expander + &__cell--expander { + text-align: center; + } + + // .kb-staging-table-body__cell--import + &__cell--import { + text-align: right; + white-space: nowrap; + } + + &__button { + // .kb-staging-table-body__decompress + &--decompress { + @extend %kbase-button-sm; + + @include button-size(0.2rem, 0.2rem, 12px, 12px, 0); + + background: transparent; + } + + // .kb-staging-table-body__button--download + &--download { + @extend %kbase-button; + + @include button-variant(use_color('white'), use_color('primary'), transparent); + @include button-size(0.5rem, 0.5rem, 12px, 12px, 0); + + margin-left: 0.5rem; + } + + // .kb-staging-table-body__button--delete + // .kb-staging-table-body__button--folder + &--delete, + &--folder { + @extend %kbase-button; + + @include button-variant(use_color('primary'), transparent, transparent); + @include button-size(0.5rem, 0.5rem, 18px, 12px, 0); + + &:active, + &:hover { + color: use_color('primary-vivid'); + } + } + } + + // .kb-staging-table-body__name + &__name { + @include ellipsis-overflow; + } + + // .kb-staging-table-body__folder:hover + &__folder:hover { + cursor: pointer; + text-decoration: underline; + } + + // kb-staging-table-body__select_container + &__select_container { + align-items: center; + display: flex; + justify-content: flex-end; + text-align: left; + } + + // style for Import As dropdown container + // set the width here + // otherwise it will get overriden by datatables when changing the sort order + // it gets set directly on the element via inline css so we have to use important to override + .select2 { + margin: 4px 0; + width: 19rem !important; + } + + // style for Import As dropdown container text + // .kb-staging-table-body .kb-staging-table-body__import-dropdown + .kb-staging-table-body__import-dropdown { + align-items: center; + background: use_color('white'); + border: 1px solid use_color('error-dark'); + border-radius: 4px; + box-sizing: border-box; + display: flex; + height: 38px; + + // style for Import As dropdown container text + .select2-selection { + // .kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__rendered + &__rendered { + color: use_color('ink'); + flex: 1 1 auto; + font-size: 14px; + line-height: 20px; + padding: 8px 20px 8px 8px; + } + + // .kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__placeholder + &__placeholder { + color: use_color('error-dark'); + } + + // style for Import As dropdown container arrow + // .kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__arrow + &__arrow { + margin-top: 4px; + + b { + border-color: use_color('error-dark') transparent transparent; + } + } + } + + &.kb-staging-table-body__import-type-selected, + &:focus { + .select2-selection__arrow b { + border-color: use_color('grey') transparent transparent; + } + } + // Import as dropdown placeholder text is red + // Change placeholder color when user clicks in + .select2-container--focus .select2-selection__placeholder, + .select2-container--open .select2-selection__placeholder { + color: use-color('ink'); + } + + // When user clicks into the dropdown or chooses a selection box, change border color + &.kb-staging-table-body__import-type-selected { + border: 1px solid use-color('silver'); + } + } + // When user clicks into the dropdown or chooses a selection box, change border color + .select2-container--focus .kb-staging-table-body__import-dropdown, + .select2-container--open .kb-staging-table-body__import-dropdown { + border: 1px solid use-color('silver'); + } + } + + // .kb-staging-table-file-metadata + &-file-metadata { + .tab-pane { + margin: 1rem; + } + + // .kb-staging-table-file-metadata__def_list + &__def_list { + display: flex; + flex-wrap: wrap; + margin: 1rem; + padding: 0; + width: 100%; + } + + // .kb-staging-table-file-metadata__term + &__term { + font-weight: 700; + padding-top: 0.5rem; + text-align: right; + width: 9rem; + } + + // .kb-staging-table-file-metadata__def + &__def { + margin-left: 0; + padding-left: 1rem; + padding-top: 0.5rem; + width: calc(100% - 9rem); + } + + &__list { + list-style-type: none; + } + + // .kb-data-staging-file-metadata__file-lines + &__file_lines { + @include code-block; + + max-height: 200px; + overflow: scroll; + white-space: pre; + } + } + + &__notice { + background-color: use_color('base-lightest'); + font: normal 700 16px/20px $typeface-page-text; + padding: 1rem; + text-align: center; + } + + // container for the 'Import selected' button and tooltip + // .kb-staging-table-import + &-import { + &__button { + @extend %kbase-button; + + @include button-variant(use_color('white'), use_color('primary'), transparent); + @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-base, $line-height-large, $btn-border-radius-large); + + bottom: -50px; + position: absolute; + right: 10px; + text-transform: none; + + &[disabled] { + background-color: use_color('info'); + cursor: not-allowed; + opacity: 1; + + &:active, + &:hover, + &:focus { + background-color: use_color('info'); + } + + &:focus { + outline: none; + } + } + } + + &__tooltip.tooltip { + background: use_color('white'); + border-radius: 4px; + box-shadow: 2px 4px 6px rgb(0 0 0 / 0.15); + + .tooltip-inner { + align-items: center; + background: use_color('white'); + color: use_color('black'); + display: flex; + font: normal 400 14px/16px $typeface-page-text; + height: 6rem; + width: 20rem; + } + } + } +} diff --git a/kbase-extension/scss/partials/_stylesheet.scss b/kbase-extension/scss/partials/_stylesheet.scss new file mode 100644 index 0000000000..d83c9606d3 --- /dev/null +++ b/kbase-extension/scss/partials/_stylesheet.scss @@ -0,0 +1,93 @@ + +body { + background-color: use-color('white'); + color: use-color('ink'); + + // default body font size: 14px + font: 1.4em $typeface-page-text; +} + +h1 { + color: rgb(35 35 35); + font: 400 1.75em/2em $typeface-display-legacy; + letter-spacing: 0; +} + +h2 { + color: rgb(137 137 137); + font: normal 400 1.5em/1.75em $typeface-display-legacy; + letter-spacing: 0; +} + +h3 { + font: normal 400 1.15em/1.25em $typeface-display-legacy; + letter-spacing: 0; +} + +h4 { + font: italic 400 1.15em/1.25em $typeface-page-text; + letter-spacing: 0.07em; +} + +b, +strong { + font-family: $typeface-page-text; + font-weight: 700; +} + +b i { + font-family: $typeface-page-text; + font-style: italic; +} + +em { + font-family: $typeface-page-text; + font-style: italic; + + strong { + font-family: $typeface-page-text; + font-style: italic; + } +} + +i { + font-family: $typeface-page-text; + font-style: italic; + + b { + font-family: $typeface-page-text; + font-style: italic; + } +} + +strong em { + font-family: $typeface-page-text; + font-style: italic; +} + +b i, +em strong, +i b, +strong em { + font-weight: 700; +} + +a { + color: use-color('blue'); + text-decoration: none; + + &:hover, + &:visited { + color: use-color('blue-hlink'); + } + + &:hover { + text-decoration: underline; + } +} + +code, +pre { + font-family: $typeface-fixed-width; + font-style: normal; +} diff --git a/kbase-extension/scss/partials/_tour.scss b/kbase-extension/scss/partials/_tour.scss new file mode 100644 index 0000000000..b4cddc2d00 --- /dev/null +++ b/kbase-extension/scss/partials/_tour.scss @@ -0,0 +1,40 @@ +.kb-tour { + background-color: use_color('mid-blue'); + + &.popover { + &.top > .arrow::after { + border-top-color: use_color('mid-blue'); + } + + &.bottom > .arrow::after { + border-bottom-color: use_color('mid-blue'); + } + + &.right > .arrow::after { + border-right-color: use_color('mid-blue'); + } + + &.left > .arrow::after { + border-left-color: use_color('mid-blue'); + } + } + + > { + h3.popover-title { + background-color: use_color('mid-blue'); + color: use_color('white'); + font: 16px $typeface-page-text; + } + + .popover-content, + .popover-navigation { + background-color: use_color('base-lightest'); + } + + .close-btn { + position: absolute; + right: 7px; + top: 7px; + } + } +} diff --git a/kbase-extension/scss/partials/_ui.scss b/kbase-extension/scss/partials/_ui.scss new file mode 100644 index 0000000000..5f7b08f896 --- /dev/null +++ b/kbase-extension/scss/partials/_ui.scss @@ -0,0 +1,13 @@ +// styles extracted from elements genenrated by common/ui.js + +.kb-ui { + &__icon, + &__button_label { + vertical-align: middle; + } + + &__text--na { + color: use-color('orange'); + font-style: italic; + } +} diff --git a/kbase-extension/scss/partials/_userMenu.scss b/kbase-extension/scss/partials/_userMenu.scss new file mode 100644 index 0000000000..780ec28757 --- /dev/null +++ b/kbase-extension/scss/partials/_userMenu.scss @@ -0,0 +1,56 @@ +.kb-user-menu { + // .kb-user-menu__menu + &__menu { + display: inline-block; + // .kb-user-menu__menu_caret + // .caret required to override bootstrap + &_caret.caret { + margin-left: 5px; + } + } + + &__icon { + // .kb-user-menu__icon--gravatar + &--gravatar { + width: 40px; + } + // .kb-user-menu__icon--signout + // .kb-user-menu__icon--user + &--signout, + &--user { + // .fa required to override fontawesome font sizing + &.fa { + font-size: 150%; + margin-right: 10px; + } + } + } + + &__block { + // .kb-user-menu__block--name + &--name { + display: inline-block; + } + // .kb-user-menu__block--userName + // .kb-user-menu__block--displayName + &--userName, + &--displayName { + display: block; + } + // .kb-user-menu__block--displayName + &--displayName { + font-style: italic; + } + // .kb-user-menu__block--signout-icon + // .kb-user-menu__block--user-icon + &--signout-icon, + &--user-icon { + display: inline-block; + width: 34px; + } + // .kb-user-menu__block--user-icon + &--user-icon { + vertical-align: top; + } + } +} diff --git a/kbase-extension/static/buildTools/loadAppWidgets.js b/kbase-extension/static/buildTools/loadAppWidgets.js index 27290639bb..07f19f6bfe 100644 --- a/kbase-extension/static/buildTools/loadAppWidgets.js +++ b/kbase-extension/static/buildTools/loadAppWidgets.js @@ -25,8 +25,6 @@ define([ 'widgets/appWidgets2/input/floatInput', 'widgets/appWidgets2/input/intInput', 'widgets/appWidgets2/input/newObjectInput', - // 'widgets/appWidgets2/input/objectInput', - // 'widgets/appWidgets2/input/objectRefInput', 'widgets/appWidgets2/input/select2ObjectInput', 'widgets/appWidgets2/input/selectInput', 'widgets/appWidgets2/input/sequenceInput', @@ -42,7 +40,6 @@ define([ 'widgets/appWidgets2/subdataMethods/manager', 'widgets/appWidgets2/subdataMethods/sampleProperty', 'widgets/appWidgets2/subdataMethods/samplePropertyHistogram', - 'widgets/appWidgets2/validators/common', 'widgets/appWidgets2/validators/customSubdata', 'widgets/appWidgets2/validators/float', 'widgets/appWidgets2/validators/int', @@ -73,12 +70,10 @@ define([ 'widgets/appWidgets2/view/textView', 'widgets/appWidgets2/view/toggleButtonView', 'widgets/appWidgets2/view/undefinedView', - 'common/appUtils', 'common/busEventManager', 'common/cellUtils', 'common/clock', 'common/data', - 'common/dom', 'common/error', 'common/events', 'common/format', @@ -86,10 +81,7 @@ define([ 'common/html', 'common/jobs', 'common/jupyter', - 'common/lang', 'common/messages', - 'common/microBus', - 'common/miniBus', 'common/monoBus', 'common/parameterSpec', 'common/props', @@ -98,11 +90,11 @@ define([ 'common/sdk', 'common/semaphore', 'common/spec', - 'common/specValidation', 'common/ui', - 'common/unodep', - 'common/utils', 'common/validation', + 'util/util', + 'util/icon', ], () => { + 'use strict'; return; }); diff --git a/kbase-extension/static/errorMain.js b/kbase-extension/static/errorMain.js index d44ee8823f..7e03d94201 100644 --- a/kbase-extension/static/errorMain.js +++ b/kbase-extension/static/errorMain.js @@ -6,7 +6,6 @@ require(['./narrative_paths'], () => { 'kb_common/jsonRpc/dynamicServiceClient', 'narrativeConfig', 'narrativeLogin', - 'css!font-awesome', ], ($, Promise, DynamicServiceClient, Config, Login) => { Config.updateConfig() .then(() => { diff --git a/kbase-extension/static/kbase/config/staging_upload.json b/kbase-extension/static/kbase/config/staging_upload.json index f2e5f71e77..32873013fc 100644 --- a/kbase-extension/static/kbase/config/staging_upload.json +++ b/kbase-extension/static/kbase/config/staging_upload.json @@ -1,13 +1,17 @@ { "dropdown_order": [ - { - "id": "fastq_reads", - "name": "FASTQ Reads" - }, { "id": "sra_reads", "name": "SRA Reads" }, + { + "id": "fastq_reads_interleaved", + "name": "FASTQ Reads Interleaved" + }, + { + "id": "fastq_reads_noninterleaved", + "name": "FASTQ Reads NonInterleaved" + }, { "id": "genbank_genome", "name": "GenBank Genome" @@ -39,11 +43,24 @@ { "id": "phenotype_set", "name": "Phenotype Set" - },{ + }, + { "id": "sample_set", "name": "Sample Set" + }, + { + "id": "decompress", + "name": "Decompress/Unpack" } ], + "bulk_import_types": [ + "sra_reads", + "fastq_reads_interleaved", + "fastq_reads_noninterleaved", + "gff_metagenome", + "assembly", + "genbank_genome" + ], "app_info": { "web_upload": { "app_id": "kb_uploadmethods/upload_web_file" @@ -66,15 +83,6 @@ "app_output_suffix": "_reads", "multiselect": false }, - "sra_reads": { - "app_id": "kb_uploadmethods/import_fastq_sra_as_reads_from_staging", - "app_input_param": "sra_staging_file_name", - "app_input_param_type": "string", - "app_static_params": {"import_type": "SRA"}, - "app_output_param": "name", - "app_output_suffix": "_reads", - "multiselect": false - }, "genbank_genome": { "app_id": "kb_uploadmethods/import_genbank_as_genome_from_staging", "app_input_param": "staging_file_subdir_path", @@ -155,6 +163,42 @@ "app_output_param": "set_name", "app_output_suffix": "_sample_set", "multiselect": false + }, + "decompress": { + "app_id": "kb_uploadmethods/unpack_staging_file", + "app_input_param": "staging_file_subdir_path", + "app_input_param_type": "string", + "app_static_params": {}, + "app_output_param": "", + "app_output_suffix": "", + "multiselect": false + }, + "sra_reads": { + "app_id": "kb_uploadmethods/import_sra_as_reads_from_staging", + "app_input_param": "sra_staging_file_name", + "app_input_param_type": "string", + "app_static_params": {"import_type": "SRA"}, + "app_output_param": "name", + "app_output_suffix": "_reads", + "multiselect": false + }, + "fastq_reads_interleaved": { + "app_id": "kb_uploadmethods/import_fastq_interleaved_as_reads_from_staging", + "app_input_param": "fastq_fwd_staging_file_name", + "app_input_param_type": "string", + "app_static_params": {"import_type": "FASTQ/FASTA"}, + "app_output_param": "name", + "app_output_suffix": "_reads", + "multiselect": false + }, + "fastq_reads_noninterleaved": { + "app_id": "kb_uploadmethods/import_fastq_noninterleaved_as_reads_from_staging", + "app_input_param": "fastq_fwd_staging_file_name", + "app_input_param_type": "string", + "app_static_params": {"import_type": "FASTQ/FASTA"}, + "app_output_param": "name", + "app_output_suffix": "_reads", + "multiselect": true } } } diff --git a/kbase-extension/static/kbase/css/all_concat.css b/kbase-extension/static/kbase/css/all_concat.css new file mode 100644 index 0000000000..20ae211189 --- /dev/null +++ b/kbase-extension/static/kbase/css/all_concat.css @@ -0,0 +1 @@ +@charset "UTF-8";@font-face{font-family:Glyphicons Halflings;src:url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot);src:url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff) format("woff"),url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}@font-face{font-family:kbase-icons;font-style:normal;font-weight:400;src:url(../fonts/kbase-icons.eot);src:url(../fonts/kbase-icons.eot?#iefix) format("embedded-opentype"),url(../fonts/kbase-icons.woff) format("woff"),url(../fonts/kbase-icons.ttf) format("truetype"),url(../fonts/kbase-icons.svg#kbase-icons) format("svg")}@font-face{font-family:Oxygen;font-style:normal;font-weight:400;src:url(../fonts/Oxygen-webfont.eot);src:url(../fonts/Oxygen-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-webfont.woff) format("woff"),url(../fonts/Oxygen-webfont.ttf) format("truetype"),url(../fonts/Oxygen-webfont.svg#OxygenRegular) format("svg")}@font-face{font-family:Oxygen;font-style:normal;font-weight:700;src:url(../fonts/Oxygen-Bold-webfont.eot);src:url(../fonts/Oxygen-Bold-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-Bold-webfont.woff) format("woff"),url(../fonts/Oxygen-Bold-webfont.ttf) format("truetype"),url(../fonts/Oxygen-Bold-webfont.svg#OxygenBold) format("svg")}@font-face{font-family:Oxygen;font-style:italic;font-weight:400;src:url(../fonts/Oxygen-Italic-webfont.eot);src:url(../fonts/Oxygen-Italic-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-Italic-webfont.woff) format("woff"),url(../fonts/Oxygen-Italic-webfont.ttf) format("truetype"),url(../fonts/Oxygen-Italic-webfont.svg#OxygenItalic) format("svg")}@font-face{font-family:Oxygen;font-style:italic;font-weight:700;src:url(../fonts/Oxygen-BoldItalic-webfont.eot);src:url(../fonts/Oxygen-BoldItalic-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-BoldItalic-webfont.woff) format("woff"),url(../fonts/Oxygen-BoldItalic-webfont.ttf) format("truetype"),url(../fonts/Oxygen-BoldItalic-webfont.svg#OxygenBoldItalic) format("svg")}@font-face{font-family:OxygenMono;font-style:normal;font-weight:400;src:url(../fonts/OxygenMono-Regular-webfont.eot);src:url(../fonts/OxygenMono-Regular-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/OxygenMono-Regular-webfont.woff) format("woff"),url(../fonts/OxygenMono-Regular-webfont.ttf) format("truetype"),url(../fonts/OxygenMono-Regular-webfont.svg#OxygenMonoRegular) format("svg")}@font-face{font-family:OxygenRegular;font-style:normal;font-weight:400;src:url(../fonts/Oxygen-webfont.eot);src:url(../fonts/Oxygen-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-webfont.woff) format("woff"),url(../fonts/Oxygen-webfont.ttf) format("truetype"),url(../fonts/Oxygen-webfont.svg#OxygenRegular) format("svg")}@font-face{font-family:OxygenBold;font-style:normal;font-weight:400;src:url(../fonts/Oxygen-Bold-webfont.eot);src:url(../fonts/Oxygen-Bold-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Oxygen-Bold-webfont.woff) format("woff"),url(../fonts/Oxygen-Bold-webfont.ttf) format("truetype"),url(../fonts/Oxygen-Bold-webfont.svg#OxygenBold) format("svg")}@font-face{font-family:RobotoBlack;font-style:normal;font-weight:400;src:url(../fonts/Roboto-Black-webfont.eot);src:url(../fonts/Roboto-Black-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/Roboto-Black-webfont.woff) format("woff"),url(../fonts/Roboto-Black-webfont.ttf) format("truetype"),url(../fonts/Roboto-Black-webfont.svg#RobotoBlack) format("svg")}#ipython-main-app{position:relative}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#337ab7;color:#fff}.select2-container--default .select2-results__option--highlighted[aria-selected] span{color:#fff!important}#site{overflow:visible}#notebook>.end_space{height:0;min-height:0}@media (min-width:1200px){#notebook-container{width:auto}}@media (min-width:992px){#notebook-container{width:auto}}@media (min-width:768px){#notebook-container{width:auto}}.celltoolbar{background:none;border:0;border-left:1px solid transparent;height:auto;padding-bottom:2px;padding-top:0}div.input_area{border:1px solid #cecece;border-radius:0;padding-top:4px}div.output_subarea{max-width:inherit}div.text_cell_input{border:1px solid #cecece;border-radius:0}div.cell{border:1px solid #ded5cb;border-left-width:5px;border-radius:0;margin:8px 0;padding:0}div.cell.selected{background:none;border:1px solid #4bb856;border-left-width:5px;box-shadow:0 1px 2px #aaa,0 5px 5px #aaa;margin-left:0;padding-left:0;transition:box-shadow .2s ease-in-out}div.cell.selected:before{background:none}div.cell.selected.kb-error{border:1px solid #d9534f;border-left-width:5px}div.cell.opened{padding-top:0}div#notebook{padding:0}.edit_mode div.cell.selected{background:none;border-left:5px solid #66bb6a;padding-left:0}#notebook_name{color:#006698}#notebook_name:hover{background-color:#e0e0e0;cursor:pointer}#notebook-container{bottom:0;box-shadow:none;left:380px;min-width:460px;overflow:auto;position:fixed;right:0;top:70px;width:auto}#notebook-container:after{content:"";display:block;height:100px}.btn-toolbar{margin-left:0;padding-left:0}.kb-narr-side-panel .btn-group .btn{font-size:16px;padding:3px 4px}.btn-subtle{color:#888;margin:0}.btn-subtle .fa,.btn-subtle .fa:before{color:#888}.notebook_app{background-color:#fff;overflow-y:hidden}.notebook_app .btn.active{box-shadow:none}.notebook_app .inner_cell{overflow:visible}.notebook_app .celltoolbar>.button_container{flex:0 auto}.notebook_app .celltoolbar>.button_container:first-child{flex:auto}.notebook_app .celltoolbar_container{border-bottom:1px solid silver}.notebook_app .prompt{color:silver;padding-top:0;width:75px}.notebook_app .cell.selected div.text_cell_render,.notebook_app .cell.unselected div.text_cell_render{border:1px solid transparent}.notebook_app .buttons .dropdown-menu{height:auto!important;overflow:visible!important}.notebook_app .btn.disabled .fa{color:silver}.text_cell.kb-cell .rendered_html table{border:1px solid #ddd}.text_cell.kb-cell .rendered_html ul{margin:0}.text_cell.opened.rendered.kb-cell .rendered_html{border:0;padding:0}.text_cell.opened.rendered.selected.kb-cell .rendered_html{border-left:1px solid #4bb856}.text_cell.opened.rendered.selected.kb-cell.kb-error .rendered_html{border-left:1px solid #d9534f}.text_cell.opened.rendered.kb-cell{padding-bottom:0}.rendered_html td,.rendered_html th,.rendered_html tr{text-align:inherit}.rendered_html table{table-layout:inherit}.cell.selected .prompt .method-icon{color:#673ab7}.cell.selected .prompt .app-icon{color:#009688}.cell.selected .prompt .app-output-icon,.cell.selected .prompt .data-viewer-icon,.cell.selected .prompt .markdown-icon,.cell.selected .prompt .method-output-icon{color:#000}.cell.selected .prompt .error-icon{color:red}.cell.selected .btn-default{color:#000}.cell>.inner_cell>.ctb_hideshow{display:block}.cell.unselected .btn-default{color:#ded5cb}.cell.unselected .app-icon,.cell.unselected .method-icon{color:silver}.kb-btn-icon{display:inline-block;padding:4px}.kb-btn-icon .fa{opacity:.5}.kb-btn-icon:hover .fa{opacity:1}p{margin-bottom:1rem}.prompt{display:none!important}div.run_this_cell{display:none;width:0}div.code_cell div.input_prompt{min-width:0}.panel-title{font:normal 700 18px/30px Oxygen,Arial,sans-serif}[data-toggle=collapse]{cursor:pointer}[data-toggle=collapse]:before{color:silver;content:"\f078";display:inline-block;font:normal 400 90%/1 FontAwesome;margin-left:0;margin-right:.2em;text-align:center;vertical-align:baseline;width:1.2em}[data-toggle=collapse].collapsed:before{content:"\f054";margin-left:.1em;margin-right:.1em}[data-toggle=vertical-collapse-after]{cursor:pointer}[data-toggle=vertical-collapse-after]:after{content:" \f0d8";display:inline-block;font:normal 400 130%/1 FontAwesome;text-align:right;vertical-align:baseline;width:1em}.vertical_collapse--open [data-toggle=vertical-collapse-after]:after{content:" \f0d7"}body{background-color:#fff;color:#171412;font:1.4em Oxygen,Arial,sans-serif}h1{color:#232323;font:400 1.75em/2em RobotoBlack,Arial,sans-serif}h1,h2{letter-spacing:0}h2{color:#898989;font:normal 400 1.5em/1.75em RobotoBlack,Arial,sans-serif}h3{font:normal 400 1.15em/1.25em RobotoBlack,Arial,sans-serif;letter-spacing:0}h4{font:italic 400 1.15em/1.25em Oxygen,Arial,sans-serif;letter-spacing:.07em}b,strong{font-family:Oxygen,Arial,sans-serif;font-weight:700}b i,em,em strong,i,i b,strong em{font-family:Oxygen,Arial,sans-serif;font-style:italic}b i,em strong,i b,strong em{font-weight:700}a{color:#037ac0;text-decoration:none}a:hover,a:visited{color:#2a6496}a:hover{text-decoration:underline}code,pre{font-style:normal}.kb-ga-seq,code,pre{font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.kb-ga-seq{word-wrap:break-word;max-height:100px;max-width:300px;overflow-y:auto}.ui-front{z-index:1000}.whiteout-pane{background-color:#fff;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:1001}#kb-narr-name{font:normal 700 18px/1.2 Oxygen,Arial,sans-serif;overflow:hidden;padding-bottom:7px;text-overflow:ellipsis;white-space:nowrap}#kb-narr-name:hover{text-overflow:inherit;white-space:normal}#save_widget{padding:0}#kb-narr-creator,#save_widget{font-family:Oxygen,Arial,sans-serif;font-weight:700}.kb-narr-namestamp{border-left:2px solid #cecece;display:block;flex:2;margin:5px auto 0;min-width:0;padding-left:20px}.kb-narr-namestamp__error_container{font-size:200%;line-height:1.5}.ui-draggable.kb-data-inflight{background-color:#feffd6!important;border:2px solid gray;border-radius:4px;display:block;z-index:1000}.ui-draggable.kb-data-inflight.-over{background-color:#effceb!important;border-color:green}.kb-data-list-drag-target{border:2px dashed orange;border-radius:4px;display:block;height:40px;padding:6px;text-align:center;transition:all .25s ease}.kb-data-list-drag-target.-drag-active{background-color:#feffd6}.kb-data-list-drag-target.-drag-hover{background-color:#effceb;border-color:green;height:90px;transition:all .25s ease}#kb-notify-area{float:left;position:relative}.kb-navbar-buttons{border-right:2px solid #cecece;display:inline-block;margin-right:15px;padding-right:15px}header[role=banner]{background-color:#fff;border-bottom:1px solid #cecece;position:absolute;top:0;width:100%;z-index:999}nav[role=navigation] ul{margin:1.2em 0 .5em;padding-left:0}nav[role=navigation] ul li{display:inline;margin:0 .5em;vertical-align:middle}nav[role=navigation] ul li a{background-color:#5c9531;border:1px solid #5c9531;border-radius:8px;box-shadow:inset 0 3px 8px rgba(0,0,0,.125);color:#fff;font:normal 700 1.2em Oxygen,Arial,sans-serif;padding:7px 1em;text-decoration:none}input#search_terms{background-image:url(../images/search.png);background-position:100%;background-repeat:no-repeat;background-size:22px;border-color:#5c9531;border-radius:0 4px 4px 0;display:inline;font-size:1em;left:-.5em;margin-top:-3px;padding:.4em 1em;position:relative}span#searchspan{display:inline-block;width:50%}@media screen and (max-width:1170px){span#searchspan{width:50%}}@media screen and (max-width:980px){span#searchspan{width:40%}}@media screen and (max-width:768px){span#searchspan{width:30%}}@media screen and (max-width:590px){span#searchspan{width:60%}}.navbar-kbase{background-color:#fff;border-bottom:5px solid #cecece!important;box-shadow:0 1px 10px rgba(0,0,0,.1);padding:8px}.navbar-kbase a:hover{cursor:pointer}.kb-nav-btn{background-color:#fff;border:0;box-shadow:0 0 3px #cecece;font-size:24px;margin:0 5px 0 0;min-width:50px;padding:1px 7px;text-shadow:none!important}.kb-nav-btn>div{color:#6a6158!important}.kb-nav-btn .kb-nav-btn-txt{font-size:13px;margin-top:-5px}.navbar-right .fa:before{color:#2196f3}.kb-navbar-container{display:flex;justify-content:space-between}#kb-nav-menu{box-shadow:none}.kb-nav-menu__container{display:inline-block}.navbar-right .kb-nav-btn:hover{background-color:#f5f5f5}.kb-nav-btn-upgrade{background-color:#4bb856!important;display:none}.kb-nav-btn-upgrade .fa:before,.kb-nav-btn-upgrade>div{color:#fff!important}.kb-nav-btn-upgrade:hover{background-color:#43a047!important}.kb-nav-btn-upgrade.warning{background-color:#f44336!important}.kb-nav-btn-upgrade.warning:hover{background-color:#dc3c31!important}.kb-nav-menu-icon{display:inline-block;margin-right:5px;text-align:center;vertical-align:middle;width:20px}.kb-nav-menu-icon .fa{font-size:150%}#kb-ipy-menu{border-radius:4px!important;margin-right:0}#signin-button .btn{border:0}.btn-xs .glyphicon-user{padding:14px 5px}.narrative-menu-container{background-color:#fff;border-bottom:1px solid #ababab;margin-left:auto;margin-top:-10px;padding-bottom:30px;padding-top:10px;position:fixed;width:100%;z-index:499}p.clear{clear:both;height:0}body>#header{box-sizing:border-box;display:block}p#site-title{background:url(../images/kbase_logo.png) no-repeat;background-size:46px;float:left;height:46px;margin:5px;text-indent:-9999px;width:46px}#login-info{font-size:1.2em;margin:5px;position:absolute;right:0;top:7px}#login-widget button{margin-left:5px}.search-box{border-collapse:separate;display:inline;margin:-11px 0;padding:10px 1px;position:relative;width:200px}#search-box-name{border:0;border-radius:4px;padding:6px 0}#search-box-name:first-child{border-bottom-right-radius:0;border-right:0;border-top-right-radius:0}#search-box-name:last-child{border-bottom-left-radius:0;border-top-left-radius:0}#search-input{margin:-11px 0}#search-select{margin:-11px 0;width:150px}#search-area{color:#686868;font:normal 400 18px Oxygen,Arial,sans-serif;margin:0 5px;padding:11px;position:relative}#search-area input{padding:11px}#workspace{bottom:42px;overflow:auto;position:fixed;top:173px;width:878px}#bottom-tabs{background-color:#fff;border-top:1px solid #aaa;bottom:0;clear:left;margin:0 auto;position:fixed;width:1170px}#bottom-tabs p a{background-color:#fff;border:1px solid #018841;font-size:14px;padding:14px 20px}#left-column{border-right:5px solid #cecece;bottom:0;position:fixed;top:70px;width:380px}.kb-side-overlay-container{background:#fff;border:1px solid #cecece;max-height:calc(100% - 73px);overflow-y:auto;position:fixed;width:730px;z-index:1030}.kb-side-overlay-header{background-color:#2196f3;color:#fff;font-size:16px;padding:6px;width:100%}.kb-side-overlay-close{color:#fff;cursor:pointer;font-size:13px;font-weight:700}.kb-side-overlay-close:hover{color:orange}.kb-side-panel{height:100%;overflow-y:hidden}#kb-side-toggle-in{background-color:#2196f3;border-bottom:6px solid #cecece;border-right:6px solid #cecece;color:#cce5f3;cursor:pointer;display:none;font-size:16px;font-weight:700;padding:12px 6px;position:fixed;text-align:center;top:70px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:22px;z-index:1}#kb-side-toggle-in:hover{color:#fff}.kb-side-toggle{background-color:#2196f3;border-bottom:6px solid #2196f3;border-top:6px solid #2196f3;color:#cce5f3;cursor:pointer;display:inline-block;font-size:16px;font-weight:700;padding:6px;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.kb-side-toggle:hover{color:#fff}.kb-side-header{background-color:#2196f3;border-bottom:6px solid #2196f3;border-top:6px solid #2196f3;color:#cce5f3;cursor:pointer;display:inline-block;font-size:16px;font-weight:700;padding:6px;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.kb-side-header.active,.kb-side-header:hover{border-bottom:6px solid #10ce34}.kb-side-header.active{color:#fff;cursor:auto}.kb-overlay-active{background-color:gray;border-bottom:6px solid gray;border-top:6px solid gray}.kb-side-separator{border-bottom:5px solid #cecece}.kb-side-tab{display:none}.kb-side-tab.active{display:inherit}.kb-narr-side-panel-set,.kb-side-tab{height:100%}.kb-narr-side-panel-set:last-child{height:50%}.kb-overlay-dimmer{background-color:#000;bottom:0;opacity:.5;position:fixed;right:0;z-index:10}#data-header{background-color:#099;color:#fff;font-weight:700;padding:10px 0}#data-title{text-align:center}#data-add-btn{font-size:.8em;padding:0 0 -10px;position:absolute;right:7px}#data-add-btn:hover{color:#000}#data-tab-nav{background-color:#0bb;border-bottom:1px solid #aaa;padding-top:10px;width:100%}#data-tab-nav ul{bottom:0;padding-bottom:0;top:50px}#data-tab-nav ul li{background-color:#ddd;border:1px solid #000;display:inline;padding:4px 10px}#data-tab-nav ul li a{color:#000}#data-tab-nav ul li a:hover{color:#a00;text-decoration:none}#data-tab-nav ul li.selected{background-color:#fff}#data-view-panel{border:1px solid #aaa;height:150px;overflow-y:auto;padding:10px}#data-pane{border-bottom:1px solid #aaa;height:69%;margin-top:1px;overflow:auto}#data-pane p{margin:0}#data-pane p span{border:1px solid #fff;color:#fff;display:block;margin:0;padding:2px 10px;text-align:center;width:123px}#data-pane p span.tab1{background:linear-gradient(#6a6158,#bbb);color:#fff;float:right}#data-pane p span.tab2{background:linear-gradient(#ded5cb,#9d9389)}#function-pane{height:30%;margin-top:1px;overflow:auto}.left-pane{font-size:14px;position:relative}#kb-function-panel .kb-function-body{height:100%;max-height:100%}h3.pane-title{background-color:#e0e0e0;border:1px solid silver;font-size:16px;margin:0;padding:3px 5px;position:relative}#add-link{font-size:.8em;position:absolute;right:7px;top:4px}ul.pane-list{margin:0;padding:2px}ul.pane-list li{list-style:none;margin-left:5px;padding:1px}.kb-method-list-logo{background-color:#607d8b;border:0 solid #555;color:#fff;cursor:pointer;display:inline-block;font-size:24px;font-weight:700;height:40px;padding-top:8px;text-align:center;text-shadow:-1px 0 #777,0 1px #777,1px 0 #777,0 -1px #777;width:40px}.kb-method-list-logo:hover{border-width:5px;padding-top:3px}.kb-method-list-more-div{color:#777;font-size:13px;margin:2px;text-align:justify}.kb-method-list-more-div>div:last-child{text-align:right}.kb-method-search-clear{border:1px solid #cecece;border-left:0;cursor:pointer}li.function a{color:#508232;text-decoration:none}li.function a:visited{color:#508232}li.dataset a{text-decoration:none}li.dataset a,li.dataset a:visited{color:#825032}.dialog-box{padding:20px}.dialog-box ul li{border-bottom:1px solid #aaa;font-size:1.2em;padding:50px 5px}.dialog-box ul li:first-child{padding-top:20px}.dialog-box ul li:last-child{border-bottom:0}fieldset{border:0;margin:0 0 15px}fieldset label{font-size:1em;font-weight:700;margin:0 0 3px}fieldset input[type=password],fieldset input[type=text],fieldset select{width:200px}div.dataset-cell,div.function-cell{border:1px solid #ded5cb;margin:3px auto;padding:7px 17px;width:90%}div.texttools p,div.tools p{margin:0}div.textarea{border:0;display:none;height:auto;min-height:1.2em;padding:0;width:650px}div.metadata{display:block;float:left;height:100px;padding:0 10px;width:200px}div.social{display:none;float:right;padding:0 10px;width:80px}img.dataset-cell{margin:3px auto;padding:0}.function-cell{background-image:url(../images/gears.png);background-position:100%;background-repeat:no-repeat}.dataset-cell p,.function-cell p{font-size:.875em}.dataset-cell h2,.function-cell h2{font-size:1.1em;margin-bottom:0}.textcell{border:2px solid #fff;color:#111;font-size:1em;height:1em;margin:3px auto;padding:5px 17px;width:90%}.textcell:hover{border:2px dotted #ded5cb}p.textcuepara{color:#ded5cb;display:none;height:1em;margin:0;padding:0}.texttools{display:none}.texttools img{vertical-align:middle}.tools{display:none;height:1em;margin-left:10px;margin-top:.5em}.tools a{border:1px solid #018841;font-size:13px;padding:2px 4px}#narrative-header .tools{display:block;left:350px;position:relative;top:-2em;width:30em}img.remove{float:right}.ui-state-highlight{background-color:fbfaed}p span.notification{background-color:#f33;border-radius:1em;color:#fff;padding:4px 8px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{border-radius:0 6px 6px;left:100%;margin-left:-1px;margin-top:-6px;top:0}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu>a:after{border-color:transparent transparent transparent #ded5cb;border-style:solid;border-width:5px 0 5px 5px;content:" ";display:block;float:right;height:0;margin-right:-10px;margin-top:5px;width:0}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{border-radius:6px 0 6px 6px;left:-100%;margin-left:10px}#maintoolbar{background-color:#f9f9f9!important;background-image:none!important;border:0!important;box-shadow:none;min-height:0;position:relative;top:5px}#maintoolbar-container{margin-left:10px}#menubar{background-color:#f9f9f9;border-bottom:2px solid #ddd;padding-bottom:5px;position:absolute;top:3px;width:100%}#menubar .navbar .container{padding-left:0;padding-right:0}.navbar{background-color:#f8f8f8;background-image:none;min-height:0}#menubar .navbar-nav>li>a{padding-bottom:7px;padding-top:10px}.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning,.panel-default>.panel-heading{background-image:none}#menus{padding-left:0}#menubar .navbar{margin-bottom:5px;width:100%}div#notebook_panel{box-shadow:none}.version-stamp a{color:#037ac0!important}.creator-stamp,.version-stamp a{padding-right:320px;text-align:right}.creator-stamp{color:#006698;font:normal 700 120% Oxygen,Arial,sans-serif;height:1em}.panel{margin-bottom:0}#kb-jobs-panel{margin-bottom:5px}.kb-narr-side-panel{height:100%;margin-bottom:5px}.kb-narr-side-panel .kb-title{color:#03517d;font:normal 700 19px Oxygen,Arial,sans-serif;padding:10px 5px;text-transform:uppercase}.kb-narr-side-panel .btn{border:0}.kbujs-table-container{height:auto}.kbujs-timestamp{color:#0066b9;cursor:pointer}.kbujs-error-cell{color:#c7254e;font-weight:700}.kbujs-error:hover{cursor:pointer}.kbujs-loading-modal{font-size:16px;font-weight:700;text-align:center}.kbujs-refresh-btn{position:absolute;right:5px}.kbujs-jobs-table{margin-left:auto!important;margin-right:auto!important}.kbujs-loading{height:100%;text-align:center;vertical-align:middle}.kbujs-delete-job{cursor:pointer}.kbujs-error-traceback{float:left;max-height:250px;max-width:516px;overflow-x:scroll;overflow-y:scroll;white-space:nowrap}.kb-line{background-color:#d3d3d3;position:absolute}.nav-tabs .glyphicon-remove{color:#aaa;margin:0 0 0 3px}.nav-tabs .glyphicon-remove:hover{color:#6a6158;cursor:hand;cursor:pointer}.paging_full_numbers{height:22px;line-height:22px}.paging_full_numbers a:active{outline:none}.paging_full_numbers a:hover{text-decoration:none}.paging_full_numbers a.paginate_active{background-color:#99b3ff}.paging_full_numbers a.paginate_active,.paging_full_numbers a.paginate_button{border:1px solid #aaa;border-radius:5px;color:#333!important;cursor:pointer;*cursor:hand;margin:0 3px;padding:2px 5px}.paging_full_numbers a.paginate_button{background-color:#ddd}.paging_full_numbers a.paginate_button:hover{background-color:#ded5cb;text-decoration:none!important}.kb-function-header{background-color:#b6e9f8;color:#0064b6;font-weight:700;margin-bottom:3px;padding:10px 0;text-align:center}.kb-narr-panel-body{height:100%;padding:3px}.kb-narr-panel-body>div{height:100%}.kb-narr-panel-body-wrapper{height:100%;overflow-y:auto}.kb-narr-panel-body-wrapper>div{height:100%}.kb-narr-panel-toggle{color:#888;cursor:pointer;margin-right:4px;margin-top:-4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.kb-function-body{height:100%;width:100%}.kb-function-body .accordion .panel .panel-body{padding:0 0 0 5px}.kb-function-body ul{border:1px solid #ddd;list-style-type:none;margin:0;padding:0;width:100%}.kb-function-body li{padding:5px;text-align:left;width:100%}.kb-function-body li:nth-child(odd){background-color:#f2efeb;border-bottom:1px solid #ddd}.kb-function-body li:nth-child(2n){background-color:#fff;border-bottom:1px solid #ddd}.kb-function-body li:hover{background-color:#f4f5d6;cursor:pointer;text-decoration:underline}.kb-function-body a,.kb-function-body a:hover{text-decoration:none}.kb-function-error{background-color:#f2dede!important;border-color:#eed3d7!important;color:#b94a48}.kb-function-help{color:#56559e;cursor:pointer;float:right;font-size:19px;padding:0 3px}.kb-function-help:hover{color:#000}.kb-function-help-popup{cursor:pointer;left:300px;max-width:300px;position:absolute;top:0;z-index:99}.kb-function-help-popup h1{color:#3076a2;font-size:110%;margin:-10px 0 0;padding:0}.kb-function-help-popup h2{color:#9d9389;font-size:80%;font-style:italic;font-weight:100;margin:10px 0 -10px;padding-top:5px;text-align:right}.kb-function-help-popup .header{background-color:#fff;color:#3076a2;font-size:120%;font-weight:700;margin:0;padding:0}.kb-function-help-popup a{font-weight:700;text-decoration:underline}.kb-function-help-popup .version{color:#6a6158;font-size:80%}.kb-function-help-popup .body{margin-bottom:10px;margin-top:10px}#kb-function-help{cursor:pointer;left:300px;position:absolute;top:400px;width:250px;z-index:99}#kb-function-help h1{color:#3076a2;font-size:110%;margin:-10px 0 0;padding:0}#kb-function-help h2{color:#9d9389;font-size:80%;font-style:italic;font-weight:100;margin:10px 0 -10px;padding-top:5px;text-align:right}#kb-function-error-traceback{float:left;max-width:250px;overflow-x:scroll;white-space:nowrap}.kb-function-dim,.kb-function-dim .panel-heading{background-color:#f2efeb!important}.kb-function-cat-dim,.kb-function-cat-dim .panel-heading{background-color:#ddd!important;border-color:#ddd!important}.kb-function-toggle{color:#037ac0;cursor:pointer;font-style:italic}.kb-toolbar-open{border-bottom:1px solid #ded5cb;border-radius:0}.selected .kb-toolbar-open{background-color:#dff0d8;border-bottom:1px solid #4bb856;border-left:1px solid #4bb856}.unselected .kb-toolbar-open{background-color:none}.selected.kb-error .kb-toolbar-open{background-color:#f2dede;border-color:#d9534f}.kb-cell-run{opacity:1;z-index:auto}.kb-cell-run h1{color:#03517d;display:inline;font:normal 400 16px/1 Oxygen,Arial,sans-serif;margin:3px 13px 3px 3px;opacity:inherit}.kb-cell-run div.kb-func-desc{display:block;opacity:inherit}.kb-cell-run h2{color:#6a6158;font:normal 400 15px/1.5 Oxygen,Arial,sans-serif;margin:3px;opacity:inherit}.kb-cell-run form input[type=submit]{float:right;margin-top:5px;opacity:inherit}.kb-cell-run form .buttons{float:right;margin-top:5px}.kb-cell-run.running form{opacity:.6}.kb-cell-params table{border:0;margin-bottom:10px;margin-left:auto;margin-right:auto;opacity:inherit}.kb-cell-params table>tr,td{border:0;vertical-align:middle}.kb-cell-params tbody>tr>td,.kb-cell-params tbody>tr>th,.kb-cell-params tfoot>tr>td,.kb-cell-params tfoot>tr>th,.kb-cell-params thead>tr>td,.kb-cell-params thead>tr>th{padding:2px 4px}.kb-cell-params tr:hover td,.kb-cell-params tr:hover th{background:#f2efeb}.kb-cell-progress{display:none}.kb-cell-progress.running{display:block}.kb-cell-progressbar{height:20px;width:400px}.kb-out-desc{color:#03517d}.kb-err-desc,.kb-out-desc{display:inline;font:normal 400 16px/1 Oxygen,Arial,sans-serif;margin:3px;opacity:inherit}.kb-err-desc{color:#a94442}.kb-err-msg{font-size:80%!important}.kb-out-header{background-color:#ddd;padding:10px}.kb-func-timestamp{color:#555;font:13px Oxygen,Arial,sans-serif;margin-top:-3px;padding-right:1em}.unselected .kb-func-timestamp{color:silver}.kb-func-panel,.kb-func-panel>.panel-heading{border-color:#bce8f1;border-radius:1px}.kb-func-panel>.panel-heading{background-color:#d9edf7;color:#31708f;padding:5px 10px}.kb-func-panel .panel-body{font-family:Oxygen,Arial,sans-serif;padding:0 10px}.kb-func-panel>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.kb-func-panel>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.kb-cell-output .panel{border-radius:0}.kb-cell-output .nav>li>a,.kb-cell-output .panel-heading{padding:5px 10px}.kb-cell-output-content{overflow-x:auto;padding:5px}.rendered_html .kb-cell-output ul{margin:0 0 10px}.rendered_html :link{text-decoration:none}.kb-jobs-title{color:#185a85;font:normal 700 1.2em Oxygen,Arial,sans-serif}.kb-jobs-title .glyphicon-info-sign{font-size:.9em}.kb-jobs-info-table{margin:0 2px;width:95%}.kb-jobs-info-table th{color:#777;font-family:Oxygen,Arial,sans-serif;font-weight:700;max-width:30%;min-width:30%;padding-right:5px;vertical-align:top;width:30%}.kb-jobs-item{border-bottom:2px solid #ddd;margin-bottom:5px;padding-bottom:3px}.kb-jobs-error{background-color:#f2dede}.kb-jobs-error-btn{font-size:13px}.kb-jobs-item:last-child{border-bottom:0}.kb-jobs-error-modal{word-wrap:break-word;max-height:220px;overflow-x:hidden;overflow-y:auto;width:485px}.kb-method-parameter-panel{border-left:3px solid #fff}.kb-method-parameter-panel-hover{border-left:3px solid #428bca}.kb-method-parameter-row{border-radius:5px;margin:0;padding:5px}.kb-method-parameter-row-hover{background:#f9f9f9}.kb-method-parameter-row-error{background:#f2dede}.kb-method-parameter-error-mssg{color:#f44336;font-family:Oxygen,Arial,sans-serif;font-size:12px;font-weight:700;padding:5px;text-align:center}.kb-method-parameter-name{color:#777;font-family:Oxygen,Arial,sans-serif;font-weight:700;margin-top:3px;padding-left:0;padding-right:0;text-align:right;vertical-align:middle;white-space:normal}.kb-method-parameter-input{padding-left:10px;vertical-align:middle;white-space:nowrap}.kb-method-parameter-input input{font-weight:700}.kb-method-parameter-input>div{padding-right:20px}.kb-method-parameter-input .kb-method-parameter-accepted-glyph,.kb-method-parameter-input .kb-method-parameter-required-glyph{font-size:15px;margin-left:-15px}.kb-parameter-data-selection{font-weight:700}.kb-method-parameter-hint{color:#777;margin-top:3px;padding-left:7px;text-align:left}.kb-method-parameter-required-glyph{color:#f44336;margin-left:7px}.kb-method-parameter-accepted-glyph{color:#4bb856;margin-left:7px}.kb-method-parameter-info{margin-left:7px}.kb-method-advanced-options-controller-inactive,.kb-method-parameter-info,.kb-parameter-data-row-add,.kb-parameter-data-row-remove{color:#777}.kb-method-advanced-options-controller,.kb-method-advanced-options-controller-inactive{font:italic 700 13px/14px Oxygen,Arial,sans-serif;text-align:center}.kb-method-advanced-options-controller{color:#037ac0;cursor:pointer}.kb-method-advanced-options-controller:hover{color:#2a6496}.kb-method-footer{background-color:#f5f5f5;overflow:none;padding:10px;width:100%}.kb-method-subtitle{background-color:#f5f5f5;padding:3px 5px}.kb-app-panel{border-radius:0}.kb-app-error{background-color:#f2dede}.kb-app-step-container{margin-top:6px}.kb-app-panel>.panel-footer{border:0;border-radius:0}.kb-app-panel-description{color:#03517d;font:normal 400 16px/1 Oxygen,Arial,sans-serif}.kb-app-step-error-mssg{color:#a63232;font:11px/14px sans-serif;margin:10px;text-align:left}.kb-app-step-error-heading{color:#555;font:normal 700 17px/1 Oxygen,Arial,sans-serif;margin-top:20px;padding-left:5px;text-align:left}.kb-app-step-error-main-heading{color:#555;cursor:pointer;font:21px/1 Oxygen,Arial,sans-serif;margin-top:20px;padding-left:20px;text-align:left}.kb-app-step-error{border:3px solid #d14836;z-index:auto}.kb-app-step-running{border:3px solid #2196f3;z-index:auto}.kb-app-next{border-top:2px solid #cecece}.kb-app-next h3{color:#888;float:left;font:italic 400 .9em Oxygen,Arial,sans-serif;margin:0;padding:.5em 0 .25em}.kb-app-next-hide{float:right;margin-right:.5em}.kb-app-next-hide,.kb-app-next-unhide{color:#2196f3;font:.9em Oxygen,Arial,sans-serif;margin-top:.25em}.kb-app-next div{clear:both}@-webkit-keyframes pulse{0%{opacity:1}50%{opacity:.7}to{opacity:1}}@keyframes pulse{0%{opacity:1}50%{opacity:.7}to{opacity:1}}button.kb-app-run,button.kb-method-run{background-color:#2196f3;border:0;border-radius:1px;box-shadow:1px 1px 1px #ded5cb;color:#fff;font-size:13px;padding:10px 20px}button.kb-app-run:hover,button.kb-method-run:hover{background-color:#1e88e5}button.kb-app-run.kb-app-cancel{background-color:#f44336}button.kb-app-run.kb-app-cancel:hover{background-color:#e53935}.modal-body{max-height:100%}div[class=tooltip-inner]{max-width:400px;text-align:left;white-space:pre-wrap}.notebook_app .tooltip-inner{background-color:#1b69b6}.notebook_app .tooltip{z-index:2000!important}.notebook_app .tooltip.top-left .tooltip-arrow,.notebook_app .tooltip.top-right .tooltip-arrow,.notebook_app .tooltip.top .tooltip-arrow{border-top-color:#1b69b6}.notebook_app .tooltip.right .tooltip-arrow{border-right-color:#1b69b6}.notebook_app .tooltip.left .tooltip-arrow{border-left-color:#1b69b6}.notebook_app .tooltip.bottom-left .tooltip-arrow,.notebook_app .tooltip.bottom-right .tooltip-arrow,.notebook_app .tooltip.bottom .tooltip-arrow{border-bottom-color:#1b69b6}.kb-data-main-panel{height:100%;margin-bottom:5px;max-height:425px}#data-tabs{height:375px!important}.kb-ws-refresh-btn{position:absolute;right:5px}#kb-ws .form-control{margin:3px;width:85%}.kb-data-control{width:95%}.kb-data-table{max-height:265px;width:100%!important}.kb-data-table thead{max-width:100%;width:281px}.kb-data-table tbody tr{border-bottom:1px solid #dcdcdc}.kb-data-table tbody tr td{font-size:1em;max-height:16px;max-width:100px;padding:5px}.kb-data-table tbody tr:hover,.kb-data-table tbody tr td.highlighted{background-color:#f4f5d6;cursor:pointer}.kb-data-obj-name{display:inline-block;max-width:88%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kb-data-loading{height:100%;text-align:center}.kb-data-loading,.kb-data-loading img{vertical-align:middle}#kb-ws .dataTables_filter{display:none}button.kb-data-obj{float:right}.kb-data-obj-panel{background:linear-gradient(180deg,#00d2ff,#b2adcb);margin:10px;padding:0}.kb-data-obj-panel button.close{font-size:125%;margin:5px}.kb-data-obj-panel-info{background:hsla(0,0%,100%,.5);height:100%;padding:10px}.kb-data-obj-panel-graph{background:transparent;height:100%;padding:10px}.kb-data-obj-panel-graph table{background-color:hsla(0,0%,100%,.9);border:0;color:rgba(0,0,0,.8);width:100%}.kb-data-obj-panel-graph thead tr{background-color:#e6e6e6;color:#000}.kb-data-obj-panel-info dl{margin:0}.kb-data-obj-panel-info dl dt{clear:left;color:rgba(0,0,0,.5);float:left;text-align:right;width:90px}.kb-data-obj-panel-info dl dt:after{content:":"}.kb-data-obj-panel-info dl dd{margin:0 0 0 100px}.kb-data-obj-panel-verlist span{background-color:hsla(0,0%,100%,.5);border-radius:2px;color:rgba(0,0,0,.5);cursor:pointer;margin-left:3px;padding:0 5px}.kb-data-obj-panel-verlist span.selected{background-color:#2196f3;color:#fff;cursor:default}.kb-data-obj-graph-ref-to{margin-top:8px}.kb-primary-btn{background-color:#2196f3;border:0;border-radius:1px;box-shadow:1px 1px 3px #aaa;color:#fff;font:normal 400 14px Oxygen,Arial,sans-serif;margin:3px;padding:10px 20px}.kb-primary-btn:hover{background-color:#1e88e5}.kb-primary-btn[disabled]{background-color:#cce5f3;box-shadow:none;color:#fff}.kb-default-btn{background-color:#f5f5f5;border:0;border-radius:1px;box-shadow:1px 1px 3px #aaa;color:#555;font:normal 400 14px Oxygen,Arial,sans-serif;margin:3px;padding:10px 20px}.kb-default-btn:hover{background-color:#cecece}.kb-default-btn[disabled]{box-shadow:none;color:#aaa}.kb-btn-sm{font-size:13px;padding:5px 10px}.narrative-card-logo{display:flex;font-size:14px;margin-right:2px;width:50px}.narrative-card-row-main{align-items:center;display:flex;margin:0 2px;padding:0 4px;width:98%}.narrative-data-list-subcontent{padding-left:10px}.narrative-card-ellipsis{align-items:center;display:flex;justify-content:center;width:20px}.narrative-card-palette-icon{color:#888;left:15px;position:relative;top:-50px}.narrative-card-action-button-wrapper{height:50px;margin:0 10px;width:80px}.narrative-card-action-button{display:flex;height:80%;justify-content:center;padding:10px 0;width:calc(100% - 6px)}.narrative-card-action-button>span{margin-right:5px}.narrative-card-action-button-name{display:inline-block}.narrative-data-panel-btnToolbar{display:flex;justify-content:center}.narrative-data-panel-btnToolbar>span{color:#888}.kb-data-list-obj-row,.narrative-card-row{border-left:5px solid #fff;box-shadow:1px 1px 1px 1px #fff;color:#333;transition:all .1s ease}.kb-function-dim .kb-data-list-obj-row{border-left-color:#f2efeb}.kb-data-list-obj-row:hover,.narrative-card-row:hover{border-left:5px solid #4bb856;box-shadow:1px 1px 1px 1px #aaa}.palette{background:#f5f5f5!important}.kb-data-list-obj-row-selected{border-left:5px solid #4bb856;box-shadow:1px 1px 1px 1px #aaa}.kb-data-list-obj-row-main{margin:0;padding:0;width:100%}.kb-data-list-nav-buttons{border:0;box-shadow:none;font-size:18px;margin:0;padding:5px 8px;text-shadow:none!important}.kb-data-list-add-data-button{background-color:#4379b1;border:0;box-shadow:1px 1px 3px #aaa;text-align:center}#kb-add-code-cell,#kb-add-md-cell,.kb-data-list-add-data-button{border-radius:50%;color:#fff;cursor:pointer;height:40px;width:40px}#kb-add-code-cell,#kb-add-md-cell{background-color:#2196f3;bottom:15px;box-shadow:2px 2px 1px #cecece;opacity:.5;padding-top:5px;position:fixed;z-index:5}#kb-add-code-cell{padding-left:7px;right:75px}#kb-add-md-cell{margin-right:20px;padding-left:8px;right:10px}#kb-add-code-cell:hover,#kb-add-md-cell:hover,.kb-data-list-add-data-button:hover{background-color:#36618e;cursor:pointer;opacity:1}.kb-data-list-add-data-text-button{background-color:#4379b1;border:0;border-radius:1px;box-shadow:1px 1px 3px #aaa;color:#fff;font:normal 400 14px Oxygen,Arial,sans-serif;margin:3px;padding:10px 20px}.kb-data-list-add-data-text-button:hover{background-color:#36618e}.kb-data-list-btn{background-color:#2196f3;border:0;border-radius:1px;box-shadow:1px 1px 3px #aaa;color:#fff;font-size:13px;margin:5px;padding:5px 10px}.kb-data-list-btn:hover{background-color:#1e88e5}.kb-data-list-btn[disabled]{background-color:#cce5f3;box-shadow:none;color:#fff}.kb-data-list-cancel-btn{background-color:#f5f5f5;border:0;border-radius:1px;box-shadow:1px 1px 3px #aaa;color:#888;font-size:13px;margin:5px;padding:5px 10px}.kb-data-list-cancel-btn:hover{background-color:#cecece}.kb-data-list-row-hr{margin:2px auto;width:70%}.kb-data-list-info{border-bottom:1px solid #e0e0e0;min-height:60px;padding:6px 0;width:80%}.kb-data-list-name{color:#03517d;cursor:pointer;font:normal 700 15px Oxygen,Arial,sans-serif;margin:2px}.kb-data-list-name:hover{text-decoration:underline}.kb-data-list-version{color:#777;cursor:default;font-size:12px;font-style:italic;white-space:nowrap}.kb-data-list-date,.kb-data-list-type{color:#777;cursor:default;font-size:13px;margin:2px}.kb-data-list-date{white-space:nowrap}.kb-data-list-edit-by{color:#777;cursor:pointer;font-size:13px;margin:2px 2px 2px 0;white-space:nowrap}.kb-data-list-edit-by:hover{color:#444}.kb-data-list-narinfo{color:#777;font-size:13px;margin:2px 2px 2px 10px}.kb-data-list-narrative-error{color:#f44336;font-size:13px;margin:2px}.kb-data-list-narrative{color:#777;font-size:13px;margin:2px 2px 2px 10px}.kb-data-list-more{color:#777;font-size:11px;margin:0 0 0 15px;padding:0;white-space:nowrap}.kb-data-list-more-btn{border:0;box-shadow:none;font-size:12px;margin:0;padding:2px 4px;text-shadow:none!important}.kb-data-list-more-div{align-items:center;display:flex;flex-direction:column}.kb-data-list-more-div,.kb-data-list-more-div td,.kb-data-list-more-div th,.kb-data-list-more-div tr{color:#777;font-size:13px;margin:2px}.kb-data-list-more-div tr:hover{background-color:#f2efeb}.kb-data-list-more-div tr:nth-child(odd){background-color:#f5f5f5}.kb-data-list-more-div tr:hover:nth-child(odd){background-color:#f2efeb}.kb-data-list-more-div th{font-weight:700;text-align:right}.kb-data-list-more-div td{padding-left:8px;text-align:left;word-break:break-all}.kb-data-list-job-name:hover{text-decoration:none}.kb-data-list-box>.kb-data-list-box{margin-left:40px}.kb-data-list-ctl span.inviso{background-color:#fff;color:#fff;text-shadow:none}.kb-data-list-ctl[enabled]{background-color:#d3d3d3}.kbcb-browser-container{margin:0 auto}.kbcb-back-link{padding:.4em}.kbcb-ctr-toolbar{margin:.4em}.kbcb-main-panel-div{margin:0}.kbcb-loading-panel-div{margin:.1em;text-align:center;vertical-align:middle}.kbcb-app-card-link{text-decoration:none}.kbcb-app-card-list-container{overflow:auto;padding:0 .2em 1em}.kbcb-app-card-container{display:block;float:left;height:110px;margin:.4em;position:relative;width:300px}.kbcb-app-card{background:#f1f1f5;border-radius:2px;color:#6a6158;height:100%;overflow:hidden;text-align:center;vertical-align:middle;width:100%}.kbcb-app-card-header{max-height:90px;overflow:hidden;text-align:left}.kbcb-app-card-title-panel{padding:10px 5px 5px 10px}.kbcb-app-card-title{font-size:1.1em}.kbcb-app-card-authors,.kbcb-app-card-module{font-size:.8em}.kbcb-app-card-subtitle{font-size:.9em;overflow:hidden;padding:10px}.kbcb-app-card-footer{bottom:0;color:#888;font-size:.9em;left:0;margin:0 5px 5px;position:absolute;width:100%}.kbcb-app-card-logo{font-size:14px;margin:0;padding:0}.kbcb-star-default{color:#888}.kbcb-star-favorite{color:orange}.kbcb-star-default,.kbcb-star-favorite,.kbcb-star-nonfavorite{cursor:pointer}.kbcb-star-default:hover,.kbcb-star-nonfavorite:hover{color:red}.kbcb-star-favorite:hover{color:orange}.kbcb-run-count,.kbcb-star-count{display:inline-block;margin-left:.3em}.kbcb-info:hover{color:orange}.kbcb-hover{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24);transition:all .2s ease-in-out}.kbcb-hover:hover{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23);cursor:pointer}.kbcb-app-page{background:#f1f1f5;border-radius:2px;color:#6a6158;text-align:center}.kbcb-app-page-header{text-align:left}.kbcb-app-page-title-panel{padding:10px 5px 5px 0}.kbcb-app-page-title{font-size:2em}.kbcb-app-page-module{font-size:1.2em}.kbcb-app-page-authors{font-size:1em;margin:.3em 0 0}.kbcb-app-page-subtitle{font-size:1.2em;padding:1.5em 2em 0}.kbcb-app-page-stats-bar{font-size:1.2em;padding:0 2em .8em 3em;width:100%}.kbcb-app-page-logo{margin:1em;padding:0;text-align:center}.kb-share-user-permissions-dropdown{border:0;box-shadow:none;height:auto;padding:2px;text-shadow:none!important;width:auto}.kb-share-select{width:200px}.kb-share-select .select2-selection__choice{background-color:#f5f5f5!important;width:100%!important}.kb-nar-manager-titles{color:#777;font:normal 700 19px Oxygen,Arial,sans-serif;margin:20px 10px 10px}.kb-import-content{display:block;margin:0;position:relative}.kb-import-content .btn-xs{border:0}.ftp-file-header{margin:1rem 0}.globus-link:hover{text-decoration:none}.kb-import-content .upload-options button{background:#fff;border:1px solid silver;border-radius:6px;box-sizing:border-box;color:#000;font:normal 400 14px/18px Oxygen,Arial,sans-serif;height:42px;margin:0 6px;text-align:center;width:140px}.kb-import-content .upload-options button:hover{background-color:#fafafa;border:1px solid #cfcfcf}.kb-import-content .upload-options button:focus{background-color:#f2f2f2}.kb-import-content .upload-options button:active{background-color:#e6e6e6}.kb-import-content .upload-options button:disabled{background-color:#fafafa;color:#929292}.kb-overlay-footer{bottom:0;height:50px;position:absolute;width:100%}.kb-overlay-footer .btn-default,.kb-overlay-footer .btn-primary{margin:15px 15px 0 0}.kb-import-filter,.kb-import-search{margin:10px 40px 10px 10px}.kb-import-item{margin:0 20px 0 5px;padding:20px 0 0}.kb-import-item:hover{background-color:#f8f8f8;box-shadow:0 0 2px #aaa}.kb-import-item hr{border:2px solid #eaeaea;margin-bottom:0;margin-top:15px;width:85%}.kb-import-item .kb-import-checkbox{font-size:1.5em;opacity:.4;padding:15px 20px}.kb-import-item .fa-check-square-o{opacity:1!important}.kb-import-info{display:inline-block;font-weight:400;margin:10px 20px 0 0}.kb-import-info span{color:#9d9389;font-size:.9em;font-weight:700}.kb-import-status{margin:20px}.kb-error-dialog strong{color:#006400}.kb-err-text{color:#000;font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.kb-err-warn{color:#b22222;font-style:italic;margin-top:.5em}#kb-ro-btn{margin-right:20px}#kb-view-mode{cursor:pointer;display:inline-block;font-size:17px;margin:-15px 0 0 1em;padding:0}#kb-view-mode-narr div{color:#2196f3;display:inline-block;font:normal 700 16px Oxygen,Arial,sans-serif;margin-top:-3px;padding:3px;position:fixed;text-align:left;width:147px}#kb-view-mode-narr-hide{background-color:#2196f3;color:#cce5f3;cursor:pointer;display:block;float:left;font-size:16px;height:41px;padding-left:5px;width:15px}#kb-view-mode-narr-hide span{margin-top:10px}.navbar-right div.label-warning{font:normal 700 16px Oxygen,Arial,sans-serif}#popdiv{background-color:#fcfcfc;border:1px solid #ded5cb;font-size:12px;position:absolute;visibility:hidden;z-index:10000}#popdiv a.alt{border:0;display:inline;font:12px OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;padding-left:9px}.kb-loading-spinner{padding:1rem;text-align:center}.fa.fa-14-pt{font-size:19px}[data-icon]:before{content:attr(data-icon)}[class*=" icon-"]:before,[class^=icon-]:before,[data-icon]:before{speak:none;font-family:kbase-icons!important;font-style:normal!important;font-variant:normal!important;font-weight:400!important;text-transform:none!important}.icon-compare:before{content:"a"}.icon-genome:before{content:"b"}.icon-metabolism:before{content:"c"}.icon-metagenome:before{content:"d"}.icon-reads:before{content:"e"}.icon-tree:before{content:"f"}.app-icon:hover{opacity:.9}.app-icon:hover,.nav>li>a:focus,.nav>li>a:hover{text-decoration:none}#kbase-search-box{width:300px}#signin-button{display:inline-block;padding:0 15px 0 0}.wrapper{margin-left:auto;margin-right:auto;width:95%}#core-model{overflow-x:auto}#logo{margin:2px 10px 0}table{font-size:14px!important}.media-info-modal{width:800px}#selected-workspace{margin:17px 14px 15px 0}.tab-view{margin:10px 0 0}.search-query{margin:0 0 5px;width:100%}.caret-up{border-color:currentcolor transparent #fff;border-style:dotted solid solid;border-width:0 4px 4px;content:"";display:inline-block;height:0;margin-left:2px;vertical-align:middle;width:0}.scroll-pane{height:200px;overflow-x:hi;overflow-y:scroll}#select-box tr td{padding:6px}.selected-ws{background-color:#428bca;color:#fff}.selected-ws:hover{background-color:#428bca!important}.side-bar-switch{margin:0 0 10px}.side-bar-switch li{font-size:12px}.side-bar-switch li a{padding:5px 10px}.selected-obj-alert{margin-bottom:5px;padding-bottom:10px;padding-top:10px}.heatmap-view{overflow-x:scroll}.ws-selector{margin:10px 0 0}.ws-selector.btn-ws-settings{position:absolute;right:30px}.object-view{margin:10px 0 0}.select-ws .badge{margin:0 4px 0 0}.select-ws .btn-ws-settings{padding:0 5px}.modal-alert{margin:10px 15px 0;padding:8px 15px}.modal-cover{position:absolute;z-index:10}.modal-cover-table{display:table;height:100%;position:static;width:100%}.modal-cover-cell{display:table-cell;position:static;vertical-align:middle}.modal-cover-box{box-shadow:7px 7px 5px #888;display:inline-block;margin:10px;padding:5px 15px}.modal-cover-content{background:#fff;border:1px solid #6a6158;border-radius:3px;padding:5px}.obj-table .dataTables_length label{margin:7px 0 0}.dataTables_info{float:left;margin:0 10px 0 0}.dataTables_length{float:left}.ellipsis{max-width:170px}.ellipsis,.ws-descript{display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ws-descript{max-width:400px}.ncheck{border:1px solid #ded5cb;height:15px;margin-left:auto;margin-right:auto;width:15px}.ncheck:hover{border:1px solid #444}.ncheck-btn{border:1px solid #888;height:15px;margin:4px 0 4px -1px;width:15px}.ncheck-btn:hover{border:1px solid #444}.ncheck-checked{background:url(../img/checkmark.png) no-repeat -5px -4px;background-image:-webkit-image-set(url(../img/checkmark.png) 1x,url(../img/checkmark_2x.png) 2x);background-image:image-set(url(../img/checkmark.png) 1x,url(../img/checkmark_2x.png) 2x)}.ncheck-minus{background:url(../img/checkmark-partial.png) no-repeat -5px -5px;background-image:-webkit-image-set(url(../img/checkmark-partial.png) 1x,url(../img/checkmark-partial_2x.png) 2x);background-image:image-set(url(../img/checkmark-partial.png) 1x,url(../img/checkmark-partial_2x.png) 2x)}.btn-select-all{height:34px}.btn-select-all,.type-filter{margin:0 20px 0 0}.btn-trash{margin:3px 20px 0 0}.btn-show-info .glyphicon{padding:0 0 4px}.open-obj{margin:7px 0 0 20px}#login-form{margin:70px 0 0}#narrative-nav,.recent-narratives,.recent-projects{margin:0;padding:0}.group-item-expander{background:#f2f2f2}.group-item-expander:hover{cursor:hand;cursor:pointer}.btn-new-narrative{margin:0 20px 0 0}.btn-delete-narrative{cursor:hand;cursor:pointer}.btn-delete-narrative:hover{opacity:.7}.navbar-fixed-top{z-index:500}.FixedHeader_Header{background:#fff;height:44px;margin:-6px 0 0}.narrative-sidebar li{list-style:none}@media (min-width:960px){.narrative-sidebar{position:fixed;width:20%}}.sidebar-header,.view-header{color:#898989;font-family:RobotoBlack,Arial,sans-serif;font-weight:400;letter-spacing:0}.sidebar-header{font-size:14px}.view-header{font-size:1.5em;line-height:1.75}.active .btn-narr{background-color:#bbe8f9;border:1px solid #428bca}.active .btn-narr,.btn-narr{padding-left:20px;padding-right:20px;text-align:left}.btn-narr{background-color:#f2f2f2;border:1px solid #dbdbdb;color:#6da8cf;font:14px RobotoBlack,Arial,sans-serif}.nav-sidebar{top:70px}.nav-sidebar li{font-family:RobotoBlack,Arial,sans-serif;list-style:none;margin-bottom:10px;text-align:left}.nav-sidebar>li>a{padding:7px 15px}.navbar-nav{color:#6da8cf;font-family:RobotoBlack,Arial,sans-serif}.navbar-nav>li>a:hover{background-color:#fff}.navbar-nav>li.active:hover,.navbar-nav>li.active>a{border-bottom:4px solid #428bca;box-sizing:border-box;color:#2a6496!important;height:50px}.navbar-nav>.active:hover{color:#2a6496}#wrap{height:auto;margin:0 auto -58px;min-height:100%;padding:0 0 80px}#footer{height:58px}#footer ul{text-align:center}#footer ul li{display:inline;padding:7px 5px 5px 1px}#footer ul li:after{color:#bbb;content:"•";font-size:10px;padding-left:10px}#footer ul li:last-child:after,#footer ul li:nth-last-child(2):after{content:""}#footer img{position:relative;top:-3px}#footer .disclaimer{background-color:#a94442;bottom:0;color:#fff;display:block;font-weight:700;position:fixed;text-align:center;width:100%}.KBSnode rect{fill:#fff;fill-opacity:.5;stroke:#3182bd;stroke-width:1.5px;cursor:pointer}.KBSnode text{font:80% sans-serif;pointer-events:none}.modal-body .dataTables_processing{background:#e8f9ff;border:1px solid #aaa;opacity:.8}.kb-editor{margin:0 10px}.kb-editor .tab-content{margin:5px 0 0}.kb-editor .controls{margin:5px 0}.kb-editor .controls .btn{margin:0 10px}.kb-editor .btn-primary{background-color:#337ab7!important;border-color:#2e6da4!important}.kb-editor .btn-danger{background-color:#d9534f!important;border-color:#d43f3a!important}.kb-editor .btn i{color:#fff}.kb-editor table{border:1px solid #bbb}.kb-editor-table{width:100%!important}.kb-editor-table>tbody>tr>td:first-child{width:1%}.kb-editor-table>tbody>tr>td:first-child .fa-square-o{color:#6a6158;font-size:1.4em}.kb-editor-table>tbody>tr>td:first-child:hover{cursor:pointer}.kb-editor-table>tbody>tr>td:first-child:hover .fa-square-o{color:#000}.kb-editor-table>tbody>tr.row-select .fa-square-o:before{color:#444;content:"\f046"}.kb-editor-table>tbody>tr>td.editable{position:relative}.kb-editor-table>tbody>tr>td.editable:hover:after{bottom:0;color:#888;content:"\f044";font-family:FontAwesome;position:absolute;right:2px}.kb-notify{background-color:#000;bottom:10px;color:#fff;opacity:.8;padding:15px;position:relative;width:300px}.kb-notify.kb-notify-success{color:#00cc09}.kb-method-cell select.form-control{margin:0}.twitter-typeahead{width:100%}.code_cell div.input_area{display:none}.code_cell div.input_area.-show{display:block}.text-silver{color:silver}.modal .modal-title{font:normal 700 18px/24px Oxygen,Arial,sans-serif}.modal.kb-modal-alert .modal-title{color:unset}.modal input[type=checkbox]{margin-top:1px}.kb-panel-batch,.kb-panel-bulk-params,.kb-panel-container,.kb-panel-light,.kb-panel-params,.kb-panel-results{border:0;box-shadow:none}.kb-panel-batch .panel-heading,.kb-panel-bulk-params .panel-heading,.kb-panel-container .panel-heading,.kb-panel-light .panel-heading,.kb-panel-params .panel-heading,.kb-panel-results .panel-heading{background-color:transparent;color:#6a6158;font-weight:700}.kb-panel-light{margin-bottom:10px}.kb-panel-light.panel-danger .panel-title{color:#a94442}.kb-panel-light.panel-success .panel-title{color:#3c763d}.kb-panel-light.panel-warning .panel-title{color:#8a6d3b}.kb-panel-light.panel-info .panel-title{color:blue}.kb-panel-light .panel-heading{border-bottom:0;padding-bottom:2px}.kb-panel-light .panel-body{padding:0}.kb-panel-batch .panel-heading,.kb-panel-bulk-params .panel-heading,.kb-panel-params .panel-heading,.kb-panel-results .panel-heading{border-bottom:0;margin:15px 15px 5px;padding:0}.kb-panel-batch .panel-body,.kb-panel-bulk-params .panel-body,.kb-panel-params .panel-body,.kb-panel-results .panel-body{border-bottom:0;margin:0 15px 5px;padding:0}.kb-panel-params .panel-body{margin:0 10px 15px}.panel-title-collapse-toggle{margin-left:-15px}.kb-panel-container .panel-heading{border-bottom:2px solid #ded5cb;padding-bottom:2px}.kb-cell-error .panel-heading{background-image:none;padding:5px 10px}.btn{box-shadow:none}.btn-toolbar.kb-btn-toolbar-cell-widget{background-color:#f2efeb;margin-bottom:20px;padding:6px}.dataTables_empty{padding:.8rem}.btn.kb-flat-btn{background-color:#fff;border:0;color:#6a6158;margin:0;text-shadow:none!important}.btn.kb-flat-btn .kb-nav-btn-txt{font-size:13px;margin-top:-5px}.btn.kb-flat-btn:active,.btn.kb-flat-btn:hover{background-color:#ded5cb}.kb-flat-btn-wrapper{box-shadow:none}.dz-clear-all__button,.kb-data-staging__button,.kb-file-path__button--add_row,.kb-file-path__button--delete,.kb-job-status__cell_action--cancel,.kb-job-status__cell_action--go-to-results,.kb-job-status__cell_action--retry,.kb-staging-table-body__button--decompress,.kb-staging-table-body__button--delete,.kb-staging-table-body__button--download,.kb-staging-table-body__button--folder,.kb-staging-table-import__button{border:1px solid transparent;border-radius:4px;cursor:pointer;display:inline-block;font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:700;line-height:1.428571429;padding:.6rem 1.2rem;text-align:center;text-transform:uppercase;touch-action:manipulation;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.dz-clear-all__button:focus,.focus.dz-clear-all__button,.focus.kb-data-staging__button,.focus.kb-file-path__button--add_row,.focus.kb-file-path__button--delete,.focus.kb-job-status__cell_action--cancel,.focus.kb-job-status__cell_action--go-to-results,.focus.kb-job-status__cell_action--retry,.focus.kb-staging-table-body__button--decompress,.focus.kb-staging-table-body__button--delete,.focus.kb-staging-table-body__button--download,.focus.kb-staging-table-body__button--folder,.focus.kb-staging-table-import__button,.kb-data-staging__button:focus,.kb-file-path__button--add_row:focus,.kb-file-path__button--delete:focus,.kb-job-status__cell_action--cancel:focus,.kb-job-status__cell_action--go-to-results:focus,.kb-job-status__cell_action--retry:focus,.kb-staging-table-body__button--decompress:focus,.kb-staging-table-body__button--delete:focus,.kb-staging-table-body__button--download:focus,.kb-staging-table-body__button--folder:focus,.kb-staging-table-import__button:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.dz-clear-all__button:focus,.dz-clear-all__button:hover,.focus.dz-clear-all__button,.focus.kb-data-staging__button,.focus.kb-file-path__button--add_row,.focus.kb-file-path__button--delete,.focus.kb-job-status__cell_action--cancel,.focus.kb-job-status__cell_action--go-to-results,.focus.kb-job-status__cell_action--retry,.focus.kb-staging-table-body__button--decompress,.focus.kb-staging-table-body__button--delete,.focus.kb-staging-table-body__button--download,.focus.kb-staging-table-body__button--folder,.focus.kb-staging-table-import__button,.kb-data-staging__button:focus,.kb-data-staging__button:hover,.kb-file-path__button--add_row:focus,.kb-file-path__button--add_row:hover,.kb-file-path__button--delete:focus,.kb-file-path__button--delete:hover,.kb-job-status__cell_action--cancel:focus,.kb-job-status__cell_action--cancel:hover,.kb-job-status__cell_action--go-to-results:focus,.kb-job-status__cell_action--go-to-results:hover,.kb-job-status__cell_action--retry:focus,.kb-job-status__cell_action--retry:hover,.kb-staging-table-body__button--decompress:focus,.kb-staging-table-body__button--decompress:hover,.kb-staging-table-body__button--delete:focus,.kb-staging-table-body__button--delete:hover,.kb-staging-table-body__button--download:focus,.kb-staging-table-body__button--download:hover,.kb-staging-table-body__button--folder:focus,.kb-staging-table-body__button--folder:hover,.kb-staging-table-import__button:focus,.kb-staging-table-import__button:hover{color:#026daa;text-decoration:none}.active.dz-clear-all__button,.active.kb-data-staging__button,.active.kb-file-path__button--add_row,.active.kb-file-path__button--delete,.active.kb-job-status__cell_action--cancel,.active.kb-job-status__cell_action--go-to-results,.active.kb-job-status__cell_action--retry,.active.kb-staging-table-body__button--decompress,.active.kb-staging-table-body__button--delete,.active.kb-staging-table-body__button--download,.active.kb-staging-table-body__button--folder,.active.kb-staging-table-import__button,.dz-clear-all__button:active,.kb-data-staging__button:active,.kb-file-path__button--add_row:active,.kb-file-path__button--delete:active,.kb-job-status__cell_action--cancel:active,.kb-job-status__cell_action--go-to-results:active,.kb-job-status__cell_action--retry:active,.kb-staging-table-body__button--decompress:active,.kb-staging-table-body__button--delete:active,.kb-staging-table-body__button--download:active,.kb-staging-table-body__button--folder:active,.kb-staging-table-import__button:active{background-image:none;box-shadow:inset 0 3px 5px rgba(0,0,0,.125);outline:0}.disabled.dz-clear-all__button,.disabled.kb-data-staging__button,.disabled.kb-file-path__button--add_row,.disabled.kb-file-path__button--delete,.disabled.kb-job-status__cell_action--cancel,.disabled.kb-job-status__cell_action--go-to-results,.disabled.kb-job-status__cell_action--retry,.disabled.kb-staging-table-body__button--decompress,.disabled.kb-staging-table-body__button--delete,.disabled.kb-staging-table-body__button--download,.disabled.kb-staging-table-body__button--folder,.disabled.kb-staging-table-import__button,[disabled].dz-clear-all__button,[disabled].kb-data-staging__button,[disabled].kb-file-path__button--add_row,[disabled].kb-file-path__button--delete,[disabled].kb-job-status__cell_action--cancel,[disabled].kb-job-status__cell_action--go-to-results,[disabled].kb-job-status__cell_action--retry,[disabled].kb-staging-table-body__button--decompress,[disabled].kb-staging-table-body__button--delete,[disabled].kb-staging-table-body__button--download,[disabled].kb-staging-table-body__button--folder,[disabled].kb-staging-table-import__button,fieldset[disabled] .dz-clear-all__button,fieldset[disabled] .kb-data-staging__button,fieldset[disabled] .kb-file-path__button--add_row,fieldset[disabled] .kb-file-path__button--delete,fieldset[disabled] .kb-job-status__cell_action--cancel,fieldset[disabled] .kb-job-status__cell_action--go-to-results,fieldset[disabled] .kb-job-status__cell_action--retry,fieldset[disabled] .kb-staging-table-body__button--decompress,fieldset[disabled] .kb-staging-table-body__button--delete,fieldset[disabled] .kb-staging-table-body__button--download,fieldset[disabled] .kb-staging-table-body__button--folder,fieldset[disabled] .kb-staging-table-import__button{box-shadow:none;cursor:not-allowed;opacity:.8}.kb-staging-table-body__button--decompress{border-radius:3px;line-height:1.5;padding:.5rem 1rem}.kbcb-tooltip{background-color:#222;color:#fff;opacity:.8;padding:.5em;position:absolute;visibility:hidden;z-index:9999999}.kbcb-widget .kbcb-axis line,.kbcb-widget .kbcb-axis path{fill:none;shape-rendering:crispEdges;stroke:#000}.kbcb-widget .kbcb-axis text{font:11px sans-serif}.kbcb-feature{fill:red;stroke:#000}.kbcb-operon{fill:lime}.kbcb-center{fill:blue}.kbcb-highlight{fill:#fff}.kbcb-buttons{margin-left:auto;margin-right:auto;width:70%}.kb-tour{background-color:#2196f3}.kb-tour.popover.top>.arrow:after{border-top-color:#2196f3}.kb-tour.popover.bottom>.arrow:after{border-bottom-color:#2196f3}.kb-tour.popover.right>.arrow:after{border-right-color:#2196f3}.kb-tour.popover.left>.arrow:after{border-left-color:#2196f3}.kb-tour>h3.popover-title{background-color:#2196f3;color:#fff;font:16px Oxygen,Arial,sans-serif}.kb-tour>.popover-content,.kb-tour>.popover-navigation{background-color:#f2efeb}.kb-tour>.close-btn{position:absolute;right:7px;top:7px}.batch-mode-col{overflow:auto;padding:5px 10px}.job-info{cursor:pointer}.job-info.job-selected{background-color:#f2efeb}.batch-mode-list{max-height:860px;overflow:auto}.batch-input-panel{max-height:300px;overflow:auto}.kb-advanced-view-cell .kb-panel-container[data-element=parameters-group]>div.panel-collapse>.panel-body{padding:0}.kb-error-dialog__body .tab-content{padding-top:1rem}.kb-error-dialog__err_message,.kb-error-dialog__err_preamble{margin:1rem 0}.kb-error-dialog__stacktrace_container{word-wrap:break-word;background-color:#f2efeb;border:0;font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:14px;font-weight:400;line-height:24px;margin:0;max-height:50rem;overflow-wrap:break-word;overflow-y:auto;padding:1rem;word-break:break-word}.kb-app-cell__container{align-items:stretch;display:flex;font-family:Oxygen,Arial,sans-serif}.kb-app-cell__prompt{align-items:stretch;display:flex;flex-direction:column}.kb-app-cell__body{align-items:stretch;display:flex;flex:1;flex-direction:column;width:100%}.kb-app-cell__widget_container{display:block;width:100%}.kb-app-cell-tab-pane__container--jobStatus,.kb-app-cell-tab-pane__container--results{padding:1.5rem}.kb-app-cell .kb-app-warning{font-family:Oxygen,Arial,sans-serif;text-align:center}.kb-app-cell .kb-app-parameter-panel{border-left:3px solid #fff}.kb-app-cell .kb-app-parameter-panel-hover{border-left:3px solid #428bca}.kb-app-cell .kb-app-parameter-row{border-radius:5px;margin:0;padding:5px}.kb-app-cell .kb-app-parameter-row:hover{background-color:rgba(0,0,0,.03)}.kb-app-cell .kb-app-parameter-row-hover{background:#f9f9f9}.kb-app-cell .kb-app-parameter-row .message{font-family:Oxygen,Arial,sans-serif;text-align:center}.kb-app-cell .kb-app-parameter-row .message.-error{background:#f2dede;color:#f44336}.kb-app-cell .kb-app-parameter-name{color:#777;font-family:Oxygen,Arial,sans-serif;padding-left:0;padding-right:4px;text-align:left;vertical-align:bottom;white-space:normal}.kb-app-cell .kb-app-parameter-input{vertical-align:middle}.kb-app-cell .kb-app-parameter-input select.form-control{margin:0}.kb-app-cell .kb-app-parameter-input .kb-app-parameter-accepted-glyph,.kb-app-cell .kb-app-parameter-input .kb-app-parameter-required-glyph{font-size:15px;margin-left:0}.kb-app-cell .kb-app-parameter-required-glyph{color:#f44336}.kb-app-cell .kb-parameter-data-selection{font-weight:700}.kb-app-cell .kb-app-parameter-hint{color:#777;margin-top:3px;padding-left:7px;text-align:left}.kb-app-cell .kb-app-parameter-accepted-glyph{color:#4bb856}.kb-app-cell .kb-app-advanced-options-controller-inactive,.kb-app-cell .kb-app-parameter-info,.kb-app-cell .kb-parameter-data-row-add,.kb-app-cell .kb-parameter-data-row-remove{color:#777}.kb-app-cell .kb-app-advanced-options-controller,.kb-app-cell .kb-app-advanced-options-controller-inactive{font:italic 700 13px/14px Oxygen,Arial,sans-serif;text-align:center}.kb-app-cell .kb-app-advanced-options-controller{color:#037ac0;cursor:pointer}.kb-app-cell .kb-app-advanced-options-controller:hover{color:#2a6496}.kb-app-cell .kb-app-footer{background-color:#f5f5f5;overflow:none;padding:10px;width:100%}.kb-app-cell .kb-app-subtitle{background-color:#f5f5f5;padding:3px 5px}.kb-app-cell .advanced-parameter-showing{display:block}.kb-app-cell .advanced-parameter-hidden{display:none}.kb-app-cell [data-element=run-control-panel]{width:100%}.kb-app-cell [data-element=tab-pane]{border-top:2px solid #2196f3}.kb-app-cell [data-element=tab-pane]>div{padding:0}.kb-app-row-clip-btn-addon,.kb-app-row-close-btn-addon{background:transparent;border:0}.kb-app-row-clip-btn-addon{height:100%;padding:0 10px 0 0}.kb-app-row-clip-btn-addon:active:after{background-color:#fff;border:1px solid gray;border-radius:3px;content:"copied!";margin-top:-5px;padding:7px;position:absolute}.kb-input-group-wide{width:100%}.kb-input-row-flex{align-items:center;display:flex;flex-direction:row}.cell.selected .btn-default.kb-app-row-close-btn{color:gray}.kb-app-row-clip-btn,.kb-app-row-close-btn{background-color:transparent;border:0;color:gray;height:100%;margin-left:1px}.kb-app-parameter-right-error-bar{background:red;height:28px}.kb-app-field-feedback,.kb-input-group-addon{background:transparent;border:0}.kb-input-group-addon{padding:0 0 0 10px}.unselected .kb-app-cell{opacity:.5}.kb-elapsed-time{font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.kb-elapsed-time.-active{color:lime}.kb-app-cell-info-desc{border:1px solid #777;margin-right:10px;min-height:100px;padding:10px}.kb-app-cell-info{color:#777;font:16px Oxygen,Arial,sans-serif}.kb-app-cell-info .header{border-bottom:1px solid #777;margin-bottom:8px;padding:5px 0}.kb-app-cell-info .value{color:#000}.btn.batch-active{background-color:#f2efeb;border-color:#adadad;box-shadow:inset 0 3px 7px rgba(0,0,0,.125)}.btn.batch-active:hover{background-color:#ded5cb}.btn.kb-app-cell-btn{background-color:#fff;margin-bottom:4px}.btn.kb-app-cell-btn,.btn.kb-app-cell-btn.btn-primary{border:2px solid #2196f3;color:#2196f3}.btn.kb-app-cell-btn.btn-primary:hover{border-bottom:6px solid #2196f3;margin-bottom:0}.btn.kb-app-cell-btn.btn-primary.active,.btn.kb-app-cell-btn.btn-primary:active,.btn.kb-app-cell-btn.btn-primary:hover{background-color:#2196f3;color:#fff}.btn.kb-app-cell-btn.btn-primary.active{border:solid #2196f3;border-width:2px 2px 6px;margin-bottom:0}.btn.kb-app-cell-btn.btn-primary.active:hover{background-color:#ddd;border-bottom:2px solid #2196f3;color:#2196f3;margin-bottom:4px}.btn.kb-app-cell-btn.btn-danger{border:2px solid #d15241;color:#d15241}.btn.kb-app-cell-btn.btn-danger:hover{border-bottom:6px solid #d15241}.btn.kb-app-cell-btn.btn-danger.active,.btn.kb-app-cell-btn.btn-danger:hover{background-color:#d15241;color:#fff;margin-bottom:0}.btn.kb-app-cell-btn.btn-danger.active{border:solid #d15241;border-width:2px 2px 6px}.btn.kb-app-cell-btn.btn-danger.active:hover{background-color:#ddd;border-bottom:2px solid #d15241;color:#d15241;margin-bottom:4px}.btn.kb-app-cell-btn.disabled,.btn.kb-app-cell-btn.disabled:active,.btn.kb-app-cell-btn.disabled:hover{background-color:#fff;border:2px solid #888;color:#888;margin-bottom:4px}.kb-app-status-ok{color:#4bb856}.kb-app-status-warning{color:orange}.kb-app-status-danger,.kb-app-status-error{color:#d15241}.kb-app-status-default{color:#2196f3}.parameter-panel .info-panel{background:transparent;padding-top:4px}.tt-input,.tt-query{box-shadow:inset 0 1px 1px rgba(0,0,0,.075);width:100%}.tt-hint{color:#999}.tt-menu{background-color:#fff;border:1px solid rgba(0,0,0,.2);border-radius:4px;box-shadow:0 5px 10px rgba(0,0,0,.2);margin-top:4px;padding:4px 0;width:100%}.tt-suggestion{line-height:24px;padding:3px 20px}.tt-suggestion.tt-cursor,.tt-suggestion:hover{background-color:#0097cf;color:#fff}.tt-suggestion p{margin:0}.tt-header{font-size:75%;font-style:italic;padding-left:5px}.kb-app-results-tab{max-width:inherit;overflow-x:auto}.kb-editor-cell .kb-app-warning{font-family:Oxygen,Arial,sans-serif;text-align:center}.kb-editor-cell .kb-app-parameter-panel{border-left:3px solid #fff}.kb-editor-cell .kb-app-parameter-panel-hover{border-left:3px solid #428bca}.kb-editor-cell .kb-app-parameter-row{margin:0}.kb-editor-cell .kb-app-parameter-row-hover{background:#f9f9f9}.kb-editor-cell .kb-app-parameter-row .message{font-family:Oxygen,Arial,sans-serif;text-align:center}.kb-editor-cell .kb-app-parameter-row .message.-error{background:#f2dede;color:#f44336}.kb-editor-cell .kb-app-parameter-name{color:#777;font-family:Oxygen,Arial,sans-serif;padding-left:0;padding-right:4px;text-align:left;vertical-align:bottom;white-space:normal}.kb-editor-cell .kb-app-parameter-input{vertical-align:middle}.kb-editor-cell .kb-app-parameter-input select.form-control{margin:0}.kb-editor-cell .kb-app-parameter-input .kb-app-parameter-accepted-glyph,.kb-editor-cell .kb-app-parameter-input .kb-app-parameter-required-glyph{font-size:15px;margin-left:0}.kb-editor-cell .kb-app-parameter-required-glyph{color:#f44336}.kb-editor-cell .kb-parameter-data-selection{font-weight:700}.kb-editor-cell .kb-app-parameter-hint{color:#777;margin-top:3px;padding-left:7px;text-align:left}.kb-editor-cell .kb-app-parameter-accepted-glyph{color:#4bb856}.kb-editor-cell .kb-app-advanced-options-controller-inactive,.kb-editor-cell .kb-app-parameter-info,.kb-editor-cell .kb-parameter-data-row-add,.kb-editor-cell .kb-parameter-data-row-remove{color:#777}.kb-editor-cell .kb-app-advanced-options-controller,.kb-editor-cell .kb-app-advanced-options-controller-inactive{font:italic 700 13px/14px Oxygen,Arial,sans-serif;text-align:center}.kb-editor-cell .kb-app-advanced-options-controller{color:#037ac0;cursor:pointer}.kb-editor-cell .kb-app-advanced-options-controller:hover{color:#2a6496}.kb-editor-cell .kb-app-footer{background-color:#f5f5f5;overflow:none;padding:10px;width:100%}.kb-editor-cell .kb-app-subtitle{background-color:#f5f5f5;padding:3px 5px}.kb-editor-cell .advanced-parameter-showing{display:block}.kb-editor-cell .advanced-parameter-hidden{display:none}.kb-editor-cell [data-element=run-control-panel]{width:100%}.kb-editor-cell [data-element=tab-pane]{border-top:2px solid #2196f3}.kb-editor-cell [data-element=tab-pane]>div{padding:4px}.kb-editor-cell [data-element=tab-pane]>div:empty{padding:0}.kb-editor-cell .btn-primary.kb-btn-action.-rerun{color:#2196f3}.kb-editor-cell .btn-primary.kb-btn-action.-run{color:#4bb856}.kb-editor-cell .btn-danger.kb-btn-action.-cancel,.kb-editor-cell .btn-danger.kb-btn-action.-reset{color:#d15241}.unselected .kb-editor-cell{opacity:.5}.kb-editor-cell-info-desc{border:1px solid #777;margin-right:10px;min-height:100px;padding:10px}.kb-editor-cell-info{color:#777;font:16px Oxygen,Arial,sans-serif}.kb-editor-cell-info .header{border-bottom:1px solid #777;margin-bottom:8px;padding:5px 0}.kb-editor-cell-info .value{color:#000}.btn.kb-editor-cell-btn{background-color:#fff;margin-bottom:4px}.btn.kb-editor-cell-btn,.btn.kb-editor-cell-btn.btn-primary{border:2px solid #2196f3;color:#2196f3}.btn.kb-editor-cell-btn.btn-primary:hover{border-bottom:6px solid #2196f3;margin-bottom:0}.btn.kb-editor-cell-btn.btn-primary.active,.btn.kb-editor-cell-btn.btn-primary:active,.btn.kb-editor-cell-btn.btn-primary:hover{background-color:#2196f3;color:#fff}.btn.kb-editor-cell-btn.btn-primary.active{border:solid #2196f3;border-width:2px 2px 6px;margin-bottom:0}.btn.kb-editor-cell-btn.btn-primary.active:hover{background-color:#ddd;border-bottom:2px solid #2196f3;color:#2196f3;margin-bottom:4px}.btn.kb-editor-cell-btn.btn-danger{border:2px solid #d15241;color:#d15241}.btn.kb-editor-cell-btn.btn-danger:hover{background-color:#d15241;border-bottom:6px solid #d15241;color:#fff;margin-bottom:0}.btn.kb-editor-cell-btn.btn-danger.active{background-color:#d15241;border:solid #d15241;border-width:2px 2px 6px;color:#fff;margin-bottom:0}.btn.kb-editor-cell-btn.btn-danger.active:hover{background-color:#ddd;border-bottom:2px solid #d15241;color:#d15241;margin-bottom:4px}.btn.kb-editor-cell-btn.disabled,.btn.kb-editor-cell-btn.disabled:active,.btn.kb-editor-cell-btn.disabled:hover{border:2px solid #888;color:#888;margin-bottom:4px}.kb-app-params__message--advanced-hidden{font-style:italic;margin-left:6px}.kb-app-params__fields--parameters-hidden{display:none}.kb-bulk-import__layout_container{align-items:stretch;display:flex}.kb-bulk-import__prompt{align-items:stretch;display:flex;flex-direction:column}.kb-bulk-import__body{align-items:stretch;display:flex;flex:1;flex-direction:column;width:100%}.kb-bulk-import__tab_pane{align-content:stretch;align-items:stretch;border-top:2px solid #2196f3;display:flex;flex-direction:row}.kb-bulk-import__tab_pane_widget{width:100%}.kb-bulk-import-configure__container,.kb-bulk-import-info__container{display:flex}.kb-bulk-import-configure__panel--filetype,.kb-bulk-import-info__panel--filetype{background-color:#f2efeb;width:24rem}.kb-bulk-import-configure__panel--configure,.kb-bulk-import-configure__panel--info,.kb-bulk-import-info__panel--configure,.kb-bulk-import-info__panel--info{flex:1}.kb-field-widget__error_message--parameters{border:1px solid #d2232a}.kb-field-cell__param_container{margin:3px}.kb-field-cell__input_control select{margin-left:0}.kb-field-cell__cell_label{margin-bottom:0;padding-top:8px}.kb-field-cell__error_message,.kb-field-cell__error_message .form-control{border-color:#d2232a;color:#d2232a}.kb-field-cell__error_message .select2-selection{border-color:#d2232a}.kb-field-cell__error_message .select2-container--default .select2-selection--single .select2-selection__placeholder,.kb-field-cell__error_message .select2-container--default .select2-selection--single .select2-selection__rendered{color:#d2232a}.kb-field-cell__message_panel,.kb-field-cell__message_panel__duplicate{border:1px solid silver;padding:.5em}.kb-field-cell__message_panel__duplicate__error,.kb-field-cell__message_panel__error{background-color:#f9dadb;border-color:#eda7aa;color:#171412}.kb-field-cell__message_panel__duplicate__error__title,.kb-field-cell__message_panel__error__title{color:#d2232a}.kb-field-cell__message_panel__duplicate__warning,.kb-field-cell__message_panel__warning{background-color:#ffefac;border-color:#fddd49;color:#171412}.kb-field-cell__message_panel__duplicate__warning__title,.kb-field-cell__message_panel__warning__title{color:#8f7700}.kb-job-status-tab__container,.kb-result-tab__container{padding:1.5rem}.kb-cell-toolbar__button{border:0}.kb-cell-toolbar__container{align-items:flex-start;display:flex;height:56px;justify-content:space-between}.kb-cell-toolbar__app_icon{flex:none;font-size:14px;height:56px;line-height:56px;margin-right:4px;padding:0;text-align:center;width:56px}.kb-cell-toolbar__title-container{flex:1 1 10rem;text-overflow:ellipsis}.cell.unselected .kb-cell-toolbar__title-container{opacity:.5}.kb-cell-toolbar__title{color:#03517d;font:normal 700 18px/20px Oxygen,Arial,sans-serif;height:20px;margin-top:8px;min-width:0;text-overflow:ellipsis}.kb-cell-toolbar__subtitle{color:#6a6158;font:normal 400 14px/20px Oxygen,Arial,sans-serif;height:20px;min-width:0;text-overflow:ellipsis}.kb-cell-toolbar__buttons-container{flex:none;min-width:9.5rem;white-space:nowrap}.cell.unselected .kb-cell-toolbar__buttons-container{opacity:.2}.kb-cell-toolbar__icon--info{color:#65798c}.kb-cell-toolbar__icon--minus{color:#f78e1e}.kb-cell-toolbar__icon--notch-spin{color:#026daa}.kb-cell-toolbar__icon--outdated{color:#f78e1e}.kb-cell-toolbar__icon--plus,.kb-cell-toolbar__icon--table,.kb-cell-toolbar__icon--terminal{color:#000}.kb-cell-toolbar__icon--times,.kb-cell-toolbar__icon--triangle{color:#d12329}.kb-cell-toolbar__dropdown_item{border:0;text-align:left;width:100%}.kb-cell-toolbar__dropdown_item_icon{display:inline-block;margin-right:4px;width:25px}.kb-data-staging__breadcrumbs{margin:1rem 0 1.5rem}.kb-data-staging__breadcrumb_link{cursor:pointer}.kb-data-staging__container{height:604px;overflow-y:auto;padding:10px}.kb-data-staging__button{background-color:#dfeef6;border-color:transparent;color:#026daa;letter-spacing:.7px;margin-left:1rem;position:relative;top:-3px}.kb-data-staging__button.focus,.kb-data-staging__button:focus,.kb-data-staging__button:hover{background-color:#b7d9eb;border-color:transparent;color:#026daa}.kb-data-staging__button.active,.kb-data-staging__button:active,.open>.kb-data-staging__button.dropdown-toggle{background-color:#b7d9eb;background-image:none;border-color:transparent;color:#026daa}.kb-data-staging__button.active.focus,.kb-data-staging__button.active:focus,.kb-data-staging__button.active:hover,.kb-data-staging__button:active.focus,.kb-data-staging__button:active:focus,.kb-data-staging__button:active:hover,.open>.kb-data-staging__button.dropdown-toggle.focus,.open>.kb-data-staging__button.dropdown-toggle:focus,.open>.kb-data-staging__button.dropdown-toggle:hover{background-color:#9bcae3;border-color:transparent;color:#026daa}.kb-data-staging__button.disabled.focus,.kb-data-staging__button.disabled:focus,.kb-data-staging__button.disabled:hover,.kb-data-staging__button[disabled].focus,.kb-data-staging__button[disabled]:focus,.kb-data-staging__button[disabled]:hover,fieldset[disabled] .kb-data-staging__button.focus,fieldset[disabled] .kb-data-staging__button:focus,fieldset[disabled] .kb-data-staging__button:hover{background-color:#dfeef6;border-color:transparent}.kb-data-staging__button .badge{background-color:#026daa;color:#dfeef6}.kb-data-staging__title{font:normal 700 24px/30px Oxygen,Arial,sans-serif}.dz-clear-all__button{background-color:#dfeef6;border-color:transparent;color:#026daa}.dz-clear-all__button.focus,.dz-clear-all__button:focus,.dz-clear-all__button:hover{background-color:#b7d9eb;border-color:transparent;color:#026daa}.dz-clear-all__button.active,.dz-clear-all__button:active,.open>.dz-clear-all__button.dropdown-toggle{background-color:#b7d9eb;background-image:none;border-color:transparent;color:#026daa}.dz-clear-all__button.active.focus,.dz-clear-all__button.active:focus,.dz-clear-all__button.active:hover,.dz-clear-all__button:active.focus,.dz-clear-all__button:active:focus,.dz-clear-all__button:active:hover,.open>.dz-clear-all__button.dropdown-toggle.focus,.open>.dz-clear-all__button.dropdown-toggle:focus,.open>.dz-clear-all__button.dropdown-toggle:hover{background-color:#9bcae3;border-color:transparent;color:#026daa}.dz-clear-all__button.disabled.focus,.dz-clear-all__button.disabled:focus,.dz-clear-all__button.disabled:hover,.dz-clear-all__button[disabled].focus,.dz-clear-all__button[disabled]:focus,.dz-clear-all__button[disabled]:hover,fieldset[disabled] .dz-clear-all__button.focus,fieldset[disabled] .dz-clear-all__button:focus,fieldset[disabled] .dz-clear-all__button:hover{background-color:#dfeef6;border-color:transparent}.dz-clear-all__button .badge{background-color:#026daa;color:#dfeef6}.dz-file{border:1px solid #f2efeb;font:16px Oxygen,Arial,sans-serif;margin:2px 0;padding:5px 0;width:100%}.dz-file__progress__bar{margin-bottom:inherit;margin-left:5px}.dz-file__upload-progress{display:inline}.dz-file__row{align-items:center;display:flex}.dz-file__name{line-height:20px;white-space:nowrap}.dz-file__msg,.dz-file__name{overflow:hidden;text-overflow:ellipsis}.dz-file__msg{font-size:14px}.dz-file__msg__success{color:#4baf4f;display:none}.dz-file__status{align-items:center;display:none;font-weight:700}.dz-file__status__icon{margin:0 4px 0 8px}.dz-file__status__success{color:#4baf4f}.dz-file__status__error{color:#df0002}.kb-dropzone{border:2px dashed #2196f3!important;margin-bottom:5px;max-height:150px;overflow-y:auto}.kb-dropzone-progress__header{border-bottom:1px solid #f2efeb;display:none;margin-bottom:5px}.kb-dropzone .dz-message{color:#000;font:normal 400 24px/28px Oxygen,Arial,sans-serif;margin:2em 4.5em;mix-blend-mode:normal;text-align:center}.kb-dropzone__message--upload{font-family:Oxygen,Arial,sans-serif;font-weight:700}.kb-error-display__container{padding:1.5rem}.kb-error-display__container.kb-log__error_container{padding:0}.kb-error-display__detail_title,.kb-error-display__stacktrace_title,.kb-error-display__summary{font:normal 700 18px/30px Oxygen,Arial,sans-serif;margin-top:1rem}.kb-app-cell-tab-pane__container--error .kb-error-display__summary{margin-top:0}.kb-error-display__stacktrace_code{word-wrap:break-word;background-color:#f2efeb;border:0;font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:14px;font-weight:400;line-height:24px;margin:0;max-height:50rem;overflow-wrap:break-word;overflow-y:auto;padding:1rem;word-break:break-word}.kb-error-display__advice_list{list-style:none;padding-left:0}.access-error__container,.generic-error__container{margin:2rem;text-align:center}.access-error__heading,.generic-error__heading{font-size:200%;font-weight:700;line-height:1.5}.access-error__text,.generic-error__text{font-size:150%;line-height:2}.access-request-form__container{margin-top:2rem}.access-request-progress__container{display:none;margin-top:1rem}.access-request-result{font-size:125%;margin-top:2rem}.error-dialog__body{margin:1rem}.kb-file-path__list{counter-reset:fpw-item;list-style-position:outside;list-style-type:none;padding:0}.kb-file-path__list_item{align-items:center;display:flex;font:normal 400 14px/18px Oxygen,Arial,sans-serif;padding:0 1.5rem .5rem}.kb-file-path__list_item:nth-child(2n){background-color:#f2efeb}.kb-file-path__list_item:before{content:counter(fpw-item) " ";counter-increment:fpw-item;font-weight:700;padding-right:.5rem;padding-top:20px;vertical-align:middle}.kb-file-path__param_container{display:flex;flex:1;flex-wrap:wrap}.kb-file-path__param_container .input-group .select2-container{display:table;table-layout:fixed}.kb-file-path__row_cell--file-path_id{flex:1;min-width:30rem;padding:4px}.kb-file-path__button--delete{background-color:#dfeef6;border-color:transparent;color:#026daa;margin-top:2rem}.kb-file-path__button--delete.focus,.kb-file-path__button--delete:focus,.kb-file-path__button--delete:hover{background-color:#b7d9eb;border-color:transparent;color:#026daa}.kb-file-path__button--delete.active,.kb-file-path__button--delete:active,.open>.kb-file-path__button--delete.dropdown-toggle{background-color:#b7d9eb;background-image:none;border-color:transparent;color:#026daa}.kb-file-path__button--delete.active.focus,.kb-file-path__button--delete.active:focus,.kb-file-path__button--delete.active:hover,.kb-file-path__button--delete:active.focus,.kb-file-path__button--delete:active:focus,.kb-file-path__button--delete:active:hover,.open>.kb-file-path__button--delete.dropdown-toggle.focus,.open>.kb-file-path__button--delete.dropdown-toggle:focus,.open>.kb-file-path__button--delete.dropdown-toggle:hover{background-color:#9bcae3;border-color:transparent;color:#026daa}.kb-file-path__button--delete.disabled.focus,.kb-file-path__button--delete.disabled:focus,.kb-file-path__button--delete.disabled:hover,.kb-file-path__button--delete[disabled].focus,.kb-file-path__button--delete[disabled]:focus,.kb-file-path__button--delete[disabled]:hover,fieldset[disabled] .kb-file-path__button--delete.focus,fieldset[disabled] .kb-file-path__button--delete:focus,fieldset[disabled] .kb-file-path__button--delete:hover{background-color:#dfeef6;border-color:transparent}.kb-file-path__button--delete .badge{background-color:#026daa;color:#dfeef6}.kb-file-path__button--add_row{background-color:#dfeef6;border-color:transparent;color:#026daa;margin:1rem}.kb-file-path__button--add_row.focus,.kb-file-path__button--add_row:focus,.kb-file-path__button--add_row:hover{background-color:#b7d9eb;border-color:transparent;color:#026daa}.kb-file-path__button--add_row.active,.kb-file-path__button--add_row:active,.open>.kb-file-path__button--add_row.dropdown-toggle{background-color:#b7d9eb;background-image:none;border-color:transparent;color:#026daa}.kb-file-path__button--add_row.active.focus,.kb-file-path__button--add_row.active:focus,.kb-file-path__button--add_row.active:hover,.kb-file-path__button--add_row:active.focus,.kb-file-path__button--add_row:active:focus,.kb-file-path__button--add_row:active:hover,.open>.kb-file-path__button--add_row.dropdown-toggle.focus,.open>.kb-file-path__button--add_row.dropdown-toggle:focus,.open>.kb-file-path__button--add_row.dropdown-toggle:hover{background-color:#9bcae3;border-color:transparent;color:#026daa}.kb-file-path__button--add_row.disabled.focus,.kb-file-path__button--add_row.disabled:focus,.kb-file-path__button--add_row.disabled:hover,.kb-file-path__button--add_row[disabled].focus,.kb-file-path__button--add_row[disabled]:focus,.kb-file-path__button--add_row[disabled]:hover,fieldset[disabled] .kb-file-path__button--add_row.focus,fieldset[disabled] .kb-file-path__button--add_row:focus,fieldset[disabled] .kb-file-path__button--add_row:hover{background-color:#dfeef6;border-color:transparent}.kb-file-path__button--add_row .badge{background-color:#026daa;color:#dfeef6}.kb-file-path__button_icon--add_row{margin-right:.5rem}.kb-file-path__params{margin-left:.5rem}.kb-filetype-panel__filetype_button,.kb-filetype-panel__filetype_button--selected,.kb-filetype-panel__filetype_button:hover:not(.kb-filetype-panel__filetype_button--selected){background-color:inherit;border:solid silver;border-width:1px 0 0;display:flex;flex-direction:row;padding:8px;text-align:left;width:100%}.kb-filetype-panel__header{border-bottom:1px solid silver;font:normal 700 18px/30px Oxygen,Arial,sans-serif;padding:8px}.kb-filetype-panel__header_icon{color:#000;padding:8px 6px 8px 8px}.kb-filetype-panel__filetype_button{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.428571429}.kb-filetype-panel__filetype_button:hover:not(.kb-filetype-panel__filetype_button--selected){background-color:#ded5cb;cursor:pointer}.kb-filetype-panel__filetype_button--selected{background-color:#fff;box-shadow:0 3px 2px -2px #6a6158}.kb-filetype-panel__filetype_button--selected+.kb-bulk-import__filetype-panel__filetype_button{box-shadow:inset 0 3px 2px -2px #6a6158}.kb-filetype-panel__filetype_icon{padding:2px 8px 0;text-align:center;width:3rem}.kb-filetype-panel__filetype_icon--complete{color:#5e9732}.kb-filetype-panel__filetype_icon--incomplete{color:#d12329}.kb-filetype-panel__filetype_label{flex:1}.kb-fsm__key{margin-left:3px}.kb-fsm__value{background-color:#6a6158;color:#fff;margin:0 3px;padding:2px}.kb-info-tab__container{padding:1.5rem}.kb-info-tab__title{line-height:30px}.kb-info-tab__name{font:normal 700 18px/30px Oxygen,Arial,sans-serif}.kb-info-tab__version{padding:0 1rem}.kb-info-tab__tag{padding:.5rem;vertical-align:middle}.kb-info-tab__authors,.kb-info-tab__description,.kb-info-tab__link--docs,.kb-info-tab__list--params{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.428571429;margin:1rem 0}.kb-info-tab__list_title--params{font:normal 700 16px/24px Oxygen,Arial,sans-serif;margin:1rem 0}ul.kb-info-tab__list--params{padding-left:2rem}.kb-job-action__dropdown_header{font-weight:700;text-transform:uppercase}.kb-job-action__dropdown-menu{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px;line-height:2.5;padding:0}.kb-job-action__dropdown-menu-item-link--cancel,.kb-job-action__dropdown-menu-item-link--retry{background:#fff;border:0;border-bottom:1px solid rgba(0,0,0,.15);font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px;line-height:2.5;text-align:left;width:100%}.kb-job-action__dropdown-menu-item-link--cancel:disabled,.kb-job-action__dropdown-menu-item-link--retry:disabled{color:#6a6158;cursor:not-allowed}.kb-job-action__dropdown-menu-item-link--cancel:active:disabled,.kb-job-action__dropdown-menu-item-link--cancel:hover:disabled,.kb-job-action__dropdown-menu-item-link--retry:active:disabled,.kb-job-action__dropdown-menu-item-link--retry:hover:disabled{background:#9d9389;color:#fff}.kb-job-action__dropdown-menu-item-link--retry{color:#026daa}.kb-job-action__dropdown-menu-item-link--retry:active,.kb-job-action__dropdown-menu-item-link--retry:hover{background:#026daa;color:#fff}.kb-job-action__dropdown-menu-item-link--cancel{color:#b6151c}.kb-job-action__dropdown-menu-item-link--cancel:active,.kb-job-action__dropdown-menu-item-link--cancel:hover{background:#d2232a;color:#fff}.kb-log__log_line_container,.kb-log__log_line_container--error{counter-reset:code-list-counter;list-style:none;margin:0;padding:0}.kb-log__line_text,.kb-log__line_text--error,.kb-log__log_line_container,.kb-log__log_line_container--error{font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:14px;font-weight:400;line-height:24px}.kb-log__line_text,.kb-log__line_text--error{counter-increment:code-list-counter;padding-left:5rem;position:relative;white-space:normal}.kb-log__container,.kb-log__dev_container{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px}.kb-log__dev_container{padding-bottom:2rem}.kb-log__controls{margin:2rem 0}.kb-log__content,.kb-log__content--expanded{background-color:#f2efeb;border:0;margin:0;overflow:scroll;padding:0;transition:height .5s}.kb-log__content{max-height:288px}.kb-log__content--expanded{max-height:720px}.kb-log__log_line_container--error{background-color:#f9dadb}.kb-log__line_text--error:before,.kb-log__line_text:before{content:counter(code-list-counter);left:1rem;position:absolute}.kb-log__line_text{background-color:#f2efeb;color:#171412}.kb-log__line_text--error{background-color:#f9dadb;color:#7c0e12}.kb-log__logs_title{font:normal 700 18px/30px Oxygen,Arial,sans-serif;margin-top:1rem}.kb-log__spinner{border:0;line-height:1.428571429;margin:0;padding:.5rem 1rem}.kb-job-params__params_container,.kb-log__spinner{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400}.kb-job-params__params_container{line-height:24px}.kb-job-params__params_title{font:normal 700 18px/30px Oxygen,Arial,sans-serif;margin-top:1rem}.kb-job-params__param_list{list-style-type:none;margin:1rem 0;padding:0}.kb-job-params__param_list,.kb-job-state-viewer__container{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px}.kb-job-state-viewer__job_status_detail_container div:first-of-type{font-weight:700}.kb-job-state-viewer__error_container.kb-error-display__container{padding:0}.kb-job-status{box-shadow:none}.kb-job-status__table{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.428571429;padding:0;table-layout:fixed;width:100%}.kb-job-status__table.dataTable{border-collapse:collapse!important}.kb-job-status__table_body{border-bottom:2px solid #9d9389;border-top:2px solid #9d9389}.kb-job-status__table_body .kb-job-status__row,.kb-job-status__table_body tr.even,.kb-job-status__table_body tr.odd{border-bottom:1px solid #f2efeb}.kb-job-status__table__row--selected,.kb-job-status__table_body .kb-job-status__row:hover,.kb-job-status__table_body tr.even:hover,.kb-job-status__table_body tr.odd:hover{background:#f2efeb}.kb-job-status__table__row--selected:hover{background:#cce5f3}.kb-job-status__table_head_row{font-weight:700}.kb-job-status__table_head_row .sorting:after,.kb-job-status__table_head_row .sorting_asc:after,.kb-job-status__table_head_row .sorting_desc:after{bottom:0!important;display:inline-block!important;left:5px!important;position:relative!important}.kb-job-status__table_head_cell--action,.kb-job-status__table_head_cell--import-type,.kb-job-status__table_head_cell--output,.kb-job-status__table_head_cell--status{border:0;margin:0;padding:.8rem}.kb-job-status__table_footer{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px}.kb-job-status__cell--action,.kb-job-status__cell--import-type,.kb-job-status__cell--output,.kb-job-status__cell--status{border:0;margin:0;overflow:hidden;padding:.8rem;text-overflow:ellipsis}.kb-job-status__cell--status{text-transform:capitalize;width:10rem}.kb-job-status__cell--action{width:11rem}.kb-job-status__cell--import-type{width:18rem}.kb-job-status__cell_action--go-to-results,.kb-job-status__cell_action--retry{background-color:#dfeef6;border-color:transparent;color:#026daa}.kb-job-status__cell_action--go-to-results.focus,.kb-job-status__cell_action--go-to-results:focus,.kb-job-status__cell_action--go-to-results:hover,.kb-job-status__cell_action--retry.focus,.kb-job-status__cell_action--retry:focus,.kb-job-status__cell_action--retry:hover{background-color:#b7d9eb;border-color:transparent;color:#026daa}.kb-job-status__cell_action--go-to-results.active,.kb-job-status__cell_action--go-to-results:active,.kb-job-status__cell_action--retry.active,.kb-job-status__cell_action--retry:active,.open>.kb-job-status__cell_action--go-to-results.dropdown-toggle,.open>.kb-job-status__cell_action--retry.dropdown-toggle{background-color:#b7d9eb;background-image:none;border-color:transparent;color:#026daa}.kb-job-status__cell_action--go-to-results.active.focus,.kb-job-status__cell_action--go-to-results.active:focus,.kb-job-status__cell_action--go-to-results.active:hover,.kb-job-status__cell_action--go-to-results:active.focus,.kb-job-status__cell_action--go-to-results:active:focus,.kb-job-status__cell_action--go-to-results:active:hover,.kb-job-status__cell_action--retry.active.focus,.kb-job-status__cell_action--retry.active:focus,.kb-job-status__cell_action--retry.active:hover,.kb-job-status__cell_action--retry:active.focus,.kb-job-status__cell_action--retry:active:focus,.kb-job-status__cell_action--retry:active:hover,.open>.kb-job-status__cell_action--go-to-results.dropdown-toggle.focus,.open>.kb-job-status__cell_action--go-to-results.dropdown-toggle:focus,.open>.kb-job-status__cell_action--go-to-results.dropdown-toggle:hover,.open>.kb-job-status__cell_action--retry.dropdown-toggle.focus,.open>.kb-job-status__cell_action--retry.dropdown-toggle:focus,.open>.kb-job-status__cell_action--retry.dropdown-toggle:hover{background-color:#9bcae3;border-color:transparent;color:#026daa}.kb-job-status__cell_action--go-to-results.disabled.focus,.kb-job-status__cell_action--go-to-results.disabled:focus,.kb-job-status__cell_action--go-to-results.disabled:hover,.kb-job-status__cell_action--go-to-results[disabled].focus,.kb-job-status__cell_action--go-to-results[disabled]:focus,.kb-job-status__cell_action--go-to-results[disabled]:hover,.kb-job-status__cell_action--retry.disabled.focus,.kb-job-status__cell_action--retry.disabled:focus,.kb-job-status__cell_action--retry.disabled:hover,.kb-job-status__cell_action--retry[disabled].focus,.kb-job-status__cell_action--retry[disabled]:focus,.kb-job-status__cell_action--retry[disabled]:hover,fieldset[disabled] .kb-job-status__cell_action--go-to-results.focus,fieldset[disabled] .kb-job-status__cell_action--go-to-results:focus,fieldset[disabled] .kb-job-status__cell_action--go-to-results:hover,fieldset[disabled] .kb-job-status__cell_action--retry.focus,fieldset[disabled] .kb-job-status__cell_action--retry:focus,fieldset[disabled] .kb-job-status__cell_action--retry:hover{background-color:#dfeef6;border-color:transparent}.kb-job-status__cell_action--go-to-results .badge,.kb-job-status__cell_action--retry .badge{background-color:#026daa;color:#dfeef6}.kb-job-status__cell_action--cancel{background-color:#f9dadb;border-color:transparent;color:#d2232a}.kb-job-status__cell_action--cancel.focus,.kb-job-status__cell_action--cancel:focus,.kb-job-status__cell_action--cancel:hover{background-color:#f2aeb0;border-color:transparent;color:#d2232a}.kb-job-status__cell_action--cancel.active,.kb-job-status__cell_action--cancel:active,.open>.kb-job-status__cell_action--cancel.dropdown-toggle{background-color:#f2aeb0;background-image:none;border-color:transparent;color:#d2232a}.kb-job-status__cell_action--cancel.active.focus,.kb-job-status__cell_action--cancel.active:focus,.kb-job-status__cell_action--cancel.active:hover,.kb-job-status__cell_action--cancel:active.focus,.kb-job-status__cell_action--cancel:active:focus,.kb-job-status__cell_action--cancel:active:hover,.open>.kb-job-status__cell_action--cancel.dropdown-toggle.focus,.open>.kb-job-status__cell_action--cancel.dropdown-toggle:focus,.open>.kb-job-status__cell_action--cancel.dropdown-toggle:hover{background-color:#ed8f92;border-color:transparent;color:#d2232a}.kb-job-status__cell_action--cancel.disabled.focus,.kb-job-status__cell_action--cancel.disabled:focus,.kb-job-status__cell_action--cancel.disabled:hover,.kb-job-status__cell_action--cancel[disabled].focus,.kb-job-status__cell_action--cancel[disabled]:focus,.kb-job-status__cell_action--cancel[disabled]:hover,fieldset[disabled] .kb-job-status__cell_action--cancel.focus,fieldset[disabled] .kb-job-status__cell_action--cancel:focus,fieldset[disabled] .kb-job-status__cell_action--cancel:hover{background-color:#f9dadb;border-color:transparent}.kb-job-status__cell_action--cancel .badge{background-color:#d2232a;color:#f9dadb}.kb-job-status__param_list{font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:24px;list-style-type:none;margin:1rem 0;padding:0}.kb-job-status__detail_container{background-color:#e7eff8;border-bottom:1px solid #f2efeb;padding:1rem}.kb-job-status__icon--action_warning{color:#d2232a;font-size:1.5em;padding:.5rem 1rem;vertical-align:middle}.kb-job-status__icon--created{color:#a1b9cf;margin:4px}.kb-job-status__summary--created{color:#a1b9cf;font-weight:700}.kb-job-status__cell_summary--created{color:#a1b9cf;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--estimating{color:#a1b9cf;margin:4px}.kb-job-status__summary--estimating{color:#a1b9cf;font-weight:700}.kb-job-status__cell_summary--estimating{color:#a1b9cf;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--queued{color:#a1b9cf;margin:4px}.kb-job-status__summary--queued{color:#a1b9cf;font-weight:700}.kb-job-status__cell_summary--queued{color:#a1b9cf;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--running{color:#a1b9cf;margin:4px}.kb-job-status__summary--running{color:#a1b9cf;font-weight:700}.kb-job-status__cell_summary--running{color:#a1b9cf;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--completed{color:#0a6258;margin:4px}.kb-job-status__summary--completed{color:#0a6258;font-weight:700}.kb-job-status__cell_summary--completed{color:#0a6258;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--terminated{color:#b99902;margin:4px}.kb-job-status__summary--terminated{color:#b99902;font-weight:700}.kb-job-status__cell_summary--terminated{color:#b99902;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--error{color:#d2232a;margin:4px}.kb-job-status__summary--error{color:#d2232a;font-weight:700}.kb-job-status__cell_summary--error{color:#d2232a;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-job-status__icon--does_not_exist{color:#d2232a;margin:4px}.kb-job-status__summary--does_not_exist{color:#d2232a;font-weight:700}.kb-job-status__cell_summary--does_not_exist{color:#d2232a;font-size:16px;font-weight:700;margin:0 .5rem;text-transform:capitalize}.kb-icon__container--image{display:block}.kb-icon__container--data,.kb-icon__container--data-stack{cursor:pointer}.kb-icon__container--app,.kb-icon__container--app-toolbar,.kb-icon__container--data,.kb-icon__container--generic,.kb-icon__container--generic-toolbar,.kb-icon__container--type,.kb-icon__container--type-toolbar{font-size:2em}.kb-icon__container--data-stack{font-size:1.7em}.kb-icon__icon_background--app,.kb-icon__icon_background--app-toolbar{color:#66489d}.kb-icon__icon_background--generic,.kb-icon__icon_background--generic-toolbar{color:silver}.kb-icon__icon_background--type,.kb-icon__icon_background--type-toolbar{color:#000}.kb-icon__img--image{margin:3px;max-height:50px;max-width:50px}.kb-icon__outline--l1{margin-left:1px;margin-top:1px}.kb-icon__stack--l1{margin-left:8px;margin-top:6px}.kb-icon__outline--l2{margin-left:9px;margin-top:7px}.kb-icon__stack--l2{margin-left:16px;margin-top:12px}.kb-icon__outline--l1,.kb-icon__outline--l2{color:#fff}.kb-data-list-logo{background-color:#607d8b;border:0 solid #555;border-radius:50%;color:#fff;display:inline-block;font-size:24px;font-weight:700;height:40px;padding-top:8px;text-align:center;text-shadow:-1px 0 #777,0 1px #777,1px 0 #777,0 -1px #777;width:40px}.kb-loading-blocker__text,.kb-loading-blocker__text--warning{font-size:16px;line-height:1.5;margin-left:30%;text-align:left;width:40%}.kb-loading-blocker__container{background-color:#fff;height:100%;left:0;position:absolute;top:0;width:100%;z-index:9999}.kb-loading-blocker__header{font-size:24px;font-weight:700;text-align:center}.kb-loading-blocker__text--warning{display:none;position:absolute}.kb-loading-blocker__image_container{padding-top:10%;text-align:center}.kb-narr-outline{padding:.5em .5em .5em .25em}.kb-narr-outline ul{list-style:none;padding:0;position:relative}.kb-narr-outline ul ul{padding-left:1.5em}.kb-narr-outline ul li:not(:last-of-type):before{border-left:2px solid #ded5cb;content:"";height:calc(100% + .25em);left:-.5em;position:absolute;top:0}.kb-narr-outline li{position:relative}.kb-narr-outline__item{align-items:center;cursor:pointer;display:flex;flex-flow:row nowrap;justify-content:left;left:0;padding:.1em .1em .3em;position:relative;top:0}.kb-narr-outline__item:before{background-color:transparent;border:1px solid transparent;border-radius:.333em;bottom:.2em;content:"";position:absolute;top:0;transition:background-color .1s linear,border .1s linear;width:100%;z-index:-1}.kb-narr-outline__item:after{border-bottom:2px solid #ded5cb;border-left:2px solid #ded5cb;bottom:calc(50% + 1px);content:"";height:calc(50% - .25em);left:-.5em;position:absolute;width:.5em}.kb-narr-outline__item-content{display:block;flex-grow:0;flex-shrink:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kb-narr-outline__item-icon{display:inline-block;transform:scale(.5);transform-origin:top left}.kb-narr-outline__item-icon-wrapper{border-radius:10px;display:block;flex-grow:0;flex-shrink:0;height:2em;margin-left:.1em;margin-right:.5em;max-width:2em;overflow:hidden}.kb-narr-outline__item--highlight:before{background-color:#f2efeb;border:1px solid #9d9389}.kb-narr-outline__item--highlight-selected:before{background-color:rgba(75,184,86,.15);border:1px solid #4bb856}.kb-narr-outline ul ul .kb-narr-outline__item:after{border-bottom:2px solid #ded5cb;border-left:2px solid #ded5cb;bottom:calc(50% + 1px);content:"";height:calc(50% - .25em);left:-.5em;position:absolute;width:.5em}.kb-rcp__layout_div{border-top:1px solid silver;display:flex;flex-direction:row;height:50px;position:relative}.kb-rcp__toolbar{flex:none;height:50px;line-height:50px}.kb-rcp__btn-toolbar{display:inline-block;line-height:50px;padding-right:.5rem;vertical-align:bottom}.kb-rcp__action-button{float:left;margin:6px;width:8rem}.kb-rcp__action-button-container{flex:none;height:50px;line-height:50px;overflow:hidden}.kb-rcp-status__container{flex:1 1 auto;font-size:14px;line-height:50px;margin:0 1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kb-rcp-status__fsm_display{background:#fff;display:inline-block;font:12px/1 Oxygen,Arial,sans-serif;margin-top:-30px;padding:6px 10px;position:absolute;top:0}.kb-rcp__tab-button{float:left;margin-left:.5rem}a.kb-rcp__tab-button--outdated{color:#ffd200;float:left;margin-left:.5rem;padding:6px 0 0}a.kb-rcp__tab-button--outdated:active,a.kb-rcp__tab-button--outdated:hover{color:#b99902}.kb-report-view__warning__count{margin:5px}.kb-report-view__warning__container{margin:0 5px 5px 10px;max-height:100px;overflow-y:auto}.kb-report-view__warning__text{margin:0 5px 5px 10px}.kb-report-view__summary{color:#544c45;font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:14px;font-weight:400;height:auto;line-height:24px;max-height:500px;overflow:auto;white-space:pre-wrap;width:100%}.kb-report-view__download-iframe{display:none}.kb-report-view__download_button{cursor:pointer}.kb-report-view__report_iframe{display:block;height:auto;margin:0;padding:0;width:100%}.kb-report-view__report_button{margin:4px 4px 8px 0}.kb-output-widget__object_link{cursor:pointer}.kb-output-widget__table.dataTable{width:100%}.kb-report__container{margin:-1.5rem}.kb-report__item_container{border-bottom:1px solid #ded5cb;padding:0 1.5rem}.kb-report__item_toggle{color:#037ac0;font:normal 700 18px/30px Oxygen,Arial,sans-serif;margin:0 -1rem;padding:.5rem 0}.kb-report__item_toggle:hover{color:#2a6496}.kb-select2-object-input__item,.kb-select2-taxonomy-ref__item{display:block;font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;height:20px;line-height:1.428571429}.kb-select2-object-input__object{word-wrap:break-word;font-family:Oxygen,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.428571429}.kb-select2-object-input__object_name{font-weight:700}.kb-select2-object-input__object_type{font-style:italic}.kb-select2-object-input__object_details{margin-left:1rem}.kb-select2-object-input__object_narrative,.kb-select2-object-input__object_type,.kb-select2-object-input__object_updated{display:block}.kb-staging-table{table-layout:fixed}.kb-staging-table .kb-staging-table-body td{vertical-align:middle}.kb-staging-table .kb-staging-table-header .sorting:before,.kb-staging-table .kb-staging-table-header .sorting_asc:before,.kb-staging-table .kb-staging-table-header .sorting_desc:before{content:"\e150";display:inline-block;font-family:Glyphicons Halflings;opacity:.2;position:relative;right:4px;top:1px}.kb-staging-table .kb-staging-table-header .sorting_asc:before{content:"\e155"}.kb-staging-table .kb-staging-table-header .sorting_desc:before{content:"\e156"}.kb-staging-table .kb-staging-table-header .sorting:after,.kb-staging-table .kb-staging-table-header .sorting_asc:after,.kb-staging-table .kb-staging-table-header .sorting_desc:after{display:none}.kb-staging-table .kb-staging-table-header .kb-staging-table-header__checkbox{padding-right:0;width:2rem}.kb-staging-table .kb-staging-table-header .kb-staging-table-header__file{padding-right:0;width:4rem}.kb-staging-table .kb-staging-table-header .kb-staging-table-header__file.sorting:before,.kb-staging-table .kb-staging-table-header .kb-staging-table-header__file.sorting_asc:before,.kb-staging-table .kb-staging-table-header .kb-staging-table-header__file.sorting_desc:before{left:9px}.kb-staging-table .kb-staging-table-header .kb-staging-table-header__age,.kb-staging-table .kb-staging-table-header .kb-staging-table-header__size{width:7rem}.kb-staging-table .kb-staging-table-header .kb-staging-table-header__import{width:24rem}.kb-staging-table-body__cell--expander{text-align:center}.kb-staging-table-body__cell--import{text-align:right;white-space:nowrap}.kb-staging-table-body__button--decompress{background:transparent;border-radius:0;font-size:12px;line-height:12px;padding:.2rem}.kb-staging-table-body__button--download{background-color:#026daa;border-color:transparent;border-radius:0;color:#fff;font-size:12px;line-height:12px;margin-left:.5rem;padding:.5rem}.kb-staging-table-body__button--download.focus,.kb-staging-table-body__button--download:focus,.kb-staging-table-body__button--download:hover{background-color:#014d78;border-color:transparent;color:#fff}.kb-staging-table-body__button--download.active,.kb-staging-table-body__button--download:active,.open>.kb-staging-table-body__button--download.dropdown-toggle{background-color:#014d78;background-image:none;border-color:transparent;color:#fff}.kb-staging-table-body__button--download.active.focus,.kb-staging-table-body__button--download.active:focus,.kb-staging-table-body__button--download.active:hover,.kb-staging-table-body__button--download:active.focus,.kb-staging-table-body__button--download:active:focus,.kb-staging-table-body__button--download:active:hover,.open>.kb-staging-table-body__button--download.dropdown-toggle.focus,.open>.kb-staging-table-body__button--download.dropdown-toggle:focus,.open>.kb-staging-table-body__button--download.dropdown-toggle:hover{background-color:#013654;border-color:transparent;color:#fff}.kb-staging-table-body__button--download.disabled.focus,.kb-staging-table-body__button--download.disabled:focus,.kb-staging-table-body__button--download.disabled:hover,.kb-staging-table-body__button--download[disabled].focus,.kb-staging-table-body__button--download[disabled]:focus,.kb-staging-table-body__button--download[disabled]:hover,fieldset[disabled] .kb-staging-table-body__button--download.focus,fieldset[disabled] .kb-staging-table-body__button--download:focus,fieldset[disabled] .kb-staging-table-body__button--download:hover{background-color:#026daa;border-color:transparent}.kb-staging-table-body__button--download .badge{background-color:#fff;color:#026daa}.kb-staging-table-body__button--delete,.kb-staging-table-body__button--folder{background-color:transparent;border-color:transparent;border-radius:0;color:#026daa;font-size:18px;line-height:12px;padding:.5rem}.kb-staging-table-body__button--delete.focus,.kb-staging-table-body__button--delete:focus,.kb-staging-table-body__button--delete:hover,.kb-staging-table-body__button--folder.focus,.kb-staging-table-body__button--folder:focus,.kb-staging-table-body__button--folder:hover{background-color:transparent;border-color:transparent;color:#026daa}.kb-staging-table-body__button--delete.active,.kb-staging-table-body__button--delete:active,.kb-staging-table-body__button--folder.active,.kb-staging-table-body__button--folder:active,.open>.kb-staging-table-body__button--delete.dropdown-toggle,.open>.kb-staging-table-body__button--folder.dropdown-toggle{background-color:transparent;background-image:none;border-color:transparent;color:#026daa}.kb-staging-table-body__button--delete.active.focus,.kb-staging-table-body__button--delete.active:focus,.kb-staging-table-body__button--delete.active:hover,.kb-staging-table-body__button--delete:active.focus,.kb-staging-table-body__button--delete:active:focus,.kb-staging-table-body__button--delete:active:hover,.kb-staging-table-body__button--folder.active.focus,.kb-staging-table-body__button--folder.active:focus,.kb-staging-table-body__button--folder.active:hover,.kb-staging-table-body__button--folder:active.focus,.kb-staging-table-body__button--folder:active:focus,.kb-staging-table-body__button--folder:active:hover,.open>.kb-staging-table-body__button--delete.dropdown-toggle.focus,.open>.kb-staging-table-body__button--delete.dropdown-toggle:focus,.open>.kb-staging-table-body__button--delete.dropdown-toggle:hover,.open>.kb-staging-table-body__button--folder.dropdown-toggle.focus,.open>.kb-staging-table-body__button--folder.dropdown-toggle:focus,.open>.kb-staging-table-body__button--folder.dropdown-toggle:hover{background-color:transparent;border-color:transparent;color:#026daa}.kb-staging-table-body__button--delete.disabled.focus,.kb-staging-table-body__button--delete.disabled:focus,.kb-staging-table-body__button--delete.disabled:hover,.kb-staging-table-body__button--delete[disabled].focus,.kb-staging-table-body__button--delete[disabled]:focus,.kb-staging-table-body__button--delete[disabled]:hover,.kb-staging-table-body__button--folder.disabled.focus,.kb-staging-table-body__button--folder.disabled:focus,.kb-staging-table-body__button--folder.disabled:hover,.kb-staging-table-body__button--folder[disabled].focus,.kb-staging-table-body__button--folder[disabled]:focus,.kb-staging-table-body__button--folder[disabled]:hover,fieldset[disabled] .kb-staging-table-body__button--delete.focus,fieldset[disabled] .kb-staging-table-body__button--delete:focus,fieldset[disabled] .kb-staging-table-body__button--delete:hover,fieldset[disabled] .kb-staging-table-body__button--folder.focus,fieldset[disabled] .kb-staging-table-body__button--folder:focus,fieldset[disabled] .kb-staging-table-body__button--folder:hover{background-color:transparent;border-color:transparent}.kb-staging-table-body__button--delete .badge,.kb-staging-table-body__button--folder .badge{background-color:#026daa;color:transparent}.kb-staging-table-body__button--delete:active,.kb-staging-table-body__button--delete:hover,.kb-staging-table-body__button--folder:active,.kb-staging-table-body__button--folder:hover{color:#03517d}.kb-staging-table-body__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kb-staging-table-body__folder:hover{cursor:pointer;text-decoration:underline}.kb-staging-table-body__select_container{align-items:center;display:flex;justify-content:flex-end;text-align:left}.kb-staging-table-body .select2{margin:4px 0;width:19rem!important}.kb-staging-table-body .kb-staging-table-body__import-dropdown{align-items:center;background:#fff;border:1px solid #b6151c;border-radius:4px;box-sizing:border-box;display:flex;height:38px}.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__rendered{color:#171412;flex:1 1 auto;font-size:14px;line-height:20px;padding:8px 20px 8px 8px}.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__placeholder{color:#b6151c}.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__arrow{margin-top:4px}.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-selection__arrow b{border-color:#b6151c transparent transparent}.kb-staging-table-body .kb-staging-table-body__import-dropdown.kb-staging-table-body__import-type-selected .select2-selection__arrow b,.kb-staging-table-body .kb-staging-table-body__import-dropdown:focus .select2-selection__arrow b{border-color:#9d9389 transparent transparent}.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-container--focus .select2-selection__placeholder,.kb-staging-table-body .kb-staging-table-body__import-dropdown .select2-container--open .select2-selection__placeholder{color:#171412}.kb-staging-table-body .kb-staging-table-body__import-dropdown.kb-staging-table-body__import-type-selected,.kb-staging-table-body .select2-container--focus .kb-staging-table-body__import-dropdown,.kb-staging-table-body .select2-container--open .kb-staging-table-body__import-dropdown{border:1px solid silver}.kb-staging-table-file-metadata .tab-pane{margin:1rem}.kb-staging-table-file-metadata__def_list{display:flex;flex-wrap:wrap;margin:1rem;padding:0;width:100%}.kb-staging-table-file-metadata__term{font-weight:700;padding-top:.5rem;text-align:right;width:9rem}.kb-staging-table-file-metadata__def{margin-left:0;padding-left:1rem;padding-top:.5rem;width:calc(100% - 9rem)}.kb-staging-table-file-metadata__list{list-style-type:none}.kb-staging-table-file-metadata__file_lines{word-wrap:break-word;background-color:#f2efeb;border:0;font-family:OxygenMono,SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:14px;font-weight:400;line-height:24px;margin:0;max-height:50rem;max-height:200px;overflow-wrap:break-word;overflow-y:auto;overflow:scroll;padding:1rem;white-space:pre;word-break:break-word}.kb-staging-table__notice{background-color:#f2efeb;font:normal 700 16px/20px Oxygen,Arial,sans-serif;padding:1rem;text-align:center}.kb-staging-table-import__button{background-color:#026daa;border-color:transparent;border-radius:6px;bottom:-50px;color:#fff;font-size:14px;line-height:1.3333333;padding:1rem 1.5rem;position:absolute;right:10px;text-transform:none}.kb-staging-table-import__button.focus,.kb-staging-table-import__button:focus,.kb-staging-table-import__button:hover{background-color:#014d78;border-color:transparent;color:#fff}.kb-staging-table-import__button.active,.kb-staging-table-import__button:active,.open>.kb-staging-table-import__button.dropdown-toggle{background-color:#014d78;background-image:none;border-color:transparent;color:#fff}.kb-staging-table-import__button.active.focus,.kb-staging-table-import__button.active:focus,.kb-staging-table-import__button.active:hover,.kb-staging-table-import__button:active.focus,.kb-staging-table-import__button:active:focus,.kb-staging-table-import__button:active:hover,.open>.kb-staging-table-import__button.dropdown-toggle.focus,.open>.kb-staging-table-import__button.dropdown-toggle:focus,.open>.kb-staging-table-import__button.dropdown-toggle:hover{background-color:#013654;border-color:transparent;color:#fff}.kb-staging-table-import__button.disabled.focus,.kb-staging-table-import__button.disabled:focus,.kb-staging-table-import__button.disabled:hover,.kb-staging-table-import__button[disabled].focus,.kb-staging-table-import__button[disabled]:focus,.kb-staging-table-import__button[disabled]:hover,fieldset[disabled] .kb-staging-table-import__button.focus,fieldset[disabled] .kb-staging-table-import__button:focus,fieldset[disabled] .kb-staging-table-import__button:hover{background-color:#026daa;border-color:transparent}.kb-staging-table-import__button .badge{background-color:#fff;color:#026daa}.kb-staging-table-import__button[disabled]{background-color:#a1b9cf;cursor:not-allowed;opacity:1}.kb-staging-table-import__button[disabled]:active,.kb-staging-table-import__button[disabled]:focus,.kb-staging-table-import__button[disabled]:hover{background-color:#a1b9cf}.kb-staging-table-import__button[disabled]:focus{outline:none}.kb-staging-table-import__tooltip.tooltip{background:#fff;border-radius:4px;box-shadow:2px 4px 6px rgba(0,0,0,.15)}.kb-staging-table-import__tooltip.tooltip .tooltip-inner{align-items:center;background:#fff;color:#000;display:flex;font:normal 400 14px/16px Oxygen,Arial,sans-serif;height:6rem;width:20rem}.kb-ui__button_label,.kb-ui__icon{vertical-align:middle}.kb-ui__text--na{color:#f78e1e;font-style:italic}.kb-user-menu__menu{display:inline-block}.kb-user-menu__menu_caret.caret{margin-left:5px}.kb-user-menu__icon--gravatar{width:40px}.kb-user-menu__icon--signout.fa,.kb-user-menu__icon--user.fa{font-size:150%;margin-right:10px}.kb-user-menu__block--name{display:inline-block}.kb-user-menu__block--displayName,.kb-user-menu__block--userName{display:block}.kb-user-menu__block--displayName{font-style:italic}.kb-user-menu__block--signout-icon,.kb-user-menu__block--user-icon{display:inline-block;width:34px}.kb-user-menu__block--user-icon{vertical-align:top} \ No newline at end of file diff --git a/kbase-extension/static/kbase/css/all_concat.css.map b/kbase-extension/static/kbase/css/all_concat.css.map new file mode 100644 index 0000000000..184d4b1372 --- /dev/null +++ b/kbase-extension/static/kbase/css/all_concat.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../../scss/modules/_colours.scss","../../../scss/modules/_fonts.scss","../../../scss/partials/_custom.scss","../../../scss/modules/_typography.scss","../../../scss/partials/_stylesheet.scss","../../../scss/modules/_variables.scss","../../../scss/partials/_narrative.scss","../../../scss/partials/_icons.scss","../../../scss/partials/_landingPages.scss","../../../scss/partials/_editor.scss","../../../scss/partials/_notify.scss","../../../scss/partials/_methodCell.scss","../../../scss/partials/_bootstrapHelper.scss","../../../scss/partials/_buttons.scss","../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins/_buttons.scss","../../../scss/partials/_contigBrowserStyles.scss","../../../scss/partials/_tour.scss","../../../scss/partials/_batchMode.scss","../../../scss/partials/_advancedViewCell.scss","../../../scss/partials/_errorDialog.scss","../../../scss/modules/_mixins.scss","../../../scss/partials/_appCell.scss","../../../scss/partials/_editorCell.scss","../../../scss/partials/_appParams.scss","../../../scss/partials/_bulkImportCell.scss","../../../scss/partials/_cellToolbar.scss","../../../scss/partials/_dataStaging.scss","../../../scss/partials/_dropzoneDz.scss","../../../scss/partials/_errorDisplay.scss","../../../scss/partials/_filePath.scss","../../../scss/partials/_filetypePanel.scss","../../../scss/partials/_fsm.scss","../../../scss/partials/_infoTab.scss","../../../scss/partials/_jobAction.scss","../../../scss/partials/_jobLog.scss","../../../scss/partials/_jobStatus.scss","../../../scss/partials/_kb-icon.scss","../../../scss/partials/_loadingBlocker.scss","../../../scss/partials/_narrativeOutline.scss","../../../scss/partials/_rcp.scss","../../../scss/partials/_reportViewer.scss","../../../scss/partials/_select2.scss","../../../scss/partials/_stagingTable.scss","../../../scss/partials/_ui.scss","../../../scss/partials/_userMenu.scss"],"names":[],"mappings":";AAAA;ACEA;EACE;EACA;EACA,KACE;;AAOJ;EACE;EACA;EACA;EACA;EACA,KACE;;AAMJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;ACtFF;AAAA;AAAA;;AAAA;AAAA;AAMA;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAIA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;AAMA;;AAJA;EACE;;AAIF;EACE;EACA;;AAIJ;EACE;;AAIJ;AAAc;EACZ;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;;AAEA;EACE;;;AAKN;EACE;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;AAIJ;EACE;;AAGF;EACE;EACA;EACA;;AAIA;EAEE;;AAIJ;EACE;EACA;;AAGF;EACE;;;AAIJ;AAII;EACE;;AAGF;EACE;;AAKF;EACE;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;;;AAMJ;AAAA;AAAA;EAGE;;AAGF;EACE;;;AAOE;EACE;;AAGF;EACE;;AAGF;AAAA;AAAA;AAAA;EAIE;;AAGF;EACE;;AAIJ;EACE;;AAIJ;EACE;;AAIA;EACE;;AAGF;AAAA;EAEE;;;AAKN;EACE;EACA;;AAEA;EACE;;AAGF;EACE;;;AAMJ;EACE;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAMA;EACE;EACA;;;AAIA;EACE;;;AAIJ;EC3UE;;;AD+UF;AACA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAIJ;AACA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;;;AEjYF;EACE;EACA;EAGA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;AAAA;EAEE,aC7BmB;ED8BnB;;;AAGF;EACE,aClCmB;EDmCnB;;;AAGF;EACE,aCvCmB;EDwCnB;;AAEA;EACE,aC3CiB;ED4CjB;;;AAIJ;EACE,aCjDmB;EDkDnB;;AAEA;EACE,aCrDiB;EDsDjB;;;AAIJ;EACE,aC3DmB;ED4DnB;;;AAGF;AAAA;AAAA;AAAA;EAIE;;;AAGF;EACE;EACA;;AAEA;EAEE;;AAGF;EACE;;;AAIJ;AAAA;EAEE,aCnFqB;EDoFrB;;;AE3FF;EACE,aDMqB;ECLrB;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;;;AAGF;AAAA;EAEE,aD1CmB;EC2CnB;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKN;AAAqB;EACnB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAkB;EAChB;EACA;;;AAGF;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;IACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;AAIJ;AACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;AAAA;EAEE;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;AACE;EACA;EACA;;;AAGF;EACE;;;AAGF;AACE;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;AAAe;EACb;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;AAAA;EAEE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;EACA;;AAIJ;EACE;;;AAMR;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;;AAMR;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;AAGE;EACE;EACA;;AAEA;AACE;EACA;;AAIJ;EACE;EACA;;AAEA;AACE;EACA;;;AAKN;AAEA;AAEA;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;AAKN;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAIA;EAEE;;AAIJ;EACE;;;AAIJ;AAGE;EAEE;EACA;EACA;EACA;;AAKA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAKA;AAAA;EACE;;AAGF;AAAA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAAA;AAKA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAKN;AAEA;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;AAAqB;EACnB;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;AAEA;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;AAAA;AAAA;AAIA;AAAA;AAAA;AAAA;AAAA;AAMA;AAAA;AAAA;AAAA;AAKA;EACE;EACA;;AAGE;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAMR;AAEA;AAAA;AAAA;AAIA;AAAA;AAAA;AAIA;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;;;AAKN;AAEA;AAEA;EACE;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;AAEA;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAIA;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAKE;AAAA;EAEE;;AAKF;AAAA;EAEE;;AAKF;AAAA;EAEE;;AAKF;AAAA;EAEE;;;AAKN;AAEA;EACE;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE,aDvlDiB;ECwlDjB;;AAIA;EACE;;AAGF;EACE;;;AAKN;AAEA;AASE;;AARA;EACE;;AAGF;EACE;;AAIF;EACE;;AAGF;EACE;EACA;;;AAKF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;AAEA;EACE;EACA,aD5pDiB;EC6pDjB;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA,aD5tDmB;EC6tDnB;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA,aDvuDmB;ECwuDnB;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;AAGF;AACE;EACA;;AAGF;AAAA;EAEE;AAEA;EACA;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAAA;EAIE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;;;AAGF;AACE;EACA;;;AAGF;AAEA;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;;AAKF;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAKF;EACE;;AAEA;EACE;;;AAMR;AAAA;AAAA;AAIA;AAAA;AAAA;AAIA;AAAA;AAAA;AAIA;EACE;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;;;AAIA;EACE;;AAGF;EACE;;AAEA;EAGE;;AAGF;EACE;;AAGF;EACE;;AAGF;EAGE;;;AAKN;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;;;AAKN;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;AAAA;AAAA;AAAA;AAAA;AAMA;AAEA;EACE;;;AAGF;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;;;AAGF;AAAA;AAAA;AAAA;AAKA;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;AAEA;AAAA;EAEE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAEA;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EAEE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;EACA;;;AAIJ;EACE;;;AAGF;AAEA;EACE;;;AAGF;AAGE;EACE;AAEA;EACA;EACA;;AAGF;EACE;;;AAIJ;AAEA;AAEA;EACE;AAEA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;AAAA;EAGE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;AAAA;EAEE;;;AAIJ;AAAA;EAEE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;;;AAGF;AAEA;EACE;;;AAGF;EACE;EACA,aD/6FqB;;;ACk7FvB;EACE;EACA;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;;;AAGF;AAEA;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;;AAIJ;EACE,SDn+F0B;ECo+F1B;;;AAIF;EACE;;;AC3gGF;EACE;;;AAGF;AAAA;AAAA;EAGE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;ACpCF;EACE;EACA;;;AAIA;EAEE;;;AAIJ;EACE;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;AACE;EACA;;;AAIJ;EACE;;AAEA;EACE;;AAEA;EACE;;;AAKN;EACE;EACA;AAEA;EACA;;;AAGF;EACE;;;AAGF;AAEA;EACE;;AAEA;EACE;EACA;;;AAIJ;EACE;;;AAIA;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAEA;EACE;;;AAGF;AAAA;AAAA;EAGE;EACA;;;AAGF;EACE;AAEA;;AAEA;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;IACE;IACA;;;AAIJ;AAAA;EAEE;EACA,aH/TwB;EGgUxB;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE,aHnWsB;EGoWtB;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA,aHhXwB;;AGoXpB;EACE;;AAIA;AAAA;EAEE;EACA;EACA;EACA;;AAKN;EACE;;;AAKN;EACE;AAEA;EACA;EACA;AAEA;EACA;;;AAGF;AAEA;EACE;;AAEA;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EAEE;;AAKN;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;;AChdJ;AAAA;AAAA;AAIA;EACE;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;;AAGF;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;;AAIJ;AAAA;AAAA;AAAA;AAKA;AAEA;AAEA;AAEA;AAAA;AAAA;AAIA;EACE;AAEA;;AAGE;EACE;AAEA;;AAEA;EACE;EACA;;AAGF;EACE;;AAEA;EACE;;AAKN;EACE;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA,aJxFgB;EIyFhB;EACA;;;AClGR;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;ACVJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;;;ACZJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA;EACE;;;AAIA;EACE;;AAKF;EACE;;AAGF;EACE;;;AAKF;EAME;EACA;;AAEA;EACE;EACA;EACA;;;AAKN;EACE;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;;;AAcF;AAAA;AAAA;AAAA;EACE;EACA;EACA;;AAGF;AAAA;AAAA;AAAA;EACE;EACA;EACA;;;AAKF;EACE;;;AAIJ;EACE;;;AAIA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAQF;EACE;EACA;EACA;;;AAGF;EACE;;;ACrIF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EAEE;;;AAIJ;EAEE;;;AAGF;AAAA;;AAAA;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAgCA;ECDE;EACA,WT1C0B;ES2C1B,aTzC0B;ES0C1B,eTrB0B;EQuB1B;EACA;EACA;EACA,aR5DmB;EQ6DnB,aRjB+B;EQkB/B;EACA;EACA;EACA;EACA;EACA;;AAKE;EAEE;EACA;;AAIJ;EAGE,ORrC6B;EQsC7B;;AAGF;EAEE;EACA;EACA;;AAGF;EAGE;EACA,QR7C6B;EQ8C7B;;;AAUJ;ECxDE;EACA,WThB0B;ESiB1B,aThB0B;ESiB1B,eTtB0B;;;AUrC5B;AAEA;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAKE;AAAA;EAEE;EACA;EACA;;AAGF;EACE;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;;;ACnEF;EACE;;AAGE;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAKF;EACE;EACA;EACA;;AAGF;AAAA;EAEE;;AAGF;EACE;EACA;EACA;;;ACpCN;EACE;EACA;;;AAGF;EACE;;AAEA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;ACpBF;EACE;;;ACCA;EACE;;AAMA;EAEE;;AAMF;EhBgBF,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;EelB1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACZF;AAAA;AAAA;AAAA;AAME;EACE;EACA;EACA,ahBLiB;;AgBQnB;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAIA;EDEF,SfC0B;;AgBG1B;EACE,ahBnCiB;EgBoCjB;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;AAGF;EACE,ahB9DiB;EgB+DjB;;AAEA;EACE;EACA;;AAIJ;EACE;EACA,ahBzEiB;EgB0EjB;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAGF;AAAA;AAEE;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;AAAA;AAAA;AAAA;EAIE;;AAGF;AAAA;EAEE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;;AAKN;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAEA;EACE;;;AAGF;EACE,ahB1OqB;;AgB4OrB;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;;AAAA;;AAAA;AAOE;EACE;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EAGE;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAEA;EACE;;AAGF;EAEE;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EAEE;EACA;EACA;EACA;;;AAMR;AAEA;AAAA;AAAA;AAIA;AAEA;AAGE;EACE;;AAGF;EACE;;AAGF;EAEE;;AAGF;EACE;;;AAIJ;AAEA;AAEA;EACE;EACA;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EAEE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;ACncF;AAAA;AAAA;AAAA;AAME;EACE,ajBHiB;EiBIjB;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE,ajBxBiB;EiByBjB;;AAEA;EACE;EACA;;AAIJ;EACE;EACA,ajBnCiB;EiBoCjB;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAGF;AAAA;AAEE;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;AAAA;AAAA;AAAA;EAIE;;AAGF;AAAA;EAEE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;;AAEA;EACE;;AAMJ;EACE;;AAGF;EACE;;AAKF;EAEE;;;AAKN;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;;AAAA;;AAAA;AAMA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EAGE;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EAEE;EACA;EACA;;;ACnQJ;EHoCA;EACA;;AG3BE;EACE;;;ACbN;AAAA;AAAA;AAYE;EACE;EACA;;AAIF;EACE;EACA;EACA;;AAOF;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE;;AAOA;EACE;;AAKF;EACE;EACA;;AAKF;EAEE;;;AAKN;EACE;;;AAKA;EACE;;AAIF;EACE;;AAIF;EACE;EACA;;AAIF;EACE;EACA;;AAEA;EACE;EACA;;AAKF;EACE;;AAIA;AAAA;EAEE;;AAMN;EAEE;EACA;;AAGA;EACE;EACA;EACA;;AAEA;EACE;;AAKJ;EACE;EACA;EACA;;AAEA;EACE;;;AAQN;AAAA;EJrHA,SfC0B;;;AoBhC1B;EACE;;AAIF;EACE;EACA;EACA,QAXgB;EAYhB;;AAIF;EACE;EACA;EACA,QAnBgB;EAoBhB,aApBgB;EAqBhB;EACA;EACA;EACA,OAxBgB;;AA4BlB;EACE;EACA;;AAGA;EACE;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIA;EACE;;AAKJ;AAiCE;AAAA;;AA/BA;EACE;;AAIF;EACE;;AAIF;EACE;;AAIF;EACE;;AAIF;EACE;;AAKF;EAEE;;AAKF;EAEE;;AAKJ;EACE;EACA;EACA;;AAGA;EACE;EACA;EACA;;;ACzHJ;EACE;;AAGF;EACE;;AAIF;EACE;EACA;EACA;;AAKF;EZZA,OYe0B;EZd1B,kBYcgD;EZbhD,cYa+E;EAE7E;EACA;EACA;EACA;;AZhBF;EAEE,OYSwB;EZRxB;EACA;;AAEF;EACE,OYIwB;EZHxB;EACA;;AAEF;EAGE,OYHwB;EZIxB;EACA;EACA;;AAEA;EAGE,OYXsB;EZYtB;EACA;;AAMF;EAGE,kBYtB4C;EZuB5C,cYvB2E;;AZ2B/E;EACE,OY5B8C;EZ6B9C,kBY7BwB;;AAS1B;EACE;;;AC/BJ;EbME,OaHwB;EbIxB,kBaJ8C;EbK9C,caL6E;;AbO7E;EAEE,OaTsB;EbUtB;EACA;;AAEF;EACE,OadsB;EbetB;EACA;;AAEF;EAGE,OarBsB;EbsBtB;EACA;EACA;;AAEA;EAGE,Oa7BoB;Eb8BpB;EACA;;AAMF;EAGE,kBaxC0C;EbyC1C,cazCyE;;Ab6C7E;EACE,Oa9C4C;Eb+C5C,kBa/CsB;;;AAG1B;EACE;EACA;EACA;EACA;EACA;;AAGA;EACE;EACA;;AAIF;EACE;;AAIF;EACE;EACA;;AAIF;EACE;EACA;;AAKF;EAEE;EACA;;AAIF;EACE;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;;AAIF;EACE;;AAIF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE,atBhGiB;EsBiGjB;;;ACnGF;ERgCA,SfC0B;;AuB7BxB;EACE;;AAIJ;EzBEA;EyBGE;;AAIA;EACE;;AAIJ;EzBQA,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;EelB1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AQiBA;EACE;EACA;;;AASA;AAAA;EACE;EACA;;AAKF;AAAA;EACE;EACA;EACA;;AAKF;AAAA;EACE;EACA;;;AAOJ;EACE;;AAIF;EACE;EACA;;AAIF;EACE;EACA;;;AAIJ;EACE;;;AChFA;EACE;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAKJ;EACE;EACA;EACA;;AAIF;Ef5CA,Oe+C0B;Ef9C1B,kBe8CgD;Ef7ChD,ce6C+E;EAE7E;;Af7CF;EAEE,OeyCwB;EfxCxB;EACA;;AAEF;EACE,OeoCwB;EfnCxB;EACA;;AAEF;EAGE,Oe6BwB;Ef5BxB;EACA;EACA;;AAEA;EAGE,OeqBsB;EfpBtB;EACA;;AAMF;EAGE,kBeU4C;EfT5C,ceS2E;;AfL/E;EACE,OeI8C;EfH9C,kBeGwB;;AAM1B;EfrDA,OewD0B;EfvD1B,kBeuDgD;EftDhD,cesD+E;EAE7E;;AftDF;EAEE,OekDwB;EfjDxB;EACA;;AAEF;EACE,Oe6CwB;Ef5CxB;EACA;;AAEF;EAGE,OesCwB;EfrCxB;EACA;EACA;;AAEA;EAGE,Oe8BsB;Ef7BtB;EACA;;AAMF;EAGE,kBemB4C;EflB5C,cekB2E;;Afd/E;EACE,Oea8C;EfZ9C,kBeYwB;;AAK1B;EACE;;AAGF;EACE;;;AClEJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIA;E3BLA;E2BQE;EACA;;AAEA;EACE;EACA;;AAMF;E3B2BF,aEvDmB;EFwDnB,WE7C0B;EF8C1B;EACA,aE7C0B;;AyBoBtB;EAGE,kBApCe;EAqCf;;AAIF;EAGE,kBA7CmB;EA8CnB;;AAGF;EACE;;AAKJ;EACE;EACA;EACA;;AAGA;EACE;;AAIF;EACE;;AAKJ;EACE;;;AC3EJ;EACE;;AAGF;EACE;EACA;EACA;EACA;;;ACPF;EZgCA,SfC0B;;A2B7B1B;EACE;;AAGF;E7BGA;;A6BCA;EACE;;AAGF;EACE;EACA;;AAMF;E7BiCA,aEvDmB;EFwDnB,WE7C0B;EF8C1B;EACA,aE7C0B;EFkD1B;;A6BlCA;E7BhBA;E6BkBE;;;AAIJ;EACE;;;ACtCE;EACE;EACA;;AAIF;E9BgCF,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;E4BXtB;EACA;;AAMI;E9BsBR,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;E4BAhB;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAKA;EACE;EACA;;AAKN;EACE;;AAEA;EAEE;EACA;;AAIJ;EACE;;AAEA;EAEE;EACA;;;ACzDd;E/B+BE,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;E6BjB1B,eALa;EAMb;EACA;EACA;;;AAGF;E/BsBE,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;E6BR1B,mBAda;EAeb;EACA;EACA;;;AAKA;E/BkBA,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;;A6BI1B;E/BcA,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;E6BOxB;;AAGF;EACE;;AAIF;EdrBA;EACA;EACA;EcuBE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAOF;EAGE;;AASF;EACE;EACA;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;;AAIJ;E/BzEA;E+B4EE;;AAGF;E/BjCA,aEvDmB;EFwDnB,WE7C0B;EF8C1B;EACA,aE7C0B;EeD1B;EACA;EACA;Ec8EE;;;AAOA;E/B/DF,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;;A6BqFxB;E/B9FF;E+BiGI;;AAIJ;E/B1EA,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;E6B+FxB;EACA;EACA;;;AAMF;E/BrFA,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;;A6B4GxB;EACE;;AAIJ;EACE;;;ACtIJ;EACE;;AAEA;EhC+CA,aEhDmB;EFiDnB,WEtC0B;EFuC1B;EACA,aEtC0B;E8BTxB;EACA;EACA;;AAEA;EACE;;AAIF;EACE;EACA;;AAMA;AAAA;AAAA;EAGE;;AAEA;AAAA;AAAA;EACE;;AAMN;EACE;;AAEA;EACE;;AAMF;EACE;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;EACA;;AAOA;EfzCR;EACA;EACA;;AeuCQ;EfzCR;EACA;EACA;;AeuCQ;EfzCR;EACA;EACA;;AeuCQ;EfzCR;EACA;EACA;;Ae+CE;EhC/BF,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;;A8ByDtB;EfzDJ;EACA;EACA;Ee0DM;EACA;;AAJF;EfzDJ;EACA;EACA;Ee0DM;EACA;;AAJF;EfzDJ;EACA;EACA;Ee0DM;EACA;;AAJF;EfzDJ;EACA;EACA;Ee0DM;EACA;;AAKJ;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;AAOA;ErBnGJ,OqBuG8B;ErBtG9B,kBqBsGoD;ErBrGpD,cqBqGmF;;ArBnGnF;EAEE,OqBiG4B;ErBhG5B;EACA;;AAEF;EACE,OqB4F4B;ErB3F5B;EACA;;AAEF;EAGE,OqBqF4B;ErBpF5B;EACA;EACA;;AAEA;EAGE,OqB6E0B;ErB5E1B;EACA;;AAMF;EAGE,kBqBkEgD;ErBjEhD,cqBiE+E;;ArB7DnF;EACE,OqB4DkD;ErB3DlD,kBqB2D4B;;AAG1B;ErB1GJ,OqB6G8B;ErB5G9B,kBqB4GkD;ErB3GlD,cqB2G+E;;ArBzG/E;EAEE,OqBuG4B;ErBtG5B;EACA;;AAEF;EACE,OqBkG4B;ErBjG5B;EACA;;AAEF;EAGE,OqB2F4B;ErB1F5B;EACA;EACA;;AAEA;EAGE,OqBmF0B;ErBlF1B;EACA;;AAMF;EAGE,kBqBwE8C;ErBvE9C,cqBuE2E;;ArBnE/E;EACE,OqBkEgD;ErBjEhD,kBqBiE4B;;AAM9B;EhCjFA,aEpCmB;EFqCnB,WE1B0B;EF2B1B;EACA,aErB0B;E8BsGxB;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAKA;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAjBF;EACE;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;;;ACvJJ;AAME;AAAA;;AAJA;EACE;;AAKF;EAEE;;AAGF;EAOE;;AAGF;EACE;;AAOF;EAEE;;AAKF;EAEE;;AAKF;EAEE;;AAKF;EACE,QA/DS;EAgET,YA/DY;EAgEZ,WAhEY;;AAqEhB;EACE,aAnEa;EAoEb,YApEa;;AAuEf;EACE,aAzES;EA0ET,YA3EQ;;AA8EV;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EAEE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AC9GF;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAYA;EACE;EACA;EACA;EACA;EACA;;;AAKA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;;AAQA;EAGE;EACA;;AAKJ;EACE;EACA;;;ACrDJ;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE,aAfY;EAgBZ;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE,eAtDY;EAuDZ,aAvDY;EAwDZ;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;;AAGF;EACE;EACA;;;AAKN;EACE,eAxGgB;EAyGhB,aAzGgB;EA0GhB;EACA;EACA;EACA;EACA;EACA;;;AC3GA;EACE;EACA;EACA;EACA,QAPS;EAQT;;AAGF;EACE;EACA,QAbS;EAcT,aAdS;;AAiBX;EACE;EACA,aAnBS;EAoBT;EACA;;AAIF;EACE;EACA;EACA;;AAGA;EACE;EACA;EACA;EACA;;AAKF;EnBbF;EACA;EACA;EmBcI;EACA;EACA,aA7CO;EA8CP;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EAEE;;;AC1EA;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAIJ;ErCgBA,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;EmCFxB;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAMF;EACE;;AAGF;EACE;;;AAKF;EACE;;AAGF;EACE;EACA;;AAIF;ErC1DA;EqC4DE;EACA;EAEA;;AACA;EACE;;;AC3EF;EtCwDF,aEvDmB;EFwDnB,WE7C0B;EF8C1B;EACA,aE7C0B;EoCXtB;EACA,QpCasB;;AoCRxB;EtC+CF,aEvDmB;EFwDnB,WE7C0B;EF8C1B;EACA,aE7C0B;EoCFtB;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EAGE;;;AChCR;EACE;;AAEA;EACE;;AAMA;AAAA;AAAA;EAGE;EACA;EACA,arCLgB;EqCMhB;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;AAAA;AAAA;EAGE;;AAKA;EACE;EACA;;AAIF;EACE;EACA;;AAGA;EAGE;;AAMJ;EAEE;;AAIF;EACE;;AAQJ;EACE;;AAIF;EACE;EACA;;AAKA;E5B7BJ;EACA,W4B+B2C;E5B9B3C,a4B8BiD;E5B7BjD,e4B6BuD;EAEjD;;AAIF;E5BxFJ,O4B2F8B;E5B1F9B,kB4B0FkD;E5BzFlD,c4ByFwE;E5BzCxE;EACA,W4ByC2C;E5BxC3C,a4BwCiD;E5BvCjD,e4BuCuD;EAEjD;;A5B1FN;EAEE,O4BqF4B;E5BpF5B;EACA;;AAEF;EACE,O4BgF4B;E5B/E5B;EACA;;AAEF;EAGE,O4ByE4B;E5BxE5B;EACA;EACA;;AAEA;EAGE,O4BiE0B;E5BhE1B;EACA;;AAMF;EAGE,kB4BsD8C;E5BrD9C,c4BqDoE;;A5BjDxE;EACE,O4BgDgD;E5B/ChD,kB4B+C4B;;AAQ1B;E5BnGJ,O4BuG8B;E5BtG9B,kB4BsGoD;E5BrGpD,c4BqGiE;E5BrDjE;EACA,W4BqD2C;E5BpD3C,a4BoDiD;E5BnDjD,e4BmDuD;;A5BpGvD;EAEE,O4BiG4B;E5BhG5B;EACA;;AAEF;EACE,O4B4F4B;E5B3F5B;EACA;;AAEF;EAGE,O4BqF4B;E5BpF5B;EACA;EACA;;AAEA;EAGE,O4B6E0B;E5B5E1B;EACA;;AAMF;EAGE,kB4BkEgD;E5BjEhD,c4BiE6D;;A5B7DjE;EACE,O4B4DkD;E5B3DlD,kB4B2D4B;;AAGxB;EAEE;;AAMN;EtB5FF;EACA;EACA;;AsB+FE;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAOF;EACE;EACA;;AAKF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKE;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE;;AAKF;EACE;;AAEA;EACE;;AAOJ;EACE;;AAKJ;AAAA;EAEE;;AAIF;EACE;;AAIJ;AAAA;EAEE;;AAMF;EACE;;AAIF;EACE;EACA;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAIF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAIF;EvCrNF,aE1BqB;EF2BrB,WEnB0B;EFoB1B;EACA,aEd0B;EelB1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EsB6OI;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAMA;E5BnQF,O4BsQ4B;E5BrQ5B,kB4BqQgD;E5BpQhD,c4BoQsE;E5BpNtE;EACA,WT1C0B;ES2C1B,aTb0B;ESc1B,eTpB0B;EqCwOtB;EACA;EACA;EACA;;A5BxQJ;EAEE,O4BgQ0B;E5B/P1B;EACA;;AAEF;EACE,O4B2P0B;E5B1P1B;EACA;;AAEF;EAGE,O4BoP0B;E5BnP1B;EACA;EACA;;AAEA;EAGE,O4B4OwB;E5B3OxB;EACA;;AAMF;EAGE,kB4BiO4C;E5BhO5C,c4BgOkE;;A5B5NtE;EACE,O4B2N8C;E5B1N9C,kB4B0N0B;;AAQxB;EACE;EACA;EACA;;AAEA;EAGE;;AAGF;EACE;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AC9SN;EAEE;;AAGF;EACE;EACA;;;ACRF;EACE;;AAGA;EACE;;AAMF;EACE;;AAOA;EACE;EACA;;AAOJ;EACE;;AAIF;EAEE;;AAGF;EACE;;AAIF;EAEE;EACA;;AAGF;EACE","file":"all_concat.css"} \ No newline at end of file diff --git a/kbase-extension/static/kbase/css/appCell.css b/kbase-extension/static/kbase/css/appCell.css deleted file mode 100644 index ad1f5522d9..0000000000 --- a/kbase-extension/static/kbase/css/appCell.css +++ /dev/null @@ -1,528 +0,0 @@ -/* Wrapper class - All classes below may be wrapped in this class to avoid polluting - the Narrative styles. -*/ - -.kb-app-cell { - font-family: Oxygen, Arial, sans-serif; -} - -.kb-app-cell .kb-app-warning { - font-family: Oxygen, Arial, sans-serif; - text-align: center; -} - -/** new app parameter styling **/ - -.kb-app-cell .kb-app-parameter-panel { - border-left: 3px solid #fff; -} - -.kb-app-cell .kb-app-parameter-panel-hover { - border-left: 3px solid #428bca; -} - -.kb-app-cell .kb-app-parameter-row { - margin: 0; - padding: 5px; - border-radius: 5px; -} - -.kb-app-cell .kb-app-parameter-row:hover { - background-color: rgba(0, 0, 0, 0.03); -} - -.kb-app-row-close-btn-addon { - background: transparent; - border: 0; -} - -.kb-app-row-clip-btn-addon { - background: transparent; - border: 0; - padding: 0; - padding-right: 10px; - height: 100%; -} - -.kb-app-row-clip-btn-addon:active::after { - background-color: #fff; - border-radius: 3px; - border: 1px solid gray; - content: 'copied!'; - margin-top: -5px; - padding: 7px; - position: absolute; -} - -.kb-input-group-wide { - width: 100%; -} - -.kb-input-row-flex { - display: flex; - flex-direction: row; - align-items: center; -} - -.cell.selected .btn-default.kb-app-row-close-btn { - color: gray; -} - -.kb-app-row-clip-btn, -.kb-app-row-close-btn { - height: 100%; - margin-left: 1px; - border-right: 0; - border-top: 0; - border-bottom: 0; - border: none; - background-color: transparent; - color: gray; -} - -/* let's just do this as a normal button hover effect */ - -.kb-app-parameter-right-error-bar { - height: 28px; - - /* firefox wants it explicitly wired */ - - background: red; -} - -/* for some reason, the css :hover doesn't work right on this div, so we use jquery to toggle this class */ - -.kb-app-cell .kb-app-parameter-row-hover { - background: #f9f9f9; -} - -.kb-app-cell .kb-app-parameter-row .message.-error { - background: #f2dede; - color: #f44336; -} - -.kb-app-cell .kb-app-parameter-row .message { - text-align: center; - font-family: Oxygen, Arial, sans-serif; -} - -/* not sure how to get text in these divs to valign middle... */ - -.kb-app-cell .kb-app-parameter-name { - font-family: Oxygen, Arial, sans-serif; - color: #777; - text-align: left; - vertical-align: bottom; - padding-right: 4px; - padding-left: 0; - white-space: normal; -} - -.kb-app-cell .kb-app-parameter-input { - vertical-align: middle; -} - -.kb-app-cell .kb-app-parameter-input select.form-control { - margin: 0; -} - -.kb-app-field-feedback { - background: transparent; - border: 0; -} - -.kb-input-group-addon { - background: transparent; - border: 0; - padding: 0 0 0 10px; -} - -/* -This set of styles is a to accomodate the required/satisfied icon which appears -between the select input control and the help text to the right of it. The -problem is that within the columns used for layout the select is set to 100% width -and yet the icon is placed right next to it. The result is that the icon is shoved -to the right of the select control and into the next column, overlapping the -help text. The old app handling this was to scoot the help text far enough over -to accomodate the the icon intruding into its space. This technique makes space -in the column in which the icon lives, and does this by shrinking the select -control with padding, and then scooting the icon back into its column. -*/ - -.kb-app-cell .kb-app-parameter-input .kb-app-parameter-accepted-glyph, -.kb-app-cell .kb-app-parameter-input .kb-app-parameter-required-glyph { - /* This scoots the icon inside */ - font-size: 15px; - margin-left: 0; -} - -.kb-app-cell .kb-app-parameter-required-glyph { - color: #f44336; -} - -.kb-app-cell .kb-parameter-data-selection { - font-weight: bold; -} - -.kb-app-cell .kb-app-parameter-hint { - color: #777; - text-align: left; - margin-top: 3px; - padding-left: 7px; -} - -.kb-app-cell .kb-app-parameter-accepted-glyph { - color: #4bb856; -} - -.kb-app-cell .kb-app-parameter-info { - color: #777; -} - -.kb-app-cell .kb-parameter-data-row-remove { - color: #777; -} - -.kb-app-cell .kb-parameter-data-row-add { - color: #777; -} - -.kb-app-cell .kb-app-advanced-options-controller-inactive { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - line-height: 14px; - color: #777; - text-align: center; -} - -.kb-app-cell .kb-app-advanced-options-controller { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - cursor: pointer; - font-style: italic; - font-size: 10pt; - line-height: 14px; - color: #08c; - text-align: center; -} - -.kb-app-cell .kb-app-advanced-options-controller:hover { - color: #2a6496; -} - -.kb-app-cell .kb-app-footer { - width: 100%; - overflow: none; - background-color: #f5f5f5; - padding: 10px; -} - -.kb-app-cell .kb-app-subtitle { - background-color: #f5f5f5; - padding: 3px 5px; -} - -.report-widget .panel-title > [data-toggle="collapse"]::before { - display: inline-block; - margin-left: 0; - margin-right: 4px; - font-family: "FontAwesome"; - font-style: normal; - font-weight: normal; - font-size: 90%; - width: 12px; - color: silver; - line-height: 1; - vertical-align: baseline; - content: "\f078 "; -} - -.report-widget .panel-title > [data-toggle="collapse"].collapsed::before { - margin-left: 2px; - margin-right: 2px; - content: "\f054 "; -} - -.kb-app-cell .panel-title > [data-toggle="collapse"]::before { - display: inline-block; - margin-left: 0; - margin-right: 4px; - font-family: "FontAwesome"; - font-style: normal; - font-weight: normal; - font-size: 90%; - width: 12px; - color: silver; - line-height: 1; - vertical-align: baseline; - content: "\f078 "; -} - -.kb-app-cell .panel-title > [data-toggle="collapse"].collapsed::before { - margin-left: 2px; - margin-right: 2px; - content: "\f054 "; -} - -/* tweaks to elemnts that should be dimmed when the cell is not selected */ - -.unselected .kb-app-cell { - opacity: 0.5; -} - -.kb-app-cell .advanced-parameter-showing { - display: block; -} - -.kb-app-cell .advanced-parameter-hidden { - display: none; -} - -.kb-elapsed-time { - font-family: monospace; -} - -.kb-elapsed-time.-active { - color: lime; -} - -.kb-app-cell-info-desc { - border: 1px solid #777; - padding: 10px; - margin-right: 10px; - min-height: 100px; -} - -.kb-app-cell-info { - color: #777; - font-family: Oxygen, Arial, sans-serif; - font-size: 12pt; -} - -.kb-app-cell-info .header { - border-bottom: 1px solid #777; - padding: 5px 0; - margin-bottom: 8px; -} - -.kb-app-cell-info .value { - color: #000; -} - -/* -New button color scheme - -blue for border, backgrounds, text: rgb(0, 147, 255)) - -official kbase color: rgb(33,150,243) - -disabled color for border, text - -enabled run color - (green) - rgb(2,140,10) -official kbase green: - -rgb(75,184,86) - -or - - rgb(42,190,159) which is the lighter variant. both used in mocks (but depends where you sample). - -disabled run color - (green) - - -error color - red - rgb(209,82,65) - -disabled (gray) - 205,205,205 - -warning: orange - -*/ - -.btn.batch-active { - box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.125); - -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.125); - background-color: #eee; - border-color: #adadad; -} - -.btn.batch-active:hover { - background-color: #ccc; -} - -.btn.kb-app-cell-btn { - color: rgb(33, 150, 243); - background-color: #fff; - border: 2px rgb(33, 150, 243) solid; - margin-bottom: 4px; -} - -/* Primary Button Style */ - -.btn.kb-app-cell-btn.btn-primary { - color: rgb(33, 150, 243); - border: 2px rgb(33, 150, 243) solid; -} - -.btn.kb-app-cell-btn.btn-primary:hover { - color: #fff; - background-color: rgb(33, 150, 243); - border-bottom: 6px rgb(33, 150, 243) solid; - margin-bottom: 0; -} - -.btn.kb-app-cell-btn.btn-primary:active { - color: #fff; - background-color: rgb(33, 150, 243); -} - -/* Hovering over an active cell is different, it tries to make the tab look - disassociated but using a gray background so it doesnt look like an inactive - tab */ - -.btn.kb-app-cell-btn.btn-primary.active { - color: white; - background-color: rgb(33, 150, 243); - border: 2px rgb(33, 150, 243) solid; - border-bottom: 6px rgb(33, 150, 243) solid; - margin-bottom: 0; -} - -.btn.kb-app-cell-btn.btn-primary.active:hover { - color: rgb(33, 150, 243); - background-color: #ddd; - border-bottom: 2px rgb(33, 150, 243) solid; - margin-bottom: 4px; -} - -/* DANGER - for error */ - -.btn.kb-app-cell-btn.btn-danger { - color: rgb(209, 82, 65); - border: 2px rgb(209, 82, 65) solid; -} - -.btn.kb-app-cell-btn.btn-danger:hover { - color: #fff; - background-color: rgb(209, 82, 65); - border-bottom: 6px rgb(209, 82, 65) solid; - margin-bottom: 0; -} - -.btn.kb-app-cell-btn.btn-danger.active { - color: white; - background-color: rgb(209, 82, 65); - border: 2px rgb(209, 82, 65) solid; - border-bottom: 6px rgb(209, 82, 65) solid; - margin-bottom: 0; -} - -.btn.kb-app-cell-btn.btn-danger.active:hover { - color: rgb(209, 82, 65); - background-color: #ddd; - border-bottom: 2px rgb(209, 82, 65) solid; - margin-bottom: 4px; -} - -/* DISABLED is gray border and text */ - -.btn.kb-app-cell-btn.disabled, -.btn.kb-app-cell-btn.disabled:hover, -.btn.kb-app-cell-btn.disabled:active { - color: #888; - background-color: #fff; - border: 2px #888 solid; - margin-bottom: 4px; -} - -.kb-app-cell [data-element="run-control-panel"] { - width: 100%; -} - -.kb-app-cell [data-element="tab-pane"] { - border-top: 2px rgb(33, 150, 243) solid; -} - -.kb-app-cell [data-element="tab-pane"] > div:empty { - padding: 0; -} - -.kb-app-cell [data-element="tab-pane"] > div { - padding: 4px; -} - -.kb-app-status-ok { - color: rgb(75, 184, 86); -} - -.kb-app-status-warning { - color: orange; -} - -.kb-app-status-danger, -.kb-app-status-error { - color: rgb(209, 82, 65); -} - -.kb-app-status-default { - color: rgb(33, 150, 243); -} - -/* app cell panels */ - -/* TODO: higher level selector */ - -.parameter-panel .info-panel { - padding-top: 4px; - background: transparent; -} - -.tt-input, -.tt-query { - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - width: 100%; -} - -.tt-hint { - color: #999; -} - -.tt-menu { - /* used to be tt-dropdown-menu in older versions */ - margin-top: 4px; - padding: 4px 0; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - width: 100%; -} - -.tt-suggestion { - padding: 3px 20px; - line-height: 24px; -} - -.tt-suggestion.tt-cursor, -.tt-suggestion:hover { - color: #fff; - background-color: #0097cf; -} - -.tt-suggestion p { - margin: 0; -} - -.tt-header { - font-size: 75%; - font-style: italic; - padding-left: 5px; -} diff --git a/kbase-extension/static/kbase/css/bootstrapHelper.css b/kbase-extension/static/kbase/css/bootstrapHelper.css deleted file mode 100644 index 3aeeabe892..0000000000 --- a/kbase-extension/static/kbase/css/bootstrapHelper.css +++ /dev/null @@ -1,111 +0,0 @@ -/* - Styles to help with bootstrap files. - Fix styles broken by other styles - Make bootstrap work better - Customize bootstrap styles -*/ - -/* - Bs recommends h4 for the modal title, but this is overridden by the general - purpose kbase h4 style. Generally, I (Erik) do not like using semantic tags - in bootstrap anyway, especially header tags which inherently imply a specific - document structure which modal dialogs in no way can represent!) -*/ - -.modal .modal-title { - font-family: Oxygen, Arial, sans-serif; - font-size: 130%; - font-weight: bold; - color: #2e618d; -} - -/* - Unset the global modal title color set above for alert modals, which - may set the color style based on the alert type. -*/ - -.modal.kb-modal-alert .modal-title { - color: unset; -} - -/* - The BS checkbox appears too low, due to the top margin, when used with a label - in the recommended manner. - Note: I don't understand why BS handles checkbox styling as they do, turning - the checkbox into relatively positioned and then pushing the label to checkbox - to the left and down (at the same time given the container extra margin to - accommodate this.) Anyway, we roll with it but try to patch up the styling - as it breaks. -*/ - -.modal input[type="checkbox"] { - margin-top: 1px; -} - -.panel.kb-panel-light { - border: none; - margin-bottom: 10px; -} - -/* colors copied shamelessly from peeking at bootstrap - http://getbootstrap.com/css/ -*/ - -.panel-danger.kb-panel-light .panel-title { - color: #a94442; -} - -.panel-success.kb-panel-light .panel-title { - color: #3c763d; -} - -.panel-warning.kb-panel-light .panel-title { - color: #8a6d3b; -} - -.panel-info.kb-panel-light .panel-title { - color: blue; -} - -.panel.kb-panel-light > .panel-heading { - background-color: transparent; - font-weight: bold; - color: #666; - padding-left: 0; - padding-bottom: 2px; - border-bottom: 1px #ccc solid; -} - -.panel.kb-panel-light .panel-body { - padding: 0; -} - -.panel.kb-panel-container { - border: none; -} - -.panel.kb-panel-container > .panel-heading { - background-color: transparent; - font-weight: bold; - padding-left: 0; - padding-bottom: 2px; - border-bottom: 2px #ccc solid; - margin-bottom: 10px; -} - -.panel.kb-panel-container .panel-body { - padding-top: 0; -} - -/* - The button bar in a cell widget is located at the bottom of the inputs, but - above any outputs. - These styles attempt to help the button toolbar, which represents the actions - the user can take, be visually distinct from the other bits surrounding it. -*/ - -.btn-toolbar.kb-btn-toolbar-cell-widget { - margin-bottom: 20px; - background-color: #eee; - padding: 6px; -} diff --git a/kbase-extension/static/kbase/css/buttons.css b/kbase-extension/static/kbase/css/buttons.css deleted file mode 100644 index 6472560e7e..0000000000 --- a/kbase-extension/static/kbase/css/buttons.css +++ /dev/null @@ -1,25 +0,0 @@ - -/* A flat button, with no unselected background color, but with hover and click - styles */ -.btn.kb-flat-btn { - background-color: #fff; - border: none; - text-shadow: none !important; - margin: 0; - color: #666; -} - -.kb-flat-btn-wrapper { /* don't add shadows here */ - box-shadow: none; -} - -/* Text spacing */ -.btn.kb-flat-btn .kb-nav-btn-txt { - font-size: 10pt; - margin-top: -5px; -} - -.btn.kb-flat-btn:hover, -.btn.kb-flat-btn:active { - background-color: #ccc; -} diff --git a/kbase-extension/static/kbase/css/editorCell.css b/kbase-extension/static/kbase/css/editorCell.css deleted file mode 100644 index 3bec90dae8..0000000000 --- a/kbase-extension/static/kbase/css/editorCell.css +++ /dev/null @@ -1,375 +0,0 @@ -/* Wrapper class - All classes below may be wrapped in this class to avoid polluting - the Narrative styles. -*/ -.kb-editor-cell .kb-app-warning { - font-family: Oxygen, Arial, sans-serif; - text-align: center; -} - -/** new app parameter styling **/ -.kb-editor-cell .kb-app-parameter-panel { - border-left: 3px solid #fff; -} - -.kb-editor-cell .kb-app-parameter-panel-hover { - border-left: 3px solid #428bca; -} - -.kb-editor-cell .kb-app-parameter-row { - margin: 0; -} - -/* for some reason, the css :hover doesn't work right on this div, so we use jquery to toggle this class */ -.kb-editor-cell .kb-app-parameter-row-hover { - background: #f9f9f9; -} - -.kb-editor-cell .kb-app-parameter-row .message.-error { - background: #f2dede; - color: #f44336; -} - -.kb-editor-cell .kb-app-parameter-row .message { - text-align: center; - font-family: Oxygen, Arial, sans-serif; -} - -/* not sure how to get text in these divs to valign middle... */ -.kb-editor-cell .kb-app-parameter-name { - font-family: Oxygen, Arial, sans-serif; - color: #777; - text-align: left; - vertical-align: bottom; - padding-right: 4px; - padding-left: 0; - white-space: normal; -} - -.kb-editor-cell .kb-app-parameter-input { - vertical-align: middle; -} - -.kb-editor-cell .kb-app-parameter-input select.form-control { - margin: 0; -} - -/* -This set of styles is to accommodate the required/satisfied icon which appears -between the select input control and the help text to the right of it. The -problem is that within the columns used for layout the select is set to 100% width -and yet the icon is placed right next to it. The result is that the icon is shoved -to the right of the select control and into the next column, overlapping the -help text. The old app handling this was to scoot the help text far enough over -to accomodate the the icon intruding into its space. This technique makes space -in the column in which the icon lives, and does this by shrinking the select -control with padding, and then scooting the icon back into its column. -*/ - -.kb-editor-cell .kb-app-parameter-input .kb-app-parameter-accepted-glyph, -.kb-editor-cell .kb-app-parameter-input .kb-app-parameter-required-glyph { - /* This scoots the icon inside */ - font-size: 15px; - margin-left: 0; -} - -.kb-editor-cell .kb-app-parameter-required-glyph { - color: #f44336; -} - -.kb-editor-cell .kb-parameter-data-selection { - font-weight: bold; -} - -.kb-editor-cell .kb-app-parameter-hint { - padding-left: 7px; - color: #777; - text-align: left; - margin-top: 3px; -} - -.kb-editor-cell .kb-app-parameter-accepted-glyph { - color: #4bb856; -} - -.kb-editor-cell .kb-app-parameter-info { - color: #777; -} - -.kb-editor-cell .kb-parameter-data-row-remove { - color: #777; -} - -.kb-editor-cell .kb-parameter-data-row-add { - color: #777; -} - -.kb-editor-cell .kb-app-advanced-options-controller-inactive { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - line-height: 14px; - color: #777; - text-align: center; -} - -.kb-editor-cell .kb-app-advanced-options-controller { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - cursor: pointer; - font-style: italic; - font-size: 10pt; - line-height: 14px; - color: #08c; - text-align: center; -} - -.kb-editor-cell .kb-app-advanced-options-controller:hover { - color: #2a6496; -} - -.kb-editor-cell .kb-app-footer { - width: 100%; - overflow: none; - background-color: #f5f5f5; - padding: 10px; -} - -.kb-editor-cell .kb-app-subtitle { - background-color: #f5f5f5; - padding: 3px 5px; -} - -.kb-editor-cell .panel-title > [data-toggle="collapse"]::before { - display: inline-block; - margin-left: 0; - margin-right: 4px; - font-family: "FontAwesome"; - font-style: normal; - font-weight: normal; - font-size: 90%; - width: 12px; - color: silver; - line-height: 1; - vertical-align: baseline; - content: "\f078 "; -} - -.kb-editor-cell .panel-title > [data-toggle="collapse"].collapsed::before { - margin-left: 2px; - margin-right: 2px; - content: "\f054 "; -} - -/* tweaks to elemnts that should be dimmed when the cell is not selected */ - -.unselected .kb-editor-cell { - opacity: 0.5; -} - -.kb-editor-cell .advanced-parameter-showing { - display: block; -} - -.kb-editor-cell .advanced-parameter-hidden { - display: none; -} - -.kb-elapsed-time { - font-family: monospace; -} - -.kb-elapsed-time.-active { - color: lime; -} - -.kb-editor-cell-info-desc { - border: 1px solid #777; - padding: 10px; - margin-right: 10px; - min-height: 100px; -} - -.kb-editor-cell-info { - color: #777; - font-family: Oxygen, Arial, sans-serif; - font-size: 12pt; -} - -.kb-editor-cell-info .header { - border-bottom: 1px solid #777; - padding: 5px 0; - margin-bottom: 8px; -} - -.kb-editor-cell-info .value { - color: #000; -} - -/* -New button color scheme - -blue for border, backgrounds, text: rgb(0, 147, 255)) - -official kbase color: rgb(33,150,243) - -disabled color for border, text - -enabled run color - (green) - rgb(2,140,10) -official kbase green: - -rgb(75,184,86) - -or - - rgb(42,190,159) which is the lighter variant. both used in mocks (but depends where you sample). - -disabled run color - (green) - - -error color - red - rgb(209,82,65) - -disabled (gray) - 205,205,205 - -warning: orange - -*/ - -.btn.kb-editor-cell-btn { - color: rgb(33, 150, 243); - background-color: #fff; - border: 2px rgb(33, 150, 243) solid; - margin-bottom: 4px; -} - -/* Primary Button Style */ -.btn.kb-editor-cell-btn.btn-primary { - color: rgb(33, 150, 243); - border: 2px rgb(33, 150, 243) solid; -} - -.btn.kb-editor-cell-btn.btn-primary:hover { - color: #fff; - background-color: rgb(33, 150, 243); - border-bottom: 6px rgb(33, 150, 243) solid; - margin-bottom: 0; -} - -.btn.kb-editor-cell-btn.btn-primary:active { - color: #fff; - background-color: rgb(33, 150, 243); -} - -/* Hovering over an active cell is different, it tries to make the tab look - disassociated but using a gray background so it doesnt look like an inactive - tab */ -.btn.kb-editor-cell-btn.btn-primary.active { - color: white; - background-color: rgb(33, 150, 243); - border: 2px rgb(33, 150, 243) solid; - border-bottom: 6px rgb(33, 150, 243) solid; - margin-bottom: 0; -} - -.btn.kb-editor-cell-btn.btn-primary.active:hover { - color: rgb(33, 150, 243); - background-color: #ddd; - border-bottom: 2px rgb(33, 150, 243) solid; - margin-bottom: 4px; -} - -/* DANGER - for error */ -.btn.kb-editor-cell-btn.btn-danger { - color: rgb(209, 82, 65); - border: 2px rgb(209, 82, 65) solid; -} - -.btn.kb-editor-cell-btn.btn-danger:hover { - color: #fff; - background-color: rgb(209, 82, 65); - border-bottom: 6px rgb(209, 82, 65) solid; - margin-bottom: 0; -} - -.btn.kb-editor-cell-btn.btn-danger.active { - color: white; - background-color: rgb(209, 82, 65); - border: 2px rgb(209, 82, 65) solid; - border-bottom: 6px rgb(209, 82, 65) solid; - margin-bottom: 0; -} - -.btn.kb-editor-cell-btn.btn-danger.active:hover { - color: rgb(209, 82, 65); - background-color: #ddd; - border-bottom: 2px rgb(209, 82, 65) solid; - margin-bottom: 4px; -} - -/* DISABLED is gray border and text */ - -.btn.kb-editor-cell-btn.disabled, -.btn.kb-editor-cell-btn.disabled:hover, -.btn.kb-editor-cell-btn.disabled:active { - color: #888; - border: 2px #888 solid; - margin-bottom: 4px; -} - -.kb-editor-cell [data-element="run-control-panel"] { - width: 100%; -} - -.kb-editor-cell [data-element="tab-pane"] { - border-top: 2px rgb(33, 150, 243) solid; -} - -.kb-editor-cell [data-element="tab-pane"] > div:empty { - padding: 0; -} - -.kb-editor-cell [data-element="tab-pane"] > div { - padding: 4px; -} - -.kb-editor-cell .btn-primary.kb-btn-action.-rerun { - color: rgb(33, 150, 243); -} - -.kb-editor-cell .btn-primary.kb-btn-action.-run { - color: rgb(75, 184, 86); -} - -.kb-editor-cell .btn-danger.kb-btn-action.-cancel { - color: rgb(209, 82, 65); -} - -.kb-editor-cell .btn-danger.kb-btn-action.-reset { - color: rgb(209, 82, 65); -} - -.kb-app-status-ok { - color: rgb(75, 184, 86); -} - -.kb-app-status-warning { - color: orange; -} - -.kb-app-status-danger, -.kb-app-status-error { - color: rgb(209, 82, 65); -} - -.kb-app-status-default { - color: rgb(33, 150, 243); -} - -/* app cell panels */ - -/* TODO: higher level selector */ - -.parameter-panel .info-panel { - padding-top: 4px; - background: transparent; -} diff --git a/kbase-extension/static/kbase/css/errorPage.css b/kbase-extension/static/kbase/css/errorPage.css deleted file mode 100644 index a24475d0fd..0000000000 --- a/kbase-extension/static/kbase/css/errorPage.css +++ /dev/null @@ -1,37 +0,0 @@ -#header, -#site { - display: block; -} - -.access-error__container, -.generic-error__container { - text-align: center; - margin: 2rem; -} - -.access-error__heading, -.generic-error__heading { - font-weight: bold; - font-size: 200%; - line-height: 1.5; -} - -.access-error__text, -.generic-error__text { - font-size: 150%; - line-height: 2; -} - -.access-request-form__container { - margin-top: 2rem; -} - -.access-request-progress__container { - display: none; - margin-top: 1rem; -} - -.access-request-result { - font-size: 125%; - margin-top: 2rem; -} diff --git a/kbase-extension/static/kbase/css/kbaseEditor.css b/kbase-extension/static/kbase/css/kbaseEditor.css deleted file mode 100644 index 42fb775e47..0000000000 --- a/kbase-extension/static/kbase/css/kbaseEditor.css +++ /dev/null @@ -1,93 +0,0 @@ -/** - * CSS related to editing tools. This currently includes media and model editing - */ - -.modal-body .dataTables_processing { - border: 1px solid #aaa; - background: #e8f9ff; - opacity: 0.8; -} - -.kb-editor { - margin: 0 10px; -} - -.kb-editor .tab-content { - margin: 5px 0 0 0; -} - -.kb-editor .controls { - margin: 5px 0; -} - -.kb-editor .controls .btn { - margin: 0 10px; -} - -/** - * Override KBase stylings that override jupyter styling. - * Should be removed after Bill's refactoring. - */ - -/* override button staylings that were removed */ -.kb-editor .btn-primary { - background-color: #337ab7 !important; - border-color: #2e6da4 !important; -} - -.kb-editor .btn-danger { - background-color: #d9534f !important; - border-color: #d43f3a !important; -} - -/* override icon stylings in button that were removed */ -.kb-editor .btn i { - color: #fff; -} - -/* override table border styling */ -.kb-editor table { - border: 1px solid #bbb; -} - -/** - * General editor styling - */ -.kb-editor-table { - width: 100% !important; /* override datatables hackory */ -} - -.kb-editor-table > tbody > tr > td:first-child { - width: 1%; /* checkboxes are expected on editable tables */ -} - -.kb-editor-table > tbody > tr > td:first-child .fa-square-o { - font-size: 1.4em; - color: #666; -} - -.kb-editor-table > tbody > tr > td:first-child:hover { - cursor: pointer; -} - -.kb-editor-table > tbody > tr > td:first-child:hover .fa-square-o { - color: #000; -} - -.kb-editor-table > tbody > tr.row-select .fa-square-o::before { - content: "\f046"; - color: #444; -} - -.kb-editor-table > tbody > tr > td.editable { - position: relative; -} - -.kb-editor-table > tbody > tr > td.editable:hover::after { - content: "\f044"; - font-family: FontAwesome; - position: absolute; - bottom: 0; - right: 2px; - color: #888; -} diff --git a/kbase-extension/static/kbase/css/kbaseIcons.css b/kbase-extension/static/kbase/css/kbaseIcons.css deleted file mode 100644 index 0adf7e13e1..0000000000 --- a/kbase-extension/static/kbase/css/kbaseIcons.css +++ /dev/null @@ -1,61 +0,0 @@ -@charset "UTF-8"; - -@font-face { - font-family: "kbase-icons"; - src: url("../fonts/kbase-icons.eot"); - src: - url("../fonts/kbase-icons.eot?#iefix") format("embedded-opentype"), - url("../fonts/kbase-icons.woff") format("woff"), - url("../fonts/kbase-icons.ttf") format("truetype"), - url("../fonts/kbase-icons.svg#kbase-icons") format("svg"); - font-weight: normal; - font-style: normal; -} - -[data-icon]::before { - font-family: "kbase-icons" !important; - content: attr(data-icon); - font-style: normal !important; - font-weight: normal !important; - font-variant: normal !important; - text-transform: none !important; - speak: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -[class^="icon-"]::before, -[class*=" icon-"]::before { - font-family: "kbase-icons" !important; - font-style: normal !important; - font-weight: normal !important; - font-variant: normal !important; - text-transform: none !important; - speak: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.icon-compare::before { - content: "a"; -} - -.icon-genome::before { - content: "b"; -} - -.icon-metabolism::before { - content: "c"; -} - -.icon-metagenome::before { - content: "d"; -} - -.icon-reads::before { - content: "e"; -} - -.icon-tree::before { - content: "f"; -} diff --git a/kbase-extension/static/kbase/css/kbaseJobLog.css b/kbase-extension/static/kbase/css/kbaseJobLog.css deleted file mode 100644 index 414e6bbb74..0000000000 --- a/kbase-extension/static/kbase/css/kbaseJobLog.css +++ /dev/null @@ -1,70 +0,0 @@ -#kblog-header { - margin: 5px 0 5px 0; -} - -#kblog-panel { - background-color: #f5f5f5; - border: 1px solid #cecece; - height: 350px; - padding: 5px; - resize: vertical; - overflow: auto; - color: #000; - vertical-align: top; -} - -.kblog-header { - display: flex; - font-family: monospace; -} - -.kblog-line { - font-family: monospace; -} - -.kblog-line:hover { - background-color: #ddd; -} - -.kblog-num-wrapper { - font-size: 85%; - display: flex; - flex-direction: row; -} - -.kblog-line-num { - flex-shrink: 0; - width: 3rem; - text-align: right; - color: #999; - white-space: nowrap; - - /* makes text unselectable */ - -moz-user-select: -moz-none; - -ms-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; -} - -.kblog-text { - word-wrap: break-word; - overflow-wrap: break-word; - flex: 1; - margin-left: 6px; -} - -.kb-error, -.kb-error .kblog-line-num { - color: darkred; - text-decoration: underline; - font-weight: bold; - font-size: 12px; - background-color: rgb(255, 245, 245); -} - -#kblog-msg, -#kblog-err { - margin-top: 5px; -} diff --git a/kbase-extension/static/kbase/css/kbaseNarrative.css b/kbase-extension/static/kbase/css/kbaseNarrative.css deleted file mode 100644 index e4ad39cae4..0000000000 --- a/kbase-extension/static/kbase/css/kbaseNarrative.css +++ /dev/null @@ -1,3686 +0,0 @@ -@import url("//fonts.googleapis.com/css?family=Roboto"); - -.kb-dropzone #upload-message { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; -} - -.kb-dropzone .dz-message { - font-family: Roboto, Arial, sans-serif; - font-style: normal; - font-weight: normal; - font-size: 24px; - line-height: 28px; - text-align: center; - color: #000; - mix-blend-mode: normal; - margin: 2em 4.5em; -} - -.btn__text { - background: #ffffff; - box-sizing: border-box; - border-radius: 4px; - border: 1px solid #ffffff; - font-family: Oxygen, Arial, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 14px; - line-height: 18px; - outline: none; - - text-transform: uppercase; - text-align: center; - color: #4379b1; - height: 34px; -} - -.btn__text:hover { - color: #36618e; - border: 1px solid #ecf2f7; - background-color: #ecf2f7; - cursor: pointer; -} - -.btn__text:focus { - color: #36618e; - border: 1px solid #e3ebf3; - background-color: #e3ebf3; -} - -.btn__text:active { - color: #36618e; - border: 1px solid #d9e4ef; - background-color: #d9e4ef; -} - -.btn__text:disabled { - color: #929292; - border: 1px solid #fafafa; - background-color: #fafafa; -} - -.clear-all-dropzone { - margin: 6px; -} - -.dz-file { - width: 100%; - border: 1px solid #eeeeee; - margin: 2px 0; - padding: 5px 0; - font: normal, 16px, Oxygen, Arial, sans-serif; -} - -.dz-file__name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - line-height: 20px; -} - -.dz-file__upload-progress { - display: inline; -} - -.dz-file__msg { - overflow: hidden; - text-overflow: ellipsis; - font-size: 14px; -} - -.dz-file__msg__success { - display: none; - color: #4baf4f; -} - -.dz-file__progress__bar { - margin-bottom: inherit; - margin-left: 5px; -} - -.dz-file__status { - display: none; - font-weight: bold; - align-items: center; -} - -.dz-file__status__icon { - margin: 0 4px 0 8px; -} - -.dz-file__status__success { - color: #4baf4f; -} - -.dz-file__status__error { - color: #df0002; -} - -#kb-data-staging-table_wrapper { - margin-top: 44px; -} - -.kb-data-staging-decompress { - border: 1px solid #ccc; - border-radius: 1px; -} - -.kb-data-staging-table-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.kb-data-staging-folder:hover { - cursor: pointer; - text-decoration: underline; -} - -.kb-data-breadcrumb .kb-pointer a { - cursor: pointer; -} - -.kb-data-staging__container { - height: 604px; - padding: 5px; - overflow-y: auto; -} - -.kb-data-staging-footer { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-size: 105%; - text-align: center; - background-color: #eee; - height: 32px; - padding: 8px; -} - -.kb-data-staging-metadata-list { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - width: 115px; - display: inline-block; - font-size: 90%; -} - -.kb-data-staging-metadata-file-lines { - white-space: pre; - overflow: scroll; - font-family: monospace; - font-size: 90%; - background-color: #ddd; - max-height: 200px; -} - -/* https://www.figma.com/file/T30UN3QhWhhV5SpXiqY1ij/KBase-component-library?node-id=1%3A4 */ -.kb-data-staging-button { - background: none; - border: none; - cursor: pointer; - font-family: Oxygen, Arial, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 14px; - line-height: 18px; - text-align: center; - text-transform: uppercase; - color: #4379b1; - height: 35px; - margin: 8px; - padding: 8px; - position: relative; - top: -3px; - letter-spacing: 0.5pt; - border-radius: 4px; -} - -.kb-data-staging-button:hover { - background: rgba(67, 121, 177, 0.1); -} - -.kb-data-staging-button:focus { - background: rgba(67, 121, 177, 0.15); -} - -.kb-data-staging-button:active { - background: rgba(67, 121, 177, 0.2); -} - -.kb-data-staging-button:disabled { - background: rgba(0, 0, 0, 0.02); - color: #929292; -} - -.kb-data-stagingarea { - font-family: Oxygen, Arial, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 24px; - line-height: 30px; -} - -.kb-data-staging-spacer { - margin-top: 10px; -} - -.kb-dropzone { - border: 2px dashed #2196f3 !important; - margin-bottom: 5px; - max-height: 150px; - overflow-y: auto; -} - -.kb-ga-seq { - word-wrap: break-word; - font-family: monospace; - max-width: 300px; - max-height: 100px; - overflow-y: auto; -} - -/** - * Shim to make sure jQuery-ui dialogs are always on front of other DOM elements. - */ -.ui-front { - z-index: 1000; -} - -.whiteout-pane { - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - background-color: #fff; - text-align: center; - z-index: 1001; -} - -#kb-narr-name { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-size: 130%; - padding-bottom: 7px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -#kb-narr-name:hover { - text-overflow: inherit; - white-space: normal; -} - -#kb-narr-name > #save_widget { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - padding: 0; -} - -#kb-narr-creator { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; -} - -.kb-narr-namestamp { - margin-bottom: 0; - margin-top: 5px; - margin-left: auto; - margin-right: auto; - padding-left: 20px; - border-left: 2px solid #cecece; - display: block; - flex: 2; - min-width: 0; -} - -.kb-narr-namestamp__error_container { - font-size: 200%; - line-height: 1.5; -} - -.ui-draggable.kb-data-inflight { - display: block; - border: 2px gray solid; - background-color: #feffd6 !important; - border-radius: 4px; - z-index: 1000; -} - -.ui-draggable.kb-data-inflight.-over { - border-color: green; - background-color: #effceb !important; -} - -.kb-data-list-drag-target { - transition: all 0.25s ease; - display: block; - border: 2px orange dashed; - height: 40px; - text-align: center; - padding: 6px; - border-radius: 4px; -} - -.kb-data-list-drag-target.-drag-active { - background-color: #feffd6; -} - -.kb-data-list-drag-target.-drag-hover { - background-color: #effceb; - border-color: green; - height: 90px; - transition: all 0.25s ease; -} - -#kb-notify-area { - float: left; - position: relative; -} - -.kb-navbar-buttons { - display: inline-block; - padding-right: 15px; - margin-right: 15px; - border-right: 2px solid #cecece; -} - -header[role="banner"] { - position: absolute; - top: 0; - width: 100%; - background-color: #fff; - border-bottom: 1px solid #cecece; - z-index: 999; -} - -nav[role="navigation"] ul { - margin: 1.2em 0 0.5em 0; - - /* text-align: center; */ - padding-left: 0; -} - -nav[role="navigation"] ul li { - display: inline; - margin: 0 0.5em; - vertical-align: middle; -} - -nav[role="navigation"] ul li a { - text-decoration: none; - font-size: 1.2em; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - padding: 7px 1em; - background-color: #5c9531; - color: #fff; - border-radius: 8px; - -moz-border-radius: 8px; - -webkit-border-radius: 8px; - -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); - border: 1px solid #5c9531; -} - -input#search_terms { - padding: 0.4em 1em; - display: inline; - border-color: #5c9531; - position: relative; - left: -0.5em; - font-size: 1em; - margin-top: -3px; - background-image: url("../images/search.png"); - background-size: 22px; - background-repeat: no-repeat; - background-position: right center; - border-top-left-radius: 0; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 0; -} - -span#searchspan { - display: inline-block; - width: 50%; -} - -@media screen and (max-width: 1170px) { - span#searchspan { - width: 50%; - } -} - -@media screen and (max-width: 980px) { - span#searchspan { - width: 40%; - } -} - -@media screen and (max-width: 768px) { - span#searchspan { - width: 30%; - } -} - -@media screen and (max-width: 590px) { - span#searchspan { - width: 60%; - } -} - -.navbar-kbase { - border-bottom: 5px solid #cecece !important; - padding: 8px; -} - -.navbar-kbase a:hover { - cursor: pointer; -} - -/* Top bar nav buttons - -------------------------- -*/ -.kb-nav-btn { - background-color: #fff; - padding: 1px 7px; - font-size: 24px; - border: none; - text-shadow: none !important; - margin: 0; - box-shadow: 0 0 3px #cecece; - margin-right: 5px; - min-width: 50px; -} - -.navbar-right .fa::before { - /* color nav icons */ - color: #2196f3; -} - -.kb-navbar-container { - display: flex; - justify-content: space-between; -} - -#kb-nav-menu { - /* don't add shadows here */ - box-shadow: none; -} - -.kb-nav-menu__container { - display: inline-block; -} - -.kb-nav-btn > div { - color: #666 !important; -} - -.kb-nav-btn .kb-nav-btn-txt { - font-size: 10pt; - margin-top: -5px; -} - -.navbar-right .kb-nav-btn:hover { - background-color: #f5f5f5; -} - -.kb-nav-btn-upgrade { - background-color: #4bb856 !important; - display: none; -} - -.kb-nav-btn-upgrade .fa::before { - color: #fff !important; -} - -.kb-nav-btn-upgrade > div { - color: #fff !important; -} - -.kb-nav-btn-upgrade:hover { - background-color: #43a047 !important; -} - -.kb-nav-btn-upgrade.warning { - background-color: #f44336 !important; -} - -.kb-nav-btn-upgrade.warning:hover { - background-color: #dc3c31 !important; -} - -.kb-nav-menu-icon { - display: inline-block; - width: 20px; - text-align: center; - vertical-align: middle; - margin-right: 5px; -} - -.kb-nav-menu-icon .fa { - font-size: 150%; -} - -#kb-ipy-menu { - /* restore rounded corners */ - border-radius: 4px !important; - margin-right: 0; -} - -.btn { - /* don't use drop-shadow on profile menu */ - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -#signin-button .btn { - border: none; -} - -.btn-xs .glyphicon-user { - /* also resize user btn */ - padding: 14px 5px 14px 5px; -} - -/* -------------------------- */ - -.narrative-menu-container { - position: fixed; - z-index: 499; - background-color: #fff; - width: 100%; - border-bottom: 1px solid #ababab; - padding-bottom: 30px; - padding-top: 10px; - margin-top: -10px; - margin-left: auto; -} - -p.clear { - height: 0; - clear: both; -} - -body > #header { - display: block; - box-sizing: border-box; -} - -p#site-title { - height: 46px; - width: 46px; - background: url("../images/kbase_logo.png") no-repeat; - background-size: 46px; - margin: 5px; - float: left; - text-indent: -9999px; -} - -#login-info { - position: absolute; - right: 0; - top: 7px; - margin: 5px; - font-size: 1.2em; -} - -#login-widget button { - margin-left: 5px; -} - -.search-box { - position: relative; - display: inline; - border-collapse: separate; - margin: -11px 0; - padding: 10px 1px; - width: 200px; -} - -#search-box-name { - padding: 6px 0; - border: 0; - border-radius: 4px; -} - -#search-box-name:first-child { - border-right: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -#search-box-name:last-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -#search-input { - margin: -11px 0; -} - -#search-select { - margin: -11px 0; - width: 150px; -} - -#search-area { - position: relative; - font: 18px RobotoBlack, Arial, sans-serif; - font-weight: normal; - color: #686868; - padding: 11px; - margin: 0 5px; -} - -#search-area input { - padding: 11px; -} - -/* center pane */ - -#workspace { - position: fixed; - width: 878px; - top: 173px; - bottom: 42px; - overflow: auto; -} - -/* footer */ -#bottom-tabs { - clear: left; - position: fixed; - bottom: 0; - width: 1170px; - margin: 0 auto; - background-color: #fff; - border-top: 1px solid #aaa; -} - -#bottom-tabs p a { - border: 1px solid #018841; - background-color: #fff; - padding: 14px 20px; - font-size: 14px; -} - -/* left pane styling */ -#left-column { - width: 380px; - position: fixed; - border-right: 5px solid #cecece; - top: 70px; - bottom: 0; -} - -.kb-side-overlay-container { - width: 730px; - border: 1px solid #cecece; - z-index: 1030; - background: white; - position: fixed; - max-height: calc(100% - 73px); - overflow-y: auto; -} - -.kb-side-overlay-header { - width: 100%; - background-color: #2196f3; - color: #fff; - padding: 6px; - font-size: 12pt; -} - -.kb-side-overlay-close { - cursor: pointer; - color: #fff; - font-weight: bold; - font-size: 10pt; -} - -.kb-side-overlay-close:hover { - color: orange; -} - -.kb-side-panel { - overflow-y: hidden; - height: 100%; -} - -#kb-side-toggle-in { - display: none; - top: 70px; - width: 22px; - border-bottom: 6px solid #cecece; - border-right: 6px solid #cecece; - font-size: 12pt; - text-align: center; - background-color: #2196f3; - padding: 12px 6px; - color: #bbdefb; - font-weight: bold; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - cursor: pointer; - z-index: 1; - position: fixed; -} - -#kb-side-toggle-in:hover { - color: #fff; -} - -.kb-side-toggle { - display: inline-block; - font-size: 12pt; - text-align: center; - border-bottom: 6px solid #2196f3; - border-top: 6px solid #2196f3; - background-color: #2196f3; - padding: 6px; - color: #bbdefb; - font-weight: bold; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - cursor: pointer; -} - -.kb-side-toggle:hover { - color: #fff; -} - -.kb-side-header { - display: inline-block; - font-size: 12pt; - text-align: center; - border-bottom: 6px solid #2196f3; - border-top: 6px solid #2196f3; - background-color: #2196f3; - padding: 6px; - color: #bbdefb; - font-weight: bold; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - cursor: pointer; -} - -.kb-overlay-active { - background-color: gray; - border-bottom: 6px solid gray; - border-top: 6px solid gray; -} - -.kb-side-header:hover { - border-bottom: 6px solid #10ce34; -} - -.kb-side-header.active { - border-bottom: 6px solid #10ce34; - color: #fff; - cursor: auto; -} - -.kb-side-separator { - border-bottom: 5px solid #cecece; -} - -.kb-side-tab.active { - display: inherit; -} - -.kb-side-tab { - display: none; - height: 100%; -} - -.kb-narr-side-panel-set { - height: 100%; -} - -.kb-narr-side-panel-set:last-child { - height: 50%; -} - -.kb-narr-outline { - --kb-narr-outline: 2px solid #cecece; - padding: 0.5em 0.5em 0.5em 0.25em; -} - -.kb-narr-outline ul { - list-style: none; - padding: 0; - position: relative; -} - -.kb-narr-outline ul ul { - list-style: none; - padding-left: 1.5em; - position: relative; -} - -.kb-narr-outline li { - position: relative; -} - -.kb-narr-outline ul ul li:not(:last-of-type)::before { - border-left: var(--kb-narr-outline); - content: ""; - height: calc(100% + 0.25em); - left: -0.5em; - position: absolute; - top: 0; -} - -.kb-narr-outline__item { - align-items: center; - display: flex; - flex-flow: row nowrap; - justify-content: left; - left: 0; - padding: 0.1em; - padding-bottom: 0.3em; - position: relative; - cursor: pointer; - top: 0; -} - -.kb-narr-outline ul ul .kb-narr-outline__item::after { - border-bottom: var(--kb-narr-outline); - border-left: var(--kb-narr-outline); - bottom: calc(50% + 1px); - content: ""; - height: calc(50% - 0.25em); - left: -0.5em; - position: absolute; - width: 0.5em; -} - -.kb-narr-outline__item-content { - display: block; - flex-grow: 0; - flex-shrink: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.kb-narr-outline__item-icon { - display: inline-block; - transform: scale(0.5); - transform-origin: top left; -} - -.kb-narr-outline__item-icon-wrapper { - border-radius: 10px; - display: block; - flex-grow: 0; - flex-shrink: 0; - height: 2em; - margin-left: 0.1em; - margin-right: 0.5em; - max-width: 2em; - overflow: hidden; -} - -.kb-narr-outline__item::before { - border-radius: 0.333em; - bottom: 0.2em; - content: ""; - position: absolute; - top: 0; - width: 100%; - z-index: -1; - border: 1px solid transparent; - background-color: transparent; - -webkit-transition: background-color 100ms linear, border 100ms linear; - -ms-transition: background-color 100ms linear, border 100ms linear; - transition: background-color 100ms linear, border 100ms linear; -} - -.kb-narr-outline__item--highlight::before { - border: 1px solid #cecece; - background-color: #eeeeee; -} - -.kb-narr-outline__item--highlight-selected::before { - border: 1px solid #4bb856; - background-color: rgba(75, 184, 86, 0.15); -} - -.kb-overlay-dimmer { - position: fixed; - z-index: 10; - right: 0; - bottom: 0; - opacity: 0.5; - background-color: #000; -} - -#data-header { - padding: 10px 0; - color: #fff; - font-weight: bold; - background-color: #099; -} - -#data-title { - text-align: center; -} - -#data-add-btn { - padding: 0 0 -10px 0; - position: absolute; - right: 7px; - font-size: 0.8em; -} - -#data-add-btn:hover { - color: #000; -} - -#data-tab-nav { - border-bottom: 1px solid #aaa; - padding-top: 10px; - background-color: #0bb; - width: 100%; -} - -#data-tab-nav ul { - padding-bottom: 0; - bottom: 0; - top: 50px; -} - -#data-tab-nav ul li { - display: inline; - border: 1px solid black; - padding: 4px 10px; - background-color: #ddd; -} - -#data-tab-nav ul li a:hover { - text-decoration: none; - color: #a00; -} - -#data-tab-nav ul li a { - color: #000; -} - -#data-tab-nav ul li.selected { - background-color: #fff; -} - -#data-view-panel { - height: 150px; - border: 1px solid #aaa; - padding: 10px; - overflow-y: auto; -} - -#data-pane { - height: 69%; - margin-top: 1px; - border-bottom: 1px solid #aaa; - overflow: auto; -} - -#data-pane p { - margin: 0; -} - -#data-pane p span { - display: block; - padding: 2px 10px; - color: #fff; - border: 1px solid #fff; - width: 123px; - margin: 0; - text-align: center; -} - -#data-pane p span.tab1 { - float: right; - background: -moz-linear-gradient(top, #666, #bbb); - background: -webkit-linear-gradient(top, #666, #bbb); - color: #fff; -} - -#data-pane p span.tab2 { - background: -moz-linear-gradient(top, #ccc, #999); - background: -webkit-linear-gradient(top, #ccc, #999); -} - -#function-pane { - height: 30%; - margin-top: 1px; - overflow: auto; -} - -.left-pane { - font-size: 14px; - position: relative; -} - -#kb-function-panel .kb-function-body { - height: 100%; - max-height: 100%; -} - -h3.pane-title { - margin: 0; - position: relative; - background-color: rgb(224, 224, 224); - padding: 3px 5px; - border: 1px solid #c2c2c2; - font-size: 16px; -} - -#add-link { - position: absolute; - top: 4px; - right: 7px; - font-size: 0.8em; -} - -ul.pane-list { - margin: 0; - padding: 2px; -} - -ul.pane-list li { - list-style: none; - padding: 1px; - margin-left: 5px; -} - -/* --- KBase method list styles -- */ - -.kb-method-list-logo { - width: 30pt; - height: 30pt; - color: #fff; - background-color: #607d8b; - border-style: solid; - border-width: 0; - border-color: #555; - text-align: center; - display: inline-block; - padding-top: 6pt; - font-size: 18pt; - font-weight: bold; - text-shadow: -1px 0 #777, 0 1px #777, 1px 0 #777, 0 -1px #777; - cursor: pointer; -} - -.kb-method-list-logo:hover { - border-width: 5px; - padding-top: 2.5pt; -} - -.kb-method-list-more-div { - color: #777; - font-size: 10pt; - margin: 2px; - text-align: justify; -} - -.kb-method-list-more-div > div:last-child { - text-align: right; -} - -.kb-method-search-clear { - cursor: pointer; - border: 1px solid #cecece; - border-left: none; -} - -/* A function in the function pane */ -li.function a { - text-decoration: none; - color: rgb(80, 130, 50); -} - -li.function a:visited { - /* don't recolor */ - color: rgb(80, 130, 50); -} - -/* dataset in data pane */ -li.dataset a { - text-decoration: none; - color: rgb(130, 80, 50); -} - -li.dataset a:visited { - /* don't recolor */ - color: rgb(130, 80, 50); -} - -/* dialog box + forms styling */ - -.dialog-box { - padding: 20px; -} - -.dialog-box ul li { - padding: 50px 5px; - border-bottom: 1px solid #aaa; - font-size: 1.2em; -} - -.dialog-box ul li:first-child { - padding-top: 20px; -} - -.dialog-box ul li:last-child { - border-bottom: none; -} - -fieldset { - margin: 0 0 15px 0; - border: none; -} - -fieldset label { - /* display: block; -*/ - - margin: 0 0 3px 0; - font-weight: bold; - font-size: 1em; -} - -fieldset input[type="text"], -fieldset select { - width: 200px; -} - -fieldset input[type="password"], -fieldset select { - width: 200px; -} - -/* styling of cells */ - -div.dataset-cell, -div.function-cell { - width: 90%; - margin: 3px auto; - padding: 7px 17px; - border: 1px solid #ccc; -} - -img.dataset-cell { - margin: 3px auto; - padding: 0; -} - -.function-cell { - background-image: url("../images/gears.png"); - background-position: right; - background-repeat: no-repeat; -} - -.dataset-cell p, -.function-cell p { - font-size: 0.875em; -} - -.dataset-cell h2, -.function-cell h2 { - margin-bottom: 0; - font-size: 1.1em; -} - -.textcell { - width: 90%; - margin: 3px auto; - padding: 5px 17px; - font-size: 1em; - height: 1em; - border: 2px solid #fff; - color: #111; -} - -.textcell:hover { - border: 2px dotted #ccc; -} - -p.textcuepara { - color: #ccc; - padding: 0; - margin: 0; - height: 1em; - display: none; -} - -div.tools p, -div.texttools p { - margin: 0; -} - -div.textarea { - width: 650px; - border: none; - padding: 0; - height: auto; - min-height: 1.2em; - display: none; -} - -.tools, -.texttools { - display: none; -} - -.tools { - height: 1em; - margin-top: 0.5em; - margin-left: 10px; -} - -.tools a { - border: 1px solid #018841; - padding: 2px 4px; - font-size: 13px; -} - -#narrative-header .tools { - display: block; - width: 30em; - position: relative; - top: -2em; - left: 350px; -} - -.texttools img { - vertical-align: middle; -} - -img.remove { - float: right; -} - -.ui-state-highlight { - background-color: fbfaed; -} - -div.metadata { - display: block; - width: 200px; - padding: 0 10px; - height: 100px; - float: left; -} - -div.social { - float: right; - width: 80px; - padding: 0 10px; - display: none; -} - -p span.notification { - background-color: #f33; - color: #fff; - padding: 4px 8px; - border-radius: 1em; -} - -/** - * Bootstrap 3 submenu hack^H^H^H^H fix - * http://stackoverflow.com/questions/18023493/bootstrap-3-dropdown-sub-menu-missing - */ - -.dropdown-submenu { - position: relative; -} - -.dropdown-submenu > .dropdown-menu { - top: 0; - left: 100%; - margin-top: -6px; - margin-left: -1px; - -webkit-border-radius: 0 6px 6px 6px; - -moz-border-radius: 0 6px 6px 6px; - border-radius: 0 6px 6px 6px; -} - -.dropdown-submenu:hover > .dropdown-menu { - display: block; -} - -.dropdown-submenu > a::after { - display: block; - content: " "; - float: right; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - border-width: 5px 0 5px 5px; - border-left-color: #ccc; - margin-top: 5px; - margin-right: -10px; -} - -.dropdown-submenu:hover > a::after { - border-left-color: #fff; -} - -.dropdown-submenu.pull-left { - float: none; -} - -.dropdown-submenu.pull-left > .dropdown-menu { - left: -100%; - margin-left: 10px; - -webkit-border-radius: 6px 0 6px 6px; - -moz-border-radius: 6px 0 6px 6px; - border-radius: 6px 0 6px 6px; -} - -/** End Bootstrap 3 submenu fix **/ - -/* Fixes to bring toolbars up to Bootstrap 3 */ -#maintoolbar { - position: relative; - top: 5px; - min-height: 0; - border: none !important; - background-image: none !important; - background-color: #f9f9f9 !important; - -webkit-box-shadow: none; - box-shadow: none; -} - -#maintoolbar-container { - margin-left: 10px; -} - -#menubar { - position: absolute; - width: 100%; - top: 3px; - background-color: #f9f9f9; - border-bottom: 2px solid #ddd; - padding-bottom: 5px; -} - -#menubar .navbar .container { - padding-left: 0; - padding-right: 0; -} - -/* some more bootstrap hacks */ -.navbar { - min-height: 0; - background-image: none; - background-color: #f8f8f8; -} - -#menubar .navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 7px; -} - -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - background-image: none; -} - -.panel-default > .panel-heading { - background-image: none; -} - -#menus { - padding-left: 0; -} - -#menubar .navbar { - margin-bottom: 5px; - width: 100%; -} - -div#notebook_panel { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} - -.version-stamp a { - padding-right: 320px; - text-align: right; - color: #08c !important; -} - -.creator-stamp { - padding-right: 320px; - height: 1em; - text-align: right; - color: #006698; - font-size: 120%; - line-height: 1em; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; -} - -.panel { - margin-bottom: 0; -} - -#kb-jobs-panel { - margin-bottom: 5px; -} - -.kb-narr-side-panel { - margin-bottom: 5px; - height: 100%; -} - -.kb-narr-side-panel .kb-title { - padding: 10px 5px; - font-size: 14pt; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - text-transform: uppercase; - color: #2e618d; -} - -.kb-narr-side-panel .btn { - border: none; -} - -/* From user and job state service */ - -.kbujs-table-container { - height: auto; -} - -.kbujs-timestamp { - color: #0066b9; - cursor: pointer; -} - -.kbujs-error-cell { - color: #c7254e; - font-weight: bold; -} - -.kbujs-error:hover { - cursor: pointer; -} - -.kbujs-loading-modal { - text-align: center; - font-size: 16px; - font-weight: bold; -} - -.kbujs-refresh-btn { - position: absolute; - right: 5px; -} - -.kbujs-jobs-table { - margin-left: auto !important; - margin-right: auto !important; -} - -.kbujs-loading { - height: 100%; - vertical-align: middle; - text-align: center; -} - -.kbujs-delete-job { - cursor: pointer; -} - -.kbujs-error-traceback { - white-space: nowrap; - float: left; - max-width: 516px; - max-height: 250px; - overflow-x: scroll; - overflow-y: scroll; -} - -/* For connecting lines */ -.kb-line { - background-color: lightgrey; - position: absolute; -} - -/* tab.js */ - -.nav-tabs .glyphicon-remove { - margin: 0 0 0 3px; - color: #aaa; -} - -.nav-tabs .glyphicon-remove:hover { - color: #666; - cursor: hand; - cursor: pointer; -} - -/** - ** Formerly in kbaseNarrFuncResults.css - **/ - -/* CSS for various narrative function results. - * This is just a hacky, dummy place to dump css for different - * result widgets. Something better will come along, once we burn - * this down... - */ - -/* Somehow a bunch of classes from jQuery.dataTables aren't getting added. - * Putting them here in the hopes they don't clobber anything else, the way - * the rest of dataTables might... - */ -.paging_full_numbers { - height: 22px; - line-height: 22px; -} - -.paging_full_numbers a:active { - outline: none; -} - -.paging_full_numbers a:hover { - text-decoration: none; -} - -.paging_full_numbers a.paginate_button, -.paging_full_numbers a.paginate_active { - border: 1px solid #aaa; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - padding: 2px 5px; - margin: 0 3px; - cursor: pointer; - *cursor: hand; - color: #333 !important; -} - -.paging_full_numbers a.paginate_button { - background-color: #ddd; -} - -.paging_full_numbers a.paginate_button:hover { - background-color: #ccc; - text-decoration: none !important; -} - -.paging_full_numbers a.paginate_active { - background-color: #99b3ff; -} - -/** End kbaseNarrFuncResults.css **/ - -/***************************************************************************** - ** Formerly in kbaseNarrFunc.css - *****************************************************************************/ - -/* - * Styles for KBase Narrative notebook page, function panel - */ - -/* Header for panel */ -.kb-function-header { - padding: 10px 0; - color: #0064b6; - font-weight: bold; - background-color: #b6e9f8; - text-align: center; - margin-bottom: 3px; -} - -.kb-narr-panel-body { - height: calc(100%); - padding: 3px; -} - -.kb-narr-panel-body > div { - height: 100%; -} - -.kb-narr-panel-body-wrapper { - overflow-y: auto; - height: 100%; -} - -.kb-narr-panel-body-wrapper > div { - height: 100%; -} - -.kb-narr-panel-toggle { - margin-right: 4px; - margin-top: -4px; - cursor: pointer; - color: #888; - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -/* Container for func list */ -.kb-function-body { - height: 100%; - width: 100%; -} - -.kb-function-body .accordion .panel .panel-body { - padding: 0; - padding-left: 5px; -} - -.kb-function-body ul { - border: 1px solid #ddd; - list-style-type: none; - margin: 0; - padding: 0; - width: 100%; -} - -.kb-function-body li { - padding: 5px 5px; - width: 100%; - text-align: left; -} - -/* alt. rows */ -.kb-function-body li:nth-child(odd) { - background-color: #eee; - border-bottom: 1px solid #ddd; -} - -.kb-function-body li:nth-child(even) { - background-color: #fff; - border-bottom: 1px solid #ddd; -} - -.kb-function-body li:hover { - text-decoration: underline; - cursor: pointer; - background-color: #f4f5d6; -} - -/* Link to a function */ -.kb-function-body a { - text-decoration: none; -} - -.kb-function-body a:hover { - text-decoration: none; -} - -.kb-function-error { - background-color: #f2dede !important; - color: #b94a48; - border-color: #eed3d7 !important; -} - -/* Help on a function */ -.kb-function-help { - padding: 0 3px; - float: right; - cursor: pointer; - color: #56559e; - font-size: 14pt; -} - -.kb-function-help:hover { - color: black; -} - -.kb-function-help-popup { - position: absolute; - top: 0; - left: 300px; - z-index: 99; - cursor: pointer; - max-width: 300px; -} - -.kb-function-help-popup h1 { - margin: 0; - margin-top: -10px; - padding: 0; - font-size: 110%; - color: #3076a2; -} - -.kb-function-help-popup h2 { - margin: 10px 0 -10px 0; - padding-top: 5px; - font-size: 80%; - color: #999; - font-style: italic; - text-align: right; - font-weight: 100; -} - -.kb-function-help-popup .header { - background-color: #fff; - color: #3076a2; - font-weight: bold; - font-size: 120%; - margin: 0; - padding: 0; -} - -.kb-function-help-popup a { - font-weight: bold; - text-decoration: underline; -} - -.kb-function-help-popup .version { - font-size: 80%; - color: #666; -} - -.kb-function-help-popup .body { - margin-top: 10px; - margin-bottom: 10px; -} - -#kb-function-help { - position: absolute; - top: 400px; - left: 300px; - z-index: 99; - cursor: pointer; - width: 250px; -} - -#kb-function-help h1 { - margin: 0; - margin-top: -10px; - padding: 0; - font-size: 110%; - color: #3076a2; -} - -#kb-function-help h2 { - margin: 10px 0 -10px 0; - padding-top: 5px; - font-size: 80%; - color: #999; - font-style: italic; - text-align: right; - font-weight: 100; -} - -#kb-function-error-traceback { - white-space: nowrap; - float: left; - max-width: 250px; - overflow-x: scroll; -} - -.kb-function-dim { - background-color: #eee !important; -} - -.kb-function-dim .panel-heading { - background-color: #eee !important; -} - -.kb-function-cat-dim { - background-color: #ddd !important; - border-color: #ddd !important; -} - -.kb-function-cat-dim .panel-heading { - background-color: #ddd !important; - border-color: #ddd !important; -} - -.kb-function-toggle { - color: #08c; - font-style: italic; - cursor: pointer; -} - -.kb-cell-toolbar .buttons { - font-weight: bold; - color: #cecece !important; -} - -.kb-cell-toolbar .btn { - border: none; -} - -.kb-cell-toolbar .icon { - display: inline-block; - padding: 0; - margin-right: 4px; - vertical-align: top; - overflow: hidden; -} - -.kb-cell-toolbar .title { - font-size: 120%; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - color: #2e618d; -} - -.kb-cell-toolbar .subtitle { - font-family: Roboto, Arial, sans-serif; - font-size: 90%; - color: #666; -} - -.kb-cell-toolbar .info-link { - font-family: Roboto, Arial, sans-serif; - font-size: 90%; - color: #666; -} - -.kb-toolbar-open { - border-bottom: 1px solid #ccc; - border-radius: 0; -} - -.selected .kb-toolbar-open { - border-left: 1px solid #4bb856; - border-bottom: 1px solid #4bb856; - background-color: #dff0d8; -} - -.unselected .kb-toolbar-open { - background-color: none; -} - -.selected.kb-error .kb-toolbar-open { - border-color: #d9534f; - background-color: #f2dede; -} - -/* ------------------------------------------------- */ - -/* IPython cells */ - -.kb-cell-run { - z-index: auto; - opacity: 1; -} - -.kb-cell-run h1 { - font-family: Roboto, Arial, sans-serif; - font-weight: normal; - font-size: 12pt; - line-height: 15px; - color: #2e618d; - margin: 3px 13px 3px 3px; - opacity: inherit; - display: inline; -} - -.kb-cell-run div.kb-func-desc { - opacity: inherit; - display: block; -} - -.kb-cell-run h2 { - font-family: Roboto, Arial, sans-serif; - font-weight: normal; - font-size: 11pt; - line-height: 150%; - color: #666; - margin: 3px; - opacity: inherit; -} - -.kb-cell-run form input[type="submit"] { - float: right; - opacity: inherit; - margin-top: 5px; -} - -.kb-cell-run form .buttons { - float: right; - margin-top: 5px; -} - -.kb-cell-params table { - border: none; - margin-bottom: 10px; - opacity: inherit; - margin-left: auto; - margin-right: auto; -} - -.kb-cell-params table > tr, -td { - border: none; - vertical-align: middle; -} - -.kb-cell-params thead > tr > th, -.kb-cell-params tbody > tr > th, -.kb-cell-params tfoot > tr > th, -.kb-cell-params thead > tr > td, -.kb-cell-params tbody > tr > td, -.kb-cell-params tfoot > tr > td { - padding: 2px 4px; -} - -.kb-cell-params tr:hover td, -.kb-cell-params tr:hover th { - background: #eee; -} - -/* grey out the form while running */ -.kb-cell-run.running form { - opacity: 0.6; -} - -/* progress "meter" */ -.kb-cell-progress { - display: none; -} - -.kb-cell-progress.running { - display: block; -} - -.kb-cell-progressbar { - width: 400px; - height: 20px; -} - -.kb-out-desc { - font-family: Roboto, Arial, sans-serif; - font-weight: normal; - font-size: 12pt; - line-height: 15px; - color: #2e618d; - margin: 3px; - opacity: inherit; - display: inline; -} - -.kb-err-desc { - font-family: Roboto, Arial, sans-serif; - font-weight: normal; - font-size: 12pt; - line-height: 15px; - color: #a94442; - margin: 3px; - opacity: inherit; - display: inline; -} - -.kb-err-msg { - font-size: 80% !important; -} - -.kb-out-header { - background-color: #ddd; - padding: 10px; -} - -.kb-func-timestamp { - color: #555; - font-size: 10pt; - font-family: Roboto, Arial, sans-serif; - margin-top: -3px; - padding-right: 1em; -} - -.unselected .kb-func-timestamp { - color: silver; -} - -.kb-func-panel { - border-color: #bce8f1; - border-radius: 1px; -} - -.kb-func-panel > .panel-heading { - color: #31708f; - border-radius: 1px; - background-color: #d9edf7; - border-color: #bce8f1; - padding: 5px 10px; -} - -.kb-func-panel .panel-body { - padding: 0 10px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -.kb-func-panel > .panel-heading + .panel-collapse .panel-body { - border-top-color: #bce8f1; -} - -.kb-func-panel > .panel-footer + .panel-collapse .panel-body { - border-bottom-color: #bce8f1; -} - -/** output cell styling */ -.kb-cell-output .panel { - border-radius: 0; -} - -.kb-cell-output-content { - overflow-x: auto; - padding: 5px; -} - -.kb-cell-output .panel .panel-heading { - padding: 5px 10px; -} - -.rendered_html .kb-cell-output ul { - margin: 0 0 10px 0; -} - -/* override from some jupyter style? */ -.rendered_html :link { - text-decoration: none; -} - -.kb-cell-output .nav > li > a { - padding: 5px 10px; -} - -.kb-cell-error .panel-heading { - background-image: none; - padding: 5px 10px; -} - -/*** styling for the jobs manager */ -.kb-jobs-title { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - color: #185a85; - font-size: 1.2em; -} - -.kb-jobs-title .glyphicon-info-sign { - font-size: 0.9em; -} - -.kb-jobs-info-table { - margin: 0 2px; - width: 95%; -} - -.kb-jobs-info-table th { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - color: #777; - vertical-align: top; - padding-right: 5px; - width: 30%; - max-width: 30%; - min-width: 30%; -} - -.kb-jobs-item { - border-bottom: 2px solid #ddd; - margin-bottom: 5px; - padding-bottom: 3px; -} - -.kb-jobs-error { - background-color: #f2dede; -} - -.kb-jobs-error-btn { - font-size: 10pt; -} - -.kb-jobs-item:last-child { - border-bottom: none; -} - -.kb-jobs-error-modal { - width: 485px; - word-wrap: break-word; - max-height: 220px; - overflow-y: auto; - overflow-x: hidden; -} - -/** new method parameter styling **/ -.kb-method-parameter-panel { - border-left: 3px solid #fff; -} - -.kb-method-parameter-panel-hover { - border-left: 3px solid #428bca; -} - -.kb-method-parameter-row { - margin: 0; - padding: 5px; - border-radius: 5px; -} - -/* for some reason, the css :hover doesn't work right on this div, so we use jquery to toggle this class */ -.kb-method-parameter-row-hover { - background: #f9f9f9; -} - -.kb-method-parameter-row-error { - background: #f2dede; -} - -.kb-method-parameter-error-mssg { - padding: 5px; - text-align: center; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-size: 9pt; - color: #f44336; -} - -/* not sure how to get text in these divs to valign middle... */ -.kb-method-parameter-name { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - color: #777; - text-align: right; - vertical-align: middle; - margin-top: 3px; - padding-right: 0; - padding-left: 0; - white-space: normal; -} - -.kb-method-parameter-input { - vertical-align: middle; - white-space: nowrap; - padding-left: 10px; -} - -.kb-method-parameter-input input { - font-weight: bold; -} - -/* -This set of styles is a to accomodate the required/satisfied icon which appears -between the select input control and the help text to the right of it. The -problem is that within the columns used for layout the select is set to 100% width -and yet the icon is placed right next to it. The result is that the icon is shoved -to the right of the select control and into the next column, overlapping the -help text. The old method handling this was to scoot the help text far enough over -to accomodate the the icon intruding into its space. This technique makes space -in the column in which the icon lives, and does this by shrinking the select -control with padding, and then scooting the icon back into its column. -*/ -.kb-method-parameter-input > div { - /* allow space for the required (red arrow), satisfied (green checkbox) icon */ - padding-right: 20px; -} - -.kb-method-parameter-input .kb-method-parameter-accepted-glyph, -.kb-method-parameter-input .kb-method-parameter-required-glyph { - /* This scoots the icon inside */ - margin-left: -15px; - font-size: 15px; -} - -.kb-parameter-data-selection { - font-weight: bold; -} - -.kb-method-parameter-hint { - padding-left: 7px; - color: #777; - text-align: left; - margin-top: 3px; -} - -.kb-method-parameter-required-glyph { - color: #f44336; - margin-left: 7px; -} - -.kb-method-parameter-accepted-glyph { - color: #4bb856; - margin-left: 7px; -} - -.kb-method-parameter-info { - color: #777; - margin-left: 7px; -} - -.kb-parameter-data-row-remove { - color: #777; -} - -.kb-parameter-data-row-add { - color: #777; -} - -.kb-method-advanced-options-controller-inactive { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - line-height: 14px; - color: #777; - text-align: center; -} - -.kb-method-advanced-options-controller { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-style: italic; - cursor: pointer; - font-size: 10pt; - line-height: 14px; - color: #08c; - text-align: center; -} - -.kb-method-advanced-options-controller:hover { - color: #2a6496; -} - -.kb-method-footer { - width: 100%; - overflow: none; - background-color: #f5f5f5; - padding: 10px; -} - -.kb-method-subtitle { - background-color: #f5f5f5; - padding: 3px 5px; -} - -/* -- App/method panel styling -- */ - -.kb-app-panel { - border-radius: 0; -} - -.kb-app-error { - background-color: #f2dede; -} - -.kb-app-step-container { - margin-top: 6px; -} - -.kb-app-panel > .panel-footer { - border: 0; - border-radius: 0; -} - -.kb-app-panel-description { - font-family: Roboto, Arial, sans-serif; - font-weight: normal; - font-size: 12pt; - line-height: 15px; - color: #2e618d; -} - -.kb-app-step-error-mssg { - font-family: sans-serif; - font-size: 11pt; - line-height: 14px; - color: #a63232; - text-align: left; - margin: 10px; -} - -.kb-app-step-error-heading { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-size: 13pt; - line-height: 14px; - color: #555; - text-align: left; - padding-left: 5px; - margin-top: 20px; -} - -.kb-app-step-error-main-heading { - font-family: Oxygen, Arial, sans-serif; - cursor: pointer; - font-size: 16pt; - line-height: 18px; - color: #555; - text-align: left; - padding-left: 20px; - margin-top: 20px; -} - -.kb-app-step-error { - border: 3px solid #d14836; - z-index: auto; -} - -.kb-app-step-running { - border: 3px solid #2196f3; - z-index: auto; -} - -/* -- "next steps" text and links -- */ - -.kb-app-next { - border-top: 2px solid #cecece; -} - -.kb-app-next h3 { - font-family: Roboto, Arial, sans-serif; - font-size: 0.9em; - font-style: italic; - color: #888; - float: left; - padding: 0.5em 0 0.25em 0; - margin: 0; - -webkit-margin-before: 0; - -webkit-margin-after: 0.25em; -} - -.kb-app-next-hide { - float: right; - color: #2196f3; - font-family: Roboto, Arial, sans-serif; - font-size: 0.9em; - margin-top: 0.25em; - margin-right: 0.5em; -} - -.kb-app-next-unhide { - color: #2196f3; - font-family: Roboto, Arial, sans-serif; - font-size: 0.9em; - margin-top: 0.25em; -} - -.kb-app-next div { - /* container for links */ - clear: both; -} - -/* -- END method/app styling -- */ - -@-webkit-keyframes pulse { - 0% { - opacity: 1; - } - - 50% { - opacity: 0.7; - } - - 100% { - opacity: 1; - } -} - -@keyframes pulse { - 0% { - opacity: 1; - } - - 50% { - opacity: 0.7; - } - - 100% { - opacity: 1; - } -} - -button.kb-app-run { - background-color: #2196f3; - border-radius: 1px; - color: #fff; - border: none; - padding: 10px 20px; - font-size: 13px; - -webkit-box-shadow: 1px 1px 1px #ccc; - -moz-box-shadow: 1px 1px 1px #ccc; - box-shadow: 1px 1px 1px #ccc; -} - -button.kb-app-run:hover { - background-color: #1e88e5; -} - -button.kb-app-run.kb-app-cancel { - background-color: #f44336; -} - -button.kb-app-run.kb-app-cancel:hover { - background-color: #e53935; -} - -button.kb-method-run { - background-color: #2196f3; - border-radius: 1px; - color: #fff; - border: none; - padding: 10px 20px; - font-size: 13px; - -webkit-box-shadow: 1px 1px 1px #ccc; - -moz-box-shadow: 1px 1px 1px #ccc; - box-shadow: 1px 1px 1px #ccc; -} - -button.kb-method-run:hover { - background-color: #1e88e5; -} - -/*********** - * END of kbaseNarrFunc.css - ***********/ - -/********* - * From kbaseData.css - *********/ - -/* -For data in KBase workspace -*/ -.modal-body { - max-height: 100%; -} - -/* === Tooltips === */ - -/* see bootstrap styles, e.g. bootstrap.css, for the original definitions */ -div[class="tooltip-inner"] { - max-width: 400px; - white-space: pre-wrap; - text-align: left; -} - -.notebook_app .tooltip { - z-index: 2000 !important; -} - -.notebook_app .tooltip-inner { - background-color: #1b69b6; -} - -.notebook_app .tooltip.top .tooltip-arrow { - border-top-color: #1b69b6; -} - -.notebook_app .tooltip.top-left .tooltip-arrow { - border-top-color: #1b69b6; -} - -.notebook_app .tooltip.top-right .tooltip-arrow { - border-top-color: #1b69b6; -} - -.notebook_app .tooltip.right .tooltip-arrow { - border-right-color: #1b69b6; -} - -.notebook_app .tooltip.left .tooltip-arrow { - border-left-color: #1b69b6; -} - -.notebook_app .tooltip.bottom .tooltip-arrow { - border-bottom-color: #1b69b6; -} - -.notebook_app .tooltip.bottom-left .tooltip-arrow { - border-bottom-color: #1b69b6; -} - -.notebook_app .tooltip.bottom-right .tooltip-arrow { - border-bottom-color: #1b69b6; -} - -.kb-data-main-panel { - height: 100%; - margin-bottom: 5px; - max-height: 425px; -} - -#data-tabs { - height: 375px !important; -} - -.kb-ws-refresh-btn { - position: absolute; - right: 5px; -} - -#kb-ws .form-control { - width: 85%; - margin: 3px; -} - -.kb-data-control { - width: 95%; -} - -/* Data table */ -.kb-data-table thead { - max-width: 100%; - width: 281px; -} - -.kb-data-table { - max-height: 265px; - width: 100% !important; -} - -.kb-data-table tbody tr { - border-bottom: 1px solid gainsboro; -} - -.kb-data-table tbody tr td { - max-height: 16px; - padding: 5px; - font-size: 1em; - max-width: 100px; -} - -.kb-data-table tbody tr:hover, -.kb-data-table tbody tr td.highlighted { - background-color: #f4f5d6; - cursor: pointer; -} - -.kb-data-obj-name { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: inline-block; - max-width: 88%; -} - -.kb-data-loading { - height: 100%; - vertical-align: middle; - text-align: center; -} - -.kb-data-loading img { - vertical-align: middle; -} - -#kb-ws .dataTables_filter { - display: none; -} - -/* ------------------------------------------ - * View details on KBase objects - * - * Namespace: kb-data-obj - */ - -/* Button to activate the object pane */ -button.kb-data-obj { - float: right; -} - -/* Panel */ -.kb-data-obj-panel { - background: linear-gradient(to bottom, #00d2ff, #b2adcb); - padding: 0; - margin: 10px; -} - -.kb-data-obj-panel button.close { - margin: 5px; - font-size: 125%; -} - -.kb-data-obj-panel-info { - background: rgba(255, 255, 255, 0.5); - height: 100%; - padding: 10px; -} - -.kb-data-obj-panel-graph { - background: transparent; - height: 100%; - padding: 10px; -} - -.kb-data-obj-panel-info dl { - margin: 0; -} - -.kb-data-obj-panel-info dl dt { - float: left; - clear: left; - width: 90px; - text-align: right; - color: rgba(0, 0, 0, 0.5); -} - -.kb-data-obj-panel-info dl dt::after { - content: ":"; -} - -.kb-data-obj-panel-info dl dd { - margin: 0 0 0 100px; -} - -.kb-data-obj-panel-verlist span { - padding: 0 5px; - margin-left: 3px; - border-radius: 2px; - background-color: rgba(255, 255, 255, 0.5); - color: rgba(0, 0, 0, 0.5); - cursor: pointer; -} - -.kb-data-obj-panel-verlist span.selected { - background-color: #2196f3; - color: white; - cursor: default; -} - -.kb-data-obj-panel-graph table { - background-color: rgba(255, 255, 255, 0.9); - width: 100%; - border: none; - color: rgba(0, 0, 0, 0.8); -} - -.kb-data-obj-panel-graph thead tr { - background-color: rgb(230, 230, 230); - color: rgba(0, 0, 0, 1); -} - -.kb-data-obj-graph-ref-to { - margin-top: 8px; -} - -/* - * END: View details on KBase objects - *------------------------------------------ - */ - -/* default blue button in the new style */ -.kb-primary-btn { - background-color: #2196f3; - border-radius: 1px; - color: #fff; - border: none; - padding: 10px 20px; - margin: 3px; - font-size: 14px; - font-family: Roboto, Arial, sans-serif; - font-weight: 400; - box-shadow: 1px 1px 3px #aaa; -} - -.kb-primary-btn:hover { - background-color: #1e88e5; -} - -.kb-primary-btn[disabled] { - box-shadow: none; - background-color: #bbdefb; - color: #fff; -} - -/* gray button in the new style */ -.kb-default-btn { - background-color: #f5f5f5; - border-radius: 1px; - color: #555; - border: none; - padding: 10px 20px; - margin: 3px; - font-size: 14px; - font-family: Roboto, Arial, sans-serif; - font-weight: 400; - box-shadow: 1px 1px 3px #aaa; -} - -.kb-default-btn:hover { - background-color: #cecece; -} - -.kb-default-btn[disabled] { - box-shadow: none; - color: #aaa; -} - -.kb-btn-sm { - padding: 5px 10px; - font-size: 13px; -} - -/* card layout */ - -.narrative-card-logo { - display: flex; - width: 50px; - margin-right: 2px; -} - -.narrative-card-row-main { - display: flex; - align-items: center; - width: 98%; - margin: 0 2px; - padding: 0 4px; -} - -.narrative-data-list-subcontent { - padding-left: 10px; -} - -.narrative-card-ellipsis { - display: flex; - align-items: center; - justify-content: center; - width: 20px; -} - -.narrative-card-palette-icon { - position: relative; - color: #888; - left: 15px; - top: -50px; -} - -.narrative-card-action-button-wrapper { - width: 80px; - height: 50px; - margin: 0 10px; -} - -.narrative-card-action-button { - width: calc(100% - 6px); - height: 80%; - display: flex; - padding: 0; - justify-content: center; -} - -.narrative-card-action-button > span { - margin-right: 5px; -} - -.narrative-card-action-button-name { - display: inline-block; -} - -.narrative-data-panel-btnToolbar > span { - color: #888; -} - -.narrative-data-panel-btnToolbar { - display: flex; - justify-content: center; -} - -/* data list */ -.kb-data-list-obj-row, -.narrative-card-row { - transition: all 0.1s ease; - border-left-style: solid; - border-left-width: 5px; - border-left-color: #fff; - color: #333; - -webkit-box-shadow: 1px 1px 1px 1px #fff; - -moz-box-shadow: 1px 1px 1px 1px #fff; - box-shadow: 1px 1px 1px 1px #fff; -} - -.kb-function-dim .kb-data-list-obj-row { - border-left-color: #eee; -} - -.kb-data-list-obj-row:hover, -.narrative-card-row:hover { - border-left-style: solid; - border-left-width: 5px; - border-left-color: #4bb856; - -webkit-box-shadow: 1px 1px 1px 1px #aaa; - -moz-box-shadow: 1px 1px 1px 1px #aaa; - box-shadow: 1px 1px 1px 1px #aaa; -} - -.palette { - background: #f5f5f5 !important; -} - -.kb-data-list-obj-row-selected { - border-left-style: solid; - border-left-width: 5px; - border-left-color: #4bb856; - -webkit-box-shadow: 1px 1px 1px 1px #aaa; - -moz-box-shadow: 1px 1px 1px 1px #aaa; - box-shadow: 1px 1px 1px 1px #aaa; -} - -.kb-data-list-obj-row-main { - width: 100%; - margin: 0; - padding: 0; -} - -/* again the css hover over div inherits strangly, so we use js */ -.kb-data-list-nav-buttons { - padding: 5px 8px; - font-size: 18px; - border: none; - text-shadow: none !important; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - margin: 0; -} - -.kb-data-list-add-data-button { - cursor: pointer; - background-color: #4379b1; - color: #fff; - border: none; - -webkit-box-shadow: 1px 1px 3px #aaa; - -moz-box-shadow: 1px 1px 3px #aaa; - box-shadow: 1px 1px 3px #aaa; - text-align: center; - width: 30pt; - height: 30pt; - border-radius: 50%; -} - -/* specialize buttons that hover over narrative */ -#kb-add-code-cell, -#kb-add-md-cell { - background-color: #2196f3; - border-radius: 50%; - bottom: 15px; - box-shadow: #cecece 2px 2px 1px; - cursor: pointer; - color: #fff; - height: 40px; - opacity: 0.5; - padding-top: 5px; - position: fixed; - width: 40px; - z-index: 5; -} - -#kb-add-code-cell { - right: 75px; - padding-left: 7px; -} - -#kb-add-md-cell { - margin-right: 20px; - right: 10px; - padding-left: 8px; -} - -#kb-add-code-cell:hover, -#kb-add-md-cell:hover, -.kb-data-list-add-data-button:hover { - opacity: 1; - cursor: pointer; - background-color: #36618e; -} - -.kb-data-list-add-data-text-button { - background-color: #4379b1; - border-radius: 1px; - color: #fff; - border: none; - padding: 10px 20px; - margin: 3px; - font-size: 14px; - font-family: Roboto, Arial, sans-serif; - font-weight: 400; - -webkit-box-shadow: 1px 1px 3px #aaa; - -moz-box-shadow: 1px 1px 3px #aaa; - box-shadow: 1px 1px 3px #aaa; -} - -.kb-data-list-add-data-text-button:hover { - background-color: #36618e; -} - -/* buttons */ -.kb-data-list-btn { - background-color: #2196f3; - border-radius: 1px; - color: #fff; - border: none; - padding: 5px 10px; - font-size: 13px; - -webkit-box-shadow: 1px 1px 3px #aaa; - -moz-box-shadow: 1px 1px 3px #aaa; - box-shadow: 1px 1px 3px #aaa; - margin: 5px; -} - -.kb-data-list-btn:hover { - background-color: #1e88e5; -} - -.kb-data-list-btn[disabled] { - box-shadow: none; - background-color: #bbdefb; - color: #fff; -} - -.kb-data-list-cancel-btn { - background-color: #f5f5f5; - border-radius: 1px; - color: #888; - border: none; - padding: 5px 10px; - font-size: 13px; - -webkit-box-shadow: 1px 1px 3px #aaa; - -moz-box-shadow: 1px 1px 3px #aaa; - box-shadow: 1px 1px 3px #aaa; - margin: 5px; -} - -.kb-data-list-cancel-btn:hover { - background-color: #cecece; -} - -.kb-data-list-row-hr { - width: 70%; - margin: 2px auto; -} - -.kb-data-list-logo { - width: 30pt; - height: 30pt; - color: #fff; - background-color: #607d8b; - border-radius: 50%; - border-style: solid; - border-width: 0; - border-color: #555; - text-align: center; - display: inline-block; - padding-top: 6pt; - font-size: 18pt; - font-weight: bold; - text-shadow: -1px 0 #777, 0 1px #777, 1px 0 #777, 0 -1px #777; -} - -/* For shifting over logos that are stacked on - * top of eachother */ -.kb-data-list-logo-shifted1 { - margin-left: 1px; - margin-top: 1px; -} - -.kb-data-list-logo-shiftedx1 { - margin-left: 8px; - margin-top: 6px; -} - -.kb-data-list-logo-shifted2 { - margin-left: 9px; - margin-top: 7px; -} - -.kb-data-list-logo-shiftedx2 { - margin-left: 16px; - margin-top: 12px; -} - -.kb-data-list-logo-font1 { - font-size: 1.7em !important; -} - -.kb-data-list-logo-font2 { - font-size: 1.5em !important; -} - -/* Nested item indent (crude!!) */ -.kb-data-list-level1 { - margin-left: 25px; -} - -.kb-data-list-level2 { - margin-left: 50px; -} - -.kb-data-list-info { - width: 80%; - padding: 6px 0; - min-height: 60px; - border-bottom: 1px solid #e0e0e0; -} - -.kb-data-list-name { - color: #2e618d; - font-size: 11pt; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - margin: 2px; - cursor: pointer; -} - -.kb-data-list-name:hover { - text-decoration: underline; -} - -.kb-data-list-version { - color: #777; - font-size: 9pt; - font-style: italic; - white-space: nowrap; - cursor: default; -} - -.kb-data-list-type { - color: #777; - font-size: 10pt; - margin: 2px; - cursor: default; -} - -.kb-data-list-date { - color: #777; - font-size: 10pt; - margin: 2px; - white-space: nowrap; - cursor: default; -} - -.kb-data-list-edit-by { - color: #777; - font-size: 10pt; - margin: 2px; - margin-left: 0; - white-space: nowrap; - cursor: pointer; -} - -.kb-data-list-edit-by:hover { - color: #444; -} - -.kb-data-list-narinfo { - color: #777; - font-size: 10pt; - margin: 2px; - margin-left: 10px; -} - -.kb-data-list-narrative-error { - color: #f44336; - font-size: 10pt; - margin: 2px; -} - -.kb-data-list-narrative { - color: #777; - font-size: 10pt; - margin: 2px; - margin-left: 10px; -} - -.kb-data-list-more { - color: #777; - font-size: 8pt; - margin: 0; - padding: 0; - margin-left: 15px; - white-space: nowrap; -} - -.kb-data-list-more-btn { - padding: 2px 4px; - font-size: 12px; - border: none; - text-shadow: none !important; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - margin: 0; -} - -.kb-data-list-more-div { - color: #777; - font-size: 10pt; - margin: 2px; - display: flex; - flex-direction: column; - align-items: center; -} - -.kb-data-list-more-div tr { - color: #777; - font-size: 10pt; - margin: 2px; -} - -.kb-data-list-more-div tr:hover { - background-color: #eee; -} - -.kb-data-list-more-div tr:nth-child(odd) { - background-color: #f5f5f5; -} - -.kb-data-list-more-div tr:hover:nth-child(odd) { - background-color: #eee; -} - -.kb-data-list-more-div th { - color: #777; - font-size: 10pt; - font-weight: bold; - text-align: right; - margin: 2px; -} - -.kb-data-list-more-div td { - color: #777; - font-size: 10pt; - text-align: left; - margin: 2px; - padding-left: 8px; - word-break: break-all; -} - -.kb-data-list-job-name:hover { - text-decoration: none; -} - -/* Indent nested box */ -.kb-data-list-box > .kb-data-list-box { - margin-left: 40px; -} - -/* Control buttons */ -.kb-data-list-ctl span.inviso { - /* like mayo on wonderbread */ - color: #fff; - background-color: #fff; - text-shadow: none; -} - -.kb-data-list-ctl[enabled] { - background-color: lightgrey; -} - -/* end data list */ - -/* APP CATALOG SLIDEOUT BROWSER STYLES */ -.kbcb-browser-container { - margin: 0 auto; - - /* background: #eeeded; */ -} - -.kbcb-back-link { - padding: 0.4em; -} - -.kbcb-ctr-toolbar { - margin: 0.4em; -} - -.kbcb-main-panel-div { - margin: 0; -} - -.kbcb-loading-panel-div { - margin: 0.1em; - text-align: center; - vertical-align: middle; -} - -.unselected .prompt .method-icon { - color: silver; -} - -.method-icon { - color: rgb(103, 58, 183); -} - -.unselected .prompt .app-icon { - color: silver; -} - -.app-icon { - color: rgb(0, 150, 136); -} - -/* when the entire app card is a link, use this style */ -.kbcb-app-card-link { - text-decoration: none; -} - -.kbcb-app-card-list-container { - overflow: auto; - padding: 0 0.2em 1em 0.2em; -} - -.kbcb-app-card-container { - margin: 0.4em; - width: 300px; - height: 110px; - display: block; - position: relative; - float: left; -} - -.kbcb-app-card { - background: #f1f1f5; - border-radius: 2px; - width: 100%; - height: 100%; - overflow: hidden; - text-align: center; - vertical-align: middle; - color: #666; -} - -.kbcb-app-card-header { - max-height: 90px; - overflow: hidden; - text-align: left; -} - -.kbcb-app-card-title-panel { - padding: 10px 5px 5px 5px; -} - -.kbcb-app-card-title { - font-size: 1.1em; -} - -.kbcb-app-card-module { - font-size: 0.8em; -} - -.kbcb-app-card-authors { - font-size: 0.8em; -} - -.kbcb-app-card-subtitle { - padding: 10px 10px 10px 10px; - font-size: 0.9em; - overflow: hidden; -} - -.kbcb-app-card-footer { - font-size: 0.9em; - position: absolute; - bottom: 0; - left: 0; - margin: 0 5px 5px 5px; - width: 100%; - color: #888; -} - -.kbcb-app-card-logo { - padding: 0; - margin: 0; - font-size: 0.9em; -} - -.kbcb-star-default { - color: #888; -} - -.kbcb-star-favorite { - color: orange; -} - -.kbcb-star-nonfavorite, -.kbcb-star-favorite, -.kbcb-star-default { - cursor: pointer; -} - -.kbcb-star-nonfavorite:hover, -.kbcb-star-default:hover { - color: red; -} - -.kbcb-star-favorite:hover { - color: orange; -} - -.kbcb-star-count { - margin-left: 0.3em; - display: inline-block; -} - -.kbcb-run-count { - margin-left: 0.3em; - display: inline-block; -} - -.kbcb-info:hover { - color: orange; -} - -.kbcb-hover { - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); - transition: all 0.2s ease-in-out; -} - -.kbcb-hover:hover { - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - cursor: pointer; -} - -/* styles for App page */ -.kbcb-app-page { - background: #f1f1f5; - border-radius: 2px; - text-align: center; - color: #666; -} - -.kbcb-app-page-header { - text-align: left; -} - -.kbcb-app-page-title-panel { - padding: 10px 5px 5px 0; -} - -.kbcb-app-page-title { - font-size: 2em; -} - -.kbcb-app-page-module { - font-size: 1.2em; -} - -.kbcb-app-page-authors { - font-size: 1em; - margin: 0.3em 0 0 0; -} - -.kbcb-app-page-subtitle { - padding: 1.5em 2em 0 2em; - font-size: 1.2em; -} - -.kbcb-app-page-stats-bar { - font-size: 1.2em; - padding: 0 2em 0.8em 3em; - width: 100%; -} - -.kbcb-app-page-logo { - padding: 0; - margin: 1em; - text-align: center; -} - -/* END CATALOG SLIDEOUT BROWSER STYLES */ - -/* share panel styles */ -.kb-share-user-permissions-dropdown { - border-style: none; - text-shadow: none !important; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - padding: 2px; - width: auto; - height: auto; -} - -.kb-share-select { - width: 200px; -} - -/* hack to make the selection fill the width */ -.kb-share-select .select2-selection__choice { - width: 100% !important; - background-color: #f5f5f5 !important; -} - -.kb-nar-manager-titles { - font-family: Oxygen, Arial, sans-serif; - color: #777; - margin: 10px; - margin-top: 20px; - font-size: 14pt; - font-weight: bold; -} - -/* End kbaseData.css */ - -/* styling for data import overlay */ - -.kb-import-content { - position: relative; - margin: 0 0 0 0; - display: block; -} - -.kb-import-content .btn-xs { - border: none; -} - -.ftp-file-header { - margin-bottom: 5px; - margin-top: 8px; -} - -.globus-link:hover { - text-decoration: none; -} - -.kb-import-content .upload-options button { - background: #fff; - border: 1px solid #c4c4c4; - box-sizing: border-box; - border-radius: 6px; - font-family: Oxygen, Arial, sans-serif; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 18px; - - /* identical to box height */ - text-align: center; - color: #000; - width: 140px; - height: 42px; - margin: 0 6px; -} - -.kb-import-content .upload-options button:hover { - border: 1px solid #cfcfcf; - background-color: #fafafa; -} - -.kb-import-content .upload-options button:focus { - background-color: #f2f2f2; -} - -.kb-import-content .upload-options button:active { - background-color: #e6e6e6; -} - -.kb-import-content .upload-options button:disabled { - background-color: #fafafa; - color: #929292; -} - -.kb-overlay-footer { - position: absolute; - width: 100%; - bottom: 0; - height: 50px; -} - -.kb-overlay-footer .btn-primary, -.kb-overlay-footer .btn-default { - margin: 15px 15px 0 0; -} - -.kb-import-search { - margin: 10px 40px 10px 10px; -} - -.kb-import-filter { - margin: 10px 40px 10px 10px; -} - -.kb-import-item { - margin: 0 20px 0 5px; - padding: 20px 0 0 0; -} - -.kb-import-item:hover { - background-color: #f8f8f8; - -moz-box-shadow: 0 0 2px #aaa; - -webkit-box-shadow: 0 0 2px #aaa; - box-shadow: 0 0 2px #aaa; -} - -.kb-import-item hr { - width: 85%; - border: 2px solid #eaeaea; - margin-top: 15px; /* override */ - margin-bottom: 0; -} - -.kb-import-item .kb-import-checkbox { - font-size: 1.5em; - padding: 15px 20px 15px 20px; - opacity: 0.4; -} - -.kb-import-item .fa-check-square-o { - opacity: 1 !important; -} - -.kb-import-info { - display: inline-block; - margin: 10px 20px 0 0; - font-weight: normal; -} - -.kb-import-info span { - font-size: 0.9em; - font-weight: 700; - color: #999; -} - -.kb-import-status { - margin: 20px; -} - -/* Error dialog */ -.kb-error-dialog strong { - color: darkgreen; /* Douglas Adams forever */ -} - -.kb-err-text { - color: black; - font-family: Consolas, "Courier New", monospace; -} - -.kb-err-warn { - color: firebrick; - font-style: italic; - margin-top: 0.5em; -} - -/* View-only (read-only) */ -#kb-ro-btn { - margin-right: 20px; -} - -#kb-view-mode { - display: inline-block; - font-size: 13pt; - margin: -15px 0 0 1em; - padding: 0; - cursor: pointer; -} - -#kb-view-mode-narr div { - display: inline-block; - color: #2196f3; - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; - font-size: 16px; - padding: 3px; - margin-top: -3px; - width: 147px; - text-align: left; - position: fixed; -} - -/* little space for control-close icon */ -#kb-view-mode-narr-hide { - display: block; - height: 41px; - width: 15px; - float: left; - font-size: 16px; - cursor: pointer; - color: #bbdefb; - background-color: #2196f3; - padding-left: 5px; -} - -/* move icon down a bit */ -#kb-view-mode-narr-hide span { - margin-top: 10px; -} - -/* this is the banner for readonly mode */ -.navbar-right div.label-warning { - font-family: Oxygen, Arial, sans-serif; - font-size: 16px; - font-weight: bold; -} - -/* Show this over the whole narrative UI until the - workspace becomes active. - - This animation was taken from: - http://cssdeck.com/labs/load - - It was created by José Barcelon-Godfrey - http://cssdeck.com/user/Jmbarcelonco - - -Dan Gunter, Feb 10, 2015 -*/ -.kb-loading-blocker__container { - position: absolute; - top: 0; - left: 0; - background-color: white; - width: 100%; - height: 100%; - z-index: 9999; -} - -.kb-loading-blocker__header { - text-align: center; - font-size: 24px; - font-weight: bold; -} - -.kb-loading-blocker__text { - width: 40%; - margin-left: 30%; - font-size: 16px; - text-align: left; - line-height: 1.5; -} - -.kb-loading-blocker__image_container { - text-align: center; - padding-top: 10%; -} - -.loading-warning { - position: absolute; - display: none; -} - -/* Override the default max-width of the narrative box */ - -/* Styles for KNHX trees to remove its document.write nonsense that breaks asynchronous loading */ -#popdiv { - position: absolute; - background-color: #fcfcfc; - border: 1px solid #ccc; - z-index: 10000; - visibility: hidden; - font-size: 12px; -} - -#popdiv a.alt { - padding-left: 9px; - font: 12px monospace; - border: none; - display: inline; -} diff --git a/kbase-extension/static/kbase/css/kbaseNotify.css b/kbase-extension/static/kbase/css/kbaseNotify.css deleted file mode 100644 index 4c01e03f91..0000000000 --- a/kbase-extension/static/kbase/css/kbaseNotify.css +++ /dev/null @@ -1,13 +0,0 @@ -.kb-notify { - position: relative; - bottom: 10px; - width: 300px; - padding: 15px; - color: #fff; - background-color: #000; - opacity: 0.8; -} - -.kb-notify.kb-notify-success { - color: #00cc09; -} diff --git a/kbase-extension/static/kbase/css/kbaseStylesheet.css b/kbase-extension/static/kbase/css/kbaseStylesheet.css deleted file mode 100644 index cd1bbf579a..0000000000 --- a/kbase-extension/static/kbase/css/kbaseStylesheet.css +++ /dev/null @@ -1,168 +0,0 @@ -@font-face { - font-family: 'Oxygen'; - src: url('../fonts/Oxygen-webfont.eot'); - src: - url('../fonts/Oxygen-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-webfont.woff') format('woff'), - url('../fonts/Oxygen-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-webfont.svg#OxygenRegular') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Oxygen'; - src: url('../fonts/Oxygen-Bold-webfont.eot'); - src: - url('../fonts/Oxygen-Bold-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-Bold-webfont.woff') format('woff'), - url('../fonts/Oxygen-Bold-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-Bold-webfont.svg#OxygenBold') format('svg'); - font-weight: bold; - font-style: normal; -} - -@font-face { - font-family: 'Oxygen'; - src: url('../fonts/Oxygen-Italic-webfont.eot'); - src: - url('../fonts/Oxygen-Italic-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-Italic-webfont.woff') format('woff'), - url('../fonts/Oxygen-Italic-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-Italic-webfont.svg#OxygenItalic') format('svg'); - font-weight: normal; - font-style: italic; -} - -@font-face { - font-family: 'Oxygen'; - src: url('../fonts/Oxygen-BoldItalic-webfont.eot'); - src: - url('../fonts/Oxygen-BoldItalic-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-BoldItalic-webfont.woff') format('woff'), - url('../fonts/Oxygen-BoldItalic-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-BoldItalic-webfont.svg#OxygenBoldItalic') format('svg'); - font-weight: bold; - font-style: italic; -} - -@font-face { - font-family: 'OxygenMono'; - src: url('../fonts/OxygenMono-Regular-webfont.eot'); - src: - url('../fonts/OxygenMono-Regular-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/OxygenMono-Regular-webfont.woff') format('woff'), - url('../fonts/OxygenMono-Regular-webfont.ttf') format('truetype'), - url('../fonts/OxygenMono-Regular-webfont.svg#OxygenMonoRegular') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'OxygenRegular'; - src: url('../fonts/Oxygen-webfont.eot'); - src: - url('../fonts/Oxygen-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-webfont.woff') format('woff'), - url('../fonts/Oxygen-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-webfont.svg#OxygenRegular') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'OxygenBold'; - src: url('../fonts/Oxygen-Bold-webfont.eot'); - src: - url('../fonts/Oxygen-Bold-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Oxygen-Bold-webfont.woff') format('woff'), - url('../fonts/Oxygen-Bold-webfont.ttf') format('truetype'), - url('../fonts/Oxygen-Bold-webfont.svg#OxygenBold') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'RobotoBlack'; - src: url('../fonts/Roboto-Black-webfont.eot'); - src: - url('../fonts/Roboto-Black-webfont.eot?#iefix') format('embedded-opentype'), - url('../fonts/Roboto-Black-webfont.woff') format('woff'), - url('../fonts/Roboto-Black-webfont.ttf') format('truetype'), - url('../fonts/Roboto-Black-webfont.svg#RobotoBlack') format('svg'); - font-weight: normal; - font-style: normal; -} - -h1 { - font: 1.75em/2em RobotoBlack, Arial, sans-serif; - letter-spacing: 0; - color: #232323; - font-weight: normal; -} - -h2 { - font: 1.5em/1.75em RobotoBlack, Arial, sans-serif; - font-weight: normal; - color: #898989; - letter-spacing: 0; -} - -h3 { - font: 1.15em/1.25em RobotoBlack, Arial, sans-serif; - letter-spacing: 0; - font-weight: normal; -} - -h4 { - font: 1.15em/1.25em Oxygen, Arial, sans-serif; - font-style: italic; - letter-spacing: 0.07em; -} - -body { - font: 1.375em Oxygen, Arial, sans-serif; - color: #333; - background-color: #fff; -} - -strong, -b { - font-family: Oxygen, Arial, sans-serif; - font-weight: bold; -} - -em, -i { - font-family: Oxygen, Arial, sans-serif; - font-style: italic; -} - -strong em, -em strong, -b i, -i b { - font-family: Oxygen, Arial, sans-serif; - font-style: italic; - font-weight: bold; -} - -a { - color: #08c; - text-decoration: none; -} - -a:visited { - color: #2a6496; -} - -a:hover { - text-decoration: underline; - color: #2a6496; -} - -pre, -code { - font-family: OxygenMono, "Lucida Console", "Lucida Sans Typewriter", monospace; - font-style: normal; -} diff --git a/kbase-extension/static/kbase/css/kbaseTour.css b/kbase-extension/static/kbase/css/kbaseTour.css deleted file mode 100644 index 69e41033ea..0000000000 --- a/kbase-extension/static/kbase/css/kbaseTour.css +++ /dev/null @@ -1,40 +0,0 @@ -.kb-tour { - background-color: #2196f3; -} - -.kb-tour.popover.top > .arrow::after { - border-top-color: #2196f3; -} - -.kb-tour.popover.bottom > .arrow::after { - border-bottom-color: #2196f3; -} - -.kb-tour.popover.right > .arrow::after { - border-right-color: #2196f3; -} - -.kb-tour.popover.left > .arrow::after { - border-left-color: #2196f3; -} - -.kb-tour > h3.popover-title { - background-color: #2196f3; - color: #fff; - font-family: Oxygen, Arial, sans-serif; - font-size: 12pt; -} - -.kb-tour > .popover-content { - background-color: #eee; -} - -.kb-tour > .popover-navigation { - background-color: #eee; -} - -.kb-tour > .close-btn { - position: absolute; - top: 7px; - right: 7px; -} diff --git a/kbase-extension/static/kbase/css/landingPages.css b/kbase-extension/static/kbase/css/landingPages.css deleted file mode 100644 index b8036798e6..0000000000 --- a/kbase-extension/static/kbase/css/landingPages.css +++ /dev/null @@ -1,469 +0,0 @@ - -/* KBase navigation bar */ -.navbar-kbase { - background-color: #fff; - border-bottom: 1px solid #e0e0e0; - -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); -} - -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; -} - -/* END KBase navigation bar */ - -.app-icon:hover { - opacity: 0.9; - text-decoration: none; -} - -#kbase-search-box { - width: 300px; -} - -#signin-button { - display: inline-block; - padding: 0 15px 0 0; -} - -/* bootstrap overrides */ - -.wrapper { - width: 95%; - margin-left: auto; - margin-right: auto; -} - -/* end bootstrap overrides */ - -#core-model { - overflow-x: auto; -} - -#logo { - margin: 2px 10px 0 10px; -} - -table { - font-size: 14px !important; -} - -.media-info-modal { - width: 800px; -} - -#selected-workspace { - margin: 17px 14px 15px 0; -} - -.tab-view { - margin: 10px 0 0 0; -} - -/* mv */ -.search-query { - width: 100%; - margin: 0 0 5px 0; -} - -.caret-up { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: none; - border-bottom: 4px solid #fff; - border-right: 4px solid transparent; - border-left: 4px solid transparent; - border-top-width: 0; - border-top-style: dotted; - content: ""; -} - -.scroll-pane { - height: 200px; - overflow-y: scroll; - overflow-x: hi; -} - -#select-box tr td { - padding: 6px; -} - -.selected-ws { - background-color: #428bca; - color: #fff; -} - -.selected-ws:hover { /* fixme */ - background-color: #428bca !important; -} - -.side-bar-switch { - margin: 0 0 10px 0; -} - -.side-bar-switch li { - font-size: 12px; -} - -.side-bar-switch li a { - padding: 5px 10px; -} - -.selected-obj-alert { - margin-bottom: 5px; /* override */ - padding-top: 10px; - padding-bottom: 10px; -} - -.heatmap-view { - overflow-x: scroll; -} - -/* ws browser */ - -.ws-selector { - margin: 10px 0 0 0; -} - -.ws-selector.btn-ws-settings { - position: absolute; - right: 30px; -} - -.object-view { - margin: 10px 0 0 0; -} - -.select-ws .badge { - margin: 0 4px 0 0; -} - -.select-ws .btn-ws-settings { - padding: 0 5px; -} - -.modal-alert { - margin: 10px 15px 0 15px; - padding: 8px 15px; -} - -.modal-cover { - position: absolute; - z-index: 10; -} - -.modal-cover-table { - display: table; - position: static; - width: 100%; - height: 100%; -} - -.modal-cover-cell { - display: table-cell; - vertical-align: middle; - position: static; -} - -.modal-cover-box { - display: inline-block; - padding: 5px 15px; - margin: 10px; - -moz-box-shadow: 7px 7px 5px #888; - -webkit-box-shadow: 7px 7px 5px #888; - box-shadow: 7px 7px 5px #888; -} - -.modal-cover-content { - padding: 5px; - background: white; - border: 1px solid #666; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; -} - -.obj-table .dataTables_length label { - margin: 7px 0 0 0; -} - -.dataTables_info { - margin: 0 10px 0 0; -} - -.dataTables_length, -.dataTables_info { - float: left; -} - -.ellipsis { - white-space: nowrap; - text-overflow: ellipsis; - max-width: 170px; - display: inline-block; - overflow: hidden; -} - -.ws-descript { - white-space: nowrap; - text-overflow: ellipsis; - max-width: 400px; - overflow: hidden; - display: inline-block; -} - -/* custom checkboxes (prototyped) */ -.ncheck { - width: 15px; - height: 15px; - border: 1px solid #ccc; - margin-left: auto; - margin-right: auto; -} - -.ncheck:hover { - border: 1px solid #444; -} - -.ncheck-btn { - width: 15px; - height: 15px; - border: 1px solid #888; - margin: 4px 0 4px -1px; -} - -.ncheck-btn:hover { - border: 1px solid #444; -} - -.ncheck-checked { - background: url(../img/checkmark.png) no-repeat -5px -4px; - background-image: -webkit-image-set(url(../img/checkmark.png) 1x, url(../img/checkmark_2x.png) 2x); -} - -.ncheck-minus { - background: url(../img/checkmark-partial.png) no-repeat -5px -5px; - background-image: -webkit-image-set(url(../img/checkmark-partial.png) 1x, url(../img/checkmark-partial_2x.png) 2x); -} - -.btn-select-all { - height: 34px; - margin: 0 20px 0 0; -} - -.type-filter { - margin: 0 20px 0 0; -} - -.btn-trash { - margin: 3px 20px 0 0; -} - -.btn-show-info .glyphicon { - padding: 0 0 4px 0; -} - -.open-obj { - margin: 7px 0 0 20px; -} - -/****************** narrative ***************/ - -#login-form { - margin: 70px 0 0 0; -} - -#narrative-nav, -.recent-narratives, -.recent-projects { - padding: 0; - margin: 0; -} - -.group-item-expander { - background: #f2f2f2; /* #d6eaff; */ -} - -.group-item-expander:hover { - cursor: hand; - cursor: pointer; -} - -.btn-new-narrative { - margin: 0 20px 0 0; -} - -.btn-delete-narrative { - cursor: hand; - cursor: pointer; -} - -.btn-delete-narrative:hover { - opacity: 0.7; -} - -/* changing z index for datatable fixed header */ -.navbar-fixed-top { - z-index: 500; -} - -.FixedHeader_Header { - background: #fff; - margin: -6px 0 0 0; - height: 44px; -} - -.narrative-sidebar li { - list-style: none; -} - -@media (min-width: 960px) { - .narrative-sidebar { - position: fixed; - width: 20%; - } -} - -.sidebar-header { - font-size: 14px; - color: #898989; - font-family: RobotoBlack, Arial, sans-serif; - letter-spacing: 0; - font-weight: normal; -} - -.view-header { - font: 1.5em/1.75 RobotoBlack, Arial, sans-serif; - font-weight: normal; - color: #898989; - letter-spacing: 0; -} - -.active .btn-narr { - background-color: #bbe8f9; - text-align: left; - padding-left: 20px; - padding-right: 20px; - border: 1px solid #428bca; -} - -.btn-narr { - background-color: #f2f2f2; - text-align: left; - padding-left: 20px; - padding-right: 20px; - border: 1px solid #dbdbdb; - color: #6da8cf; - font-family: RobotoBlack, Arial, sans-serif; - font-size: 14px; -} - -.nav-sidebar { - top: 70px; -} - -.nav-sidebar li { - list-style: none; - margin-bottom: 10px; - text-align: left; - font-family: RobotoBlack, Arial, sans-serif; -} - -.nav-sidebar > li > a { - padding: 7px 15px; -} - -.navbar-nav { - color: #6da8cf; - font-family: RobotoBlack, Arial, sans-serif; -} - -.navbar-nav > li > a:hover { - background-color: #fff; -} - -.navbar-nav > li.active > a, -.navbar-nav > li.active:hover { - color: #2a6496 !important; - box-sizing: border-box; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - height: 50px; - border-bottom: 4px solid #428bca; -} - -.navbar-nav > .active:hover { - color: #2a6496; -} - -#wrap { - min-height: 100%; - height: auto; - - /* Negative indent footer by its height */ - margin: 0 auto -58px; - - /* Pad bottom by footer height */ - padding: 0 0 80px; -} - -/* for the footer */ -#footer { - height: 58px; -} - -#footer ul { - text-align: center; -} - -#footer ul li { - display: inline; - padding: 7px 5px 5px 1px; -} - -#footer ul li::after { - content: "\2022"; - padding-left: 10px; - color: #bbb; - font-size: 10px; -} - -#footer ul li:last-child::after, -#footer ul li:nth-last-child(2)::after { - content: ""; -} - -#footer img { - position: relative; - top: -3px; -} - -#footer .disclaimer { - background-color: #a94442; - color: #fff; - display: block; - text-align: center; - font-weight: bold; - position: fixed; - bottom: 0; - width: 100%; -} - -.KBSnode rect { - cursor: pointer; - fill: #fff; - fill-opacity: 0.5; - stroke: #3182bd; - stroke-width: 1.5px; -} - -.KBSnode text { - font: 80% sans-serif; - pointer-events: none; -} diff --git a/kbase-extension/static/kbase/custom/custom.css b/kbase-extension/static/kbase/custom/custom.css deleted file mode 100644 index 33a946f084..0000000000 --- a/kbase-extension/static/kbase/custom/custom.css +++ /dev/null @@ -1,372 +0,0 @@ -/* -This CSS file is for overriding the Jupyter CSS. Any KBase-component -CSS should go in /kbase-extension/static/kbase/css/kbaseNarrative.css - -(or the specific css file if available) -*/ -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot'); - src: url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../../ext_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} - -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color: #337ab7; - color: #fff; -} - -.select2-container--default .select2-results__option--highlighted[aria-selected] span { - color: #fff !important; -} - -#site { - overflow: visible; -} - -/* disable the standard notebook, bottom spacer */ - -#notebook > .end_space { - min-height: 0; - height: 0; -} - -@media (min-width: 1200px) { - #notebook-container { - width: auto; - } -} - -@media (min-width: 992px) { - #notebook-container { - width: auto; - } -} - -@media (min-width: 768px) { - #notebook-container { - width: auto; - } -} - -.notebook_app { - overflow-y: hidden; - background-color: #fff; -} - -.celltoolbar { - height: auto; - background: none; - border: none; - border-left: 1px solid transparent; - padding-top: 0; - padding-bottom: 2px; -} - -div.input_area { - border: 1px solid #cecece; - border-radius: 0; - - /* a little padding to match the prompt */ - padding-top: 4px; -} - -div.output_subarea { - max-width: inherit; -} - -div.text_cell_input { - border: 1px solid #cecece; - border-radius: 0; -} - -div.cell { - margin: 8px 0 8px 0; - padding: 0; - border: 1px solid #ccc; - border-left: 5px solid #ccc; - border-radius: 0; -} - -div.cell.selected { - margin-left: 0; - border: 1px solid #4bb856; - border-left: 5px solid #4bb856; - padding-left: 0; - background-color: #e8efff; - background: none; - -webkit-transition: box-shadow 0.2s ease-in-out; - -moz-transition: box-shadow 0.2s ease-in-out; - -o-transition: box-shadow 0.2s ease-in-out; - transition: box-shadow 0.2s ease-in-out; - -webkit-box-shadow: 0 1px 2px #aaa, 0 5px 5px #aaa; - -moz-box-shadow: 0 1px 2px #aaa, 0 5px 5px #aaa; - box-shadow: 0 1px 2px #aaa, 0 5px 5px #aaa; -} - -div.cell.selected::before { - background: none; -} - -div.cell.opened { - padding-top: 0; -} - -.edit_mode div.cell.selected { - border-left: 5px solid #66bb6a; - background: none; - padding-left: 0; -} - -div.cell.selected.kb-error { - border: 1px solid #d9534f; - border-left: 5px solid #d9534f; -} - -div#notebook { - padding: 0; -} - -#notebook_name { - color: #006698; -} - -#notebook_name:hover { - cursor: pointer; - background-color: #e0e0e0; -} - -.prompt { - min-width: 10ex; - padding-top: 10px; -} - -#notebook-container { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - width: auto; - min-width: 460px; - position: fixed; - left: 380px; - top: 70px; - right: 0; - bottom: 0; - overflow: auto; -} - -#notebook-container::after { - content: ""; - height: 100px; - display: block; -} - -/* margin left doesn't really seeem to play well */ - -.btn-toolbar { - margin-left: 0; - padding-left: 0; -} - -.kb-narr-side-panel .btn-group .btn { - padding: 3px 4px 3px 4px; - font-size: 16px; -} - -.btn-subtle { - color: #888; - margin: 0; -} - -.btn-subtle .fa { - color: #888; -} - -.btn-subtle .fa::before { - color: #888; -} - -.notebook_app .btn.active { - box-shadow: none; -} - -/* Fooling around with the narrative cells */ - -.notebook_app .inner_cell { - overflow: visible; -} - -.notebook_app .celltoolbar > .button_container { - flex: 0 auto; -} - -.notebook_app .celltoolbar > .button_container:nth-child(1) { - flex: auto; -} - -.notebook_app .celltoolbar_container { - border-bottom: 1px silver solid; -} - -.notebook_app .prompt { - padding-top: 0; - color: silver; - width: 75px; -} - -.notebook_app .cell.unselected div.text_cell_render { - border: 1px solid transparent; -} - -.notebook_app .cell.selected div.text_cell_render { - border: 1px solid transparent; -} - -.notebook_app .buttons .dropdown-menu { - height: auto !important; - overflow: visible !important; -} - -.notebook_app .btn.disabled .fa { - color: silver; -} - -.text_cell.kb-cell .rendered_html table { - border: 1px solid #ddd; -} - -.text_cell.kb-cell .rendered_html ul { - margin: 0; -} - -.text_cell.opened.rendered.kb-cell .rendered_html { - padding: 0; - border: 0; -} - -.text_cell.opened.rendered.selected.kb-cell .rendered_html { - border-left: 1px solid #4bb856; -} - -.text_cell.opened.rendered.selected.kb-cell.kb-error .rendered_html { - border-left: 1px solid #d9534f; -} - -.text_cell.opened.rendered.kb-cell { - padding-bottom: 0; -} - -.rendered_html tr, -.rendered_html th, -.rendered_html td { - text-align: inherit; -} - -.rendered_html table { - table-layout: inherit; -} - -/* narrative cell icons */ - -.cell.selected .prompt .method-icon { - color: rgb(103, 58, 183); -} - -.cell.selected .prompt .app-icon { - color: rgb(0, 150, 136); -} - -.cell.selected .prompt .markdown-icon { - color: #000; -} - -.cell.selected .prompt .app-output-icon { - color: #000; -} - -.cell.selected .prompt .method-output-icon { - color: #000; -} - -.cell.selected .prompt .data-viewer-icon { - color: #000; -} - -.cell.selected .prompt .error-icon { - color: red; -} - -.cell.selected .prompt.input_prompt { - color: #303f9f; -} - -.cell.selected .prompt.output_prompt { - color: #d84315; -} - -.cell > .inner_cell > .ctb_hideshow { - display: block; -} - -.cell.selected .btn-default { - color: #000; -} - -.cell.unselected .btn-default { - color: #ccc; -} - -.cell.unselected .kb-cell-toolbar .title-container { - opacity: 0.5; -} - -.cell.unselected .kb-cell-toolbar .buttons-container { - opacity: 0.2; -} - -.kb-btn-icon { - display: inline-block; - padding: 4px; -} - -.kb-btn-icon .fa { - opacity: 0.5; -} - -.kb-btn-icon:hover .fa { - opacity: 1; -} - -/* - For some reason Jupyter removes the bottom margin. This causes problems with - bootstrap, since bootstrap removes the top margin! -*/ - -p { - margin-bottom: 9px; -} - -.prompt { - display: none !important; -} - -/* - * Jupyter 5.6.0 introduced a "run this cell" button that appears on mouseover. - * Cool though this is, it's a problem on app cells. - * This hunk of CSS should remove it until we can better work it in to our design. - */ -div.code_cell:hover .run_this_cell { - visibility: hidden !important; -} - -div.code_cell:hover div.input_prompt.run_this_cell { - visibility: hidden !important; -} - -div.code_cell div.input_prompt { - min-width: 0; -} - -.run_this_cell { - padding: 0 !important; - width: 0 !important; -} diff --git a/kbase-extension/static/kbase/custom/custom.js b/kbase-extension/static/kbase/custom/custom.js index 99b1b3f7f3..dfabcc21d7 100644 --- a/kbase-extension/static/kbase/custom/custom.js +++ b/kbase-extension/static/kbase/custom/custom.js @@ -107,7 +107,7 @@ define([ 'base/js/keyboard', 'notebook/js/keyboardmanager', 'notebook/js/cell', - 'common/utils', + 'common/cellUtils', 'common/jupyter', 'kb_common/html', @@ -301,9 +301,9 @@ define([ })(); p.renderMinMax = function () { - let $cellNode = $(this.element), - metaToggleMode = utils.getCellMeta(this, 'kbase.cellState.toggleMinMax'), - toggleMode = $cellNode.data('toggleMinMax'); + const $cellNode = $(this.element), + metaToggleMode = utils.getCellMeta(this, 'kbase.cellState.toggleMinMax'); + let toggleMode = $cellNode.data('toggleMinMax'); if (metaToggleMode) { if (!toggleMode) { @@ -323,7 +323,7 @@ define([ switch (toggleMode) { case 'maximized': if (!this.maximize) { - console.log('HELP', this); + console.warn('HELP', this); return; } this.maximize(); @@ -335,8 +335,8 @@ define([ }; p.toggleMinMax = function () { - let $cellNode = $(this.element), - toggleMode = $cellNode.data('toggleMinMax') || 'maximized'; + const $cellNode = $(this.element); + let toggleMode = $cellNode.data('toggleMinMax') || 'maximized'; switch (toggleMode) { case 'maximized': @@ -457,12 +457,12 @@ define([ } }); - $cellNode.on('hide-title.cell', (e) => { + $cellNode.on('hide-title.cell', () => { const $menu = $(cell.celltoolbar.element).find('.button_container'); $menu.trigger('hide-title.toolbar'); }); - $cellNode.on('show-title.cell', (e) => { + $cellNode.on('show-title.cell', () => { const $menu = $(cell.celltoolbar.element).find('.button_container'); $menu.trigger('show-title.toolbar'); }); @@ -532,7 +532,6 @@ define([ $cellNode.find('.inner_cell > div:nth-child(2)').addClass('hidden'); $cellNode.find('.inner_cell > div:nth-child(3)').addClass('hidden'); utils.setCellMeta(this, 'kbase.cellState.showTitle', true); - utils.setCellMeta(this, 'kbase.cellState.message', '', true); }; p.maximize = function () { @@ -540,14 +539,6 @@ define([ $cellNode.find('.inner_cell > div:nth-child(2)').removeClass('hidden'); $cellNode.find('.inner_cell > div:nth-child(3)').removeClass('hidden'); utils.setCellMeta(this, 'kbase.cellState.showTitle', false); - - // this is a little distracting to see in all markdown cells at all times. - // it might make more sense in some kind of help settings? or as a tooltip? - // but there's also already a placeholder in shiny new markdown cells, making this - // redundant. This text has been moved there. - // if (NarrativeRuntime.canEdit()) { - // utils.setCellMeta(this, 'kbase.cellState.message', 'Double click content to edit; click out of the edit area to preview', true); - // } }; // We need this method because the layout of each type of cell and @@ -625,12 +616,12 @@ define([ } }); - $cellNode.on('hide-title.cell', (e) => { + $cellNode.on('hide-title.cell', () => { const $menu = $(cell.celltoolbar.element).find('.button_container'); $menu.trigger('hide-title.toolbar'); }); - $cellNode.on('show-title.cell', (e) => { + $cellNode.on('show-title.cell', () => { const $menu = $(cell.celltoolbar.element).find('.button_container'); $menu.trigger('show-title.toolbar'); }); @@ -672,7 +663,7 @@ define([ $menu.trigger('selected.toolbar'); }); - this.events.on('rendered.MarkdownCell', (e, data) => { + this.events.on('rendered.MarkdownCell', () => { // cell.setCellState('icon', 'paragraph'); utils.setCellMeta(cell, 'kbase.attributes.icon', 'paragraph'); @@ -681,8 +672,8 @@ define([ // cell.renderToggleState(); // get the h1 if it exists. - let title, - renderedContent = cell.element.find('.rendered_html'), + let title; + const renderedContent = cell.element.find('.rendered_html'), h1 = renderedContent.find('h1'); if (h1.length > 0) { title = h1[0].innerText; @@ -752,7 +743,7 @@ define([ */ (function () { - keyboardManager.KeyboardManager.prototype.register_events = function (e) { + keyboardManager.KeyboardManager.prototype.register_events = function () { // NOOP return; }; @@ -784,9 +775,7 @@ define([ }; p.maximize = function () { - const inputArea = this.input.find('.input_area').get(0), - outputArea = this.element.find('.output_wrapper'); - + const outputArea = this.element.find('.output_wrapper'); outputArea.removeClass('hidden'); }; @@ -804,8 +793,7 @@ define([ p.getIcon = function () { const iconColor = 'silver'; - let icon; - icon = span({ class: 'fa fa-inverse fa-stack-1x fa-spinner fa-pulse fa-fw' }); + const icon = span({ class: 'fa fa-inverse fa-stack-1x fa-spinner fa-pulse fa-fw' }); return span({ style: '' }, [ span( @@ -845,9 +833,9 @@ define([ }); if (this.code_mirror) { - this.code_mirror.on('change', (cm, change) => { + this.code_mirror.on('change', (cm) => { const lineCount = cm.lineCount(), - commentRe = /^\.*?\#\s*(.*)$/; + commentRe = /^\.*?#\s*(.*)$/; for (let i = 0; i < lineCount; i += 1) { const line = cm.getLine(i), m = commentRe.exec(line); @@ -879,6 +867,8 @@ define([ const codeInputArea = this.input.find('.input_area')[0]; if (codeInputArea) { codeInputArea.classList.toggle('-show'); + + // eslint-disable-next-line no-self-assign this.metadata = this.metadata; } }; @@ -1008,9 +998,8 @@ define([ Jupyter.narrative.getUserPermissions().then((perm) => { const canChangeName = perm === 'a' && !Jupyter.narrative.readonly; options = options || {}; - let that = this, - dialogBody, - buttons; + const that = this; + let dialogBody, buttons; if (canChangeName) { dialogBody = $('
') .append( @@ -1071,7 +1060,7 @@ define([ OK: {}, }; } - var d = dialog.modal({ + const d = dialog.modal({ title: 'Rename Narrative', body: dialogBody, notebook: options.notebook, diff --git a/kbase-extension/static/kbase/js/api/StagingServiceClient.js b/kbase-extension/static/kbase/js/api/StagingServiceClient.js index ea2c2c8bf2..a1b4c8bc4d 100644 --- a/kbase-extension/static/kbase/js/api/StagingServiceClient.js +++ b/kbase-extension/static/kbase/js/api/StagingServiceClient.js @@ -20,6 +20,7 @@ define(['RestAPIClient'], (RestAPIClient) => { delete: { method: 'delete', path: 'delete/${path}' }, rename: { method: 'post', path: 'rename/${path}' }, decompress: { method: 'patch', path: 'decompress/${path}' }, + importer_mappings: { method: 'get', path: 'importer_mappings/?${file_list}' }, }, }); }; diff --git a/kbase-extension/static/kbase/js/common/appUtils.js b/kbase-extension/static/kbase/js/common/appUtils.js deleted file mode 100644 index d4ad81c4f5..0000000000 --- a/kbase-extension/static/kbase/js/common/appUtils.js +++ /dev/null @@ -1,201 +0,0 @@ -define(['kb_common/html', 'common/props', 'common/runtime', 'narrativeConfig'], ( - html, - Props, - Runtime, - narrativeConfig -) => { - 'use strict'; - - const t = html.tag, - span = t('span'), - img = t('img'); - - function makeToolbarAppIcon(appSpec) { - // icon is in the spec ... - const runtime = Runtime.make(), - nmsBase = runtime.config('services.narrative_method_store_image.url'), - iconUrl = Props.getDataItem(appSpec, 'info.icon.url'); - - if (iconUrl) { - return span({ style: { padding: '3px 3px 3px 3px' } }, [ - img({ - src: nmsBase + iconUrl, - style: { maxWidth: '50px', maxHeight: '50px', margin: '0x' }, - }), - ]); - } - - return span( - { - class: 'fa-stack fa-2x', - style: { - verticalAlign: 'top', - textAlign: 'center', - color: 'rgb(103,58,183)', - lineHeight: '56px', - }, - }, - [ - span({ - class: 'fa fa-square fa-stack-2x', - style: { color: 'rgb(103,58,183)', lineHeight: '56px' }, - }), - span({ class: 'fa fa-inverse fa-stack-1x fa-cube' }), - ] - ); - } - - function makeToolbarGenericIcon(fontAwesomeIconName, color) { - const iconColor = color || 'silver'; - - return span({ style: '' }, [ - span( - { - class: 'fa-stack fa-2x', - style: { verticalAlign: 'top', padding: '0', lineHeight: '56px' }, - }, - [ - span({ - class: 'fa fa-square fa-stack-2x', - style: { color: iconColor, lineHeight: '56px' }, - }), - span({ class: 'fa fa-inverse fa-stack-1x fa-' + fontAwesomeIconName }), - ] - ), - ]); - } - - function makeAppIcon(appSpec) { - // icon is in the spec ... - const runtime = Runtime.make(), - nmsBase = runtime.config('services.narrative_method_store_image.url'), - iconUrl = Props.getDataItem(appSpec, 'info.icon.url'); - - if (iconUrl) { - return span({ style: { padding: '3px 3px 3px 3px' } }, [ - img({ - src: nmsBase + iconUrl, - style: { maxWidth: '50px', maxHeight: '50px', margin: '0x' }, - }), - ]); - } - - return span({ style: '' }, [ - span( - { - class: 'fa-stack fa-2x', - style: { textAlign: 'center', color: 'rgb(103,58,183)', 'padding-top': '5px' }, - }, - [ - span({ - class: 'fa fa-square fa-stack-2x', - style: { color: 'rgb(103,58,183)' }, - }), - span({ class: 'fa fa-inverse fa-stack-1x fa-cube' }), - ] - ), - ]); - } - - function makeGenericIcon(fontAwesomeIconName, color) { - const iconColor = color || 'silver'; - - return span({ style: '' }, [ - span({ class: 'fa-stack fa-2x', style: { textAlign: 'center', color: iconColor } }, [ - span({ class: 'fa fa-square fa-stack-2x', style: { color: iconColor } }), - span({ class: 'fa fa-inverse fa-stack-1x fa-' + fontAwesomeIconName }), - ]), - ]); - } - - function parseType(typeId) { - var parsed = typeId.split('-'), - typeId = parsed[0].split('.'), - module = typeId[0], - name = typeId[1], - version = parsed[1]; - return { - name: name, - module: module, - version: version, - }; - } - - function makeTypeIcon(typeId) { - let type = parseType(typeId), - iconSpec = narrativeConfig.get('icons'), - color, - iconDef, - icon; - - if (iconSpec) { - color = iconSpec.color_mapping[type.name]; - iconDef = iconSpec.data[type.name]; - } - - if (iconDef) { - icon = iconDef[0]; - } else { - icon = iconSpec.data.DEFAULT[0]; - } - - if (!color) { - color = 'black'; - } - - return span([ - span({ class: 'fa-stack fa-2x', style: { textAlign: 'center', color: color } }, [ - span({ class: 'fa fa-circle fa-stack-2x', style: { color: color } }), - span({ class: 'fa fa-inverse fa-stack-1x ' + icon }), - ]), - ]); - } - - function makeToolbarTypeIcon(typeId) { - let type = parseType(typeId), - iconSpec = narrativeConfig.get('icons'), - color, - iconDef, - icon; - - if (iconSpec) { - color = iconSpec.color_mapping[type.name]; - iconDef = iconSpec.data[type.name]; - } - - if (iconDef) { - icon = iconDef[0]; - } else { - icon = iconSpec.data.DEFAULT[0]; - } - - if (!color) { - color = 'black'; - } - - return span([ - span( - { - class: 'fa-stack fa-2x', - style: { textAlign: 'center', color: color, lineHeight: '56px' }, - }, - [ - span({ - class: 'fa fa-circle fa-stack-2x', - style: { color: color, lineHeight: '56px' }, - }), - span({ class: 'fa fa-inverse fa-stack-1x ' + icon }), - ] - ), - ]); - } - - return { - makeAppIcon: makeAppIcon, - makeGenericIcon: makeGenericIcon, - makeToolbarAppIcon: makeToolbarAppIcon, - makeToolbarGenericIcon: makeToolbarGenericIcon, - makeTypeIcon: makeTypeIcon, - makeToolbarTypeIcon: makeToolbarTypeIcon, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/busEventManager.js b/kbase-extension/static/kbase/js/common/busEventManager.js index 3c11a9ac4b..ae2904c24b 100644 --- a/kbase-extension/static/kbase/js/common/busEventManager.js +++ b/kbase-extension/static/kbase/js/common/busEventManager.js @@ -1,28 +1,35 @@ define([], () => { + 'use strict'; + function factory(config) { - let listeners = [], - bus = config.bus; + let listeners = []; + const bus = config.bus; + function add(listenerId) { listeners.push(listenerId); } + function remove(listenerId) { bus.removeListener(listenerId); delete listeners[listenerId]; } + function removeAll() { listeners.forEach((id) => { try { bus.removeListener(id); } catch (ex) { + // eslint-disable-next-line no-console console.log('Error removing bus listener', ex, id, listeners); } }); listeners = []; } + return { - add: add, - remove: remove, - removeAll: removeAll, + add, + remove, + removeAll, }; } diff --git a/kbase-extension/static/kbase/js/common/cellComponents/actionButtons.js b/kbase-extension/static/kbase/js/common/cellComponents/actionButtons.js new file mode 100644 index 0000000000..05628020b2 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/actionButtons.js @@ -0,0 +1,96 @@ +define(['common/html'], (html) => { + 'use strict'; + function factory(config) { + /** + * + * @param {object} options has the following keys: + * - bus - the message bus + * - ui - the ui manager + * - runAction - invoked when the user clicks an active button, takes in the button name + * as a single parameter + * - actionButtons object that defines the action buttons: + * - current: which is the current button, and its state: + * - name - string, one of the action keys + * - disabled - if truthy, then should not be clickable + * - availableButtons - the set of available buttons, each key + * is the button name, values have the following properties: + * - help - tooltip string + * - type - Bootstrap class types + * - classes - array of classes to add to the button component + * - label - button text + */ + + const t = html.tag, + div = t('div'), + cssBaseClass = 'kb-rcp'; + + const actionButtons = config.actionButtons, + ui = config.ui, + bus = config.bus, + runAction = config.runAction; + + function buildLayout(events) { + return div( + { + class: `${cssBaseClass}__action-button-container`, + }, + buildActionButtons(events) + ); + } + + function buildActionButtons(events) { + const buttonList = Object.keys(actionButtons.availableButtons).map((key) => { + const button = actionButtons.availableButtons[key], + classes = [`${cssBaseClass}__action-button`].concat(button.classes); + let icon; + if (button.icon) { + icon = { + name: button.icon.name, + size: 2, + }; + } + return ui.buildButton({ + tip: button.help, + name: key, + events: events, + type: button.type || 'default', + classes: classes, + event: { + type: 'actionButton', + data: { + action: key, + }, + }, + icon: icon, + label: button.label, + }); + }); + bus.on('actionButton', (message) => { + const action = message.data.action; + runAction(action); + }); + + return buttonList; + } + + function setState(newState) { + const state = newState; + for (const btnName of Object.keys(actionButtons.availableButtons)) { + ui.hideButton(btnName); + } + ui.showButton(state.name); + state.disabled ? ui.disableButton(state.name) : ui.enableButton(state.name); + } + + return { + setState, + buildLayout, + }; + } + + return { + make: function (config) { + return factory(config); + }, + }; +}); diff --git a/nbextensions/appCell2/widgets/appInfoDialog.js b/kbase-extension/static/kbase/js/common/cellComponents/appInfoDialog.js similarity index 84% rename from nbextensions/appCell2/widgets/appInfoDialog.js rename to kbase-extension/static/kbase/js/common/cellComponents/appInfoDialog.js index 4c4e52e666..35f5a0d75a 100644 --- a/nbextensions/appCell2/widgets/appInfoDialog.js +++ b/kbase-extension/static/kbase/js/common/cellComponents/appInfoDialog.js @@ -44,21 +44,24 @@ define([ div({ class: 'header' }, 'Information'), div([ 'Author', - data.authors.length > 1 ? 's' : '', + data.authors.length === 1 ? '' : 's', ': ', - span( - { class: 'value' }, - (function () { - if (data.authors.length === 1) { - return data.authors[0]; - } - return ul(data.authors.map(li)); - })() - ), + data.authors.length > 1 + ? ul( + { + class: 'value', + }, + data.authors.map((auth) => { + return li(auth); + }) + ) + : span( + { + class: 'value', + }, + data.authors[0] || 'No authors listed' + ), ]), - //div([ - // 'ID: ', span({class: 'value'}, data.id) - //]), div(['Tag: ', span({ class: 'value' }, data.tag)]), div(['Version: ', span({ class: 'value' }, data.version)]), div(['Updated: ', span({ class: 'value' }, data.updateDate)]), @@ -116,10 +119,8 @@ define([ }); }) .then((result) => { - switch (result.action) { - case 'link': - window.open(result.result.url, result.result.name); - break; + if (result.action === 'link') { + window.open(result.result.url, result.result.name); } }) .catch((err) => { diff --git a/kbase-extension/static/kbase/js/common/cellComponents/cellControlPanel.js b/kbase-extension/static/kbase/js/common/cellComponents/cellControlPanel.js new file mode 100644 index 0000000000..221278f21f --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/cellControlPanel.js @@ -0,0 +1,82 @@ +define(['common/html', 'common/cellComponents/actionButtons'], (html, ActionButton) => { + 'use strict'; + + const div = html.tag('div'), + cssBaseClass = 'kb-rcp'; + + /** + * options - + * - bus - the message bus + * - ui - the UI controller + * - action: object that defines the action buttons: + * - current: which is the current button, and its state: + * - name - string, one of the action keys + * - disabled - if truthy, then should not be clickable + * - availableButtons - the set of available buttons, each key + * is the button name, values have the following properties: + * - help - tooltip string + * - type - Bootstrap class types + * - classes - array of classes to add to the button component + * - label - button text + * - runAction - function that gets run when the button is clicked, + * takes the button name as the single parameter + * @param {object} options + */ + function CellControlPanel(options) { + const bus = options.bus; + const ui = options.ui; + const actionButton = ActionButton.make({ + ui: ui, + bus: bus, + runAction: options.action.runAction, + actionButtons: options.action.actions, + }); + + function setActionState(newState) { + actionButton.setState(newState); + } + + function setExecMessage(message) { + if (message === null || message === undefined) { + message = ''; + } + ui.setContent('run-control-panel.execMessage', message); + } + + function buildLayout(events) { + return div( + { + class: `${cssBaseClass}__layout_div`, + dataElement: 'run-control-panel', + }, + [ + // action button widget + actionButton.buildLayout(events), + // status stuff + div({ + class: `${cssBaseClass}-status__fsm_display hidden`, + dataElement: 'fsm-display', + }), + div({ + class: `${cssBaseClass}-status__container`, + dataElement: 'execMessage', + }), + div({ + class: `${cssBaseClass}__toolbar`, + dataElement: 'toolbar', + }), + ] + ); + } + + return { + buildLayout, + setActionState, + setExecMessage, + }; + } + + return { + make: CellControlPanel, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/cellTabs.js b/kbase-extension/static/kbase/js/common/cellComponents/cellTabs.js new file mode 100644 index 0000000000..f60a1528e7 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/cellTabs.js @@ -0,0 +1,168 @@ +define(['bluebird', 'common/html', 'common/ui', 'common/events'], (Promise, html, UI, Events) => { + 'use strict'; + + const div = html.tag('div'), + span = html.tag('span'), + a = html.tag('a'), + cssBaseClass = 'kb-rcp'; + + /** + * This is the factory function for the CellTabs component. + * @param {object} options + * - bus - the message bus + * - toggleAction - a function that should be run when toggling tabs, takes + * the tab name as a single parameter + * - tabs: also an object: + * - selected - string, one of the keys under "tabs" + * - tabs - an object where each key is a tab key, and has a display label: + * { + * configure: { + * label: "Configure" + * } + * , etc. + * } + */ + function CellTabs(options) { + const bus = options.bus, + tabToggleAction = options.toggleAction, + controlBarTabs = options.tabs; + let ui, state, container; + + /** + * State should have the following structure: + * { + * selected: string (or null) - id of tab to activate. If null, no tab is activated + * tabs: { + * tabId: { + * enabled: boolean - if true, tab is clickable, otherwise should be disabled + * visible: boolean - if true, tab is visible + * } + * } + * } + * @param {object} newState + */ + function setState(newState) { + state = newState; + if (state.tabs) { + for (const tabId of Object.keys(state.tabs)) { + const tabState = state.tabs[tabId]; + if (tabState) { + tabState.enabled ? ui.enableButton(tabId) : ui.disableButton(tabId); + tabState.visible ? ui.showButton(tabId) : ui.hideButton(tabId); + } + ui.deactivateButton(tabId); + } + } + if (state.selected) { + ui.activateButton(state.selected); + } + } + + function renderLayout() { + const events = Events.make(), + content = div( + { + class: `${cssBaseClass}__btn-toolbar btn-toolbar`, + }, + [buildTabButtons(events)] + ); + return { + content, + events, + }; + } + + function buildTabButtons(events) { + const buttons = Object.keys(controlBarTabs.tabs) + .filter((key) => { + // ensure that the tab data is an object with key 'label' + return ( + controlBarTabs.tabs[key] && + typeof controlBarTabs.tabs[key] === 'object' && + controlBarTabs.tabs[key].label + ); + }) + .map((key) => { + const tab = controlBarTabs.tabs[key]; + let icon; + if (tab.icon && typeof tab.icon === 'string') { + icon = { + size: 2, + name: tab.icon, + }; + } + return ui.buildButton({ + label: tab.label, + name: key, + events: events, + type: tab.type || 'primary', + hidden: true, + features: tab.features, + classes: [`${cssBaseClass}__tab-button kb-app-cell-btn`], + event: { + type: 'control-panel-tab', + data: { + tab: key, + }, + }, + icon: icon, + }); + }); + + bus.on('control-panel-tab', (message) => { + const tab = message.data.tab; + tabToggleAction(tab); + }); + + const outdatedBtn = a( + { + tabindex: '0', + type: 'button', + class: `${cssBaseClass}__tab-button--outdated btn hidden`, + dataContainer: 'body', + container: 'body', + dataToggle: 'popover', + dataPlacement: 'bottom', + dataTrigger: 'focus', + dataElement: 'outdated', + role: 'button', + title: 'New version available', + }, + span({ + class: 'fa fa-exclamation-triangle fa-2x', + }) + ); + buttons.unshift(outdatedBtn); + + return buttons; + } + + function start(args) { + return Promise.try(() => { + container = args.node; + ui = UI.make({ + node: container, + bus, + }); + const layout = renderLayout(); + container.innerHTML = layout.content; + layout.events.attachEvents(container); + }); + } + + function stop() { + return Promise.try(() => {}); + } + + return { + start, + stop, + setState, + }; + } + + return { + make: CellTabs, + cssBaseClass: cssBaseClass, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/fieldTableCellWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/fieldTableCellWidget.js new file mode 100644 index 0000000000..e9626ce693 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/fieldTableCellWidget.js @@ -0,0 +1,318 @@ +define([ + 'google-code-prettify/prettify', + 'common/html', + 'common/runtime', + 'widgets/appWidgets2/errorControl', + 'util/string', + 'css!google-code-prettify/prettify', +], (PR, html, Runtime, ErrorControlFactory, StringUtil) => { + 'use strict'; + + const t = html.tag, + div = t('div'), + label = t('label'), + strong = t('strong'), + span = t('span'), + iTag = t('i'), + cssBaseClass = 'kb-field-cell', + messageBaseClass = `${cssBaseClass}__message_panel`, + MESSAGE = { + error: { + prefix: 'Error', + messageClass: `${messageBaseClass}__error`, + containerClass: `${cssBaseClass}__error_message`, + icon: 'fa fa-exclamation-triangle', + }, + warning: { + prefix: 'Warning', + messageClass: `${messageBaseClass}__warning`, + icon: 'fa fa-exclamation-circle', + }, + }; + + function factory(config) { + const runtime = Runtime.make(), + bus = runtime.bus().makeChannelBus({ + description: 'Field bus', + }), + inputControlFactory = config.inputControlFactory, + spec = config.parameterSpec, + state = { + isValid: true, + isDuplicate: false, + }; + let places, parent, container, inputControl; + + try { + const controlConfig = Object.assign({}, config, { bus, channelName: bus.channelName }); + inputControl = inputControlFactory.make(controlConfig); + } catch (ex) { + console.error('Error creating input control', ex); + inputControl = ErrorControlFactory.make({ + message: ex.message, + }).make(); + } + + function render() { + const ids = { + fieldPanel: html.genId(), + inputControl: html.genId(), + }; + + const content = div( + { + class: `${cssBaseClass}__rowCell`, + id: ids.fieldPanel, + dataElement: 'field-panel', + }, + [ + label( + { + class: `${cssBaseClass}__cell_label`, + title: spec.ui.label || spec.ui.id, + for: ids.inputControl, + }, + [spec.ui.label || spec.ui.id] + ), + div({ + class: `${cssBaseClass}__input_control`, + id: ids.inputControl, + dataElement: 'input-control', + }), + div({ + class: `${messageBaseClass} hidden`, + dataElement: 'message-panel', + }), + div({ + class: `${messageBaseClass}__duplicate hidden`, + dataElement: 'duplicate-message-panel', + }), + ] + ); + + return { + content, + ids, + }; + } + + // LIFECYCLE + + function attach(node) { + parent = node; + const containerDiv = document.createElement('div'); + containerDiv.classList.add(`${cssBaseClass}__param_container`); + + container = parent.appendChild(containerDiv); + + const rendered = render(); + container.innerHTML = rendered.content; + PR.prettyPrint(null, container); + + places = { + inputControl: container.querySelector('#' + rendered.ids.inputControl), + }; + } + + function generateMessage(messageInfo, text) { + return ( + span( + { + class: `${messageInfo.messageClass}__title`, + }, + [ + iTag({ + class: messageInfo.icon, + }), + strong(` ${messageInfo.prefix}: `), + ] + ) + text + ); + } + + function renderMessage(messageInfo, text, messagePanelSelector) { + const message = generateMessage(messageInfo, text); + if (messageInfo.containerClass) { + parent + .querySelector(`.${cssBaseClass}__rowCell`) + .classList.add(messageInfo.containerClass); + } + const msgPanel = parent.querySelector(messagePanelSelector); + msgPanel.innerHTML = message; + msgPanel.classList.add(messageInfo.messageClass); + msgPanel.classList.remove('hidden'); + } + + function showMessage(messageInfo, text) { + clearMessage(messageBaseClass); + renderMessage(messageInfo, text, `.${messageBaseClass}`); + } + + function clearMessage(selector) { + if (state.isValid && !state.isDuplicate) { + parent + .querySelector(`.${cssBaseClass}__rowCell`) + .classList.remove(`${cssBaseClass}__error_message`); + } + const msgPanel = parent.querySelector(`.${selector}`); + msgPanel.className = `${selector} hidden`; + } + + /** + * Validates the field by showing a message to the user if it's in a few different states. + * States included are: + * - error - if the value is invalid, or missing yet required, an error message is shown + * - suspect - if the value is "suspect", a warning is shown. "Suspect" values are + * technically valid, but might not be what the user wants, like overwriting existing + * data + * - valid or optional and missing - if the value is ok, don't show anything, and hide any + * previously shown messages + * @param {object} message expected to have various keys from the Validator module, especially: + * - diagnosis (string), one of valid, required-missing, optional-empty, suspect, invalid + * - isError (boolean, optional) + * - shortMessage (string, optional) + * - errorMessage (string, optional), only present if isError is present, and true + */ + function validateField(message) { + // always clear the existing message to start with + switch (message.diagnosis) { + case 'required-missing': + case 'invalid': + state.isValid = false; + showMessage(MESSAGE.error, message.errorMessage); + break; + case 'suspect': + state.isValid = true; + showMessage(MESSAGE.warning, message.shortMessage); + break; + default: + state.isValid = true; + clearMessage(messageBaseClass); + break; + } + } + + /** + * Most of the code here is wrangling the English language. + * This translates a structure like + * { + * thisTab: [1, 2], + * otherTabs: { + * "Elsewhere": [3] + * } + * } + * into text like: + * "duplicate values found on rows 1 and 2, and on tab "Elsewhere" row 3" + * @param {object} rows + */ + function setDuplicateValue(rows) { + state.isDuplicate = true; + let message = 'duplicate value'; + let rowMessage = ''; + let tabMessage = ''; + let tabPrefix = ' on tab'; + let rowTabSeparator = ''; + let plural = false; + + function buildRowsText(rows) { + let rowsText = 'row'; + if (rows.length > 1) { + rowsText += 's'; + } + rowsText += ' ' + StringUtil.arrayToEnglish(rows); + return rowsText; + } + + // text for this tab's rows + if (rows && rows.thisTab && rows.thisTab.length) { + rowMessage = ' on '; + if (rows.thisTab.length > 1) { + plural = true; + } + rowMessage += buildRowsText(rows.thisTab); + } + + // get text for other tab rows + if (rows && rows.otherTabs) { + const tabs = Object.keys(rows.otherTabs); + tabMessage = StringUtil.arrayToEnglish( + tabs.map((tab) => { + return `"${tab}" ${buildRowsText(rows.otherTabs[tab])}`; + }) + ); + if (tabs.length > 1) { + tabPrefix += 's'; + plural = true; + } + } + + // if we have dups on both this tab and other tab(s), combine them + if (rowMessage.length && tabMessage.length) { + rowTabSeparator = ', and'; + plural = true; + } + message += (plural ? 's' : '') + ' found' + rowMessage + rowTabSeparator; + if (tabMessage.length) { + message += tabPrefix + ' ' + tabMessage; + } + renderMessage(MESSAGE.error, message, `.${messageBaseClass}__duplicate`); + } + + function clearDuplicateValue() { + state.isDuplicate = false; + clearMessage(`${messageBaseClass}__duplicate`); + } + + function start(arg) { + attach(arg.node); + bus.on('validation', validateField); + + return inputControl + .start({ + node: places.inputControl, + }) + .then(() => { + // TODO: get rid of this pattern + bus.emit('run', { + node: places.inputControl, + }); + }) + .catch((error) => { + throw new Error(`Unable to start fieldTableCellWidget: ${error}`); + }); + } + + function stop() { + const clearElements = () => { + if (parent && container) { + parent.removeChild(container); + } + bus.stop(); + }; + return inputControl + .stop() + .then(() => { + clearElements(); + return null; + }) + .catch((err) => { + console.error('Error stopping fieldTableCellWidget: ', err); + clearElements(); + }); + } + + return { + start, + stop, + bus, + setDuplicateValue, + clearDuplicateValue, + }; + } + + return { + make: function (config) { + return factory(config); + }, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/filePathWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/filePathWidget.js new file mode 100644 index 0000000000..79e842692d --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/filePathWidget.js @@ -0,0 +1,922 @@ +define([ + 'bluebird', + 'common/html', + 'common/ui', + 'common/events', + 'common/props', + 'common/cellComponents/fieldTableCellWidget', + 'widgets/appWidgets2/paramResolver', + 'common/runtime', +], (Promise, html, UI, Events, Props, FieldWidget, ParamResolver, Runtime) => { + 'use strict'; + + const tag = html.tag, + form = tag('form'), + span = tag('span'), + button = tag('button'), + div = tag('div'), + ol = tag('ol'), + li = tag('li'), + icon = tag('icon'), + cssBaseClass = 'kb-file-path', + cssClassType = 'parameter'; + + /** + * + * @param {object} config keys: + * - paramIds: Array of strings, parameter ids + * - bus - the main bus that's used for communicating up to the controlling widget + * - workspaceId - the id of the workspace we should use for searching for objects + * - initialParams - Array of objects. Each item represents a row of parameter values. + * So each item has an object with parameter id -> value + * - availableFiles - Array of strings, one for each file that should be available here. + * - unavailableFiles - Set of strings, for files that the cell thinks are available, but + * are no longer available through the staging service + * - viewOnly - boolean, if true start with the view version of input widgets + * - unselectedOutputValues - the set of output values from the other file types, rather + * than this one. + * @returns + */ + function factory(config) { + const viewOnly = config.viewOnly || false; + const { workspaceId, initialParams, paramIds, availableFiles, unavailableFiles } = config; + const runtime = Runtime.make(), + // paramsBus is used to communicate from this parameter container to the parent that + // created and owns it + paramsBus = config.bus, + model = Props.make(), + otherTabOutputValues = config.unselectedOutputValues, + /** + * Internal data model + * The data model / view model is a structure that maintains the overall internal + * state of this widget. It holds: + * - all input widgets in the cell + * - all values, kept in an order that's useful for the cell + * - all row information, in a way that's (generally) easily accessible and easy to + * update. + * - each row gets its own immutible unique id, which is used for widget communication and + * management + * - row order, since we can delete from anywhere, can shift and wobble, so that's not helpful. + * instead, we map from rowId -> row order, and update that on insertions and deletions. + * In general, we need to support 4 actions. + * 1. add row + * - rows get added to the end, new id, new object for data. + * 2. update data + * - when a field widget updates its data, we get a message with row id, parameter id, and new + * value. + * 3. push data to main widget (configure tab) + * - we use the mapped row order to fetch the values in the right order, which is a quick loop + * 4. delete row + * - rows can get deleted from anywhere, which breaks all the ordering. We need to run a loop + * to figure out the new order after the one that was deleted. + * + * dataModel structure: + * This is a simple object with a few keys: + * rowOrder: array of unique ids, this is the row ids in the order they should appear on + * screen, thus the order the data should be stored in + * rowIdToIndex: mapping from unique rowId to the index in rowOrder. Mainly used on row + * updates + * rows: a map from the unique rowId to an object containing two keys: + * - values: a set of key-value-pairs, parameter id -> parameter value + * - widgets: a set of key-value-pairs, parameter id -> widget + * + * example: + * { + * rowOrder: ['rowA', 'rowB'], + * rowIdToIndex: { + * rowA: 0, + * rowB: 1 + * }, + * rows: { + * rowA: { + * widgets: { + * param1: Object[], + * param2: Object[] + * }, + * values: { + * param1: 'value1', + * param2: 'value2' + * } + * }, + * rowB: { + * widgets: { + * param1: Object[], + * param2: Object[] + * }, + * values: { + * param1: 'value3', + * param2: 'value4' + * } + * } + * } + * } + */ + dataModel = { + rowOrder: [], + rowIdToIndex: {}, + rows: {}, + }, + paramResolver = ParamResolver.make(), + events = Events.make(), + bus = runtime.bus().makeChannelBus({ + description: 'A file path widget', + }); + let container, ui; + + function makeFieldWidget(rowId, inputWidget, appSpec, parameterSpec, value) { + const fieldWidget = FieldWidget.make({ + inputControlFactory: inputWidget, + showHint: true, + useRowHighight: true, + initialValue: value, + appSpec, + parameterSpec, + workspaceId, + referenceType: 'name', + paramsChannelName: paramsBus.channelName, + availableValues: availableFiles.map((filename) => { + return { + value: filename, + display: filename, + }; + }), + invalidValues: unavailableFiles, + invalidError: 'file not found', + disabledValues: getAllSelectedFiles(), + }); + + /** + * We only expect file input and object output parameters here. These are either the + * selectInput widget (for file lookups) or the newObjectInput widget for the + * object outputs. Bus messages that get listened to here reflect that - specialized + * messages like "sync-params" are only used by a few inputs, none of which are + * expected here. + */ + fieldWidget.bus.on('changed', (message) => { + const newValue = message.newValue; + dataModel.rows[rowId].values[parameterSpec.id] = newValue; + updateDisabledFileValues(); + // TODO: get up to date with all values across uploaders + // But it still might be a duplicate! So do the following on each change. + // 1. Get all output values + // 2. If duplicates, mark those + // 2a. emit 'invalid-param-value' messages for the duplicates + // 3. Send results to all widgets + // 4. All widgets update their state + let duplicateValues = []; + if (model.getItem('outputParamIds').includes(parameterSpec.id)) { + duplicateValues = updateDuplicateOutputValues(); + } + if (duplicateValues.includes(newValue)) { + paramsBus.emit('invalid-param-value', { + parameter: parameterSpec.id, + }); + } else { + paramsBus.emit('parameter-changed', { + parameter: parameterSpec.id, + newValue: newValue, + isError: message.isError, + rowId: rowId, + rowIndex: dataModel.rowIdToIndex[rowId], + }); + } + }); + + fieldWidget.bus.on('validation', (message) => { + // only propagate if invalid. value changes come through + // the 'changed' message + let duplicateValues = []; + if (model.getItem('outputParamIds').includes(parameterSpec.id)) { + duplicateValues = updateDuplicateOutputValues(); + } + if (!message.isValid || duplicateValues.length) { + paramsBus.emit('invalid-param-value', { + parameter: parameterSpec.id, + }); + } + }); + + fieldWidget.bus.on('sync', () => { + const newValue = dataModel.rows[rowId].values[parameterSpec.id]; + fieldWidget.bus.send( + { + value: newValue, + }, + { + // This points the update back to a listener on this key + key: { + type: 'update', + }, + } + ); + }); + + fieldWidget.bus.respond({ + key: { + type: 'get-param-state', + }, + handle: function () { + return paramsBus.request( + { id: parameterSpec.id }, + { + key: { + type: 'get-param-state', + }, + } + ); + }, + }); + + fieldWidget.bus.respond({ + key: { + type: 'get-parameter', + }, + handle: function (message) { + if (message.parameterName) { + return dataModel.rows[rowId].values[message.parameterName]; + } + return null; + }, + }); + + /** + * This is used by the newObjectInput widget to determine whether there's + * any duplicated parameters. Since we, effectively, have a sequence of the same + * input ids here, this gets tricky. + * TODO: deal with trickiness and make this return any values that might be + * duplicate. Note, might have to twiddle appWidgets2/input/newObjectInput.js + * around line 125. It expects a key-value-pair set of parameters. + */ + fieldWidget.bus.respond({ + key: { + type: 'get-parameters', + }, + handle: (message) => { + if (message.parameterNames) { + return Promise.all( + message.parameterNames.map((paramName) => { + return paramsBus + .request( + { + parameterName: paramName, + }, + { + key: { + type: 'get-parameter', + }, + } + ) + .then((result) => { + const returnVal = {}; + returnVal[paramName] = result.value; + return returnVal; + }); + }) + ).then((results) => { + const combined = {}; + results.forEach((res) => { + Object.keys(res).forEach((key) => { + combined[key] = res[key]; + }); + }); + return combined; + }); + } + }, + }); + + return fieldWidget; + } + + /** + * Adds a new file path parameter row. Has the following responsibilities: + * - make row id + * - make initial table row layout + * - set up row parameters + * - make the row + * @param {object} params - key value pairs for the set of parameterIds -> values + * to start this row with. If falsy, this uses the default parameters. + */ + function addRow(params) { + if (!params) { + // initialize params + params = Object.assign({}, model.getItem('defaultParams')); + } + const filePathRows = ui.getElement(`${cssClassType}-fields`); + const rowId = html.genId(); + dataModel.rowOrder.push(rowId); + dataModel.rowIdToIndex[rowId] = dataModel.rowOrder.length - 1; + dataModel.rows[rowId] = { + values: params, + widgets: [], + }; + + const newRowNode = ui.createNode( + li({ + class: `${cssBaseClass}__list_item`, + dataElement: `${cssClassType}-fields-row`, + dataRowId: rowId, + }) + ); + filePathRows.appendChild(newRowNode); + return makeFilePathRow(newRowNode, rowId, params).then(() => { + syncDataModel(); + }); + } + + function syncDataModel() { + // map structure of data into an array of parameter objects + const dataValues = dataModel.rowOrder.map((rowId) => dataModel.rows[rowId].values); + paramsBus.emit('sync-data-model', { + values: dataValues, + }); + } + + /** + * Renders the layout structure without any parameter rows. + * @returns {string} layout HTML content + */ + function renderLayout() { + let formContent = []; + const panelBody = [ + ol({ + class: `${cssBaseClass}__list`, + dataElement: `${cssClassType}-fields`, + }), + ]; + if (!viewOnly) { + panelBody.push( + button( + { + class: `${cssBaseClass}__button--add_row`, + type: 'button', + id: events.addEvent({ + type: 'click', + handler: function () { + addRow(); + }, + }), + }, + [ + span({ + class: `${cssBaseClass}__button_icon--add_row fa fa-plus`, + }), + 'Add Row', + ] + ) + ); + } + + formContent = formContent.concat([ + ui.buildPanel({ + title: span(['File Paths']), + name: `${cssClassType}s-area`, + body: panelBody, + classes: ['kb-panel-bulk-params'], + }), + ]); + + return form( + { + dataElement: `${cssClassType}-widget-form`, + }, + [formContent] + ); + } + + // MESSAGE HANDLERS + function doAttach() { + const layout = renderLayout(); + container.innerHTML = layout; + events.attachEvents(container); + } + + // EVENTS + function attachEvents() { + bus.on('reset-to-defaults', () => { + Object.values(dataModel.rows).forEach((row) => { + Object.values(row.widgets).forEach((widget) => { + widget.bus.emit('reset-to-defaults'); + }); + }); + }); + + runtime.bus().on('workspace-changed', () => { + // if the workspace magically changes, pass that along to + // each registered widget. + + Object.values(dataModel.rows).forEach((row) => { + Object.values(row.widgets).forEach((widget) => { + widget.bus.emit('workspace-changed'); + }); + }); + }); + } + + /** + * + * @param {object} params + * @returns {object} + * - content: string, the html layout + * - layout: array, the parameter ids in the right order + * - view: object where keys = parameter ids, objects = {id: random generated div id where they should get rendered} + * - paramMap: same as params, but a mapping from paramId to the param spec + */ + function makeFilePathsLayout(params) { + const view = {}, + paramMap = {}; + + const orderedParams = params.map((param) => { + paramMap[param.id] = param; + return param.id; + }); + + const layout = orderedParams + .map((parameterId) => { + const id = html.genId(); + view[parameterId] = { id }; + + return tag('div')({ + class: `${cssBaseClass}__row_cell--file-path_id`, + id: id, + dataParameter: parameterId, + }); + }) + .join('\n'); + + return { + content: layout, + layout: orderedParams, + view, + paramMap, + }; + } + + /** + * Creates a new single file path widget for the parameter id with some initial value. + * This returns a Promise that resolves into the new widget. + * @param {string} rowId - the unique row id where this widget will live + * @param {object} appSpec - the entire app spec to pass along to the widget + * @param {object} filePathParams - the object holding all file path param specs + * @param {string} parameterId - the parameter id from the app spec + * @param {any} parameterValue - the initial value of the parameter + * @returns the created and started widget, linked to its parameterId in a tiny object + */ + function createFilePathWidget(rowId, appSpec, filePathParams, parameterId, parameterValue) { + const spec = filePathParams.paramMap[parameterId]; + let widget; + let controlPromise; + if (viewOnly) { + controlPromise = paramResolver.loadViewControl(spec); + } else { + // patch to turn dynamic_dropdowns (the usual file widget) to regular dropdowns (which get set + // to enabled / disabled) + if (spec.ui.control === 'dynamic_dropdown') { + spec.ui.control = 'dropdown'; + } + controlPromise = paramResolver.loadInputControl(spec); + } + return controlPromise + .then((inputWidget) => { + widget = makeFieldWidget(rowId, inputWidget, appSpec, spec, parameterValue); + + return widget.start({ + node: container.querySelector('#' + filePathParams.view[spec.id].id), + }); + }) + .then(() => { + const retValue = {}; + retValue[parameterId] = widget; + return retValue; + }) + .catch((ex) => { + console.error(`Error making input field widget: ${ex}`); + const errorDisplay = div( + { + class: 'kb-field-widget__error_message--file-paths', + }, + [ex.message] + ); + + container.querySelector('#' + filePathParams.view[spec.id].id).innerHTML = + errorDisplay; + + throw new Error(`Error making input field widget: ${ex}`); + }); + } + + /** + * Deletes a parameter row. This does the following steps: + * 1. Stop all widgets (which should remove them and their events from the DOM). + * 2. Delete that row of data and widgets from the data model. + * 3. Renumber the existing rows in both the model and the DOM. + * 4. Fire off a sync-data-model message up to the controller. + * @param {Event} e - the click event on the delete button + * @param {string} rowId - the id of the row to delete + * @returns a promise that resolves when the steps are done. + */ + function deleteRow(e, rowId) { + const rowWidgets = Object.values(dataModel.rows[rowId].widgets); + return Promise.all(rowWidgets.map((widget) => widget.stop())).then(() => { + delete dataModel.rows[rowId]; + const rowIdx = dataModel.rowIdToIndex[rowId]; + + dataModel.rowOrder.splice(rowIdx, 1); + delete dataModel.rowIdToIndex[rowId]; + // redo the ordering + dataModel.rowOrder.forEach((_rowId, idx) => { + dataModel.rowIdToIndex[_rowId] = idx; + }); + e.target.closest('li').remove(); + updateDisabledFileValues(); + updateDuplicateOutputValues(); + syncDataModel(); + }); + } + + /** + * Makes a file path row of widgets and stores them in the data model. + * @param {DOM Element} filePathRow - the container element for a file path row + * @param {string} rowId - the unique id to assign to the row. + * @param {object} params - key value pair for paramId -> paramValue + * @returns a Promise that resolves when all widgets are created and saved. + */ + function makeFilePathRow(filePathRow, rowId, params) { + const appSpec = model.getItem('appSpec'); + const filePathParams = makeFilePathsLayout(model.getItem('parameterSpecs')); + const rowEvents = Events.make(); + + if (!filePathParams.layout.length) { + return Promise.resolve( + ui.getElement(`${cssClassType}s-area`).classList.add('hidden') + ); + } + + const fpRowHtml = (filePathRow.innerHTML = [ + div( + { + class: `${cssBaseClass}__params`, + }, + [ + div( + { + class: `${cssBaseClass}__param_container row`, + }, + [filePathParams.content] + ), + ] + ), + ]); + if (!viewOnly) { + fpRowHtml.push( + button( + { + class: `${cssBaseClass}__button--delete`, + type: 'button', + id: rowEvents.addEvent({ + type: 'click', + handler: function (e) { + deleteRow(e, rowId); + }, + }), + }, + [ + icon({ + class: 'fa fa-trash-o fa-lg', + }), + ] + ) + ); + } + + filePathRow.innerHTML = fpRowHtml.join(''); + + return Promise.all( + filePathParams.layout.map(async (parameterId) => { + return await createFilePathWidget( + rowId, + appSpec, + filePathParams, + parameterId, + params[parameterId] + ); + }) + ).then((widgets) => { + // dataModel.rows[rowId] was already created by addRow + dataModel.rows[rowId].widgets = Object.assign({}, ...widgets); + rowEvents.attachEvents(filePathRow); + }); + } + + /** + * Build the layout structure. + * Populate with initial parameter rows + * Keep parameter rows as data model + * Update as changed, and propagate entire parameter list up to parent bus + * @param {object} arg - has keys: + * node - the containing DOM node + * appSpec - the appSpec for the app having its parameters portrayed here + * parameters - the parameter set with the layout order + * @returns a promise that resolves when all file path rows are created from + * the initial set of parameters + */ + function start(arg) { + container = arg.node; + ui = UI.make({ + node: container, + bus: bus, + }); + doAttach(); + attachEvents(); + model.setItem('parameterIds', paramIds); + model.setItem('appSpec', arg.appSpec); + model.setItem('parameterValues', initialParams); + // get the parameter specs in the right order. + const parameterSpecs = []; + const defaultParams = {}; + const fileParamIds = []; + const outputParamIds = []; + arg.parameters.layout.forEach((id) => { + if (paramIds.includes(id)) { + const paramSpec = arg.parameters.specs[id]; + parameterSpecs.push(paramSpec); + defaultParams[id] = paramSpec.data.defaultValue; + const isOutput = + paramSpec.original.text_options && + paramSpec.original.text_options.is_output_name === 1; + if (!isOutput) { + // if it's not an "output", it's a file + fileParamIds.push(id); + } else { + outputParamIds.push(id); + } + } + }); + model.setItem('fileParamIds', fileParamIds); + model.setItem('parameterSpecs', parameterSpecs); + model.setItem('defaultParams', defaultParams); + model.setItem('outputParamIds', outputParamIds); + return Promise.all( + initialParams.map((paramRow) => { + return addRow(paramRow); + }) + ) + .then(() => { + // once all rows are set up and we have the data model + // disable all relevant files from each input widget. + // TODO: set this up to disable files from each column (i.e. parameter id) instead + // TODO: set this widget up to link filetypes to parameter ids. Work also needed in + // bulkImportWidget.js and configure.js. + updateDisabledFileValues(); + const dups = updateDuplicateOutputValues(); + // if there are any duplicates, we need to send a message that this is an + // invalid setup + if (dups.length) { + paramsBus.emit('invalid-param-value'); + } + }) + .catch((error) => { + throw new Error(`Unable to start filePathWidget: ${error}`); + }); + } + + /** + * Gets all the selected files and tells all file input widgets to make those disabled. + * So the user can only select one file from the list. + */ + function updateDisabledFileValues() { + const values = getAllSelectedFiles(); + getAllFileParameterWidgets().forEach((widget) => { + widget.bus.emit('set-disabled-values', { values }); + }); + } + + /** + * Steps. + * 1. Get all values from output parameters, ignore null values + * 2. Filter down to those with duplicates + * 3. Send those to just the inputs those came from. + * 4. Up to widgets to act accordingly + * 5. Return the row ids that are duplicates + */ + function updateDuplicateOutputValues() { + /* otherTabValues = all output values from other tabs, what tab they're in, and which row. + * + * looks like this: + * { + * value: { + * tabId: [rowIds, rowIdx] + * } + * } + */ + + /* First, set up the output value counts for this tab. + * outputValueCounts will look like this: + * { + * value: [{rowId, paramId}] + * } + * for each value. + */ + const outputValueCounts = model.getItem('outputParamIds').reduce((counts, paramId) => { + const outputValues = getParameterValuesByRow(paramId); + for (const [rowId, value] of Object.entries(outputValues)) { + if (!value) { + return counts; + } else if (!(value in counts)) { + counts[value] = []; + } + counts[value].push({ rowId, paramId }); + } + return counts; + }, {}); + /* Second, set up values to only track duplicates, and fold in the + * results from all other tabs. + * + * values should now look like: + * { + * 'field_value': { + * otherTabs: { + * tabId: [rows], + * }, + * thisTab: { + * [{ rowId1, paramId }, { rowId2, paramId }] + * } + * } + * } + * with only entries where there are at least two row ids, or + * a value found from another tab + */ + const values = Object.keys(outputValueCounts).reduce((duplicateValues, value) => { + const dup = {}; + if (outputValueCounts[value].length > 1 || value in otherTabOutputValues) { + dup.thisTab = outputValueCounts[value]; + } + if (value in otherTabOutputValues) { + dup.otherTabs = otherTabOutputValues[value]; + } + if (Object.keys(dup).length) { + duplicateValues[value] = dup; + } + return duplicateValues; + }, {}); + /* Get the parameter widgets and translate to something easier to work with - + * { + * rowId: { + * paramId1: { + * widget: Object + * duplicateRows: { + * thisTab: [rowIdx, rowIdx], + * otherTabs: { + * tabId1: [rowIdx, rowIdx] + * } + * }, + * } + * } + * + * otherTabs may not exist, though thisTab must (since we're looking at this tab's + * duplicate values). + */ + const duplicates = {}; + Object.values(values).forEach((dupEntry) => { + const allRowsThisTab = new Set(); + dupEntry.thisTab.forEach((entry) => { + allRowsThisTab.add(dataModel.rowIdToIndex[entry.rowId] + 1); + }); + dupEntry.thisTab.forEach((entry) => { + const rowId = entry.rowId, + rowIdx = dataModel.rowIdToIndex[rowId] + 1, + paramInfo = { + widget: dataModel.rows[rowId].widgets[entry.paramId], + duplicateRows: { + thisTab: [...allRowsThisTab].filter((x) => x !== rowIdx), + otherTabs: dupEntry.otherTabs || {}, + }, + }; + if (!(rowId in duplicates)) { + duplicates[rowId] = {}; + } + duplicates[rowId][entry.paramId] = paramInfo; + }); + }); + + const outputParams = new Set(getAllOutputParameterWidgets()); + + Object.values(duplicates).forEach((duplicateEntry) => { + Object.values(duplicateEntry).forEach((entry) => { + entry.widget.setDuplicateValue(entry.duplicateRows); + outputParams.delete(entry.widget); + }); + }); + outputParams.forEach((widget) => widget.clearDuplicateValue()); + + return Object.keys(values); + } + + /** + * + * @param {Object} paramId maps from rowId -> value for this parameter, includes nulls + */ + function getParameterValuesByRow(paramId) { + const valueMap = {}; + for (const [rowId, entry] of Object.entries(dataModel.rows)) { + valueMap[rowId] = entry.values[paramId]; + } + return valueMap; + } + + /** + * Goes through all current rows and builds an array of all values for the given parameter. + * @param {string} paramId + */ + function getParameterValues(paramId) { + return getAllRowElements(paramId, 'values'); + } + + /** + * Returns the list of all selected files among all file input widgets. + * This filters out any null values. + * @returns an array of strings + */ + function getAllSelectedFiles() { + return model.getItem('fileParamIds').reduce((acc, paramId) => { + return acc.concat(getParameterValues(paramId).filter((value) => value !== null)); + }, []); + } + + /** + * Returns all widgets used for file parameters. + * @returns an array of widgets + */ + function getAllFileParameterWidgets() { + return getWidgetsByType('fileParamIds'); + } + + /** + * + * @returns an object where keys are row ids and values are a list of + * output parameter widgets + */ + function getAllOutputParameterWidgets() { + return getWidgetsByType('outputParamIds'); + } + + /** + * + * @param {string} type one of 'fileParamIds', 'outputParamIds' + * @returns an object with widgets keyed on their row. + */ + function getWidgetsByType(type) { + return model.getItem(type).reduce((widgets, paramId) => { + return widgets.concat(getParameterWidgets(paramId)); + }, []); + } + + /** + * Returns an array of all widgets for a given parameter id, extracted from each row. + * @param {string} paramId + */ + function getParameterWidgets(paramId) { + return getAllRowElements(paramId, 'widgets'); + } + + /** + * Returns either widgets or values for one parameter id as an Array of elements extracted from all rows. + * @param {string} paramId the parameter to get the element for + * @param {string} element one of 'values', 'widgets' + * @returns + */ + function getAllRowElements(paramId, element) { + return Object.values(dataModel.rows).map((row) => { + return row[element][paramId]; + }); + } + + function stop() { + // Stop all widgets. Note this is an array of arrays of promises. + const widgetStopProms = Object.values(dataModel.rows).map((row) => { + return Object.values(row.widgets).map((widget) => { + return widget.stop(); + }); + }); + + // ...so we need to flatten it first to pass to Promise.all() + return Promise.all([].concat(...widgetStopProms)).then(() => { + if (container) { + container.innerHTML = ''; + } + }); + } + + return { + start, + stop, + bus, + }; + } + + return { + make: function (config) { + return factory(config); + }, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/fsmBar.js b/kbase-extension/static/kbase/js/common/cellComponents/fsmBar.js new file mode 100644 index 0000000000..ffa1562ab0 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/fsmBar.js @@ -0,0 +1,96 @@ +define(['common/html', 'util/developerMode'], (html, devMode) => { + 'use strict'; + + const span = html.tag('span'), + developerMode = devMode.mode; + + /** + * Display the state of the FSM and the job (if appropriate) in the cell + * toolbar for debugging purposes. + * + * The FSM info will be placed under the node with data element 'fsm-display' + * + * @param {object} args with keys + * ui: common/ui object with the appropriate node set + * state: current FSM state + * job: current jobState object OR + * indexedJobs: object containing jobState objects, indexed by job ID + */ + function showFsmBar(args) { + const { ui, state, job, indexedJobs } = args; + + if (!developerMode) { + return; + } + + const stateObj = state && state.state ? state.state : {}; + if (!state) { + console.warn('FSMBar: no current FSM state found'); + // return; + } + let content = Object.keys(stateObj || {}) + .map((key) => { + return span([ + span( + { + class: 'kb-fsm__key', + }, + `${key}:` + ), + span( + { + class: 'kb-fsm__value', + }, + stateObj[key] + ), + ]); + }) + .join(' '); + + if (job && job.job_id) { + content = + span([ + span( + { + class: 'kb-fsm__key', + }, + 'job ID:' + ), + span( + { + class: 'kb-fsm__value', + }, + job.job_id + ), + ]) + content; + } + if (indexedJobs && Object.keys(indexedJobs).length) { + content = + span([ + span( + { + class: 'kb-fsm__key', + }, + 'jobs:' + ), + Object.keys(indexedJobs) + .sort() + .map((jobId) => { + return span( + { + class: 'kb-fsm__value', + }, + jobId + ); + }) + .join('; '), + ]) + content; + } + + ui.getElement('fsm-display').classList.remove('hidden'); + ui.setContent('fsm-display', content); + } + return { + showFsmBar, + }; +}); diff --git a/nbextensions/viewCell/widgets/inputWrapperWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/inputWrapperWidget.js similarity index 91% rename from nbextensions/viewCell/widgets/inputWrapperWidget.js rename to kbase-extension/static/kbase/js/common/cellComponents/inputWrapperWidget.js index 9f19495832..364f1a40fc 100644 --- a/nbextensions/viewCell/widgets/inputWrapperWidget.js +++ b/kbase-extension/static/kbase/js/common/cellComponents/inputWrapperWidget.js @@ -1,10 +1,10 @@ -define(['kb_common/html'], (html) => { +define(['common/html'], (html) => { 'use strict'; const t = html.tag, div = t('div'); function factory(config) { - let container, - wrappedWidget = config.widget, + let container; + const wrappedWidget = config.widget, wrappedId = html.genId(); function layout() { diff --git a/kbase-extension/static/kbase/js/common/cellComponents/paramsWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/paramsWidget.js new file mode 100644 index 0000000000..9bed6c1aed --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/paramsWidget.js @@ -0,0 +1,534 @@ +define([ + 'bluebird', + 'common/html', + 'common/ui', + 'common/events', + 'common/props', + 'common/cellComponents/fieldTableCellWidget', + 'widgets/appWidgets2/paramResolver', + 'common/runtime', +], (Promise, html, UI, Events, Props, FieldWidget, ParamResolver, Runtime) => { + 'use strict'; + + const tag = html.tag, + form = tag('form'), + span = tag('span'), + div = tag('div'), + cssBaseClass = 'kb-app-params'; + + function factory(config) { + const viewOnly = config.viewOnly || false; + const { bus, workspaceId, paramIds, initialParams } = config; + const runtime = Runtime.make(), + model = Props.make(), + paramResolver = ParamResolver.make(), + settings = { + showAdvanced: null, + }, + widgets = []; + let container, + ui, + places = {}; + + // DATA + /* + * + * Evaluates the parameter spec to determine which input widget needs to be + * invoked, but doesn't know what the widget does. + * Provides the communication bus for each input to route info to the widget + * and out of it. + * + * In terms of widgets it looks like this: + * + * InputCellWidget + * inputWidgets + * FieldWidget + * textInputWidget + * FieldWidget + * objectInputWidget + * FieldWidget + * newObjectInputWidget + * FieldWidget + * integerInputWidget + * FieldWidget + * floatInputWidget + */ + + /* + * The input control widget is selected based on these parameters: + * - data type - (text, int, float, workspaceObject (ref, name) + * - input app - input, select + */ + + // RENDERING + + /* + The field widget is a generic wrapper around the input. It serves the following purposes: + - intercepts messages in order to display status. + appSpec - specifies the whole app + parameterSpec - just the segment of the appSpec that specifies this individual parameter + value - the initial value of this field + closeParameters - a list of "close" parameters, which might be context-dependent. E.g. for an output + field, this would be the list of all parameters meant to be output objects, so their names can + be cross-validated for uniqueness (optional) + */ + + function makeFieldWidget(inputWidget, appSpec, parameterSpec, value, closeParameters) { + const fieldWidget = FieldWidget.make({ + inputControlFactory: inputWidget, + showHint: true, + useRowHighight: true, + initialValue: value, + referenceType: 'name', + paramsChannelName: bus.channelName, + appSpec, + parameterSpec, + workspaceId, + closeParameters, + viewOnly, + }); + + if (!viewOnly) { + // Forward all changed parameters to the controller. That is our main job! + fieldWidget.bus.on('changed', (message) => { + bus.send( + { + parameter: parameterSpec.id, + newValue: message.newValue, + isError: message.isError, + }, + { + key: { + type: 'parameter-changed', + parameter: parameterSpec.id, + }, + } + ); + + bus.emit('parameter-changed', { + parameter: parameterSpec.id, + newValue: message.newValue, + isError: message.isError, + }); + }); + + fieldWidget.bus.on('validation', (message) => { + // only propagate if invalid. value changes come through + // the 'changed' message + if (!message.isValid) { + bus.emit('invalid-param-value', { + parameter: parameterSpec.id, + }); + } + }); + + fieldWidget.bus.on('touched', () => { + bus.emit('parameter-touched', { + parameter: parameterSpec.id, + }); + }); + + // An input widget may ask for the current model value at any time. + fieldWidget.bus.on('sync', () => { + bus.emit('parameter-sync', { + parameter: parameterSpec.id, + }); + }); + + fieldWidget.bus.on('sync-params', (message) => { + bus.emit('sync-params', { + parameters: message.parameters, + replyToChannel: fieldWidget.bus.channelName, + }); + }); + + fieldWidget.bus.on('set-param-state', (message) => { + bus.emit('set-param-state', { + id: parameterSpec.id, + state: message.state, + }); + }); + + fieldWidget.bus.respond({ + key: { + type: 'get-param-state', + }, + handle: function () { + return bus.request( + { id: parameterSpec.id }, + { + key: { + type: 'get-param-state', + }, + } + ); + }, + }); + + fieldWidget.bus.respond({ + key: { + type: 'get-parameter', + }, + handle: function (message) { + if (message.parameterName) { + return bus.request(message, { + key: { + type: 'get-parameter', + }, + }); + } else { + return null; + } + }, + }); + + fieldWidget.bus.respond({ + key: { + type: 'get-parameters', + }, + handle: (message) => { + if (message.parameterNames) { + return Promise.all( + message.parameterNames.map((paramName) => { + return bus + .request( + { + parameterName: paramName, + }, + { + key: { + type: 'get-parameter', + }, + } + ) + .then((result) => { + const returnVal = {}; + returnVal[paramName] = result.value; + return returnVal; + }); + }) + ).then((results) => { + const combined = {}; + results.forEach((res) => { + Object.keys(res).forEach((key) => { + combined[key] = res[key]; + }); + }); + return combined; + }); + } + }, + }); + } + + // Just pass the update along to the input widget. + bus.listen({ + key: { + type: 'update', + parameter: parameterSpec.id, + }, + handle: function (message) { + fieldWidget.bus.emit('update', { + value: message.value, + }); + }, + }); + + return fieldWidget; + } + + function showHideAdvanced() { + const areaElement = 'parameters-area', + areaSelector = '[data-element="' + areaElement + '"]', + advancedInputs = container.querySelectorAll( + areaSelector + ' [data-advanced-parameter]' + ); + + //remove or add the hidden field class + for (const [, entry] of Object.entries(advancedInputs)) { + entry.classList.toggle(`${cssBaseClass}__fields--parameters-hidden`); + } + + return advancedInputs; + } + + function renderAdvanced() { + //remove or add the hidden field class + const areaElement = 'parameters-area', + advancedInputs = showHideAdvanced(); + + // Also update the count in the paramters. + const events = Events.make({ node: container }); + + let showAdvancedButton, + label, + message = String(advancedInputs.length) + ' advanced parameter'; + + if (!advancedInputs.length) { + ui.setContent([areaElement, 'advanced-hidden-message'], ''); + } else { + if (advancedInputs.length > 1) { + message += 's'; + } + + if (settings.showAdvanced) { + message += ' showing'; + label = 'hide advanced'; + } else { + label = 'show advanced'; + message += ' hidden'; + } + + showAdvancedButton = ui.buildButton({ + class: `${cssBaseClass}__toggle--advanced-hidden`, + label, + type: 'link', + name: 'advanced-parameters-toggler', + event: { + type: 'toggle-advanced', + }, + events, + }); + + ui.setContent( + [areaElement, 'advanced-hidden-message'], + '(' + message + ') ' + showAdvancedButton + ); + } + + events.attachEvents(); + } + + function renderLayout() { + const events = Events.make(); + let formContent = []; + + formContent = formContent.concat([ + ui.buildPanel({ + title: span([ + 'Parameters', + span({ + class: `${cssBaseClass}__message--advanced-hidden`, + dataElement: 'advanced-hidden-message', + }), + ]), + name: 'parameters-area', + body: div({ + class: `${cssBaseClass}__fields--parameters`, + dataElement: 'parameter-fields', + }), + classes: ['kb-panel-bulk-params'], + }), + ]); + + const content = form({ dataElement: 'input-widget-form' }, formContent); + return { + content: content, + events: events, + }; + } + + // MESSAGE HANDLERS + function doAttach(node) { + container = node; + ui = UI.make({ + node: container, + bus: bus, + }); + const layout = renderLayout(); + container.innerHTML = layout.content; + layout.events.attachEvents(container); + + places = { + parameterFields: ui.getElement('parameter-fields'), + advancedParameterFields: ui.getElement('advanced-parameter-fields'), + }; + } + + // EVENTS + function attachEvents() { + bus.on('reset-to-defaults', () => { + widgets.forEach((widget) => { + widget.bus.emit('reset-to-defaults'); + }); + }); + + bus.on('toggle-advanced', () => { + settings.showAdvanced = !settings.showAdvanced; + showHideAdvanced(); + }); + + runtime.bus().on('workspace-changed', () => { + // tell each input widget about this amazing event! + widgets.forEach((widget) => { + widget.bus.emit('workspace-changed'); + }); + }); + } + + function makeParamsLayout(params) { + const view = {}, + paramMap = {}; + + const orderedParams = params.map((param) => { + paramMap[param.id] = param; + return param.id; + }); + + const layout = orderedParams + .map((parameterId) => { + const elementId = html.genId(); + const advanced = paramMap[parameterId].ui.advanced ? parameterId : false; + + view[parameterId] = { + id: elementId, + }; + + return div({ + id: elementId, + dataParameter: parameterId, + dataAdvancedParameter: advanced, + }); + }) + .join('\n'); + + return { + content: layout, + layout: orderedParams, + params, + view, + paramMap, + }; + } + + function createParameterWidget(appSpec, parameterInfo, parameterId) { + const paramSpec = parameterInfo.paramMap[parameterId]; + let controlPromise; + if (viewOnly) { + controlPromise = paramResolver.loadViewControl(paramSpec); + } else { + controlPromise = paramResolver.loadInputControl(paramSpec); + } + return controlPromise + .then((inputWidget) => { + const widget = makeFieldWidget( + inputWidget, + appSpec, + paramSpec, + initialParams[paramSpec.id] + ); + + widgets.push(widget); + return widget.start({ + node: container.querySelector('#' + parameterInfo.view[paramSpec.id].id), + }); + }) + .catch((ex) => { + console.error(`Error making params input field widget: ${ex}`); + const errorDisplay = div( + { + class: 'kb-field-widget__error_message--parameters', + }, + [ex.message] + ); + container.querySelector('#' + parameterInfo.view[paramSpec.id].id).innerHTML = + errorDisplay; + + throw new Error(`Error making input field widget: ${ex}`); + }); + } + + // LIFECYCLE API + function renderParameters() { + // First get the app specs, which is stashed in the model, + // with the parameters returned. + // Separate out the params into the primary groups. + const appSpec = model.getItem('appSpec'); + const params = model.getItem('parameterSpecs'); + const filteredParams = makeParamsLayout(params); + + //if there aren't any parameters we can just hide the whole area + if (!filteredParams.layout.length) { + return Promise.resolve(ui.getElement('parameters-area').classList.add('hidden')); + } + + places.parameterFields.innerHTML = filteredParams.content; + + return Promise.all( + filteredParams.layout.map(async (parameterId) => { + await createParameterWidget(appSpec, filteredParams, parameterId); + }) + ).then(() => { + renderAdvanced(); + }); + } + + /** + * + * @param {object} arg - should have keys: + * - appSpec - the app spec to be passed along to the individual widgets + * - parameters - an object with parameter specs and their proper layout order + * @returns + */ + function start(arg) { + doAttach(arg.node); + + // get the parameter specs in the right order. + const parameterSpecs = []; + arg.parameters.layout.forEach((id) => { + if (paramIds.includes(id)) { + const paramSpec = arg.parameters.specs[id]; + parameterSpecs.push(paramSpec); + } + }); + + // keep the appSpec + model.setItem('appSpec', arg.appSpec); + // keep an ordered list of used parameter specs + model.setItem('parameterSpecs', parameterSpecs); + + bus.on('parameter-changed', (message) => { + // Also, tell each of our inputs that a param has changed. + widgets.forEach((widget) => { + widget.bus.send(message, { + key: { + type: 'parameter-changed', + parameter: message.parameter, + }, + }); + }); + }); + + return renderParameters() + .then(() => { + // do something after success + attachEvents(); + }) + .catch((error) => { + throw new Error(`Unable to start paramsWidget: ${error}`); + }); + } + + function stop() { + return Promise.try(() => { + if (container) { + container.innerHTML = ''; + } + }); + } + + return { + start, + stop, + bus: function () { + return bus; + }, + }; + } + + return { + make: function (config) { + return factory(config); + }, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/infoTab.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/infoTab.js new file mode 100644 index 0000000000..5add106314 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/infoTab.js @@ -0,0 +1,204 @@ +define(['common/format', 'common/html', 'util/string'], (format, html, string) => { + 'use strict'; + + const cssBaseClass = 'kb-info-tab', + div = html.tag('div'), + span = html.tag('span'), + ul = html.tag('ul'), + li = html.tag('li'), + a = html.tag('a'); + + function paramsList(appSpec) { + if (!appSpec.parameters.length) { + return div( + { + class: `${cssBaseClass}__list--params`, + }, + 'No parameters specified' + ); + } + + const parameterArray = appSpec.parameters.map((param) => { + const textOptions = param.text_options; + let types = null; + if (textOptions && Array.isArray(textOptions.valid_ws_types)) { + const typesArray = textOptions.valid_ws_types.map((type) => { + return a( + { + class: `${cssBaseClass}__link--kb-type`, + href: `/#spec/type/${type}`, + target: '_blank', + title: `KBase type ${type}`, + }, + type + ); + }); + if (typesArray.length) { + types = string.arrayToEnglish(typesArray); + } + } + + return li( + { + class: `${cssBaseClass}__list_item--params`, + }, + span({}, param.ui_name) + (types ? ': ' + types : '') + ); + }); + + return ul( + { + class: `${cssBaseClass}__list--params`, + }, + parameterArray + ); + } + + function authorList(appSpec) { + // AUTHORS/OWNERS + const authors = appSpec.full_info.authors; + if (!authors) { + return undefined; + } + return string.arrayToEnglish( + authors.map((author) => { + return a( + { + class: `${cssBaseClass}__link--author`, + href: `/#people/${author}`, + target: '_blank', + }, + author + ); + }) + ); + } + + function factory(config) { + const model = config.model; + let containerNode; + + function start(arg) { + let appSpec = model.getItem('app.spec'); // for plain app cell + if (!appSpec && arg.currentApp) { + appSpec = model.getItem(`app.specs.${arg.currentApp}`); // for bulk cells + } + + containerNode = arg.node; + const appTag = appSpec.full_info.tag || model.getItem('app.tag'); + const fullInfo = appSpec.full_info; + const appRef = [fullInfo.id, appTag].filter((v) => !!v).join('/'); + const parameterList = paramsList(appSpec); + const authors = authorList(appSpec); + + // run count and average runtime + const execStats = model.getItem('executionStats'); + let avgRuntime, runtimeInfo; + if (execStats && execStats.total_exec_time && execStats.number_of_calls > 0) { + avgRuntime = format.niceDuration( + 1000 * (execStats.total_exec_time / execStats.number_of_calls) + ); + runtimeInfo = div( + { + class: `${cssBaseClass}__runstats`, + }, + `This app has been run ${execStats.number_of_calls} times` + + ` and its average execution time is ${avgRuntime}.` + ); + } + + const infoContainer = div( + { + class: `${cssBaseClass}__container`, + }, + [ + div( + { + class: `${cssBaseClass}__title`, + }, + [ + span( + { + class: `${cssBaseClass}__name`, + }, + fullInfo.name + ), + span( + { + class: `${cssBaseClass}__version`, + title: `App version v${fullInfo.ver}`, + }, + `v${fullInfo.ver}` + ), + appTag + ? span( + { + class: `${cssBaseClass}__tag label label-primary`, + title: `${appTag} version of the app`, + }, + appTag + ) + : '', + ] + ), + div( + { + class: `${cssBaseClass}__authors`, + }, + authors ? `by ${authors}` : 'No authors specified' + ), + div( + { + class: `${cssBaseClass}__description`, + }, + fullInfo.description && fullInfo.description.length + ? fullInfo.description + : 'No description specified' + ), + div( + { + class: `${cssBaseClass}__link--docs`, + }, + a( + { + href: `/#appcatalog/app/${appRef}`, + target: '_blank', + }, + 'View full documentation' + ) + ), + div( + { + class: `${cssBaseClass}__list_title--params`, + }, + 'Parameters' + ), + parameterList, + runtimeInfo, + ] + ); + containerNode.innerHTML = infoContainer; + return Promise.resolve(containerNode); + } + + function stop() { + containerNode.innerHTML = ''; + return Promise.resolve(); + } + + return { + hide: () => { + /* no op */ + }, + start, + stop, + }; + } + + return { + make: function (config) { + return factory(config); + }, + cssBaseClass, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobActionDropdown.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobActionDropdown.js new file mode 100644 index 0000000000..446af8b9ba --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobActionDropdown.js @@ -0,0 +1,244 @@ +define([ + 'jquery', + 'bluebird', + 'common/html', + 'common/dialogMessages', + 'common/jobs', + 'util/developerMode', +], ($, Promise, html, DialogMessages, Jobs, DevMode) => { + 'use strict'; + + const t = html.tag, + span = t('span'), + button = t('button'), + ul = t('ul'), + li = t('li'), + div = t('div'), + cssBaseClass = 'kb-job-action'; + + const actionArr = [ + { + label: 'Cancel queued jobs', + action: 'cancel', + target: ['created', 'estimating', 'queued'], + }, + { + label: 'Cancel running jobs', + action: 'cancel', + target: ['running'], + }, + { + label: 'Retry cancelled jobs', + action: 'retry', + target: ['terminated'], + }, + { + label: 'Retry failed jobs', + action: 'retry', + target: ['error'], + }, + ]; + + const TARGET_SEPARATOR = '||'; + + /** + * create a job action dropdown instance + * + * The config should be an object with a property 'jobManager', an object with + * functions that are executed when one of the dropdown options is selected. + * The job manager supplies the model that holds the cell's job data, which the widget uses + * to enable or disable dropdown options, depending on the statuses of the current jobs. It + * also supplies the bus, using which the appropriate messages to cancel or retry jobs are sent. + * + * @param {object} config + * @returns jobActionDropdown instance + */ + function factory(config) { + const { jobManager } = config; + + const api = { + start, + stop, + updateState, + }; + + if (config.devMode || DevMode.mode) { + api.doBatchJobAction = doBatchJobAction; + } + + if (!jobManager) { + throw new Error( + 'Cannot create jobActionDropdown: must supply config object with key "jobManager"' + ); + } + let container; + + /** + * Kick off a batch job action + * + * @param {event} e - event + * + * The target element's "data-" properties encode the action to be performed: + * + * - data-action - either "cancel" or "retry" + * - data-target - the job status(es) of the jobs to perform the action on + * + * @returns {Promise} that resolves to false if there is some error with the input or + * if the user cancels the batch action. If the users confirms the action, the appropriate + * message will be emitted by the bus. + */ + + function doBatchJobAction(e) { + const el = e.target; + const action = el.getAttribute('data-action'), + statusList = el.getAttribute('data-target').split(TARGET_SEPARATOR); + + // valid actions: cancel or retry + if (!['cancel', 'retry'].includes(action)) { + return Promise.resolve(false); + } + + const jobList = jobManager.getCurrentJobsByStatus( + statusList, + Jobs.validStatusesForAction[action] + ); + if (!jobList || !jobList.length) { + return Promise.resolve(false); + } + + return DialogMessages.showDialog({ action: `${action}Jobs`, statusList, jobList }).then( + (confirmed) => { + if (confirmed) { + const jobIdList = + action === 'retry' + ? jobList.map((job) => { + return job.retry_parent || job.job_id; + }) + : jobList.map((job) => { + return job.job_id; + }); + jobManager.doJobAction(action, jobIdList); + } + return Promise.resolve(confirmed); + } + ); + } + + function createActionsDropdown() { + // each button has an action, either 'cancel' or 'retry', + // and a target, which refers to the status of the jobs + // that the action will be performed upon. + const uniqueId = html.genId(); + return div( + { + class: `${cssBaseClass}__dropdown dropdown`, + }, + [ + button( + { + id: uniqueId, + type: 'button', + dataToggle: 'dropdown', + ariaHaspopup: true, + ariaExpanded: false, + ariaLabel: 'Job options', + class: `btn btn-default ${cssBaseClass}__dropdown_header`, + }, + [ + 'Cancel / retry all', + span({ + class: `fa fa-caret-down kb-pointer ${cssBaseClass}__icon`, + }), + ] + ), + ul( + { + class: `${cssBaseClass}__dropdown-menu dropdown-menu`, + ariaLabelledby: uniqueId, + }, + actionArr.map((actionObj) => { + return li( + { + class: `${cssBaseClass}__dropdown-menu-item`, + }, + button( + { + class: `${cssBaseClass}__dropdown-menu-item-link--${actionObj.action}`, + type: 'button', + title: actionObj.label, + dataElement: `${actionObj.action}-${actionObj.target.join( + TARGET_SEPARATOR + )}`, + dataAction: actionObj.action, + dataTarget: actionObj.target.join(TARGET_SEPARATOR), + }, + actionObj.label + ) + ); + }) + ), + ] + ); + } + + function updateState() { + const jobCountsByStatus = Jobs.getCurrentJobCounts( + jobManager.model.getItem('exec.jobs') + ); + + actionArr.forEach((action) => { + const result = action.target.some((status) => { + return jobCountsByStatus[status]; + }); + // if the status exists, enable the button; otherwise, set it to disabled + container.querySelector( + `[data-target="${action.target.join(TARGET_SEPARATOR)}"]` + ).disabled = !result; + }); + } + + /** + * + * @param {object} args -- with keys + * node: DOM node to attach to + * + * @returns {Promise} started JobActionDropdown widget + */ + function start(args) { + return Promise.try(() => { + if (!args.node) { + throw new Error('start argument must have the key "node"'); + } + container = args.node; + container.innerHTML = createActionsDropdown(); + updateState(); + + container.querySelector('.kb-job-action__dropdown-menu').onclick = (e) => { + e.stopPropagation(); + const $currentButton = $(e.target).closest('button'); + if ($currentButton[0]) { + return Promise.resolve(doBatchJobAction(e)); + } + return Promise.resolve(false); + }; + }); + } + + function stop() { + return Promise.try(() => { + if (container) { + container.remove(); + } + }); + } + + return api; + } + + return { + make: (config) => { + return factory(config); + }, + cssBaseClass, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTab.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTab.js new file mode 100644 index 0000000000..fa625084c7 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTab.js @@ -0,0 +1,67 @@ +define(['bluebird', 'common/html', './jobStatusTable'], (Promise, html, JobStatusTableModule) => { + 'use strict'; + + const t = html.tag, + div = t('div'), + dataElementName = 'kb-job-list-wrapper', + { JobStatusTable } = JobStatusTableModule; + + function factory(config) { + let container, jobStatusTableWidget; + + function renderLayout() { + return div({ + class: 'kb-job-status-tab__container', + dataElement: dataElementName, + }); + } + + /** + * starts the jobStatus tab + * + * @param {object} arg, with key 'node' containing the node where the + * job status tab will be built + */ + + function start(arg) { + return Promise.try(() => { + container = arg.node; + container.innerHTML = renderLayout(); + + jobStatusTableWidget = new JobStatusTable(config); + return Promise.try(() => { + jobStatusTableWidget.start({ + node: container.querySelector(`[data-element="${dataElementName}"]`), + }); + }); + }); + } + + function stop() { + if (jobStatusTableWidget) { + jobStatusTableWidget.stop(); + } + if (container) { + container.innerHTML = ''; + } + return Promise.resolve(); + } + + return { + start, + stop, + }; + } + + return { + /** + * This should be (or resemble) the config object from the bulkImportCell + * with keys 'model' and 'jobManager' + * + * @param {object} config + */ + make: function (config) { + return factory(config); + }, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTable.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTable.js new file mode 100644 index 0000000000..e72a7b017a --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/jobStatus/jobStatusTable.js @@ -0,0 +1,751 @@ +define([ + 'jquery', + 'bluebird', + 'common/html', + 'common/jobs', + './jobActionDropdown', + 'util/jobLogViewer', + 'util/appCellUtil', + 'util/string', + 'jquery-dataTables', +], ($, Promise, html, Jobs, JobActionDropdown, JobLogViewerModule, Util, String) => { + 'use strict'; + + const { JobLogViewer } = JobLogViewerModule; + const t = html.tag, + table = t('table'), + thead = t('thead'), + tr = t('tr'), + th = t('th'), + tbody = t('tbody'), + div = t('div'), + ul = t('ul'), + li = t('li'), + button = t('button'), + span = t('span'), + dataTablePageLength = 50, + cssBaseClass = 'kb-job-status'; + + function createTable() { + return table( + { + class: `${cssBaseClass}__table table-striped`, + dataElement: 'job-status-table', + caption: 'Job Status', + }, + [ + thead( + { + class: `${cssBaseClass}__table_head`, + }, + [ + tr( + { + class: `${cssBaseClass}__table_head_row`, + }, + [ + th( + { + class: `${cssBaseClass}__table_head_cell--import-type`, + }, + 'Import type' + ), + th( + { + class: `${cssBaseClass}__table_head_cell--output`, + }, + 'Output' + ), + th( + { + class: `${cssBaseClass}__table_head_cell--status`, + }, + 'Status' + ), + th( + { + class: `${cssBaseClass}__table_head_cell--action`, + }, + 'Action' + ), + ] + ), + ] + ), + tbody({ + class: `${cssBaseClass}__table_body`, + }), + ] + ); + } + + /** + * Compile the information required to display a job in the job status table + * @param {Object} args with keys + * {Object} appData: specs and outputParamIds from the cell config + * {Object} jobInfo: job data from the backend, including app ID and params + * {Object} fileTypeMapping: display text for each import type + * {Object} typesToFiles: mapping of import type to app ID + * @returns {Object} with keys + * analysisType: abbreviated name for the app + * outputParams: any output files or objects + * params: all user-specified job params, output-friendly format + */ + + function generateJobDisplayData(args) { + const { appData, jobInfo, fileTypeMapping, typesToFiles } = args; + let analysisType = ''; + const params = {}; + const outputParams = {}; + // unfortunate data structure makes generating this data long-winded + Object.keys(typesToFiles).forEach((type) => { + // match app_id to get the abbreviated name for the analysis + if (typesToFiles[type].appId === jobInfo.app_id) { + analysisType = fileTypeMapping[type]; + Object.keys(jobInfo.job_params[0]).forEach((param) => { + // find each parameter in job_params in the app specs + appData.specs[jobInfo.app_id].parameters.forEach((appParam) => { + // only add values that are in the parameters + // as these are the user-settable params + if (appParam.id === param) { + const value = jobInfo.job_params[0][param]; + const label = appData.specs[jobInfo.app_id].parameters + .filter((p) => { + return param === p.id; + }) + .map((p) => { + return p.ui_name; + })[0]; + params[param] = { + value, + label, + }; + } + }); + }); + const outputParamIds = appData.outputParamIds[type]; + outputParamIds.forEach((param) => { + outputParams[param] = params[param]; + }); + } + }); + + return { + analysisType, + outputParams, + params, + }; + } + + function displayAppType(typeName) { + return div( + { + class: `${cssBaseClass}__app_type`, + title: typeName, + }, + typeName + ); + } + + function displayParamList(params) { + return ul( + { + class: `${cssBaseClass}__param_list`, + }, + Object.keys(params) + .sort() + .map((key) => { + return li( + { + class: `${cssBaseClass}__param_item`, + }, + [ + span( + { + class: `${cssBaseClass}__param_key`, + }, + `${params[key].label}: ` + ), + span( + { + class: `${cssBaseClass}__param_value`, + }, + `${params[key].value}` + ), + ] + ); + }) + ); + } + + /** + * create a job status table instance + * + * the config should be an object with a property 'jobManager', which executes + * the job actions available as part of the job status table, and 'toggleTab', + * a function used to view the results of finished jobs. + * + * @param {object} config + * @returns jobStatusTable instance + */ + class JobStatusTable { + constructor(config = {}) { + this._init(config); + } + + /** + * Set up the Job Status Table class + * @param {object} config + */ + _init(config) { + this.config = config; + this.showHistory = true; // whether or not the embedded widgets should show job history + + const { jobManager, toggleTab } = config; + if (!jobManager.model || !jobManager.model.getItem('exec.jobs.byId')) { + throw new Error('Cannot start JobStatusTable without a jobs object in the config'); + } + this.jobManager = jobManager; + this.toggleTab = toggleTab; + + this.fileTypeMapping = config.fileTypeMapping; + this.jobsByRetryParent = {}; + this.widgetsById = {}; + this.errors = {}; + } + + /** + * + * @param {object} args -- with key + * node: DOM node to attach to + * + * @returns {Promise} started JobStatusTable widget + */ + start(args) { + const requiredArgs = ['node']; + if (!requiredArgs.every((arg) => arg in args && args[arg])) { + throw new Error('start argument must have these keys: ' + requiredArgs.join(', ')); + } + + const indexedJobs = this.jobManager.model.getItem('exec.jobs.byId'); + if (!indexedJobs || !Object.keys(indexedJobs).length) { + throw new Error('Must provide at least one job to show the job status table'); + } + const jobs = Object.values(indexedJobs); + + this.container = args.node; + this.container.innerHTML = [ + div({ + class: `${cssBaseClass}__dropdown_container pull-right`, + }), + div( + { + class: `${cssBaseClass}__table_instructions`, + }, + 'Click on a row to view the job details and logs.' + ), + createTable(), + ].join('\n'); + + return Promise.try(() => { + // start the dropdown widget + this.dropdownWidget = JobActionDropdown.make(this.config); + return this.dropdownWidget.start({ + node: this.container.querySelector(`.${cssBaseClass}__dropdown_container`), + }); + }).then(() => { + this.renderTable(jobs); + this.setUpEventHandlers(); + this.setUpJobListeners(jobs); + }); + } + + stop() { + Object.values(this.widgetsById).map((widget) => widget.stop()); + this.jobManager.removeEventHandler('modelUpdate', 'jobStatusTable_status'); + this.jobManager.removeEventHandler('modelUpdate', 'dropdown'); + this.jobManager.removeEventHandler('job-info', 'jobStatusTable_info'); + this.jobManager.removeEventHandler('job-error', 'jobStatusTable_error'); + this.container.innerHTML = ''; + if (this.dropdownWidget) { + return this.dropdownWidget.stop(); + } + } + + /** + * convert the table to a DataTable object to get sorting/paging functionality + * @param {array} rows - table rows + * @returns {DataTable} datatable object + */ + renderTable(rows) { + // group jobs by their retry parents + const jobsByOriginalId = Jobs.getCurrentJobs(rows, this.jobsByRetryParent); + const allJobInfo = this.jobManager.model.getItem('exec.jobs.info'); + const appData = this.jobManager.model.getItem('app'); + if (allJobInfo) { + Object.keys(jobsByOriginalId).forEach((jobId) => { + if (allJobInfo[jobId]) { + const jobDisplayData = generateJobDisplayData({ + jobInfo: allJobInfo[jobId], + fileTypeMapping: this.fileTypeMapping, + appData, + typesToFiles: this.config.typesToFiles, + }); + this.jobManager.model.setItem( + `exec.jobs.paramDisplayData.${jobId}`, + jobDisplayData + ); + } + }); + } + + this.dataTable = $(this.container.querySelector('table')).dataTable({ + autoWidth: false, + data: Object.values(jobsByOriginalId), + destroy: true, + rowId: (row) => { + return `job_${row.retry_parent || row.job_id}`; + }, + lengthChange: false, + pageLength: dataTablePageLength, + searching: false, + columns: [ + { + className: `${cssBaseClass}__cell--import-type`, + render: (data, type, row) => { + const jobParams = + this.jobManager.model.getItem('exec.jobs.paramDisplayData') || {}; + const relevantJobId = + row.retry_parent && jobParams[row.retry_parent] + ? row.retry_parent + : row.job_id; + + if (jobParams[relevantJobId]) { + return displayAppType(jobParams[relevantJobId].analysisType); + } + return `Job ID: ${row.job_id}`; + }, + }, + { + className: `${cssBaseClass}__cell--output`, + render: (data, type, row) => { + const jobParams = + this.jobManager.model.getItem('exec.jobs.paramDisplayData') || {}; + const relevantJobId = + row.retry_parent && jobParams[row.retry_parent] + ? row.retry_parent + : row.job_id; + + if (jobParams[relevantJobId]) { + return displayParamList(jobParams[relevantJobId].outputParams); + } + return ''; + }, + }, + { + className: `${cssBaseClass}__cell--status`, + render: (data, type, row) => { + const statusLabel = String.capitalize(Jobs.jobLabel(row)); + return div( + { + dataToggle: 'tooltip', + dataPlacement: 'bottom', + title: statusLabel, + }, + [ + span({ + class: `fa fa-circle ${cssBaseClass}__icon--${row.status}`, + title: statusLabel, + }), + statusLabel, + ] + ); + }, + }, + { + className: `${cssBaseClass}__cell--action`, + render: (data, type, row) => { + const jobAction = Jobs.jobAction(row); + if (!jobAction) { + return ''; + } + const jsActionString = jobAction.replace(/ /g, '-'); + const dataTarget = + jobAction === 'retry' && row.retry_parent + ? row.retry_parent + : row.job_id; + + const buttonHtml = button( + { + role: 'button', + dataTarget, + dataAction: jsActionString, + dataElement: 'job-action-button', + class: `${cssBaseClass}__cell_action--${jsActionString}`, + }, + jobAction + ); + + if ( + ['cancel', 'retry'].includes(jobAction) && + this.errors[row.job_id] + ) { + return ( + buttonHtml + + span({ + class: `fa fa-exclamation-triangle ${cssBaseClass}__icon--action_warning`, + ariaHidden: 'true', + dataToggle: 'popover', + dataContainer: 'body', + dataContent: 'Could not ' + jobAction + ' job.', + }) + ); + } + return buttonHtml; + }, + }, + ], + createdRow: (el) => { + el.onclick = (e) => { + e.stopPropagation(); + const $currentButton = $(e.target).closest( + '[data-element="job-action-button"]' + ); + const $currentRow = $(e.target).closest('tr.odd, tr.even'); + // not an expandable row or a job action button + if (!$currentRow[0] && !$currentButton[0]) { + return Promise.resolve(); + } + // job action button + if ($currentButton[0]) { + return Promise.resolve(this.doSingleJobAction(e)); + } + // expandable row + return this.showHideChildRow(e); + }; + }, + drawCallback: function () { + // Hide pagination controls if length is less than or equal to table length + if (this.api().data().length <= dataTablePageLength) { + $(this.container).find('.dataTables_paginate').hide(); + } + }, + }); + } + + /** + * Add a new row to the table (if applicable) + * @param {object} jobState + */ + addTableRow(jobState) { + // check whether this job is part of the same batch as the other jobs; + // if so, it should be added to the table + const expectedBatchId = this.jobManager.model.getItem('exec.jobState.batch_id'); + if ( + // no batch job + !expectedBatchId || + // incoming job has no batch ID + !jobState.batch_id || + // incoming job is in a different batch + jobState.batch_id !== expectedBatchId || + // this is the batch job + jobState.job_id === expectedBatchId + ) { + return; + } + const rowIx = jobState.retry_parent || jobState.job_id; + const currentJobIx = `${jobState.created || 0}__${jobState.job_id}`; + + this.jobsByRetryParent[rowIx] = { + job_id: rowIx, + jobs: {}, + }; + this.jobsByRetryParent[rowIx].jobs[currentJobIx] = jobState; + // do we have job info for it? + if (!this.jobManager.model.getItem(`exec.jobs.info.${rowIx}`)) { + this.jobManager.requestJobInfo([rowIx]); + } + + // is this a job that has been retried? + if (jobState.retry_ids && jobState.retry_ids.length) { + return; + } + // this is a new job that is part of the same batch + this.dataTable.DataTable().row.add(jobState).draw(); + } + + /** + * Click handler for the table. Rows can be expanded to show job details + * and clicked again to hide them. + * + * @param {Event} e - row click event + * @returns {Promise} that resolves when the appropriate row action completes + */ + showHideChildRow(e) { + const $currentRow = $(e.target).closest('tr'); + const $table = $(e.target).closest('table'); + const $dtTable = $table.DataTable(); + const dtRow = $dtTable.row($currentRow); + const jobState = dtRow.data(); + + // remove the existing row selection + $table + .find(`.${cssBaseClass}__row--selected`) + .removeClass(`${cssBaseClass}__row--selected`); + // select the current row + $currentRow.addClass(`${cssBaseClass}__row--selected`); + + if (dtRow.child.isShown()) { + // This row is already open - close it + dtRow.child.hide(); + $currentRow.removeClass('vertical_collapse--open'); + if (this.widgetsById[`${jobState.job_id}_LOG`]) { + this.widgetsById[`${jobState.job_id}_LOG`].stop(); + } + dtRow.child.remove(); + return Promise.resolve(); + } + + // create the child row contents, add to the child row, and show it + const str = div( + { + class: `${cssBaseClass}__detail_container`, + dataElement: 'job-detail-container', + }, + [ + div({ + dataElement: `job-logs-div`, + }), + ] + ); + dtRow.child(str).show(); + + // add the log widget to the next `tr` element + this.widgetsById[`${jobState.job_id}_LOG`] = new JobLogViewer({ + jobManager: this.jobManager, + showHistory: this.showHistory, + }); + return Promise.try(() => { + this.widgetsById[`${jobState.job_id}_LOG`].start({ + jobId: dtRow.data().job_id, + jobState, + config: this.config, + node: $currentRow.next().find('[data-element="job-logs-div"]')[0], + }); + }) + .then(() => { + $currentRow.addClass('vertical_collapse--open'); + }) + .catch((err) => { + console.error(err); + }); + } + + closeRow(e) { + if ($(e.target).closest('tr')[0].classList.contains('vertical_collapse--open')) { + this.showHideChildRow(e); + } + } + + /** + * Execute an action for a single job + * + * @param {event} e - event + * + * The target element's "data-" properties encode the action to be performed: + * + * - data-action - "cancel", "retry", or "go-to-results" + * - data-target - the job ID of the job to perform the action on + */ + doSingleJobAction(e) { + const el = e.target; + e.stopPropagation(); + const action = el.getAttribute('data-action'), + target = el.getAttribute('data-target'); + + if (!['cancel', 'retry', 'go-to-results'].includes(action)) { + return false; + } + + const jobState = this.jobManager.model.getItem(`exec.jobs.byId.${target}`); + if (!jobState) { + return false; + } + + if (action === 'go-to-results') { + // switch to results tab + return this.toggleTab('results'); + } + if (!Jobs.canDo(action, jobState)) { + // make sure that the button is disabled so it cannot be clicked again + e.target.disabled = true; + return false; + } + // make sure any errors associated with the row ID are deleted + if (this.errors[target]) { + delete this.errors[target]; + } + this.jobManager.doJobAction(action, [target]); + + // if the job is being retried and the log viewer is open, close it + // TODO: a better fix! + if (action === 'retry') { + this.closeRow(e); + } + + // disable the button to prevent further clicks + e.target.disabled = true; + e.target.textContent = action + 'ing'; + // remove any error indicator from the DOM + $(e.target).closest('td').find(`.${cssBaseClass}__icon--action_warning`).remove(); + return true; + } + + /** + * Set up event handlers + */ + setUpEventHandlers() { + const self = this; + this.jobManager.addEventHandler('modelUpdate', { + dropdown: () => { + this.dropdownWidget.updateState(); + }, + jobStatusTable_status: (_, jobArray) => { + jobArray.forEach((jobState) => + this.updateJobStatusInTable.bind(self)(jobState) + ); + }, + }); + this.jobManager.addEventHandler('job-error', { + jobStatusTable_error: (_, jobError) => { + this.handleJobError.bind(self)(jobError); + }, + }); + this.jobManager.addEventHandler('job-info', { + jobStatusTable_info: (_, jobInfo) => { + this.handleJobInfo.bind(self)(jobInfo); + }, + }); + } + + /** + * Set up job event listeners and handlers + * + * @param {array} jobs + */ + setUpJobListeners(jobs) { + const batchId = this.jobManager.model.getItem('exec.jobState.job_id'); + const paramsRequired = []; + const jobIdList = []; + jobs.forEach((jobState) => { + if (!jobState.batch_job) { + jobIdList.push(jobState.job_id); + if ( + !this.jobManager.model.getItem(`exec.jobs.info.${jobState.job_id}`) && + jobState.job_id !== batchId + ) { + paramsRequired.push(jobState.job_id); + } + } + }); + + ['job-status', 'job-error'].forEach((event) => { + this.jobManager.addListener(event, [batchId].concat(jobIdList)); + }); + + if (paramsRequired.length) { + this.jobManager.addListener('job-info', paramsRequired); + const jobInfoRequestParams = + paramsRequired.length === jobIdList.length + ? { batchId } + : { jobIdList: paramsRequired }; + this.jobManager.bus.emit('request-job-info', jobInfoRequestParams); + } + this.jobManager.bus.emit('request-job-status', { batchId }); + } + + // HANDLERS + + /** + * Update the table with a new jobState object + * @param {object} jobState + */ + updateJobStatusInTable(jobState) { + const rowIx = jobState.retry_parent || jobState.job_id; + + if (!this.jobsByRetryParent[rowIx]) { + return this.addTableRow(jobState); + } + + const jobIx = `${jobState.created || 0}__${jobState.job_id}`; + this.jobsByRetryParent[rowIx].jobs[jobIx] = jobState; + + // make sure that this job is the most recent version + const sortedJobs = Object.keys(this.jobsByRetryParent[rowIx].jobs).sort(); + const lastJob = sortedJobs[sortedJobs.length - 1]; + const jobUpdateData = this.jobsByRetryParent[rowIx].jobs[lastJob]; + // irrelevant update -- data is not for the most recent retry + if (jobUpdateData.job_id !== jobState.job_id) { + return; + } + + // select the appropriate row + try { + // update the row + this.dataTable.DataTable().row(`#job_${rowIx}`).data(jobState); + } catch (e) { + console.error('Error trying to update jobs table:', e); + } + } + + /** + * parse and update the row with job info + * @param {object} message + */ + handleJobInfo(message) { + const { jobId, jobInfo } = message; + const jobState = this.jobManager.model.getItem(`exec.jobs.byId.${jobId}`); + const appData = this.jobManager.model.getItem('app'); + const rowIx = jobState && jobState.retry_parent ? jobState.retry_parent : jobId; + + const jobDisplayData = generateJobDisplayData({ + jobInfo, + fileTypeMapping: this.fileTypeMapping, + appData, + typesToFiles: this.config.typesToFiles, + }); + this.jobManager.model.setItem(`exec.jobs.paramDisplayData.${rowIx}`, jobDisplayData); + + // update the table + this.dataTable.DataTable().row(`#job_${rowIx}`).invalidate().draw(); + } + + /** + * parse a job error message + * @param {object} message + */ + handleJobError(message) { + const { jobId, error } = message; + if (!error) { + return; + } + const jobState = this.jobManager.model.getItem(`exec.jobs.byId.${jobId}`); + const rowIx = jobState && jobState.retry_parent ? jobState.retry_parent : jobId; + + this.errors[rowIx] = error; + + // update the table + this.dataTable.DataTable().row(`#job_${rowIx}`).invalidate().draw(); + $('.' + `${cssBaseClass}__icon--action_warning`).popover({ + placement: 'auto', + trigger: 'hover focus', + }); + } + } + + return { + JobStatusTable, + generateJobDisplayData, + cssBaseClass, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/outputWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/outputWidget.js new file mode 100644 index 0000000000..582bddba83 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/outputWidget.js @@ -0,0 +1,186 @@ +/** + * Should get fed data to view. + */ +define([ + 'bluebird', + 'base/js/namespace', + 'common/html', + 'common/ui', + 'util/kbaseApiUtil', + 'jquery', + 'jquery-dataTables', +], (Promise, Jupyter, html, UI, APIUtil, $) => { + 'use strict'; + + const tag = html.tag, + div = tag('div'), + tr = tag('tr'), + th = tag('th'), + thead = tag('thead'), + table = tag('table'), + a = tag('a'), + tablePageLength = 50, + cssBaseClass = 'kb-output-widget'; + + function OutputWidget() { + let container, ui; + + /** + * Renders the table of created output objects. Each row will have 3 elements: + * - name - the name of the created object. This will be a link that, when clicked, will spawn a + * viewer cell for that object + * - type - a string for the object's type + * - description - a string for the object's description + * - wsInfo - the object info array from the Workspace service, + * - ref - string - the object's workspace reference + * @param {Array} objectData an array of object data. Each element is expected + * to have the properties detailed above. If any are missing, they'll get placeholder values instead. + */ + function renderOutput(objectData) { + if (objectData.length === 0) { + return div('No objects created'); + } + + return table( + { + class: `table table-striped ${cssBaseClass}__table`, + style: 'width: 100%', + dataElement: 'objects-table', + }, + [thead(tr([th('Created Object Name'), th('Type'), th('Description')]))] + ); + } + + /** + * 1. Make the main layout + * 2. Put the layout in place + * 3. Add the ui.buildCollapsiblePanel thing, with the table in the body + * @param {object} + * - node - the DOM node to attach to + * - objectData - + * @returns {Promise} resolves when the layout is complete + */ + function doAttach(arg) { + container = arg.node; + ui = UI.make({ + node: container, + }); + // this is the main layout div. don't do anything yet. + container.innerHTML = div({ + dataElement: 'created-objects', + class: 'kb-created-objects', + }); + + ui.setContent( + 'created-objects', + ui.buildCollapsiblePanel({ + title: 'Objects', + name: 'created-objects-toggle', + hidden: false, + type: 'default', + classes: ['kb-panel-container'], + body: renderOutput(arg.objectData), + }) + ); + + if (arg.objectData.length) { + const $objTable = $(ui.getElement('objects-table')); + + const tableData = arg.objectData.map((obj) => { + let name = obj.name; + if (!name) { + name = obj.wsInfo ? obj.wsInfo[1] : 'Unknown object name'; + } + let type = obj.type; + if (!type) { + type = obj.wsInfo ? obj.wsInfo[2] : 'Missing type'; + } + const parsedType = APIUtil.parseWorkspaceType(type) || { type }; + const description = obj.description || 'Missing description'; + return { + wsInfo: obj.wsInfo, + name, + type, + parsedType, + description, + }; + }); + + $objTable.DataTable({ + data: tableData, + dom: "<'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>", + lengthChange: false, + pageLength: tablePageLength, + paging: arg.objectData.length > tablePageLength, + searching: false, + columns: [ + { + render: (data, type, row) => { + if (!row.wsInfo) { + return row.name; + } + + return a( + { + class: `${cssBaseClass}__object_link`, + dataObjRef: row.ref, + type: 'button', + ariaLabel: 'show viewer for ' + row.name, + }, + [row.name] + ); + }, + }, + { + render: (data, type, row) => { + return row.parsedType.type; + }, + }, + { + render: (data, type, row) => { + return row.description || 'Missing description'; + }, + }, + ], + createdRow: (el, row) => { + if (row.wsInfo) { + const objLink = el.querySelector('.kb-output-widget__object_link'); + $(objLink).on('click', () => { + Jupyter.narrative.addViewerCell(row.wsInfo); + }); + } + }, + }); + } + } + + /** + * Starts the widget. This gets fed its data to render directly, so this just + * wraps a Promise around the rendering process. + * @param {object} arg + * - node - the DOM node to build this under + * - objectData - an array of object data to render + */ + function start(arg) { + // send parent the ready message + return Promise.try(() => doAttach(arg)).catch((err) => { + console.error('Error while starting the created objects view', err); + }); + } + + function stop() { + return Promise.try(() => { + container.innerHTML = ''; + }); + } + + return { + start, + stop, + }; + } + + return { + make: OutputWidget, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/reportWidget.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/reportWidget.js new file mode 100644 index 0000000000..61e74e604c --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/reportWidget.js @@ -0,0 +1,166 @@ +/** + * This renders a list of KBase Report objects. Each one is expandable and loads up under a caret. + */ +define(['bluebird', 'jquery', 'common/html', 'common/ui', 'common/events', 'kbaseReportView'], ( + Promise, + $, + html, + UI, + Events, + KBaseReportView +) => { + 'use strict'; + + const tag = html.tag, + div = tag('div'); + + function ReportWidget() { + let container, ui; + + /** + * + * @param {Array} objectData - an Array of object data, each element is an Object with + * these properties: + * - ref - string, the object ref + * - reportRef - string, the report object ref that this object is from + * - description - string, object description + * - name - string, the object name + * - type - string, the type of workspace object (full form - Module.Type-Major.Minor) + * @param {Object} events - the events object + */ + function renderReports(objectData, events) { + return objectData.map((objInfo) => { + // clean up objInfo before rendering / passing to toggleReportView + let name = objInfo.name; + if (!name) { + if (objInfo.ref) { + name = `Object ${objInfo.ref} not found, may have been deleted`; + } else { + name = 'Object not found'; + } + } + + let reportElem = name + ' (report not found)'; + if (objInfo.reportRef) { + reportElem = div( + { + class: 'kb-report__item_toggle collapsed', + dataToggle: 'collapse', + ariaExpanded: false, + id: events.addEvent({ + type: 'click', + handler: (e) => toggleReportView(e, objInfo.reportRef), + }), + }, + name + ); + } + return div( + { + class: 'kb-report__item_container', + }, + [reportElem] + ); + }); + } + + /** + * Toggles viewing of a report. If it's currently viewed, then this will remove the + * report view. Otherwise, it will instantiate a new report view widget. + * @param {Event} e - a DOM event that triggered this report. + * @param {String} reportRef - from the arg, same as used to render the list of reports + * this is required to have the reportRef key, which is an object UPA + */ + function toggleReportView(e, reportRef) { + const toggleHeader = e.target; + if (!toggleHeader.classList.contains('collapsed')) { + toggleHeader.parentElement.lastElementChild.remove(); + } else { + const reportContainer = document.createElement('div'); + toggleHeader.parentElement.appendChild(reportContainer); + new KBaseReportView($(reportContainer), { + report_ref: reportRef, + autoRender: false, + }).loadAndRender(); + } + toggleHeader.classList.toggle('collapsed'); + } + + /** + * Attaches the widget to its DOM node and renders the report options. + * @param {object} arg + * - node - the DOM node to attach to + * - objectData - an array of report references to render from + * - workspaceClient - a workspace client to use to fetch report info + */ + function doAttach(arg) { + container = arg.node; + ui = UI.make({ + node: container, + }); + const events = Events.make(); + // this is the main layout div. don't do anything yet. + container.innerHTML = div({ + dataElement: 'reports-view', + class: 'kb-reports-view', + }); + + let content = ''; + if (arg.objectData && arg.objectData.length) { + content = ui.buildCollapsiblePanel({ + title: 'Reports', + name: 'reports-view-toggle', + hidden: false, + type: 'default', + classes: ['kb-panel-container'], + body: div( + { + class: 'kb-report__container', + }, + renderReports(arg.objectData, events) + ), + }); + } + + ui.setContent('reports-view', content); + + events.attachEvents(container); + } + + /** + * + * @param {object} arg + * - node - the DOM node to build this under + * - objectData - a list of objectData elements, used to produce reports. + * Each element has these keys: + * - description - string, description of the object + * - name - string, name of the object + * - ref - string, the object UPA + * - reportRef - string, the report UPA + * - type - string, the registered workspace type of the object + * - workspaceClient - a workspace client to use + */ + function start(arg) { + // send parent the ready message + return Promise.resolve(doAttach(arg)).catch((err) => { + console.error('Error while starting the created objects view', err); + }); + } + + function stop() { + return Promise.try(() => { + container.innerHTML = ''; + }); + } + + return { + start, + stop, + toggleReportView, + }; + } + + return { + make: ReportWidget, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/resultsTab.js b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/resultsTab.js new file mode 100644 index 0000000000..ba731d042e --- /dev/null +++ b/kbase-extension/static/kbase/js/common/cellComponents/tabs/results/resultsTab.js @@ -0,0 +1,221 @@ +define(['bluebird', 'common/ui', 'common/events', './outputWidget', './reportWidget'], ( + Promise, + UI, + Events, + OutputWidget, + ReportWidget +) => { + 'use strict'; + + /** + * + * @param {object} config each tab for a cell needs the following: + * - model - a Props object containing the data model for the cell + * - workspaceClient - a workspace client authenticated to the current user + */ + function ResultsTab(config) { + const model = config.model, + workspaceClient = config.workspaceClient; + let container = null; + + /** + * This should know how to examine the model for report info. Not sure how to do that yet, + * but we can make some assumptions. + * 1. There will be a list of outputs for each data type. + * 2. That will be processed and put in place by whatever controlling cell. (as of 12/17/20, + * this is focused on the bulk processing / import cells, so start there). + * 3. Those outputs will include a list of report object ids for each data type. + * 4. At this point, this function should just return the list of report object UPAs / + * references. To minimize service calls and data transfer, the individual components + * should just fetch the data they need. + * That means the "created objects" view should just fetch down that path and collate them + * all, and the report view should know how to fetch and render an individual report on + * request. + * @returns {Array} an array of report ids. + */ + function getReportRefs() { + const jobIndex = model.getItem('exec.jobs.byId'); + const jobStates = Object.values(jobIndex); + + const reportRefs = []; + jobStates.forEach((job) => { + if ( + 'job_output' in job && + 'result' in job.job_output && + job.job_output.result.length > 0 + ) { + job.job_output.result.forEach((result) => { + if ('report_ref' in result) { + reportRefs.push(result.report_ref); + } + }); + } + }); + return reportRefs; + } + + /** + * + * @param {*} arg + */ + function buildOutputWidget(node, objectData) { + const outputWidget = OutputWidget.make(); + + return outputWidget.start({ + node, + objectData, + workspaceClient, + }); + } + + function buildReportWidget(node, objectData) { + const reportWidget = ReportWidget.make(); + + return reportWidget.start({ + node, + objectData, + workspaceClient, + }); + } + + /** + * Returns a Promise that resolves into the following structure (repeat the object info structure): + * [{ + * name: string, + * type: string, + * description: string, + * reportRef: string, // the report that references this object + * ref: string // the created object + * }] + * @param {Array} reports an array of report workspace references + * @param {Object} workspaceClient an authenticated workspace client + * @returns a Promise resolving into a list of objects, with keys name, ref, description, and type: + */ + function fetchReportData(reports) { + // making a list of the following will just fetch the + // 'objects_created' lists from each report. Should be a more + // lightweight call. + const reportLookupParam = reports.map((ref) => { + return { + ref: ref, + included: ['objects_created'], + }; + }); + /* when we do the lookup, data will get returned as: + * { + * data: [{ + * data: { + * objects_created: [{ + * description: str, + * ref: str + * }] + * } + * }] + * } + */ + // key this off of the object id to make lookups easier once we + // fetch the names later. + const createdObjects = {}; + let objectKeys = []; + return workspaceClient + .get_objects2({ objects: reportLookupParam, ignoreErrors: 1 }) + .then((reportData) => { + reportData.data.forEach((report, idx) => { + if (report !== null && 'objects_created' in report.data) { + report.data.objects_created.forEach((obj) => { + createdObjects[obj.ref] = obj; + createdObjects[obj.ref].reportRef = reportLookupParam[idx].ref; + }); + } + }); + // we'll use this later to unpack object infos, and JS doesn't guarantee + // the same order. + objectKeys = Object.keys(createdObjects); + // turn the refs into an array: [{"ref": ref}] + const infoLookupParam = objectKeys.map((ref) => ({ ref })); + return workspaceClient.get_object_info_new({ + objects: infoLookupParam, + ignoreErrors: 1, + }); + }) + .then((objectInfo) => { + objectInfo.forEach((info, idx) => { + const ref = objectKeys[idx]; + if (info) { + createdObjects[ref].name = info[1]; + createdObjects[ref].type = info[2]; + createdObjects[ref].wsInfo = info; + } else { + createdObjects[ + ref + ].name = `Object ${ref} not found, may have been deleted`; + createdObjects[ref].type = null; + createdObjects[ref].wsInfo = null; + } + }); + return Object.values(createdObjects); + }); + } + + /** + * + * @param {object} arg startup arguments + */ + function start(arg) { + container = arg.node; + const events = Events.make(); + + const reports = getReportRefs(); + + const spinnerNode = document.createElement('div'); + spinnerNode.classList.add('kb-loading-spinner'); + spinnerNode.innerHTML = UI.loading({ size: '2x' }); + container.appendChild(spinnerNode); + + const objectNode = document.createElement('div'); + const reportNode = document.createElement('div'); + const containerNode = document.createElement('div'); + containerNode.classList.add('hidden', 'kb-result-tab__container'); + container.appendChild(containerNode); + return fetchReportData(reports) + .then((reportData) => { + containerNode.appendChild(objectNode); + containerNode.appendChild(reportNode); + + return Promise.all([ + buildOutputWidget(objectNode, reportData), + buildReportWidget(reportNode, reportData), + ]); + }) + .then(() => { + events.attachEvents(container); + }) + .catch((error) => { + console.error('An error occurred while starting the results tab', error); + const errorNode = document.createElement('div'); + errorNode.innerHTML = + 'An error occurred preventing results from being displayed.'; + containerNode.appendChild(errorNode); + }) + .finally(() => { + container.removeChild(spinnerNode); + containerNode.classList.remove('hidden'); + }); + } + + function stop() { + return Promise.try(() => { + container.innerHTML = ''; + }); + } + + return { + start, + stop, + }; + } + + return { + make: ResultsTab, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/cellUtils.js b/kbase-extension/static/kbase/js/common/cellUtils.js index 89594b6d6a..ec3d804891 100644 --- a/kbase-extension/static/kbase/js/common/cellUtils.js +++ b/kbase-extension/static/kbase/js/common/cellUtils.js @@ -1,17 +1,27 @@ -define(['kb_common/html', 'kb_common/format', 'common/props', 'base/js/namespace'], ( - html, - format, - Props, - Jupyter -) => { +define(['common/props', 'base/js/namespace'], (Props, Jupyter) => { 'use strict'; - function createMeta(cell, initial) { - const meta = cell.metadata; - meta.kbase = initial; - cell.metadata = meta; - } - + /** + * This assumes a cell metadata structure built for KBase-enhanced cells. + * This should have a minimal metadata structure under the "kbase" namespace, e.g. + * { + * kbase: { + * someCellType: { + * info: 'information' + * } + * } + * } + * + * Example calls here: + * getMeta(cell, 'someCellType') => { info: 'information' } + * getMeta(cell, 'someCellType', 'info') => 'information' + * getMeta(cell, 'someCellType', 'notFound') => undefined + * + * @param {Object} cell a Jupyter notebook cell object + * @param {string} group a metadata group name under the kbase metadata namespace + * @param {string} name the specific key in the group to return + * @returns the value matching the group and name in the cell's metadata + */ function getMeta(cell, group, name) { if (!cell.metadata.kbase) { return; @@ -25,6 +35,56 @@ define(['kb_common/html', 'kb_common/format', 'common/props', 'base/js/namespace return cell.metadata.kbase[group][name]; } + /** + * Sets a value in the Jupyter notebook cell metadata. By convention, all KBase + * metadata is keyed under the "kbase" namespace. The "group" parameter refers to + * the top level key under that, and name is the optional next level beneath that. + * + * examples: + * setMeta(cell, 'cellType', 'params', {p1: 1, p2: 2}) + * sets metadata: + * { + * kbase: { + * cellType: { + * params: { p1: 1, p2: 2 } + * } + * } + * } + * + * setMeta(cell, 'cellType', {a: 1}) + * sets metadata: + * { + * kbase: { + * cellType: { a: 1 } + * } + * } + * + * Note that values under name should not be set to 'undefined'. If the + * "value" parameters is given as "undefined", then the "group" section will + * be set to the "name" value. That is if running: + * setMeta(cell, 'cellType', 'someKey', undefined) + * you might expect to get: + * { + * kbase: { + * cellType: { + * someKey: undefined + * } + * } + * } + * but will actually get: + * { + * kbase: { + * cellType: 'someKey' + * } + * } + * + * If cell is not a KBase-extended cell, this will throw a TypeError. + * @param {Object} cell a Jupyter notebook cell + * @param {string} group a metadata group name under the kbase metadata namespace + * @param {string} name the specific key in the group to set + * @param {any} value the value to set (optional - if not given, then the "group" + * key will be set to the "name" value) + */ function setMeta(cell, group, name, value) { /* * This funny business is because the trigger on setting the metadata @@ -45,27 +105,69 @@ define(['kb_common/html', 'kb_common/format', 'common/props', 'base/js/namespace cell.metadata = temp; } - function pushMeta(cell, props, value) { - const meta = Props.make(cell.metadata.kbase); - meta.incrItem(props, value); + /** + * Sets a metadata item in the given cell by following the path to the + * metadata key. Note that this requires the full path from the root of the + * metadata item (i.e. kbase.appCell.params...). + * e.g.: + * setCellMeta(cell, 'kbase.appCell.params', {}) + * @param {Object} cell a Jupyter notebook cell + * @param {string|Array} path a path to the value to set (with dots between + * layers for a string, or an array of layers) + * @param {any} value the value to set + * @param {boolean} forceRefresh (optional, default false) if true, will force + * the cell to refresh itself by resetting the metadata object + */ + function setCellMeta(cell, path, value, forceRefresh) { + if (!cell.metadata) { + cell.metadata = {}; + } + Props.setDataItem(cell.metadata, path, value); + if (forceRefresh) { + // eslint-disable-next-line no-self-assign + cell.metadata = cell.metadata; + } } + /** + * Gets a value from a Jupyter notebook cell's metadata. If that value isn't + * present along the given path, returns the defaultValue or undefined. + * + * Note this this requires the full path down the metadata (i.e. kbase.appCell.params...) + * @param {Object} cell a Jupyter notebook cell + * @param {string|Array} path a path to the value to retrieve + * @param {any} defaultValue if the value at the end of the path is undefined, + * return this as a default instead (default undefined) + * @returns a value from the cell metadata + */ + function getCellMeta(cell, path, defaultValue) { + return Props.getDataItem(cell.metadata, path, defaultValue); + } + + /** + * Given a cell with the given id (as stored in the kbase.attributes metadata), this + * returns the title of that cell (as stored in kbase.attributes.title). + * If no cell is present with that id, returns undefined + * @param {string} cellId the cell id stored in the kbase.attributes.id metadata field + * @returns {string} the title of the cell with that id or undefined + */ function getTitle(cellId) { - const cells = Jupyter.notebook.get_cells().filter((cell) => { - return cellId === Props.getDataItem(cell.metadata, 'kbase.attributes.id'); - }); - if (cells.length === 0) { - return; + const cellWithId = findById(cellId); + if (cellWithId) { + return getCellMeta(cellWithId, 'kbase.attributes.title'); } - return Props.getDataItem(cells[0].metadata, 'kbase.attributes.title'); } + /** + * Given a unique cell id, this attempts to find and return associated the Jupyter + * notebook cell. If there is no cell with that id, or more than one cell with that id, + * this returns undefined. If and only if there's a single cell with that id, it gets returned. + * @param {string} id the unique cell id stored in the kbase.attributes.id metadata + * @returns a Jupyter notebook cell with the id, or undefined + */ function findById(id) { const matchingCells = Jupyter.notebook.get_cells().filter((cell) => { - if (cell.metadata && cell.metadata.kbase && cell.metadata.kbase.attributes) { - return cell.metadata.kbase.attributes.id === id; - } - return false; + return id === getCellMeta(cell, 'kbase.attributes.id'); }); if (matchingCells.length === 1) { return matchingCells[0]; @@ -73,15 +175,14 @@ define(['kb_common/html', 'kb_common/format', 'common/props', 'base/js/namespace if (matchingCells.length > 1) { console.warn('Too many cells matched the given id: ' + id); } - return null; } return { - createMeta: createMeta, - getMeta: getMeta, - setMeta: setMeta, - pushMeta: pushMeta, - getTitle: getTitle, - findById: findById, + getMeta, + setMeta, + getCellMeta, + setCellMeta, + getTitle, + findById, }; }); diff --git a/kbase-extension/static/kbase/js/common/clock.js b/kbase-extension/static/kbase/js/common/clock.js index ac63393847..ffd21401a1 100644 --- a/kbase-extension/static/kbase/js/common/clock.js +++ b/kbase-extension/static/kbase/js/common/clock.js @@ -1,9 +1,9 @@ define([], () => { + 'use strict'; function factory(config) { - let timer, - ticks, - resolution = config.resolution, - bus = config.bus; + let timer, ticks; + const { resolution } = config, + { bus } = config; function start() { ticks = 0; @@ -24,8 +24,8 @@ define([], () => { } return { - start: start, - stop: stop, + start, + stop, }; } diff --git a/kbase-extension/static/kbase/js/common/dialogMessages.js b/kbase-extension/static/kbase/js/common/dialogMessages.js new file mode 100644 index 0000000000..2770c267cc --- /dev/null +++ b/kbase-extension/static/kbase/js/common/dialogMessages.js @@ -0,0 +1,149 @@ +define(['common/html', 'common/jobs', 'common/ui', 'util/string'], (html, Jobs, UI, String) => { + 'use strict'; + + const div = html.tag('div'), + p = html.tag('p'), + blockquote = html.tag('blockquote'); + + const outputObjectsBlurb = + 'Any output objects already created will remain in your narrative and can be removed from the Data panel.'; + + const messages = { + deleteCell: { + title: 'Confirm cell deletion', + body: div([ + p([ + 'Deleting this cell will cancel any running or queued jobs, as well as deleting all input parameters and other configuration data for the cell. ', + ]), + p(outputObjectsBlurb), + blockquote([ + 'Note: It is not possible to "undo" the deletion of a cell, ', + 'but if the Narrative has not been saved you can refresh the browser window ', + 'to load the Narrative from its previous state.', + ]), + p('Continue to delete the cell?'), + ]), + }, + cancelBulkImport: { + title: 'Cancel batch job?', + body: div([ + p([ + 'Canceling the job will halt the processing of all jobs in the batch. ', + outputObjectsBlurb, + ]), + p('Cancel the running batch job?'), + ]), + }, + cancelApp: { + title: 'Cancel job?', + body: div([ + p(['Canceling the job will halt the job processing. ', outputObjectsBlurb]), + p('Cancel the running job?'), + ]), + }, + appReset: { + // in case of general error + title: 'Reset app?', + body: div([ + p( + 'This action will clear any parameters, results, and logs, and re-enable the Configure tab for editing.' + ), + p('Reset the app and resume editing?'), + ]), + }, + appRerun: { + title: 'Reset and resume editing?', + body: div([ + p( + 'This action will clear the results and re-enable the Configure tab for editing. You may then change inputs and run the app again.' + ), + p( + 'Any output you have already produced will be left intact in the Narrative and Data Panel' + ), + p('Reset the app and resume editing?'), + ]), + }, + }; + + /** + * @param {object} args with keys + * {string} actionString - what action is to occur (cancel or retry) + * {array} statusList - array of statuses that the action applies to + * {array} jobList - array of job objects + * (only used to get the number of jobs) + * @returns {object} arguments to create a modal to confirm or cancel the action + */ + function generateCancelRetryDialogArgs(args) { + const { actionString, statusList, jobList } = args; + const statusSet = new Set(statusList); + + // for presentation, created and estimating jobs are listed as 'queued' + ['created', 'estimating'].forEach((status) => { + if (statusSet.has(status)) { + statusSet.delete(status); + statusSet.add('queued'); + } + }); + + const jobLabelString = String.arrayToEnglish( + Array.from(statusSet.keys()) + .sort() + .map((status) => Jobs.jobLabel({ status })) + ); + const jobString = jobList.length === 1 ? '1 job' : jobList.length + ' jobs'; + const ucfirstAction = String.capitalize(actionString); + const synonym = { + cancel: 'terminate the processing of', + retry: 'rerun', + }; + return { + title: `${ucfirstAction} ${jobLabelString} jobs`, + body: div([ + p([ + `${ucfirstAction}ing all ${jobLabelString} jobs will ${synonym[actionString]} ${jobString}. `, + outputObjectsBlurb, + ]), + statusList.includes('error') + ? p( + 'Please note that jobs are rerun using the same parameters. Any jobs that failed due to issues with the input, such as misconfigured parameters or corrupted input data, are likely to throw the same errors when run again.' + ) + : '', + p(`${ucfirstAction} all ${jobLabelString} jobs?`), + ]), + }; + } + + /** + * @param {object} args with keys + * {string} action - the action to generate the message for + * @returns {object} arguments to create a modal to confirm or cancel the action + */ + + function generateDialogArgs(args = {}) { + const { action } = args; + + if (action === 'retryJobs' || action === 'cancelJobs') { + // remove the 'Jobs' + args.actionString = action.substring(0, action.indexOf('J')); + return generateCancelRetryDialogArgs(args); + } + if (action in messages) { + return messages[action]; + } + throw new Error(`Cannot generate dialog args for invalid action "${action}"`); + } + + /** + * + * @param {object} args - see generateDialogArgs for arguments + * @returns {Promise} that resolves to either true or false, depending on whether the user clicked cancel or OK + */ + function showDialog(args) { + return UI.showConfirmDialog(generateDialogArgs(args)); + } + + return { + generateDialogArgs, + showDialog, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/dom.js b/kbase-extension/static/kbase/js/common/dom.js deleted file mode 100644 index 6267c3bdec..0000000000 --- a/kbase-extension/static/kbase/js/common/dom.js +++ /dev/null @@ -1,306 +0,0 @@ -define(['kb_common/html'], (html) => { - 'use strict'; - const t = html.tag, - div = t('div'), - span = t('span'), - button = t('button'); - - function factory(config) { - const container = config.node, - bus = config.bus; - - /* - * Just a wrapper around querySelector - */ - function getElement(names) { - if (typeof names === 'string') { - names = names.split('.'); - } - const selector = names - .map((name) => { - return '[data-element="' + name + '"]'; - }) - .join(' '); - - return container.querySelector(selector); - } - - function getButton(name) { - if (typeof name !== 'string') { - // names = names.split('.'); - // TODO: support a path of elements up to the button. - throw new Error('Currently only a single string supported to get a button'); - } - const selector = '[data-button="' + name + '"]', - buttonNode = container.querySelector(selector); - - if (!buttonNode) { - throw new Error('Button ' + name + ' not found'); - } - return buttonNode; - } - - function getNode(names) { - if (typeof names === 'string') { - names = [names]; - } - const selector = names - .map((dataSelector) => { - return '[data-' + dataSelector.type + '="' + dataSelector.name + '"]'; - }) - .join(' '); - - return container.querySelector(selector); - } - - function confirmDialog(prompt) { - return window.confirm(prompt); - } - - function addButtonClickEvent(events, eventName) { - return events.addEvent({ - type: 'click', - handler: function (e) { - bus.send({ event: e }, { key: { type: eventName } }); - }, - }); - } - - function makeButton(label, name, options) { - const klass = options.type || 'default', - events = options.events; - return button( - { - type: 'button', - class: ['btn', 'btn-' + klass].join(' '), - dataButton: name, - id: addButtonClickEvent(events, name), - }, - label - ); - } - - function enableButton(name) { - getButton(name).classList.remove('disabled'); - } - - function disableButton(name) { - getButton(name).classList.add('disabled'); - } - - function setButtonLabel(name, label) { - getButton(name).innerHTML = label; - } - - // Hmm, something like this, but need to think it through more. - // function setButton(name, options) { - // var buttonNode = getButton(name); - // if (options.label) { - // buttonNode.innerHTML = options.label; - // } - // if (options.classes) { - // // who no classList.empty()? - // options.className = null; - // options.classes.forEach(function (klass) { - // buttonNode.classList.add(klass); - // }); - // } - // - // } - - function ensureOriginalDisplayStyle(el) { - if (el.getAttribute('data-original-display-style') === null) { - el.setAttribute('data-original-display-style', el.style.display); - } - } - - function hideElement(name) { - const el = getElement(name); - if (!el) { - return; - } - //ensureOriginalDisplayStyle(el); - //el.style.display = 'none'; - el.classList.add('hidden'); - } - - function showElement(name) { - let el = getElement(name), - original; - if (!el) { - return; - } - //original = el.getAttribute('data-original-display-style'); - //el.style.display = original; - el.classList.remove('hidden'); - } - - function makePanel(title, elementName) { - return div({ class: 'panel panel-primary' }, [ - div({ class: 'panel-heading' }, [div({ class: 'panel-title' }, title)]), - div({ class: 'panel-body' }, [ - div({ dataElement: elementName, class: 'container-fluid' }), - ]), - ]); - } - - function buildPanel(args) { - const type = args.type || 'primary', - classes = ['panel', 'panel-' + type]; - if (args.hidden) { - classes.push('hidden'); - // style.display = 'none'; - } - return div({ class: classes.join(' '), dataElement: args.name }, [ - (function () { - if (args.title) { - return div({ class: 'panel-heading' }, [ - div({ class: 'panel-title' }, args.title), - ]); - } - })(), - div({ class: 'panel-body' }, [args.body]), - ]); - } - - function makeCollapsiblePanel(title, elementName) { - const collapseId = html.genId(); - - return div({ class: 'panel panel-default' }, [ - div({ class: 'panel-heading' }, [ - div( - { class: 'panel-title' }, - span( - { - class: 'collapsed', - dataToggle: 'collapse', - dataTarget: '#' + collapseId, - style: { cursor: 'pointer' }, - }, - title - ) - ), - ]), - div( - { id: collapseId, class: 'panel-collapse collapse' }, - div({ class: 'panel-body' }, [ - div({ dataElement: elementName, class: 'container-fluid' }), - ]) - ), - ]); - } - - function buildCollapsiblePanel(args) { - const collapseId = html.genId(), - type = args.type || 'primary', - classes = ['panel', 'panel-' + type], - collapseClasses = ['panel-collapse collapse'], - toggleClasses = []; - if (args.hidden) { - classes.push('hidden'); - // style.display = 'none'; - } - if (!args.collapsed) { - collapseClasses.push('in'); - } else { - toggleClasses.push('collapsed'); - } - - return div({ class: classes.join(' '), dataElement: args.name }, [ - div({ class: 'panel-heading' }, [ - div( - { class: 'panel-title' }, - span( - { - class: toggleClasses.join(' '), - dataToggle: 'collapse', - dataTarget: '#' + collapseId, - style: { cursor: 'pointer' }, - }, - args.title - ) - ), - ]), - div( - { id: collapseId, class: collapseClasses.join(' ') }, - div({ class: 'panel-body' }, [args.body]) - ), - ]); - } - - function collapsePanel(path) { - const node = getElement(path); - if (!node) { - return; - } - const collapseToggle = node.querySelector('[data-toggle="collapse"]'), - targetSelector = collapseToggle.getAttribute('data-target'), - collapseTarget = node.querySelector(targetSelector); - - collapseToggle.classList.add('collapsed'); - collapseToggle.setAttribute('aria-expanded', 'false'); - collapseTarget.classList.remove('in'); - collapseTarget.setAttribute('aria-expanded', 'false'); - } - function expandPanel(path) { - const node = getElement(path); - if (!node) { - return; - } - const collapseToggle = node.querySelector('[data-toggle="collapse"]'), - targetSelector = collapseToggle.getAttribute('data-target'), - collapseTarget = node.querySelector(targetSelector); - - collapseToggle.classList.remove('collapsed'); - collapseToggle.setAttribute('aria-expanded', 'true'); - collapseTarget.classList.add('in'); - collapseTarget.setAttribute('aria-expanded', 'true'); - } - - function createNode(markup) { - const node = document.createElement('div'); - node.innerHTML = markup; - return node.firstChild; - } - - function setContent(path, content) { - const node = getElement(path); - if (node) { - node.innerHTML = content; - } - } - - function na() { - return span({ style: { fontStyle: 'italic', color: 'orange' } }, 'NA'); - } - - return { - getElement: getElement, - getButton: getButton, - // setButton: setButton, - getNode: getNode, - makeButton: makeButton, - enableButton: enableButton, - disableButton: disableButton, - setButtonLabel: setButtonLabel, - confirmDialog: confirmDialog, - hideElement: hideElement, - showElement: showElement, - makePanel: makePanel, - buildPanel: buildPanel, - makeCollapsiblePanel: makeCollapsiblePanel, - buildCollapsiblePanel: buildCollapsiblePanel, - collapsePanel: collapsePanel, - expandPanel: expandPanel, - createNode: createNode, - setContent: setContent, - na: na, - }; - } - - return { - make: function (config) { - return factory(config); - }, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/error.js b/kbase-extension/static/kbase/js/common/error.js index ceac4eb5b6..133e61d17d 100644 --- a/kbase-extension/static/kbase/js/common/error.js +++ b/kbase-extension/static/kbase/js/common/error.js @@ -1,4 +1,4 @@ -define([], () => { +define(['common/ui', 'common/html'], (UI, html) => { 'use strict'; function KBError(arg) { @@ -73,8 +73,48 @@ define([], () => { } } + /** + * This creates and displays a little dialog that shows some error information. It + * makes 3 tabs: Summary, Details, and Stack Trace + * Summary contains the preamble and text error. + * Details contains more details based on the error message. + * Stack Trace contains the Javascript stack trace included with the Error object. + * @param {string} title the title of the error dialog (populates the header inside the + * dialog) + * @param {string} preamble the "preamble" of the error dialog. This is a + * string that overall describes what the case is that led to the error. + * @param {Error} error the error object to be rendered in the error tab. + */ + function reportCellError(title, preamble, error) { + const { tag } = html, + div = tag('div'); + + const ui = UI.make({ + node: document.body, + }); + ui.showInfoDialog({ + title: 'Error', + body: div( + { + class: 'kb-error-dialog__body', + }, + [ + ui.buildPanel({ + title: title, + type: 'danger', + body: ui.buildErrorTabs({ + preamble: preamble, + error: error, + }), + }), + ] + ), + }); + } + return { grokError: grokError, KBError: KBError, + reportCellError: reportCellError, }; }); diff --git a/kbase-extension/static/kbase/js/common/errorDisplay.js b/kbase-extension/static/kbase/js/common/errorDisplay.js new file mode 100644 index 0000000000..49c7de4b28 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/errorDisplay.js @@ -0,0 +1,335 @@ +define(['bluebird', 'common/html'], (Promise, html) => { + 'use strict'; + + const t = html.tag, + div = t('div'), + pre = t('pre'), + ul = t('ul'), + li = t('li'), + span = t('span'), + cssBaseClass = 'kb-error-display', + defaultAdvice = + 'If the app fails consistently, please contact us at ' + + 'https://www.kbase.us/support.'; + + /** + * Ingests the raw object from the model supplied when initialising the module, or + * passed in directly. Note that jobState objects containing errors should be passed in + * in the form `{ jobState: }` so that the function knows it is a + * jobState without going through the hassle of creating a proper object system. + * + * @param {object} rawObject + * @returns {object} normalised error object + */ + + function normaliseErrorObject(rawObject) { + if (rawObject.exec && rawObject.exec.jobState) { + rawObject = { jobState: rawObject.exec.jobState }; + } + + let errorObject; + if (rawObject.jobState && (rawObject.jobState.error || rawObject.jobState.errormsg)) { + errorObject = convertJobError(rawObject.jobState); + } else if (rawObject.internalError) { + errorObject = convertInternalError(rawObject.internalError); + } else if (rawObject.appError) { + errorObject = convertAppError(rawObject.appError); + } + + if (!errorObject) { + // unknown error format + return convertUnknownError(rawObject); + } + return errorObject; + } + + function defaultError() { + return { + location: 'unknown', + type: 'unknown', + message: 'An unknown error was detected.', + advice: [defaultAdvice], + }; + } + + /** + * + * @param {Object} inputErrorObject can render attributes: + * detail - str, + * errorDump - JSON, + * stacktrace - preformatted text str, + * type - str, + * message - str, + * advice - array of str, made into bullet points + * @returns + */ + function renderErrorLayout(inputErrorObject) { + const errorObject = Object.assign(defaultError(), inputErrorObject); + + const uniqueID = html.genId(); + const elements = [ + div( + { + class: `${cssBaseClass}__summary`, + }, + [ + span( + { + class: `${cssBaseClass}__type`, + }, + [errorObject.type] + ), + ': ', + span( + { + class: `${cssBaseClass}__message`, + }, + [errorObject.message] + ), + ] + ), + + div( + { + class: `${cssBaseClass}__advice`, + }, + ul( + { class: `${cssBaseClass}__advice_list` }, + errorObject.advice.map((adv) => { + return li( + { + class: `${cssBaseClass}__advice_list_item`, + }, + adv + ); + }) + ) + ), + + errorObject.detail + ? div( + { + class: `${cssBaseClass}__detail_container`, + }, + [ + div( + { + class: `${cssBaseClass}__detail_title`, + }, + ['Details'] + ), + div( + { + class: `${cssBaseClass}__detail_text`, + }, + [errorObject.detail] + ), + ] + ) + : '', + + errorObject.stacktrace + ? div( + { + class: `${cssBaseClass}__stacktrace_container`, + }, + [ + div( + { + class: `${cssBaseClass}__stacktrace_title collapsed`, + role: 'button', + dataToggle: 'collapse', + dataTarget: `#${uniqueID}__trace`, + ariaExpanded: 'false', + ariaControls: `${uniqueID}__trace`, + }, + [span({}, ['Error stacktrace'])] + ), + pre( + { + class: `${cssBaseClass}__stacktrace_code collapse`, + id: `${uniqueID}__trace`, + }, + [errorObject.stacktrace] + ), + ] + ) + : '', + + errorObject.errorDump + ? div( + { + class: `${cssBaseClass}__error_dump_container`, + }, + [ + div( + { + class: `${cssBaseClass}__error_dump_title collapsed`, + role: 'button', + dataToggle: 'collapse', + dataTarget: `#${uniqueID}__dump`, + ariaExpanded: 'false', + ariaControls: `${uniqueID}__dump`, + }, + [span({}, ['Raw error JSON'])] + ), + pre( + { + class: `${cssBaseClass}__error_dump_code collapse`, + id: `${uniqueID}__dump`, + }, + [JSON.stringify(errorObject.errorDump, null, 1)] + ), + ] + ) + : '', + ]; + return elements.join('\n'); + } + + function convertUnknownError(rawError) { + return { + type: 'Unknown error', + message: 'error in unknown format', + errorDump: rawError, + }; + } + + function convertInternalError(rawError) { + return Object.assign(defaultError(), { + location: 'app cell', + type: rawError.title, + message: rawError.message, + advice: rawError.advice, + detail: rawError.detail, + }); + } + + /** + * + * @param {Object} rawError - can contain keys: + * type: str, + * message: str, + * stacktract: str, + * code: int or str, + * source: str, + * method: str, + * exceptionType: str + * @returns + */ + function convertAppError(rawError) { + return Object.assign(defaultError(), { + location: 'app manager', + type: rawError.type, + message: rawError.message, + stacktrace: rawError.stacktrace, + errorDump: rawError, + }); + } + + /** + * Convert job execution errors for display in the UI. The errors are part of the jobState + * object, and can be in several formats; see inline docs for info. + * + * @param {object} jobState + * @returns {object} normalised error object for the UI + */ + function convertJobError(jobState) { + const errorObj = { + location: 'job execution', + detail: 'This error occurred during execution of the app job.', + }; + + if (jobState.error) { + if (jobState.error.error) { + /** KBase RPC error message, in the format + * jobState.error = { + * name: , + * message: , + * error: + * } + */ + errorObj.type = jobState.error.name; + errorObj.message = jobState.error.message; + errorObj.stacktrace = jobState.error.error; + delete errorObj.detail; + } else if (jobState.error.name) { + /** + * jobState.error = { + * name: , + * code: , + * } + */ + errorObj.type = jobState.error.name; + errorObj.message = 'Error code: ' + String(jobState.error.code); + } + + if (!errorObj.message) { + jobState.error.location = 'job execution'; + + return convertUnknownError(jobState.error); + } + } else if (jobState.errormsg) { + /** + * Error with no separate 'error' object; keys are part of the jobState object: + * + * jobState.error_code = + * jobState.errormsg = + */ + errorObj.message = jobState.errormsg; + errorObj.type = 'Error code ' + String(jobState.error_code); + } + + return errorObj; + } + + function factory(config) { + // unless config is an object with a 'model' attribute, + // which has functions 'getItem' and 'hasItem', + // throw an error + if ( + !( + config && + typeof config === 'object' && + Object.prototype.hasOwnProperty.call(config, 'model') && + Object.prototype.hasOwnProperty.call(config.model, 'getItem') && + Object.prototype.hasOwnProperty.call(config.model, 'hasItem') + ) + ) { + throw new Error(`Invalid input for the ErrorDisplay module: ${JSON.stringify(config)}`); + } + + const { model } = config; + let container; + + function start(arg) { + return Promise.try(() => { + const rawObject = model.getRawObject(); + container = arg.node; + container.classList.add(`${cssBaseClass}__container`); + container.innerHTML = renderErrorLayout(normaliseErrorObject(rawObject)); + }); + } + + function stop() { + return Promise.try(() => { + container.innerHTML = ''; + container.classList.remove(`${cssBaseClass}__container`); + }); + } + + return { + start: start, + stop: stop, + }; + } + + return { + make: function (config) { + return factory(config); + }, + defaultAdvice, + cssBaseClass, + normaliseErrorObject, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/events.js b/kbase-extension/static/kbase/js/common/events.js index 3aecdb3c8a..48f08e0da7 100644 --- a/kbase-extension/static/kbase/js/common/events.js +++ b/kbase-extension/static/kbase/js/common/events.js @@ -1,10 +1,11 @@ -define(['jquery', 'kb_common/html'], ($, html) => { +define(['jquery', 'common/html'], ($, html) => { 'use strict'; - function factory(config) { - var events = [], - config = config || {}, + function factory(args) { + let events = []; + const config = args || {}, globalRoot = config.node; + function addEvent(event) { let selector, id; if (event.id) { @@ -27,6 +28,7 @@ define(['jquery', 'kb_common/html'], ($, html) => { }); return id; } + function addEvents(newEvents) { let selector, id; if (newEvents.id) { @@ -50,6 +52,7 @@ define(['jquery', 'kb_common/html'], ($, html) => { }); return id; } + function attachEvents(eventsRoot) { const root = globalRoot || eventsRoot; events.forEach((event) => { diff --git a/kbase-extension/static/kbase/js/common/fsm.js b/kbase-extension/static/kbase/js/common/fsm.js index 1929be3ca9..a96b278787 100644 --- a/kbase-extension/static/kbase/js/common/fsm.js +++ b/kbase-extension/static/kbase/js/common/fsm.js @@ -5,17 +5,14 @@ * */ -define(['./unodep', 'common/runtime'], (utils, Runtime) => { +define(['underscore', 'common/runtime'], (_, Runtime) => { 'use strict'; function factory(config) { - let allStates = config.states, + const allStates = config.states, initialState = config.initialState, - fallbackState = config.fallbackState, - currentState, - api, - timer, newStateHandler = config.onNewState; + let currentState, timer; const runtime = Runtime.make(); @@ -24,14 +21,6 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { const busConnection = runtime.bus().connect(), bus = busConnection.channel(null); - /* - * Validate the state machine configuration 'states'. - */ - function validate() { - // find initial state - // ... - } - function run() { if (!newStateHandler) { return; @@ -44,14 +33,14 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { timer = null; newStateHandler(api); } catch (ex) { - console.error('ERROR in fms newStateHandler', ex); + console.error('ERROR in FSM newStateHandler', ex); } }, 0); } function findState(stateToFind) { const foundStates = allStates.filter((stateDef) => { - return utils.isEqual(stateToFind, stateDef.state); + return _.isEqual(stateToFind, stateDef.state); }); if (foundStates.length === 1) { return foundStates[0]; @@ -64,16 +53,14 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { function doMessages(changeType) { const state = currentState; - if (state.on && state.on[changeType]) { - if (state.on[changeType].messages) { - state.on[changeType].messages.forEach((msg) => { - if (msg.emit) { - bus.emit(msg.emit, msg.message); - } else if (msg.send) { - bus.send(msg.send.message, msg.send.address); - } - }); - } + if (state.on && state.on[changeType] && state.on[changeType].messages) { + state.on[changeType].messages.forEach((msg) => { + if (msg.emit) { + bus.emit(msg.emit, msg.message); + } else if (msg.send) { + bus.send(msg.send.message, msg.send.address); + } + }); } } @@ -81,17 +68,9 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { doMessages('resume'); } - function doEnterState() { - doMessages('enter'); - } - - function doLeaveState() { - doMessages('leave'); - } - function findNextState(stateList, stateToFind) { const foundStates = stateList.filter((state) => { - if (utils.isEqual(state, stateToFind)) { + if (_.isEqual(state, stateToFind)) { return true; } }); @@ -115,19 +94,19 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { throw new Error('Cannot find the new state'); } - const newState = findState(state); - if (!newState) { + const _newState = findState(state); + if (!_newState) { throw new Error('Next state found, but that state does not exist'); } - if (utils.isEqual(newState.state, currentState.state)) { + if (_.isEqual(_newState.state, currentState.state)) { return; } doMessages('exit'); // make it the current state - currentState = newState; + currentState = _newState; doMessages('enter'); @@ -169,14 +148,14 @@ define(['./unodep', 'common/runtime'], (utils, Runtime) => { // API - api = Object.freeze({ - start: start, - stop: stop, - newState: newState, - updateState: updateState, - getCurrentState: getCurrentState, - findState: findState, - bus: bus, + const api = Object.freeze({ + start, + stop, + newState, + updateState, + getCurrentState, + findState, + bus, }); return api; diff --git a/kbase-extension/static/kbase/js/common/html.js b/kbase-extension/static/kbase/js/common/html.js index 935b24c7e5..3361851ac9 100644 --- a/kbase-extension/static/kbase/js/common/html.js +++ b/kbase-extension/static/kbase/js/common/html.js @@ -29,6 +29,7 @@ define(['uuid'], (Uuid) => { return '-' + m.toLowerCase(); }); } + function makeStyleAttribs(attribs) { return Object.keys(attribs) .map((rawKey) => { @@ -42,9 +43,7 @@ define(['uuid'], (Uuid) => { case 'number': return String(rawValue); case 'boolean': - return false; case 'undefined': - return false; case 'object': return false; } @@ -130,7 +129,6 @@ define(['uuid'], (Uuid) => { case 'number': return String(children); case 'boolean': - return ''; case 'undefined': return ''; case 'object': @@ -140,11 +138,8 @@ define(['uuid'], (Uuid) => { return renderContent(item); }) .join(''); - } else if (children === null) { - return ''; - } else { - return ''; } + return ''; } } @@ -212,14 +207,6 @@ define(['uuid'], (Uuid) => { return 'kb_html_' + new Uuid(4).format(); } - function makePanel(arg) { - const klass = arg.class || 'default'; - return div({ class: 'panel panel-' + klass }, [ - div({ class: 'panel-heading' }, [span({ class: 'panel-title' }, arg.title)]), - div({ class: 'panel-body' }, [arg.content]), - ]); - } - function loading(msg) { const prompt = msg ? `${msg}   ` : ''; return span([prompt, i({ class: 'fa fa-spinner fa-pulse fa-2x fa-fw margin-bottom' })]); @@ -235,14 +222,6 @@ define(['uuid'], (Uuid) => { * @param {type} arg * @returns {unresolved} */ - function reverse(arr) { - const newArray = []; - for (let index = arr.length - 1; index >= 0; index -= 1) { - newArray.push(arr[index]); - } - return newArray; - } - function makeTabs(arg) { const tabsId = arg.id, tabsAttribs = {}, @@ -261,7 +240,7 @@ define(['uuid'], (Uuid) => { tab.id = genId(); }); if (arg.alignRight) { - tabTabs = reverse(tabs); + tabTabs = tabs.slice().reverse(); tabStyle.float = 'right'; activeIndex = tabs.length - 1; } else { @@ -318,7 +297,6 @@ define(['uuid'], (Uuid) => { genId, isSimpleObject, loading, - makePanel, makeTabs, merge, tag, diff --git a/kbase-extension/static/kbase/js/common/iframe/boot.js b/kbase-extension/static/kbase/js/common/iframe/boot.js index 0dc89dbb14..d4e0ac64a8 100644 --- a/kbase-extension/static/kbase/js/common/iframe/boot.js +++ b/kbase-extension/static/kbase/js/common/iframe/boot.js @@ -1,4 +1,5 @@ -define(['messages', 'heightNotifier'], (Messages, HeightNotifier) => { +define(['./messages', './heightNotifier'], (Messages, HeightNotifier) => { + 'use strict'; function getParams(id) { if (!document.getElementById(id).hasAttribute('data-params')) { console.warn( diff --git a/kbase-extension/static/kbase/js/common/jobCommChannel.js b/kbase-extension/static/kbase/js/common/jobCommChannel.js new file mode 100644 index 0000000000..f9519aa736 --- /dev/null +++ b/kbase-extension/static/kbase/js/common/jobCommChannel.js @@ -0,0 +1,617 @@ +/** + * This is the Communication Channel that handles talking between the front end and the kernel. + * It's used by creating a new JobCommChannel(), then running initCommChannel() on it. + * + * i.e. + * let channel = new JobCommChannel(); + * channel.initCommChannel(); + * + * This creates a handle on a Jupyter Kernel Comm channel object and holds it. + * Note that this should only be run once the Kernel is ready (see kbaseNarrative.js for + * implementation). + * + * Communication can flow in two directions. From the front end to the back, that goes + * like this: + * 1. An App Cell (or other controller) sends a message over the global bus with some + * request. + * 2. This gets captured by one of the callbacks in handleBusMessages() + * 3. These all invoke sendCommMessage(). This builds a message packet and sends it across the + * comm channel where it gets heard by a capturing function in biokbase.narrative.jobmanager. + * ...that's it. These messages are asynchronous by design. They're meant to be requests that + * get responses eventually. + * + * From the back end to the front, the flow is slightly different. On Comm channel creation time, + * the handleCommMessages function is set up as the callback for anything that comes across the + * channel to the front end. Each of these has a message type associated with it. + * 1. The type is interepreted (a big ol' switch statement) and responded to. + * 2. Most responses (job status updates, log messages) are parceled out to the App Cells that + * invoked them. Or, really, anything listening on the appropriate channel (either the cell, + * or the job id) + */ +define([ + 'bluebird', + 'jquery', + 'handlebars', + 'kbaseAccordion', + 'util/bootstrapDialog', + 'util/developerMode', + 'base/js/namespace', + 'common/runtime', + 'services/kernels/comm', + 'common/semaphore', + 'text!kbase/templates/job_panel/job_init_error.html', +], ( + Promise, + $, + Handlebars, + kbaseAccordion, + BootstrapDialog, + devMode, + Jupyter, + Runtime, + JupyterComm, + Semaphore, + JobInitErrorTemplate +) => { + 'use strict'; + + const COMM_NAME = 'KBaseJobs', + CELL = 'cell', + JOB = 'jobId', + RESULT = 'result', + // messages to be sent to backend for job request types + JOB_REQUESTS = { + CANCEL: 'cancel_job', + INFO: 'job_info', + LOGS: 'job_logs', + RETRY: 'retry_job', + STATUS: 'job_status', + START_UPDATE: 'start_job_update', + STOP_UPDATE: 'stop_job_update', + }, + BACKEND_RESPONSES = { + INFO: JOB_REQUESTS.INFO, + LOGS: JOB_REQUESTS.LOGS, + RESULT: RESULT, + RETRY: 'jobs_retried', + RUN_STATUS: 'run_status', + STATUS: JOB_REQUESTS.STATUS, + }, + RESPONSES = { + CELL_JOB_STATUS: 'cell-job-status', + ERROR: 'job-error', + INFO: 'job-info', + LOGS: 'job-logs', + RESULT: RESULT, + RETRY: 'job-retry-response', + RUN_STATUS: 'run-status', + STATUS: 'job-status', + }, + // these job request types also have a 'batch' version + batchRequests = ['INFO', 'STATUS', 'START_UPDATE', 'STOP_UPDATE'], + BATCH_JOB_REQUESTS = {}; + // the batch version of JOB_REQUESTS[type] + batchRequests.forEach((type) => { + BATCH_JOB_REQUESTS[JOB_REQUESTS[type]] = JOB_REQUESTS[type] + '_batch'; + }); + + // Conversion of the message type of an incoming message + // to the type used for the message to be sent to the backend + const requestTranslation = { + 'ping-comm-channel': 'ping', + + // cancels the job + 'request-job-cancel': JOB_REQUESTS.CANCEL, + + // Fetches info (not state) about a job, including the app id, name, and inputs. + 'request-job-info': JOB_REQUESTS.INFO, + + // Fetches job logs from kernel. + 'request-job-log': JOB_REQUESTS.LOGS, + + // retries the job + 'request-job-retry': JOB_REQUESTS.RETRY, + + // Fetches job status from kernel. + 'request-job-status': JOB_REQUESTS.STATUS, + + // Requests job status updates for this job via the job channel, and also + // ensures that job polling is running. + 'request-job-updates-start': JOB_REQUESTS.START_UPDATE, + // Tells kernel to stop including a job in the lookup loop. + 'request-job-updates-stop': JOB_REQUESTS.STOP_UPDATE, + }; + + const JobCommMessages = { + validIncomingMessageTypes: function () { + return Object.keys(requestTranslation); + }, + validOutgoingMessageTypes: function () { + return Object.values(RESPONSES); + }, + }; + + class JobCommChannel { + /** + * Grabs the runtime, inits the set of job states, and registers callbacks against the + * main Bus. + */ + constructor(config = {}) { + this.runtime = Runtime.make(); + this.handleBusMessages(); + this.devMode = config.devMode || devMode.mode; + this.debug = this.devMode + ? (...args) => { + // eslint-disable-next-line no-console + console.log(...args); + } + : () => { + /* no op */ + }; + + this.messageQueue = []; + this.ERROR_COMM_CHANNEL_NOT_INIT = 'Comm channel not initialized, not sending message.'; + } + + /** + * Sends a message over the bus. The channel should have a single key of either + * cell or jobId. + * @param {string} channelName - either CELL or JOB + * @param {string} channelId - id for the channel + * @param {string} msgType - one of the msg types + * @param {any} message + */ + sendBusMessage(channelName, channelId, msgType, message) { + const channel = {}; + channel[channelName] = channelId; + this.runtime.bus().send(JSON.parse(JSON.stringify(message)), { + channel, + key: { + type: msgType, + }, + }); + this.debug(`sending bus message: ${channelName} ${channelId} ${msgType}`); + } + + /** + * Registers callbacks for handling bus messages. This listens to the global runtime bus. + * Mostly, it relays bus messages into comm channel requests that are satisfied by the + * kernel. + */ + handleBusMessages() { + const bus = this.runtime.bus(); + + Object.keys(requestTranslation).forEach((msgType) => { + bus.on(msgType, (msgData) => { + this.sendCommMessage(msgType, msgData); + }); + }); + } + + /** + * transform a message received from the frontend to the format + * expected by the backend + * @param {object} rawMessage with key/values: + * + * {string} msgType + * {object} msgData + */ + _transformMessage(rawMessage) { + const { msgType, msgData } = rawMessage; + let transformedMsg = { + target_name: COMM_NAME, + request_type: requestTranslation[msgType], + }; + + const translations = { + // backend uses `job_id` instead of `batch_id` + batchId: 'job_id', + jobId: 'job_id', + jobIdList: 'job_id_list', + pingId: 'ping_id', + }; + + for (const [key, value] of Object.entries(msgData)) { + if (key !== 'options') { + const msgKey = translations[key] || key; + transformedMsg[msgKey] = value; + } + // convert to the batch form of the request + if (key === 'batchId' && BATCH_JOB_REQUESTS[transformedMsg.request_type]) { + transformedMsg.request_type = BATCH_JOB_REQUESTS[transformedMsg.request_type]; + } + } + + if (msgData.options) { + transformedMsg = Object.assign({}, transformedMsg, msgData.options); + } + + return transformedMsg; + } + + /** + * Sends the messages in this.messageQueue to the JobManager in the kernel. + * If there's no comm channel ready, the message will be added to the + * message queue. + * + * If no arguments are supplied to sendCommMessage, the stored messages (if any) + * will be sent. + * + * @param {string} msgType - message type; will be one of the + * keys in the requestTranslation object (optional) + * + * @param {object} msgData - additional parameters for the request, + * such as jobId or jobIdList, or an 'options' object (optional) + */ + sendCommMessage(msgType, msgData) { + if (msgType && msgData) { + this.messageQueue.push({ msgType, msgData }); + } + return this._sendCommMessages(); + } + + /** + * Sends the messages in this.messageQueue to the JobManager in the kernel. + * If there's no comm channel ready, the message will be added to the + * message queue. + * + * If no arguments are supplied to sendCommMessage, the stored messages (if any) + * will be sent. + */ + + _sendCommMessages() { + let topMessage; + return Promise.try(() => { + if (!this.comm) { + // TODO: try to init comm channel here + throw new Error('Comm channel not initialized, not sending message.'); + } + while (this.messageQueue.length) { + topMessage = this.messageQueue.shift(); + this.comm.send(this._transformMessage(topMessage)); + this.debug(`sending comm message: ${COMM_NAME} ${topMessage.msgType}`); + } + }).catch((err) => { + console.error('ERROR sending comm message', err.message, err, topMessage); + throw new Error('ERROR sending comm message: ' + err.message); + }); + } + + convertJobState(backendJobState) { + const output = {}; + const translateBEtoFE = { + state: 'jobState', + widget_info: 'outputWidgetInfo', + cell_id: 'cellId', + }; + + for (const key in translateBEtoFE) { + if (backendJobState[key]) { + output[translateBEtoFE[key]] = backendJobState[key]; + } + } + + if (output.jobState) { + output.jobId = output.jobState.job_id; + } + return output; + } + + /** + * Callback attached to the comm channel. This gets called with the message when + * a message is passed. + * The message is expected to have the following structure (at a minimum): + * { + * content: { + * data: { + * msg_type: string, + * content: object + * } + * } + * } + * Where msg_type is one of: + * start, new_job, job_status, job_status_all, job_comm_err, job_init_err, job_init_lookup_err, + * or any of the values of BACKEND_RESPONSES + * + * @param {object} msg + */ + handleCommMessages(msg) { + const msgType = msg.content.data.msg_type; + const msgData = msg.content.data.content; + let msgTypeToSend = null; + this.debug(`received ${msgType} from backend`); + switch (msgType) { + case 'start': + break; + + case 'new_job': + Jupyter.notebook.save_checkpoint(); + break; + + // CELL messages + case BACKEND_RESPONSES.RUN_STATUS: + this.sendBusMessage(CELL, msgData.cell_id, RESPONSES.RUN_STATUS, msgData); + break; + + case BACKEND_RESPONSES.RESULT: + this.sendBusMessage(CELL, msgData.address.cell_id, RESPONSES.RESULT, msgData); + break; + + // JOB messages + // Send job-related notifications on the default channel, + // with a key on the message type and the job id. + // This allows widgets which are interested in the job + // to subscribe to just that job, and nothing else. + // If there is a need for a generic broadcast message, we + // can either send a second message or implement key + // filtering. + // + // errors + case 'job_comm_error': + console.error('Error from job comm:', msg); + if (!msgData) { + break; + } + // treat messages relating to single jobs as if they were for a job list + // eslint-disable-next-line no-case-declarations + const jobIdList = msgData.job_id ? [msgData.job_id] : msgData.job_id_list; + if (msgData.source === JOB_REQUESTS.LOGS) { + msgTypeToSend = RESPONSES.LOGS; + } else { + msgTypeToSend = 'job-error'; + } + + jobIdList.forEach((_jobId) => { + this.sendBusMessage(JOB, _jobId, msgTypeToSend, { + jobId: _jobId, + error: msgData, + request: msgData.source, + }); + }); + break; + + case 'job_init_err': + case 'job_init_lookup_err': + this.displayJobError(msgData); + console.error('Error from job comm:', msg); + break; + + // job information for one or more jobs + // Object with keys jobId and values { jobId: jobId, jobInfo: { ...job params... } } + case BACKEND_RESPONSES.INFO: + Object.keys(msgData).forEach((_jobId) => { + this.sendBusMessage(JOB, msgData[_jobId].job_id, RESPONSES.INFO, { + jobId: msgData[_jobId].job_id, + jobInfo: msgData[_jobId], + }); + }); + break; + + case BACKEND_RESPONSES.LOGS: + this.sendBusMessage(JOB, msgData.job_id, RESPONSES.LOGS, { + jobId: msgData.job_id, + logs: msgData, + latest: msgData.latest, + }); + break; + + case BACKEND_RESPONSES.RETRY: + msgData.forEach((jobRetried) => { + const output = { + job: this.convertJobState(jobRetried.job), + }; + if (jobRetried.error) { + output.error = jobRetried.error; + } + if (jobRetried.retry) { + output.retry = this.convertJobState(jobRetried.retry); + } + + this.sendBusMessage( + JOB, + jobRetried.job.state.job_id, + RESPONSES.RETRY, + output + ); + }); + break; + + /* + * The job status for one or more jobs. + * The job_status_all message covers all active jobs. + * + * data structure: object with key jobId and value + * { jobState: job.state, outputWidgetInfo: job.widget_info } + */ + case BACKEND_RESPONSES.STATUS: + case 'job_status_all': + Object.keys(msgData).forEach((_jobId) => { + // check whether or not this is an ee2 error + if (msgData[_jobId].state.status === 'ee2_error') { + this.sendBusMessage(JOB, _jobId, RESPONSES.ERROR, { + jobId: _jobId, + error: { + job_id: _jobId, + message: 'ee2 connection error', + code: msgData[_jobId].state.status, + }, + request: 'job-status', + }); + } else { + this.sendBusMessage( + JOB, + _jobId, + RESPONSES.STATUS, + this.convertJobState(msgData[_jobId]) + ); + } + }); + break; + + default: + console.warn( + `Unhandled KBaseJobs message from kernel (type='${msgType}'):`, + msg + ); + } + } + + displayJobError(msgData) { + // code, error, job_id (opt), message, name, source + const $modalBody = $(Handlebars.compile(JobInitErrorTemplate)(msgData)); + const modal = new BootstrapDialog({ + title: 'Job Initialization Error', + body: $modalBody, + buttons: [ + $('') + .append('OK') + .click(() => { + modal.hide(); + }), + ], + }); + new kbaseAccordion($modalBody.find('div#kb-job-err-trace'), { + elements: [ + { + title: 'Detailed Error Information', + body: $( + '' + + '' + + (function () { + if (msgData.service) { + return ( + '' + ); + } + return ''; + })() + + '' + + '
code:' + + msgData.code + + '
error:' + + msgData.message + + '
service:' + + msgData.service + + '
type:' + + msgData.name + + '
source:' + + msgData.source + + '
' + ), + }, + ], + }); + + $modalBody.find('button#kb-job-err-report').click(() => { + // no action + }); + modal.getElement().on('hidden.bs.modal', () => { + modal.destroy(); + }); + modal.show(); + } + + /** + * Initializes the comm channel to the back end. Stores the generated + * channel in this.comm. + * Returns a Promise that should resolve when the channel's ready. But + * the nature of setting these up means that a nested Promise gets made by + * the Jupyter kernel front-end and not necessarily returned. + * + * Thus, it uses a semaphore lock. When the semaphore becomes ready, it + * gets signaled. + * + * Once ready, this starts a kernel call (over the main channel, not the + * new comm) to initialize the JobManager and have it fetch the set of + * running jobs from the execution engine. If it's already running, this + * overwrites everything. + */ + initCommChannel() { + const _this = this; + _this.comm = null; + const commSemaphore = Semaphore.make(); + commSemaphore.add('comm', false); + return new Promise((resolve) => { + // First we check to see if our comm channel already + // exists. If so, we do some funny business to create a + // new client side for it, register it, and set up our + // handler on it. + Jupyter.notebook.kernel.comm_info(COMM_NAME, (msg) => { + if (msg.content && msg.content.comms) { + // skim the reply for the right id + for (const id in msg.content.comms) { + if (msg.content.comms[id].target_name === COMM_NAME) { + _this.comm = new JupyterComm.Comm(COMM_NAME, id); + Jupyter.notebook.kernel.comm_manager.register_comm(_this.comm); + _this.comm.on_msg(_this.handleCommMessages.bind(_this)); + } + } + } + resolve(); + }); + }) + .then(() => { + // If no existing comm channel could be hooked up to, we have an alternative + // strategy, apparently. We register our channel endpoint, even though there is + // no back end yet, and our next call to utilize it below will create it. + if (_this.comm) { + commSemaphore.set('comm', 'ready'); + return; + } + return Promise.try(() => { + Jupyter.notebook.kernel.comm_manager.register_target(COMM_NAME, (comm) => { + _this.comm = comm; + comm.on_msg(_this.handleCommMessages.bind(_this)); + commSemaphore.set('comm', 'ready'); + }); + }); + }) + .then(() => { + return new Promise((resolve, reject) => { + const callbacks = { + shell: { + reply: function (reply) { + if (!reply.content.error) { + return resolve(); + } + console.error('ERROR executing jobInit', reply); + commSemaphore.set('comm', 'error'); + reject( + new Error(reply.content.name + ': ' + reply.content.evalue) + ); + }, + }, + }; + Jupyter.notebook.kernel.execute(_this.getJobInitCode(), callbacks); + if (_this.messageQueue.length) { + _this.sendCommMessage(); + } + }); + }); + } + + getJobInitCode() { + const currentCells = Jupyter.notebook + .get_cells() + .map((cell) => { + try { + return cell.metadata.kbase.attributes.id; + } catch (e) { + // do nothing + } + + return null; + }) + .filter((cellId) => !!cellId); + + return [ + 'from biokbase.narrative.jobs.jobcomm import JobComm', + 'cell_list = ' + JSON.stringify(currentCells), + // DATAUP-575: temporarily removing cell_list + 'JobComm().start_job_status_loop(init_jobs=True)', + ].join('\n'); + } + } + + return { JobCommChannel, JobCommMessages }; +}); diff --git a/kbase-extension/static/kbase/js/common/jobManager.js b/kbase-extension/static/kbase/js/common/jobManager.js new file mode 100644 index 0000000000..8b92bbf49b --- /dev/null +++ b/kbase-extension/static/kbase/js/common/jobManager.js @@ -0,0 +1,841 @@ +define(['common/dialogMessages', 'common/jobs', 'common/jobCommChannel'], ( + DialogMessages, + Jobs, + JobComms +) => { + 'use strict'; + + const jcm = JobComms.JobCommMessages; + + const validOutgoingMessageTypes = jcm.validOutgoingMessageTypes(); + + const jobCommand = { + cancel: { + command: 'request-job-cancel', + listener: 'job-status', + }, + retry: { + command: 'request-job-retry', + listener: 'job-retry-response', + }, + }; + + class JobManagerCore { + /** + * Initialise the job manager + * + * @param {object} config with keys + * {object} model: cell model, including job information under `exec.jobs` + * {object} bus: the bus for communicating messages to the kernel + * + * @returns {object} the initialised job manager + */ + constructor(config) { + ['bus', 'model'].forEach((key) => { + if (!config[key]) { + throw new Error( + 'cannot initialise Job Manager without params "bus" and "model"' + ); + } + this[key] = config[key]; + }); + // kbase cell ID + this.cellId = null; + if (config.cell) { + try { + this.cellId = config.cell.metadata.kbase.attributes.id; + } catch (error) { + throw new Error('cannot initialise Job Manager with invalid cell metadata'); + } + } + // handler function store + this.__handlerFns = {}; + // an object containing event handlers, categorised by event name + this.handlers = {}; + // bus listeners, indexed by job ID then message type + this.listeners = {}; + } + + _isValidEvent(event) { + return event && validOutgoingMessageTypes.concat('modelUpdate').includes(event); + } + + _isValidMessage(type, message) { + switch (type) { + case 'job-status': + return message.jobId && Jobs.isValidJobStateObject(message.jobState); + case 'job-info': + return message.jobId && Jobs.isValidJobInfoObject(message.jobInfo); + case type.indexOf('job') !== -1: + return !!message.jobId; + default: + return true; + } + } + + /** + * Add one or more handlers to the job manager; these are executed when one of the + * valid job-related events occurs (job-related message received, model updated) + * or when `runHandler` is executed. + * + * By default, the handler functions receive the jobManager context as + * first argument and any additional arguments passed in by the caller + * (e.g. the message and job ID in the case of job listeners) + * + * @param {string} event - event that triggers the handler + * @param {object|array} handlers + * object: handlers has handler name as keys and the handler function as values + * if the handler function is already installed on the job manager, + * 'null' can be supplied in place of the function + * array: handlers is a list of handler names; these are assumed to be installed + * on the job manager already + * Example: + * + * jm.addEventListener('job-status', { + * handlerA: () => {}, // this function gets added to the job manager + * handlerB: null, // this handler function is expected to exist + * }) + * // handlers assumed to already exist: + * jm.addEventListener('job-error', ['handlerA', 'handlerB']); + */ + + addEventHandler(event, handlers) { + if (!this._isValidEvent(event)) { + throw new Error(`addEventHandler: invalid event ${event} supplied`); + } + + const handlersType = Object.prototype.toString.call(handlers) || null; + + if ( + !handlers || + (handlersType !== '[object Array]' && handlersType !== '[object Object]') + ) { + throw new Error( + 'addEventHandler: invalid handlers supplied (must be of type array or object)' + ); + } + + if (handlersType === '[object Array]') { + const handlerObj = {}; + handlers.forEach((h) => { + handlerObj[h] = null; + }); + handlers = handlerObj; + } + + if (!Object.keys(handlers).length) { + throw new Error('addEventHandler: no handlers supplied'); + } + + if (!this.handlers[event]) { + this.handlers[event] = {}; + } + const errors = []; + for (const [handlerName, handlerFn] of Object.entries(handlers)) { + const typeError = this.addHandlerFunction({ handlerName, handlerFn }); + if (typeError) { + errors.push(typeError); + } else { + this.handlers[event][handlerName] = this.__handlerFns[handlerName]; + } + } + + if (errors.length) { + throw new Error( + `addEventHandler: handlers must be of type function. Recheck these handlers: ${errors + .sort() + .join(', ')}` + ); + } + } + + /** + * Add one or more handler functions + * @param {object} handlerObject - object with keys + * {string} handlerName - the name of the handler to add + * {function} handlerFn - the function to be performed + * if the handler function is already installed on the job manager, + * `handlerFn` will replace the existing function. If the existing + * function can be used, `null` can be supplied. + * + * Example: + * + * jm.addEventListener('job-status', { + * handlerA: () => {}, // this function gets added to the job manager + * handlerB: null, // this handler function is expected to exist + * }) + * + */ + addHandlerFunction(handlerArgs) { + const { handlerName, handlerFn } = handlerArgs; + + if (typeof handlerName !== 'string') { + return handlerName; + } else if (this.__handlerFns[handlerName]) { + // a function with this name already exists + // this handler can be used unchanged + if (handlerFn === null) { + return; + } + // otherwise, replace the handler + console.warn(`Replaced existing ${handlerName} handler`); + } else if (handlerFn === null && !this.__handlerFns[handlerName]) { + // expected the handler to exist already, but it does not + console.error(`No handler function supplied for ${handlerName}`); + return handlerName; + } else if (typeof handlerFn !== 'function') { + return handlerName; + } + + this.__handlerFns[handlerName] = handlerFn; + } + + /** + * Remove a handler from an event + * + * @param {string} event + * @param {string} handlerName + * @returns {function} handlerFunction + */ + removeEventHandler(event, handlerName) { + if (this.handlers[event]) { + const handlerFunction = this.handlers[event][handlerName]; + delete this.handlers[event][handlerName]; + return handlerFunction; + } + } + + /** + * Remove a handler function completely + * @param {string} handlerName + */ + removeHandlerFunction(handlerName) { + const handlerFunction = this.__handlerFns[handlerName]; + Object.keys(this.handlers).forEach((event) => { + delete this.handlers[event][handlerName]; + }); + delete this.__handlerFns[handlerName]; + return handlerFunction; + } + + /** + * Trigger the ${event} handlers. Event handlers are executed alphabetically by name. + * + * By default, the handler functions receive the jobManager context as + * first argument and any additional arguments passed in by the caller + * (e.g. the message and job ID in the case of job listeners) + * + * @param {string} event + * @param {...any} args + */ + runHandler(event, ...args) { + const ctx = this; + if ( + !this._isValidEvent(event) || + !this.handlers[event] || + !Object.keys(this.handlers[event]) + ) { + return; + } + + Object.keys(this.handlers[event]) + .sort() + .forEach((handlerName) => { + try { + this.handlers[event][handlerName](ctx, ...args); + } catch (err) { + console.warn(`Error executing handler ${handlerName}:`, err); + } + }); + } + + /* LISTENERS */ + + /** + * Add a bus listener for ${event} messages + * + * @param {string} type - a valid message type to listen for (see validOutgoingMessageTypes) + * @param {array} channelList - array of channels (job or cell IDs) to apply the listener to + * @param {object} handlerObject (optional) - object with key(s) handler name and value(s) function to execute on receiving a message + */ + addListener(type, channelList, handlerObject) { + if (!validOutgoingMessageTypes.includes(type)) { + throw new Error(`addListener: invalid listener ${type} supplied`); + } + + if (Object.prototype.toString.call(channelList) !== '[object Array]') { + channelList = [channelList]; + } + + channelList + .filter((channel) => { + return channel && channel.length > 0 ? 1 : 0; + }) + .forEach((channel) => { + if (!this.listeners[channel]) { + this.listeners[channel] = {}; + } + if (!this.listeners[channel][type]) { + let channelObject = { jobId: channel }; + if (type === 'cell-job-status') { + channelObject = { cell: channel }; + } + + this.listeners[channel][type] = this.bus.listen({ + channel: channelObject, + key: { + type, + }, + handle: (message) => { + if (!this._isValidMessage(type, message)) { + return; + } + this.runHandler(type, message, channel); + }, + }); + } + }); + + // add the handler -- the message type is used as the event + if (handlerObject) { + this.addEventHandler(type, handlerObject); + } + } + + /** + * Remove the listener for ${type} messages for a channel + * + * @param {string} channel + * @param {string} type - the type of the listener + */ + removeListener(channel, type) { + try { + this.bus.removeListener(this.listeners[channel][type]); + delete this.listeners[channel][type]; + } catch (err) { + // do nothing + } + } + + /** + * Remove all listeners associated with a certain channel ID + * @param {string} channel + */ + removeJobListeners(channel) { + if (this.listeners[channel] && Object.keys(this.listeners[channel]).length) { + Object.keys(this.listeners[channel]).forEach((type) => { + this.removeListener(channel, type); + }); + delete this.listeners[channel]; + } + } + + /** + * Update the model with the supplied jobState objects + * @param {array} jobArray list of jobState objects to update the model with + */ + updateModel(jobArray) { + const jobIndex = this.model.getItem('exec.jobs'); + const batchId = this.model.getItem('exec.jobState.job_id'); + let batchJob; + jobArray.forEach((jobState) => { + // update the job object + jobIndex.byId[jobState.job_id] = jobState; + if (jobState.job_id === batchId) { + batchJob = jobState; + } + }); + this.model.setItem('exec.jobs', jobIndex); + // check whether the batch parent needs updating + if (batchJob) { + this.model.setItem('exec.jobState', batchJob); + } + this.runHandler('modelUpdate', jobArray); + return this.model; + } + + /* UTIL FUNCTIONS */ + + _checkStates(statusList, validStates) { + if (validStates && validStates.length) { + const allInTheList = statusList.every((status) => { + return validStates.includes(status); + }); + if (!allInTheList) { + console.error( + `Invalid status supplied! Valid statuses: ${validStates.join( + '; ' + )}; supplied: ${statusList.join('; ')}.` + ); + return null; + } + } + return statusList; + } + + /** + * Get jobs with a certain status, excluding the batch parent job + * + * @param {array} statusList - array of statuses to find + * @param {array} validStates - array of valid statuses for this action (optional) + * @returns {array} job IDs + */ + getCurrentJobIDsByStatus(rawStatusList, validStates) { + return this.getCurrentJobsByStatus(rawStatusList, validStates).map((job) => { + return job.job_id; + }); + } + + /** + * Get jobs with a certain status, excluding the batch parent job + * + * @param {array} statusList - array of statuses to find + * @param {array} validStates - array of valid statuses for this action (optional) + * @returns {array} job objects + */ + getCurrentJobsByStatus(rawStatusList, validStates) { + const statusList = this._checkStates(rawStatusList, validStates); + const jobsById = this.model.getItem('exec.jobs.byId'); + if (!statusList || !jobsById || !Object.keys(jobsById).length) { + return []; + } + const batchId = this.model.getItem('exec.jobState.job_id'); + + // this should only use current jobs + const currentJobs = Jobs.getCurrentJobs(Object.values(jobsById)); + + // return only jobs with the appropriate status and that are not the batch parent + return Object.keys(currentJobs) + .filter((job_id) => { + return statusList.includes(currentJobs[job_id].status) && job_id !== batchId; + }) + .map((job_id) => { + return currentJobs[job_id]; + }); + } + } + + /** + * A set of generic message handlers + * + * @param {class} Base + * @returns + */ + const DefaultHandlerMixin = (Base) => + class extends Base { + constructor(config) { + // run the constructor for the base class + super(config); + this.addDefaultHandlers(); + } + + addDefaultHandlers() { + const defaultHandlers = { + 'job-info': this.handleJobInfo, + 'job-retry-response': this.handleJobRetry, + 'job-status': this.handleJobStatus, + }; + + Object.keys(defaultHandlers).forEach((event) => { + const toAdd = {}; + toAdd[`__default_${event}`] = defaultHandlers[event]; + this.addEventHandler(event, toAdd); + }, this); + } + + /** + * parse job info and update the appropriate part of the model + * + * @param {object} message + */ + handleJobInfo(self, message) { + const { jobInfo, error } = message; + if (error) { + return; + } + self.model.setItem(`exec.jobs.info.${jobInfo.job_id}`, jobInfo); + self.removeListener(jobInfo.job_id, 'job-info'); + } + + handleJobRetry(self, message) { + const { job, retry, error } = message; + if (error) { + return; + } + + // request job updates for the new job + ['status', 'error'].forEach((type) => { + self.addListener(`job-${type}`, [retry.jobState.job_id]); + }); + self.bus.emit('request-job-updates-start', { + jobId: retry.jobState.job_id, + }); + // update the model with the job data + self.updateModel( + [job, retry].map((j) => { + return j.jobState; + }) + ); + } + + /** + * @param {Object} message + */ + handleJobStatus(self, message) { + const { jobId, jobState } = message; + const { status, updated } = jobState; + + // if the job is in a terminal state and cannot be retried, + // stop listening for updates + if (Jobs.isTerminalStatus(status) && !Jobs.canRetry(jobState)) { + self.removeListener(jobId, 'job-status'); + if (status === 'does_not_exist') { + self.removeJobListeners(jobId); + } + self.bus.emit('request-job-updates-stop', { + jobId, + }); + self.updateModel([jobState]); + return; + } + + // check if the job object has been updated since we last saved it + const previousUpdate = self.model.getItem(`exec.jobs.byId.${jobId}.updated`); + if (updated && previousUpdate === updated) { + return; + } + + if (jobState.batch_job) { + const missingJobIds = []; + // do we have all the children? + jobState.child_jobs.forEach((job_id) => { + if (!self.model.getItem(`exec.jobs.byId.${job_id}`)) { + missingJobIds.push(job_id); + } + }); + if (missingJobIds.length) { + ['status', 'error', 'info'].forEach((type) => { + self.addListener(`job-${type}`, missingJobIds); + }); + self.bus.emit('request-job-updates-start', { + jobIdList: missingJobIds, + }); + } + } + + // otherwise, update the state + self.updateModel([jobState]); + } + }; + + const JobShortcutsMixin = (Base) => + class extends Base { + /* JOB SHORTCUTS */ + + /** + * Request job info and add message listeners + * @param {array[string]} jobIdList + */ + requestJobInfo(jobIdList) { + this.addListener('job-info', jobIdList); + this.bus.emit('request-job-info', { jobIdList }); + } + + /** + * Request job status and add message listeners + * @param {array[string]} jobIdList + */ + requestJobStatus(jobIdList) { + this.addListener('job-status', jobIdList); + this.bus.emit('request-job-status', { jobIdList }); + } + }; + + const JobActionsMixin = (Base) => + class extends Base { + /* JOB ACTIONS */ + + /** + * Cancel or retry a list of jobs + * + * @param {string} action - either 'cancel' or 'retry' + * @param {array} jobIdList + */ + doJobAction(action, jobIdList) { + this.bus.emit(jobCommand[action].command, { + jobIdList, + }); + // add the appropriate listener + this.addListener(jobCommand[action].listener, jobIdList); + } + + /** + * + * @param {object} args with keys + * action {string} the action to perform -- cancel or retry + * jobId {string} job to perform it on + * + * @returns {boolean} + */ + executeActionOnJobId(args) { + const { action, jobId } = args; + const jobState = this.model.getItem(`exec.jobs.byId.${jobId}`); + if (jobState && Jobs.canDo(action, jobState)) { + const actionJobId = + action === 'retry' && jobState.retry_parent ? jobState.retry_parent : jobId; + this.doJobAction(action, [actionJobId]); + return true; + } + return false; + } + + /** + * + * @param {object} args with keys + * action: {string} action to perform (cancel or retry) + * statusList: {array} list of statuses to perform it on + * + * @returns {Promise} that resolves to false if there is some error with the input or + * if the user cancels the batch action. If the users confirms the action, the appropriate + * message will be emitted by the bus. + */ + executeActionByJobStatus(args) { + const { action, statusList } = args; + // valid actions: cancel or retry + if (!['cancel', 'retry'].includes(action)) { + return Promise.resolve(false); + } + + const jobList = this.getCurrentJobsByStatus( + statusList, + Jobs.validStatusesForAction[action] + ); + if (!jobList || !jobList.length) { + return Promise.resolve(false); + } + + return DialogMessages.showDialog({ + action: `${action}Jobs`, + statusList, + jobList, + }).then((confirmed) => { + if (confirmed) { + const jobIdList = + action === 'retry' + ? jobList.map((job) => { + // return the retry_parent (if available) + return job.retry_parent || job.job_id; + }) + : jobList.map((job) => { + return job.job_id; + }); + this.doJobAction(action, jobIdList); + } + return Promise.resolve(confirmed); + }); + } + + /** + * Cancel a single job from the batch + * + * @param {string} jobId + */ + cancelJob(jobId) { + return this.executeActionOnJobId({ jobId, action: 'cancel' }); + } + + /** + * Retry a single job from the batch + * + * @param {string} jobId + */ + retryJob(jobId) { + return this.executeActionOnJobId({ jobId, action: 'retry' }); + } + + /** + * Cancel all jobs with the specified statuses + * + * @param {array} statusList - array of statuses to cancel + */ + cancelJobsByStatus(statusList) { + return this.executeActionByJobStatus({ + action: 'cancel', + statusList, + validStatuses: Jobs.validStatusesForAction.cancel, + }); + } + + /** + * Retry all jobs that ended with the specified status(es) + * Although only jobs in status 'error' or 'terminated' can be retried, + * the frontend job status may be out of date, so it is left to ee2 to + * verify whether a job can be retried or not. + * + * @param {array} statusList - array of statuses to retry + */ + retryJobsByStatus(statusList) { + return this.executeActionByJobStatus({ + action: 'retry', + statusList, + validStatuses: Jobs.validStatusesForAction.retry, + }); + } + + /** + * Cancel a batch job by submitting a cancel request for the batch job container + * + * This action is triggered by hitting the 'Cancel' button at the top right of the + * bulk cell + */ + cancelBatchJob() { + const batchId = this.model.getItem('exec.jobState.job_id'); + if (batchId) { + this.doJobAction('cancel', [batchId]); + } + } + + /** + * Reset the job manager, removing all listeners and stored job data + */ + resetJobs() { + const allJobs = this.model.getItem('exec.jobs.byId'), + batchJob = this.model.getItem('exec.jobState'); + if (!allJobs || !Object.keys(allJobs).length) { + this.model.deleteItem('exec'); + this.resetCell(); + return; + } + + if (batchJob && !allJobs[batchJob.job_id]) { + allJobs[batchJob.job_id] = batchJob; + } + + this.bus.emit('request-job-updates-stop', { + batchId: batchJob.job_id, + }); + // ensure that job updates are turned off and listeners removed + Object.keys(allJobs).forEach((jobId) => { + this.removeJobListeners(jobId); + }); + + this.model.deleteItem('exec'); + this.resetCell(); + } + + resetCell() { + if (this.cellId) { + this.bus.emit('reset-cell', { + cellId: this.cellId, + ts: Date.now(), + }); + } + } + }; + + const BatchInitMixin = (Base) => + class extends Base { + /** + * set up the job manager to handle a batch job + * + * @param {object} batchJob - with keys + * {string} batch_id + * {array} child_job_ids + */ + initBatchJob(batchJob) { + const { batch_id, child_job_ids } = batchJob; + + if ( + !batch_id || + !child_job_ids || + Object.prototype.toString.call(child_job_ids) !== '[object Array]' || + !child_job_ids.length + ) { + throw new Error('Batch job must have a batch ID and at least one child job ID'); + } + const allJobIds = [batch_id].concat(child_job_ids), + // create the child jobs + allJobs = child_job_ids.map((job_id) => { + return { + job_id: job_id, + batch_id: batch_id, + status: 'created', + created: 0, + }; + }); + + // add the parent job + allJobs.push({ + job_id: batch_id, + batch_id: batch_id, + batch_job: true, + child_jobs: [], + status: 'created', + created: 0, + }); + + Jobs.populateModelFromJobArray(this.model, Object.values(allJobs)); + + this._initJobs({ allJobIds, batchId: batch_id }); + + // request job info + this.addListener('job-info', allJobIds); + this.bus.emit('request-job-info', { batchId: batch_id }); + } + + _initJobs(args) { + const { allJobIds, batchId } = args; + + ['status', 'error'].forEach((type) => { + this.addListener(`job-${type}`, allJobIds); + }); + // request job updates + this.bus.emit('request-job-updates-start', { batchId }); + } + + restoreFromSaved() { + const batchJob = this.model.getItem('exec.jobState'), + allJobs = this.model.getItem('exec.jobs.byId'); + if (!batchJob || !allJobs) { + return; + } + + // make sure that all the child jobs of batchJob are in allJobs + if (batchJob.child_jobs && batchJob.child_jobs.length) { + batchJob.child_jobs.forEach((job_id) => { + if (!allJobs[job_id]) { + allJobs[job_id] = { job_id }; + } + }); + } + + this._initJobs({ + batchId: batchJob.job_id, + allJobIds: Object.keys(allJobs), + }); + + return this.getFsmStateFromJobs(); + } + + /** + * Use the current jobs to work out the FSM state + * @returns {string} bulk import cell FSM state + */ + getFsmStateFromJobs() { + return Jobs.getFsmStateFromJobs(this.model.getItem('exec.jobs')); + } + }; + + class JobManager extends BatchInitMixin( + JobActionsMixin(JobShortcutsMixin(DefaultHandlerMixin(JobManagerCore))) + ) {} + + return { + JobManagerCore, + DefaultHandlerMixin, + JobShortcutsMixin, + JobActionsMixin, + BatchInitMixin, + JobManager, + }; +}); diff --git a/kbase-extension/static/kbase/js/common/jobs.js b/kbase-extension/static/kbase/js/common/jobs.js index 7c4880bba4..4d6014ca9a 100644 --- a/kbase-extension/static/kbase/js/common/jobs.js +++ b/kbase-extension/static/kbase/js/common/jobs.js @@ -1,137 +1,863 @@ -define(['bluebird', 'base/js/namespace'], (Promise, Jupyter) => { +define(['common/errorDisplay', 'common/format', 'common/html', 'common/ui'], ( + ErrorDisplay, + Format, + html, + UI +) => { 'use strict'; - /* - * Exceptions + const t = html.tag, + span = t('span'); + + const JOB = { + created: 'created', + estimating: 'estimating', + queued: 'queued', + running: 'running', + completed: 'completed', + error: 'error', + terminated: 'terminated', + does_not_exist: 'does_not_exist', + }; + + const cssBaseClass = 'kb-job-status', + jobNotFound = [ + 'This job was not found, or may not have been registered with this narrative.', + 'You will not be able to inspect the job status or view the job log.', + ], + jobStatusUnknown = ['Awaiting job data...'], + // valid job states from ee2, plus 'does_not_exist' + validJobStatuses = [ + JOB.created, + JOB.estimating, + JOB.queued, + JOB.running, + JOB.completed, + JOB.error, + JOB.terminated, + JOB.does_not_exist, + ], + // job states where there will be no more updates + terminalStates = [JOB.completed, JOB.error, JOB.terminated, JOB.does_not_exist]; + + const jobStrings = { + does_not_exist: { + action: null, + nice: 'does not exist', + status: 'not found', + statusBatch: 'not found', + }, + + queued: { + action: 'cancel', + nice: 'queued', + status: 'queued', + statusBatch: 'queued', + }, + + running: { + action: 'cancel', + nice: 'running', + status: 'running', + statusBatch: 'running', + }, + + completed: { + action: 'go to results', + nice: 'success', + status: 'success', + statusBatch: 'successes', + }, + + error: { + action: 'retry', + nice: 'error', + status: 'failed', + statusBatch: 'failed', + }, + + terminated: { + action: 'retry', + nice: 'cancellation', + status: 'cancelled', + statusBatch: 'cancelled', + }, + }; + jobStrings.created = jobStrings.queued; + jobStrings.estimating = jobStrings.queued; + + function isTerminalStatus(status) { + return !!(status && terminalStates.includes(status)); + } + + function getJobString(jobState, stringType) { + if (!jobState || !stringType) { + throw new Error('Please supply a job state object and a string type'); + } + + const validStringTypes = ['action', 'status', 'statusBatch', 'nice']; + if (!validStringTypes.includes(stringType)) { + throw new Error(`${stringType} is not a valid string type`); + } + + const status = + jobState.status && isValidJobStatus(jobState.status) + ? jobState.status + : JOB.does_not_exist; + + return jobStrings[status][stringType]; + } + + /** + * A jobState object is deemed valid if + * 1. It's an object (not an array or atomic type) + * 2. It has a 'job_id' key + * 3. It has a 'status' key, which appears in the array validJobStatuses + * 4. If the status is NOT 'does_not_exist', it must have a 'created' key + * + * There are other required fields, but these checks are sufficient for the UI. + * + * See execution_engine2/lib/execution_engine2/db/models/models.py in the + * execution_engine2 repo for the full job spec. + * + * This function should be updated to stay in sync with ee2's output. + * + * @param {object} jobState + * @returns {boolean} true|false + */ + function isValidJobStateObject(jobState) { + const requiredProperties = ['job_id', 'status']; + return !!( + jobState !== null && + typeof jobState === 'object' && + requiredProperties.every((prop) => prop in jobState) && + validJobStatuses.includes(jobState.status) && + // require the 'created' key if the status is not 'does_not_exist' + (jobState.status === JOB.does_not_exist + ? true + : Object.prototype.hasOwnProperty.call(jobState, JOB.created)) + ); + } + + /** + * Given a job status string, ensure that it is valid + * + * @param {*} status + * @returns {boolean} true|false + */ + function isValidJobStatus(status) { + return status && validJobStatuses.includes(status); + } + + /** + * A jobInfo object should have the minimal structure + * + * { + * job_id: ... + * job_params: [ + * { + * param_one: 'value one', + * param_two: 'value two', + * ... + * } + * ] + * } + * + * This function should be updated to stay in sync with ee2's output. + * + * @param {object} jobInfo + * @returns {boolean} true|false + */ + function isValidJobInfoObject(jobInfo) { + try { + if ( + 'job_id' in jobInfo && + Object.prototype.toString.call(jobInfo.job_params[0]) === '[object Object]' && + Object.keys(jobInfo.job_params[0]).length > 0 + ) { + return true; + } + // eslint-disable-next-line no-empty + } catch (err) {} + return false; + } + + /** + * Given a job state, return the appropriate action to offer in the UI + * + * @param {object} jobState + * @returns {string} label + */ + + function jobAction(jobState) { + return getJobString(jobState, 'action'); + } + + const validStatusesForAction = { + cancel: [JOB.created, JOB.estimating, JOB.queued, JOB.running], + retry: [JOB.created, JOB.estimating, JOB.queued, JOB.running, JOB.error, JOB.terminated], + }; + + /** + * Given a job state object and an action, return a boolean indicating whether the job can perform that action + * + * @param {string} action + * @param {object} jobState + * @returns {boolean} whether or not the job can do the action */ - function JobError(message, remoteStacktrace) { - this.name = 'JobError'; - this.message = message; - this.stack = new Error().stack; - this.remoteStacktrace = remoteStacktrace; + function canDo(action, jobState) { + return ( + isValidJobStateObject(jobState) && + validStatusesForAction[action].includes(jobState.status) && + !(action === 'retry' && jobState.batch_job) + ); } - JobError.prototype = Object.create(Error.prototype); - JobError.prototype.constructor = JobError; - function defaultHandler(call, content) { - if (content.status === 'error') { - console.error('Jupyter kernel request error', 'Call: ' + call, 'Content', content); + /** + * Given a job state object, return a boolean indicating whether the job can be cancelled + * + * A job in a non-terminal state can be cancelled. + * + * @param {object} jobState + * @returns {boolean} whether or not the job can be cancelled + */ + function canCancel(jobState) { + return canDo('cancel', jobState); + } + + /** + * Given a job state object, return a boolean indicating whether the job can be retried + * + * As the job data held by the front end may be out of date, any job not in state 'completed' + * can be retried + * + * @param {object} jobState + * @returns {boolean} whether or not the job can be retried + */ + function canRetry(jobState) { + return canDo('retry', jobState); + } + + /** + * Convert a job state into a short string to present in the UI + * + * @param {object} jobState + * @param {boolean} includeError // whether or not to include error info + * @returns {string} label + */ + + function jobLabel(jobState, includeError = false) { + const jobString = getJobString(jobState, 'status'); + if (includeError && jobState.status && jobState.status === JOB.error) { + return ( + `${jobString}: ` + ErrorDisplay.normaliseErrorObject({ jobState: jobState }).type + ); } + return jobString; } - /* - * Strip out console commands from text captured from console: - * http://search.cpan.org/~jlmorel/Win32-Console-ANSI-1.10/lib/Win32/Console/ANSI.pm + /** + * Translate from EE2's job status (or other job state strings interpreted by the app cell) + * to a presentable string. This returns a span with the text colored and bolded, and the + * "nice" readable state string. * + * Translated strings = completed, error, terminated, and does_not_exist. Those all get + * different colors. Other strings are rendered black. + * @param {string} jobStatus */ - function consoleToText(consoleText) { - return consoleText.replace(/\[([\s\S]*?)m/g, ''); + + function niceState(jobStatus) { + if (!isValidJobStatus(jobStatus)) { + jobStatus = 'invalid'; + } + + let label = jobStatus, + cssClass = `--${jobStatus}`; + switch (jobStatus) { + case JOB.does_not_exist: + label = 'does not exist'; + break; + case JOB.completed: + label = 'success'; + break; + case JOB.error: + break; + case JOB.terminated: + label = 'cancellation'; + break; + default: + cssClass = ''; + } + + return span( + { + class: `${cssBaseClass}__summary${cssClass}`, + }, + label + ); } - function runPython(command) { - return new Promise((resolve, reject) => { - const callbacks = { - shell: { - reply: function (content) { - defaultHandler('reply', content); - }, - payload: { - set_next_input: function (content) { - defaultHandler('set_next_input', content); - }, - }, - }, - iopub: { - output: function (content) { - if (content.msg_type === 'error') { - const trace = content.content.traceback.map((line) => { - return consoleToText(line); - }), - message = - 'Error in iopub output: ' + - content.content.ename + - ':' + - content.content.evalue; - reject(new JobError(message, trace)); - } else { - // console.log('IOPUB', content); - resolve(JSON.parse(content.content.text)); - } - }, - clear_output: function (content) { - defaultHandler('clear_output', content); + /** + * Use the app cell FSM mode and stage data to generate the appropriate job status summary + * @param {string} mode + * @param {string} stage + * @returns {string} HTML string with a single word status + */ + + function createJobStatusFromFsm(mode, stage) { + let label, cssClass; + switch (mode) { + case 'success': + label = 'success'; + cssClass = 'completed'; + break; + case 'error': + case 'internal-error': + label = 'error'; + cssClass = 'error'; + break; + case 'canceled': + case 'canceling': + label = 'canceled'; + cssClass = 'terminated'; + break; + case 'processing': + if (stage === 'running' || stage === 'queued') { + label = stage; + cssClass = stage; + } + break; + } + return _createJobStatus({ cssClass, label }); + } + + function createJobStatusFromBulkCellFsm(mode) { + let label, cssClass; + switch (mode) { + case 'launching': + case 'inProgress': + case 'inProgressResultsAvailable': + label = 'in progress'; + cssClass = 'running'; + break; + // all jobs completed, none successfully + case 'jobsFinished': + label = 'jobs finished'; + cssClass = 'error'; + break; + // all jobs complete, at least some successful + case 'jobsFinishedResultsAvailable': + label = 'jobs finished'; + cssClass = 'completed'; + break; + case 'error': + label = 'error'; + cssClass = 'error'; + break; + } + return _createJobStatus({ cssClass, label }); + } + + function _createJobStatus(args) { + const { cssClass, label } = args; + + if (!cssClass || !label) { + return ''; + } + + return span( + { + class: `${cssBaseClass}__cell_summary--${cssClass}`, + dataElement: 'job-status', + }, + label + ); + } + + /** + * Add an array of jobs to a model + * + * @param {object} model - the model to add the jobs to + * @param {array} jobArray - array of job state objects + */ + function populateModelFromJobArray(model, jobArray = []) { + if (!model) { + throw new Error('Missing a model to populate'); + } + const batchIds = new Set(), + batchJobs = new Set(); + + if (!jobArray.length) { + model.deleteItem('exec.jobs'); + model.deleteItem('exec.jobState'); + return; + } + + jobArray.forEach((job) => { + if (job.batch_id) { + batchIds.add(job.batch_id); + } + if (job.batch_job) { + batchJobs.add(job); + } + }); + + if (batchIds.size > 1) { + // more than one batch present + throw new Error('More than one batch ID found'); + } + if (batchJobs.size > 1) { + // more than one batch parent present + throw new Error('More than one batch parent found'); + } + + model.setItem('exec.jobs', jobArrayToIndexedObject(jobArray)); + + model.setItem('exec.jobState', Array.from(batchJobs)[0]); + return model; + } + + /** + * Given an array of jobState objects, create an object with jobs indexed by ID + * + * @param {array} jobArray array of jobState objects + * @returns {object} with indexed job data: + * byId key: job_id, value: jobState object + */ + + function jobArrayToIndexedObject(jobArray = []) { + const jobIx = { + byId: {}, + }; + + jobArray.forEach((job) => { + jobIx.byId[job.job_id] = job; + }); + return jobIx; + } + + /** + * Ensures that the child_jobs data is in the correct place in the jobState object + * + * This is a horrible hack to move job data from the old batch implementation + * into the correct place + * + * @param {object} jobState + * @return {object} jobState, possibly edited + */ + function updateJobModel(jobState) { + // current structure of jobs data -- child jobs are hidden under + // jobState.job_output.result[0].batch_results. + // Remap to be under jobState.child_jobs + if (jobState.batch_size && jobState.child_jobs.length === 0) { + try { + jobState.child_jobs = Object.keys(jobState.job_output.result[0].batch_results).map( + (item) => { + return jobState.job_output.result[0].batch_results[item].final_job_state; + } + ); + } catch (e) { + // eslint-disable-next-line no-empty + } + } + return jobState; + } + + /** + * createJobStatusLines + * Record the current state of the job and its previous states, including time + * spent in those states. + * + * The job execution engine records timestamps for certain job states; in + * chronological order, these are: + * - created + * - queued + * - running + * - finished + * + * Go through job statuses in reverse order to get the most recent state, + * and work backwards from that to generate the previous states. + * + * @param {object} jobState - object representing the current job state + * @param {boolean} includeHistory - whether to generate a single line status (false), or + * to include the history (e.g. how long the job queued for) + * + * @returns {array} jobLines - an array of lines representing the current job status + */ + + function createJobStatusLines(jobState, includeHistory = false) { + if (!isValidJobStateObject(jobState)) { + return jobStatusUnknown; + } + + if (jobState.status === JOB.does_not_exist) { + return jobNotFound; + } + + const jobLines = []; + // Finished status + if (jobState.finished) { + // Amount of time it was in the queue + jobLines.push(queueTime(jobState)); + + if (jobState.running) { + // how long it ran for (if it ran) + jobLines.push(runTime(jobState)); + } + + jobLines.push( + _finishString(jobState.status) + + ' at ' + + span( + { + class: 'kb-timestamp', }, + Format.niceTime(jobState.finished) + ) + ); + + if (includeHistory) { + return jobLines.reverse(); + } + return [jobLines.pop()]; + } + + // the job is still running + if (jobState.running) { + // Amount of time it was in the queue + jobLines.push(queueTime(jobState)); + // How long it has been running for + jobLines.push(runTime(jobState)); + + if (includeHistory) { + return jobLines.reverse(); + } + return [jobLines.pop()]; + } + + // the job is in the queue + return [queueTime(jobState)]; + } + + function _finishString(status) { + return `Finished with ${niceState(status)}`; + } + + function runTime(jobState) { + if (!jobState.finished) { + return ( + UI.loading({ color: 'green' }) + + ' Started running job at ' + + span( + { + class: 'kb-timestamp', }, - input: function (content) { - defaultHandler('input', content); + Format.niceTime(jobState.running) + ) + + span({ class: 'runClock', dataElement: 'clock' }) + ); + } + return ( + 'Ran for ' + + span( + { + class: 'kb-elapsed-time', + }, + Format.niceDuration(jobState.finished - jobState.running) + ) + ); + } + + /** + * In successful job execution, there should be 'running' and 'finished' + * timestamps; the queue time is jobState.running - jobState.created. + * + * If the job was terminated before it left the queue, there will be no + * running timestamp, and we use jobState.finished (the termination timestamp) + * to calculate the queue time. + * + * @param {object} jobState + * @returns {string} queueString - HTML text description of queue time + */ + function queueTime(jobState) { + const endTime = jobState.running || jobState.finished; + if (endTime) { + return ( + 'Queued for ' + + span( + { + class: 'kb-elapsed-time', }, + Format.niceDuration(endTime - jobState.created) + ) + ); + } + // job hasn't run or finished (yet) + return ( + UI.loading({ color: 'orange' }) + + ' In the queue since ' + + span( + { + class: 'kb-timestamp', }, - options = { - silent: true, - user_expressions: {}, - allow_stdin: false, - store_history: false, + Format.niceTime(jobState.created) + ) + ); + } + + /** + * + * @param {array} jobArray array of job objects + * @param {object} jobsByRetryParent (optional) index, to be completed, with key retry_parent job IDs + * and an array of retry job IDs as values + * @returns {object} jobsByOriginalId object, with key retry_parent job and value job_id of most recent retry + */ + function getCurrentJobs(jobArray, jobsByRetryParent = {}) { + // group jobs by their retry parents + jobArray.forEach((job) => { + if (job.batch_job) { + return; + } + const indexId = job.retry_parent || job.job_id; + const jobIx = `${job.created || 0}__${job.job_id}`; + if (!jobsByRetryParent[indexId]) { + jobsByRetryParent[indexId] = { + job_id: indexId, + jobs: {}, }; + } + jobsByRetryParent[indexId].jobs[jobIx] = job; + }); + + // collect jobs that are not retries + const jobsByOriginalId = {}; + // now take the most recent job from jobsByRetryParent + Object.keys(jobsByRetryParent).forEach((indexId) => { + const sortedJobs = Object.keys(jobsByRetryParent[indexId].jobs).sort(), + lastJob = sortedJobs[sortedJobs.length - 1]; + jobsByOriginalId[indexId] = jobsByRetryParent[indexId].jobs[lastJob]; + }); + return jobsByOriginalId; + } + + const batch = 'batch job'; + /** + * create a summary string for a set of jobs + * + * @param {object} jobLabelFreq - an object with keys job labels and values their frequencies + * @returns {string} - a string summarising the state of the batch job + */ + function _createBatchSummaryState(jobLabelFreq) { + // if at least one job is queued or running, the batch job is in progress + if (jobLabelFreq.running || jobLabelFreq.queued) { + return `${batch} in progress`; + } + + if (Object.keys(jobLabelFreq).length === 1) { + const status = Object.keys(jobLabelFreq)[0]; + if (status === JOB.does_not_exist) { + return `${batch} finished with error`; + } + return `${batch} ${_finishString(status).toLowerCase()}`; + } + return `${batch} finished`; + } - if (Jupyter.notebook.kernel.is_connected()) { - Jupyter.notebook.kernel.execute(command, callbacks, options); + /** + * Get counts for jobs in each status + * Squashes 'estimating' and 'created' job states into 'queued' + * + * @param {object} jobsByStatus + * @returns {object} statuses with keys job status and values the number of jobs in that state + */ + function _jobCountByStatus(jobsByStatus) { + const statuses = {}; + Object.keys(jobsByStatus).forEach((status) => { + const nJobs = jobsByStatus[status].size; + // reduce down the queued states + if (status === JOB.estimating || status === JOB.created) { + status = JOB.queued; + } + if (statuses[status]) { + statuses[status] += nJobs; } else { - console.error('Not looking up jobs - kernel is not connected'); - reject(new Error('Not looking up jobs - kernel is not connected')); + statuses[status] = nJobs; } }); + return statuses; } - function deleteJob(jobId) { - /*var command = [ - 'from biokbase.narrative.common.kbjob_manager import KBjobManager', - 'jm = KBjobManager()', - 'print jm.delete_jobs(["' + jobId + '"], as_json=True)' - ].join('\n'); - return runPython(command);*/ - throw new Error('Method is not supported anymore'); + /** + * group job IDs by job status + * + * @param {object} jobIx + * @returns {object} jobsByStatus with keys job status and values Set object containing job IDs + */ + function _groupJobsByStatus(jobIx) { + // index by status + const jobsByStatus = {}; + Object.values(jobIx).forEach((job) => { + const { job_id, status } = job; + if (!jobsByStatus[status]) { + jobsByStatus[status] = new Set(); + } + jobsByStatus[status].add(job_id); + }); + return jobsByStatus; } - /* - * For a given job, returns the log lines after "skip" lines, as an Promise - * which will deliver an array of strings. + /** + * Classify current jobs by status and return counts for each status + * 'created' and 'estimating' statuses are collapsed into 'queued' + * + * @param {object} jobsIndex jobs indexed by ID + * @param {object} options extra options for the count; currently only 'withRetries' allowed + * @returns {object} with keys job status and values number of jobs in that state */ - function getLogData(jobId, skip) { - /*var command = [ - 'from biokbase.narrative.common.kbjob_manager import KBjobManager', - 'import json', - 'job_manager = KBjobManager()', - 'print json.dumps(job_manager.get_job_logs({"job_id":"' + jobId + '","skip_lines":' + skip + '}))' - ].join('\n'); - return runPython(command) - .then(function (data) { - return data.lines; - });*/ - throw new Error('Method is not supported anymore'); + function getCurrentJobCounts(jobsIndex, options = {}) { + if ( + !jobsIndex || + !Object.keys(jobsIndex).length || + !jobsIndex.byId || + !Object.keys(jobsIndex.byId).length + ) { + return {}; + } + const jobsByRetryParent = {}; + const currentJobs = getCurrentJobs(Object.values(jobsIndex.byId), jobsByRetryParent); + + // get job count for each status + const statuses = _jobCountByStatus(_groupJobsByStatus(currentJobs)); + + if (options && options.withRetries) { + // any retryParents in jobsByRetryParents with more than one child in jobs have been retried + const nRetriedParents = Object.values(jobsByRetryParent) + .filter((parent) => { + return Object.keys(parent.jobs).length > 1; + }) + .map((job) => { + return job.job_id; + }).length; + if (nRetriedParents) { + statuses.retried = nRetriedParents; + } + } + return statuses; } /** - * A jobState is deemed valid if - * 1. It's an object (not an array or atomic type) - * 2. It has a created key - * 3. It has a job_id key - * 4. There's others that are necessary, but the top two are sufficient to judge if it's valid - * enough and up to date. This function should be updated as necessary. + * Given an object containing jobs indexed by ID, summarise the status of the jobs + * @param {object} jobsIndex the index of job data, e.g. from retrieving `exec.jobs` in a batch cell + * @returns {string} summary string + */ + + function createCombinedJobState(jobsIndex) { + if ( + !jobsIndex || + !Object.keys(jobsIndex).length || + !jobsIndex.byId || + !Object.keys(jobsIndex.byId).length + ) { + return ''; + } + + // get job count for each status and retries + const statuses = getCurrentJobCounts(jobsIndex, { withRetries: 1 }); + + const textStatusSummary = _createBatchSummaryState(statuses); + const orderedStatuses = [ + JOB.queued, + JOB.running, + JOB.completed, + JOB.error, + JOB.terminated, + JOB.does_not_exist, + ]; + let summaryHtml = + `${textStatusSummary}: ` + + orderedStatuses + .filter((state) => { + return statuses[state]; + }) + .map((state) => { + const jobString = getJobString( + { status: state }, + statuses[state] === 1 ? 'status' : 'statusBatch' + ); + + return ( + `${statuses[state]} ` + + span( + { + class: `${cssBaseClass}__summary--${state}`, + }, + jobString + ) + ); + }) + .join(', '); + + if (statuses.retried) { + summaryHtml += ` (${ + statuses.retried > 1 ? statuses.retried + ' jobs' : '1 job' + } retried)`; + } + + const jobSpan = document.createElement('span'); + jobSpan.innerHTML = summaryHtml; + jobSpan.title = jobSpan.textContent; + return jobSpan.outerHTML; + } + + /** + * Get the FSM state for a bulk cell from the jobs * - * This is intended to be used to make sure that jobStates are of the latest version of the - * execution engine. - * @param {object} jobState + * @param {object} jobsIndex jobs index + * @returns {string} FSM state */ - function isValidJobState(jobState) { - if (typeof jobState === 'object' && jobState !== null) { - return jobState.hasOwnProperty('created') && jobState.hasOwnProperty('job_id'); + function getFsmStateFromJobs(jobsIndex) { + if ( + !jobsIndex || + !Object.keys(jobsIndex).length || + !jobsIndex.byId || + !Object.keys(jobsIndex.byId).length + ) { + return null; } - return false; + + const statuses = getCurrentJobCounts(jobsIndex); + // if at least one job is running or queued, the status is 'inProgress'; otherwise, + // all jobs are in a terminal state, so the status is 'jobsFinished' + const baseStatus = statuses.running || statuses.queued ? 'inProgress' : 'jobsFinished'; + // check whether or not any jobs ran successfully and hence whether results are available + return statuses.completed ? `${baseStatus}ResultsAvailable` : baseStatus; } return { - getLogData: getLogData, - deleteJob: deleteJob, - isValidJobState: isValidJobState, + canCancel, + canDo, + canRetry, + createCombinedJobState, + createJobStatusFromFsm, + createJobStatusFromBulkCellFsm, + createJobStatusLines, + getCurrentJobCounts, + getCurrentJobs, + getFsmStateFromJobs, + isTerminalStatus, + isValidJobStatus, + isValidJobStateObject, + isValidJobInfoObject, + jobAction, + jobArrayToIndexedObject, + jobLabel, + jobNotFound, + jobStatusUnknown, + jobStrings, + niceState, + populateModelFromJobArray, + updateJobModel, + validJobStatuses, + validStatusesForAction, }; }); diff --git a/kbase-extension/static/kbase/js/common/lang.js b/kbase-extension/static/kbase/js/common/lang.js deleted file mode 100644 index caca8d2bd6..0000000000 --- a/kbase-extension/static/kbase/js/common/lang.js +++ /dev/null @@ -1,22 +0,0 @@ -define(['bluebird'], (Promise) => { - function copyValue(obj) { - if (obj !== undefined) { - return JSON.parse(JSON.stringify(obj)); - } - } - - function pRequire(require, module) { - return new Promise((resolve, reject) => { - require(module, function () { - resolve(arguments); - }, (err) => { - reject(err); - }); - }); - } - - return Object.freeze({ - copy: copyValue, - pRequire: pRequire, - }); -}); diff --git a/kbase-extension/static/kbase/js/common/microBus.js b/kbase-extension/static/kbase/js/common/microBus.js deleted file mode 100644 index d0968264f7..0000000000 --- a/kbase-extension/static/kbase/js/common/microBus.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - * MiniBus - * A lightweight message bus implementation. - * The metaphor of the bus, is that of the hardware bus ... a set of devices - * connected over a communication channel (wires) which can send and receive - * data over the channel. - * As with hardware buses, this requires that each thing on the bus -- components -- - * need to have the smarts to deal with the bus. However, in our case those - * requirements are very minimal. - * - * bus - the object which manages the communication and to which components may send and receive messages - * message - a piece of data sent from one component to another, a plain javascript object - * envelope - an internal container for the message which acts both as a wrapper and - * place to retain message state. - * timeout - each message may provide a timeout - * - * api - * send - a component places a message onto the bus - * listen - a component requests that messages meeting a certain pattern invoke a function it provides - * - */ -define([], () => { - 'use strict'; - let instanceId = 0; - function newInstance() { - instanceId += 1; - return instanceId; - } - - function factory(config) { - let listeners = [], - queue = [], - interval = 0, - timer, - instanceId = newInstance(); - - function testListener(message, tester) { - try { - return tester(message); - } catch (ex) { - console.error('microBus test failure', ex); - return false; - } - } - function letListenerHandle(message, handle) { - try { - handle(message); - } catch (ex) { - console.error('microBus handle failure', ex); - } - } - function processPending() { - const processing = queue; - queue = []; - processing.forEach((item) => { - //console.log('processing ' + item.message.type, instanceId, item); - listeners.forEach((listener) => { - if (testListener(item.message, listener.test)) { - //console.log('handling ' + item.message.type, instanceId, item); - letListenerHandle(item.message, listener.handle); - //console.log('done ' + item.message.type, instanceId, item); - } - }); - }); - } - /* - * - * running the bus queue is as - */ - function run() { - if (timer) { - return; - } - timer = window.setTimeout(() => { - timer = null; - try { - processPending(); - } catch (ex) { - console.error('microBus processing error', ex); - } - }, interval); - } - /* - * Listening registers a function which when true given a message, - * invokes the receiving function. - * - */ - function listen(spec) { - listeners.push({ - test: spec.test, - handle: spec.handle, - }); - } - function on(type, handler) { - listeners.push({ - test: function (message) { - return type === message.type; - }, - handle: handler, - }); - } - - /* - * sending places a message into the queue (bus) - */ - function send(message, options) { - if (typeof message === 'string') { - if (!options) { - options = {}; - } - options.type = message; - message = options; - } - - queue.push({ - message: message, - envelope: { - created: new Date(), - }, - }); - run(); - } - - return { - listen: listen, - send: send, - on: on, - }; - } - - return { - make: function (config) { - return factory(config); - }, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/miniBus.js b/kbase-extension/static/kbase/js/common/miniBus.js deleted file mode 100644 index 0a120e9cb7..0000000000 --- a/kbase-extension/static/kbase/js/common/miniBus.js +++ /dev/null @@ -1,267 +0,0 @@ -/* - * MiniBus - * A lightweight message bus implementation. - * The metaphor of the bus, is that of the hardware bus ... a set of devices - * connected over a communication channel (wires) which can send and receive - * data over the channel. - * As with hardware buses, this requires that each thing on the bus -- components -- - * need to have the smarts to deal with the bus. However, in our case those - * requirements are very minimal. - * - * bus - the object which manages the communication and to which components may send and receive messages - * message - a piece of data sent from one component to another, a plain javascript object - * envelope - an internal container for the message which acts both as a wrapper and - * place to retain message state. - * timeout - each message may provide a timeout - * - * api - * send - a component places a message onto the bus - * listen - a component requests that messages meeting a certain pattern invoke a function it provides - * - */ -define(['uuid', 'bluebird'], (Uuid, Promise) => { - 'use strict'; - let instanceId = 0; - function newInstance() { - instanceId += 1; - return instanceId; - } - - function factory(config) { - let testListeners = [], - keyListeners = {}, - listenerRegistry = {}, - sendQueue = [], - requestMap = [], - interval = 0, - timer, - instanceId = newInstance(); - - function letListenerHandle(item, handle) { - try { - handle(item.message, item.envelope); - } catch (ex) { - console.error('miniBus handle failure', ex); - } - } - - function processKeyListeners(item) { - const listeners = keyListeners[item.envelope.key]; - if (!listeners) { - return; - } - listeners.forEach((listener) => { - letListenerHandle(item, listener.handle); - }); - } - - function testListener(item, tester) { - try { - return tester(item.message, item.envelope); - } catch (ex) { - console.error('miniBus test failure', ex); - return false; - } - } - function processTestListeners(item) { - testListeners.forEach((listener) => { - if (testListener(item, listener.test)) { - letListenerHandle(item, listener.handle); - } - }); - } - function processPending() { - const processingQueue = sendQueue; - sendQueue = []; - processingQueue.forEach((item) => { - if (item.envelope.key) { - processKeyListeners(item); - } else { - processTestListeners(item); - } - }); - } - /* - * - * running the bus queue is as - */ - function run() { - if (timer) { - return; - } - timer = window.setTimeout(() => { - timer = null; - try { - processPending(); - } catch (ex) { - console.error('miniBus processing error', ex); - } - }, interval); - } - /* - * Listening registers a function which when true given a message, - * invokes the receiving function. - * - * Update: for more efficient routing, it is now possible to - * specify a listen key which is used as an absolute address for - * messages. The key may be a string or an object. Objects are converted - * to a hash of sorts and then used as strings. - * - * Also, each listener now gets a unique id to allow unsubscription. - * - */ - function encodeKey(tryKey) { - if (typeof tryKey === 'object') { - return JSON.stringify(tryKey); - } - return tryKey; - } - function listen(spec) { - let id = new Uuid(4).format(), - key, - listener = { - spec: spec, - id: id, - created: new Date(), - }; - - if (spec.key) { - key = encodeKey(spec.key); - if (!keyListeners[key]) { - keyListeners[key] = []; - } - listener.handle = spec.handle; - listener.key = key; - - keyListeners[key].push(listener); - } else if (spec.test) { - listener.test = spec.test; - listener.handle = spec.handle; - testListeners.push(listener); - } - - listenerRegistry[id] = listener; - - return id; - } - - /* - * sending places a message into the queue (bus). - * - * The optional address can be used to route the message to some - * some specific key or other strategy not yet thunk up. - */ - function send(message, address) { - // support simple message sending ... - - const envelope = { - created: new Date(), - id: new Uuid(4).format(), - address: address, - }; - if (address) { - if (address.key) { - envelope.key = encodeKey(address.key); - } - } - sendQueue.push({ - message: message, - envelope: envelope, - }); - run(); - } - - /* - * - * Respond sets up a special listener, which when it matches, - * will put a return value into the bus as a new message targeted - * at the requestId - * - */ - function respond(spec) { - const originalHandle = spec.handle; - function newHandle(message, envelope) { - try { - const result = originalHandle(message); - send(result, { - key: { requestId: envelope.address.requestId }, - }); - } catch (ex) { - console.error('Error handling in respond', ex); - } - } - spec.handle = newHandle; - listen(spec); - } - - /* - * Request supports request/response style messaging. - * It accomplishes this by using a special store for messages - - * pending requests - which is a map of all request messages. - */ - function request(message, address) { - return new Promise((resolve, reject) => { - const requestId = new Uuid(4).format(); - - // when this listener with a key set to the request id - // is called, it will resolve the promise, and it is - // configured to be removed from the listeners once it - // is run, as well as to invoke the error handler upon - // timeout. (TODO) - listen({ - key: { requestId: requestId }, - once: true, - timeout: address.timeout || 10000, - handle: function (message) { - resolve(message); - }, - }); - - // NB - respond understands requestId in the envelope. - if (!address) { - address = {}; - } - address.requestId = requestId; - send(message, address); - }); - } - - // convenience strategies. - function on(type, handler) { - // listen({ - // test: function (message) { - // return (message.type === type); - // }, - // handle: handler - // }); - listen({ - key: JSON.stringify({ type: type }), - handle: handler, - }); - } - - function emit(type, message) { - if (message === undefined) { - message = {}; - } - send(message, { - key: { type: type }, - }); - } - - return { - listen: listen, - send: send, - respond: respond, - request: request, - on: on, - emit: emit, - }; - } - - return { - make: function (config) { - return factory(config); - }, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/monoBus.js b/kbase-extension/static/kbase/js/common/monoBus.js index 76146af084..23e2c66d47 100644 --- a/kbase-extension/static/kbase/js/common/monoBus.js +++ b/kbase-extension/static/kbase/js/common/monoBus.js @@ -14,39 +14,40 @@ * * */ -define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) => { +define(['uuid', 'bluebird', 'underscore', 'util/util'], (Uuid, Promise, _, Util) => { 'use strict'; - let instanceId = 0; - - function newInstance() { - instanceId += 1; - return instanceId; - } function factory(cfg) { - let api, - config = cfg || {}, + const config = cfg || {}, listenerRegistry = {}, verbose = config.verbose || false, chatty = config.chatty || false, - transientMessages = [], - requestMap = [], interval = 0, - timer, - instanceId = newInstance(), channels = {}, - doLogMessages = false, strictMode = config.strict; - function warn(message) { + let transientMessages = [], + timer; + + function warn(...messages) { if (verbose) { - console.warn(message); + console.warn(messages); } } - function log(message) { + function log(...messages) { if (chatty) { - console.log(message); + // eslint-disable-next-line no-console + console.log(messages); + } + } + + /** + * Clears all variables and the timer, if it exists + */ + function destroy() { + if (timer) { + clearTimeout(timer); } } @@ -89,8 +90,8 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) function ensureChannel(name) { if (!channels[name]) { - warn('Channel implicitly created', name); - makeChannel({ name: name }); + warn(`Channel implicitly created: ${name}`); + makeChannel({ name }); } return channels[name]; } @@ -130,11 +131,11 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } function processKeyListeners(channel, item) { - let listeners = channel.keyListeners[item.envelope.key], - handled = false; + const listeners = channel.keyListeners[item.envelope.key]; if (!listeners) { return; } + let handled = false; listeners.forEach((listener) => { handled = true; log('PROCESSING KEY LISTENER', channel, item); @@ -193,19 +194,18 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } function listen(spec) { - let id = new Uuid(4).format(), - key, + const id = new Uuid(4).format(), channelName = canonicalizeChannelName(spec.channel), channel = ensureChannel(channelName), listener = { - spec: spec, - id: id, + spec, + id, created: new Date(), - channelName: channelName, + channelName, }; if (spec.key) { - key = encodeKey(spec.key); + const key = encodeKey(spec.key); if (!channel.keyListeners[key]) { channel.keyListeners[key] = []; } @@ -236,17 +236,15 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } function removeListener(id) { - let listenerToRemove = listenerRegistry[id], - channel, - listeners, - newListeners; + const listenerToRemove = listenerRegistry[id]; if (!listenerToRemove) { return; } - channel = getChannel(listenerToRemove.channelName); + const channel = getChannel(listenerToRemove.channelName); if (!channel) { return; } + let listeners; if (listenerToRemove.key) { listeners = channel.keyListeners[listenerToRemove.key]; if (!listeners) { @@ -274,8 +272,6 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) // PROCESSING ENGINE - // PROCESSING ENGINE - function processTestListeners(channel, item) { let handled = false; channel.testListeners.forEach((listener) => { @@ -290,13 +286,13 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } function processQueueItem(item) { - let channel = getChannel(item.envelope.channel), - handled; + const channel = getChannel(item.envelope.channel); if (!channel) { return; } + let handled; if (item.envelope.listenerId) { log('PROCESSING BY LISTENER ID', channel, item); handled = processListener(channel, item); @@ -345,27 +341,26 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) // SENDING function setPersistentMessage(message, envelope) { - let channel = ensureChannel(envelope.channel), - key = envelope.key, - existingMessage; + const channel = ensureChannel(envelope.channel), + key = envelope.key; if (!key) { throw new Error('Persistent messages require a key'); } - existingMessage = channel.persistentMessages[key]; + const existingMessage = channel.persistentMessages[key]; if (existingMessage) { - if (utils.isEqual(existingMessage.message, message)) { + if (_.isEqual(existingMessage.message, message)) { return; } } channel.persistentMessages[key] = { - message: message, - envelope: envelope, + message, + envelope, }; transientMessages.push({ - message: message, - envelope: envelope, + message, + envelope, }); run(); } @@ -375,11 +370,11 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) if (!persistentMessage) { return; } - const envelope = lang.copy(persistentMessage.envelope); + const envelope = Util.copy(persistentMessage.envelope); envelope.listenerId = id; transientMessages.push({ message: persistentMessage.message, - envelope: envelope, + envelope, }); run(); } @@ -397,7 +392,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) const envelope = { created: new Date(), id: new Uuid(4).format(), - address: address, + address, }; if (address.key) { @@ -409,8 +404,8 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) log('SEND', message, envelope); transientMessages.push({ - message: message, - envelope: envelope, + message, + envelope, }); run(); } @@ -422,7 +417,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) const envelope = { created: new Date(), id: new Uuid(4).format(), - address: address, + address, }; if (!address.key) { @@ -439,12 +434,11 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } function get(spec, defaultValue) { - let key, - channelName = canonicalizeChannelName(spec.channel), + const channelName = canonicalizeChannelName(spec.channel), channel = ensureChannel(channelName); if (spec.key) { - key = encodeKey(spec.key); + const key = encodeKey(spec.key); const persistentMessage = channel.persistentMessages[key]; if (!persistentMessage) { @@ -452,17 +446,12 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } return persistentMessage.message; - } else if (spec.test) { - // TODO - // listener.test = spec.test; - // listener.handle = spec.handle; - // channel.testListeners.push(listener); - warn('Not current support for test filtering on get'); - return defaultValue; - } else { - warn('listen: nothing to listen on (test or key)'); - return defaultValue; } + const msg = spec.test + ? 'No current support for test filtering on get' + : 'listen: nothing to listen on (test or key)'; + warn(msg); + return defaultValue; } /* @@ -497,7 +486,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) * pending requests - which is a map of all request messages. */ function request(message, address) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const requestId = new Uuid(4).format(); // when this listener with a key set to the request id @@ -507,7 +496,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) // timeout. (TODO) listen({ channel: address.channel, - key: { requestId: requestId }, + key: { requestId }, once: true, timeout: address.timeout || 10000, handle: function (responseMessage) { @@ -524,24 +513,10 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) }); } - // function get(address) { - // return new Promise(function(resolve, reject) { - // console.log('GOT? request with', address); - // listen({ - // channel: address.channel, - // key: address.key, - // // once: true, - // timeout: address.timeout || 10000, - // handle: function(message) { - // resolve(message); - // } - // }); - // }); - // } function plisten(spec) { let initialized = false; let id; - const p = new Promise((resolve) => { + const promise = new Promise((resolve) => { id = listen({ channel: spec.channel, key: spec.key, @@ -560,8 +535,8 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) }); }); return { - promise: p, - id: id, + promise, + id, }; } @@ -590,8 +565,8 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) // convenience strategies. function on(type, handler, channel) { return listen({ - channel: channel, - key: JSON.stringify({ type: type }), + channel, + key: JSON.stringify({ type }), handle: handler, }); } @@ -601,18 +576,10 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) message = {}; } send(message, { - key: { type: type }, + key: { type }, }); } - function logMessages(doLog) { - if (doLog) { - doLogMessages = true; - } else { - doLogMessages = false; - } - } - // CHANNEL BUS /* @@ -636,7 +603,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) function on(type, handler) { return listen({ channel: channelName, - key: { type: type }, + key: { type }, handle: handler, }); } @@ -647,7 +614,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } return send(message, { channel: channelName, - key: { type: type }, + key: { type }, }); } @@ -667,7 +634,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) const address = { channel: channelName, key: { - type: type, + type, }, }; return set(message, address); @@ -704,7 +671,7 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) function channelWhen(type) { return when({ channel: channelName, - key: { type: type }, + key: { type }, }); } @@ -733,21 +700,21 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } return { - on: on, - emit: emit, + on, + emit, set: channelSet, set2: channelSet2, get: channelGet, - bus: bus, + bus, listen: channelListen, send: channelSend, respond: channelRespond, request: channelRequest, when: channelWhen, plisten: channelPlisten, - stop: stop, - channelName: channelName, - stats: stats, + stop, + channelName, + stats, }; } @@ -823,17 +790,17 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } return { - on: on, - emit: emit, - listen: listen, - send: send, - respond: respond, - request: request, - plisten: plisten, - set: set, - get: get, - when: when, - stats: stats, + on, + emit, + listen, + send, + respond, + request, + plisten, + set, + get, + when, + stats, }; } @@ -862,10 +829,10 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) } return { - channel: channel, - genName: genName, - stats: stats, - stop: stop, + channel, + genName, + stats, + stop, // global listeners, etc. listen: connectionListen, }; @@ -875,23 +842,23 @@ define(['uuid', 'bluebird', './lang', './unodep'], (Uuid, Promise, lang, utils) makeChannel({ name: 'default', description: 'The Default Channel' }); // API - api = { - listen: listen, - send: send, - respond: respond, - request: request, - on: on, - emit: emit, - set: set, - plisten: plisten, - when: when, - makeChannelBus: makeChannelBus, - makeChannel: makeChannel, - removeChannel: removeChannel, - logMessages: logMessages, - removeListener: removeListener, - removeListeners: removeListeners, - connect: connect, + const api = { + listen, + send, + respond, + request, + on, + emit, + set, + plisten, + when, + makeChannelBus, + makeChannel, + removeChannel, + removeListener, + removeListeners, + connect, + destroy, }; return api; diff --git a/kbase-extension/static/kbase/js/common/parameterSpec.js b/kbase-extension/static/kbase/js/common/parameterSpec.js index e200b28f49..19a36685c5 100644 --- a/kbase-extension/static/kbase/js/common/parameterSpec.js +++ b/kbase-extension/static/kbase/js/common/parameterSpec.js @@ -1,13 +1,10 @@ -/*jstlint white:true,browser:true*/ - define([], () => { 'use strict'; function factory(config) { const spec = config.parameterSpec, multiple = spec.allow_multiple ? true : false, - _required = spec.optional ? false : true, - isOutputName = spec.text_options && spec.text_options.is_output_name; + _required = spec.optional ? false : true; function id() { return spec.id; @@ -31,14 +28,6 @@ define([], () => { function info() { return 'info disabled for now'; - // return table({class: 'table table-striped'}, [ - // tr([ - // th('Types'), - // tr(td(table({class: 'table'}, spec.text_options.valid_ws_types.map(function (type) { - // return tr(td(type)); - // })))) - // ]) - // ]); } function multipleItems() { @@ -60,16 +49,6 @@ define([], () => { return false; } - function customTextSubdata() { - // try dispatching on name... - switch (spec.id) { - case 'input_property_x': - return 'sample_property'; - case 'input_property_y': - return 'sample_property'; - } - } - function dataType() { /* * Special case here -- @@ -91,24 +70,16 @@ define([], () => { return '[]string'; } return 'string'; - //var custom = customTextSubdata(); - //if (custom) { - // return custom; - //} case 'custom_button': - switch (spec.id) { - case 'input_check_other_params': - return 'boolean'; - default: - return 'unspecified'; + if (spec.id === 'input_check_other_params') { + return 'boolean'; } + return 'unspecified'; case 'custom_widget': if (spec.dropdown_options) { return '[]string'; } break; - // case 'reads_group_editor': - // return 'reads_group_editor'; } /* @@ -141,14 +112,11 @@ define([], () => { // Okay, if it has no specific type assigned (validate_as), and is // not flagged from the various properties above by grousing through // the text_options, we assume it is a string. - - switch (spec.field_type) { - case 'text': - if (spec.allow_multiple) { - return '[]string'; - } else { - return 'string'; - } + if (spec.field_type === 'text') { + if (spec.allow_multiple) { + return '[]string'; + } + return 'string'; } return 'unspecified'; @@ -175,21 +143,19 @@ define([], () => { /* * Default values are strings. */ - function defaultToNative(defaultValue) { + function defaultToNative(_defaultValue) { switch (dataType()) { - case 'string': - return defaultValue; case 'int': - return parseInt(defaultValue); + return parseInt(_defaultValue); case 'float': - return parseFloat(defaultValue); - case 'workspaceObjectName': - return defaultValue; + return parseFloat(_defaultValue); case 'boolean': - return coerceToBoolean(defaultValue); + return coerceToBoolean(_defaultValue); + case 'string': + case 'workspaceObjectName': default: // Assume it is a string... - return defaultValue; + return _defaultValue; } } @@ -231,23 +197,18 @@ define([], () => { const defaultValues = spec.default_values; // No default value and not required? null value - // special special cases. - switch (spec.field_type) { - case 'checkbox': - /* - * handle the special case of a checkbox with no or empty - * default value. It will promote to the "unchecked value" - * TODO: more cases of bad default value? Or a generic - * default value validator? - */ - if (!defaultValues || defaultValues.length === 0) { - return spec.checkbox_options.unchecked_value; - } else { - return coerceToIntBoolean(defaultValues[0]); - } - case 'custom_textsubdata': - if (!defaultValues) { - } + if (spec.field_type === 'checkbox') { + /* + * handle the special case of a checkbox with no or empty + * default value. It will promote to the "unchecked value" + * TODO: more cases of bad default value? Or a generic + * default value validator? + */ + if (!defaultValues || defaultValues.length === 0) { + return spec.checkbox_options.unchecked_value; + } else { + return coerceToIntBoolean(defaultValues[0]); + } } if (!defaultValues && !required()) { @@ -266,31 +227,22 @@ define([], () => { if (!multipleItems()) { return defaultToNative(defaultValues[0]); } else { - return defaultValues.map((defaultValue) => { - return defaultToNative(defaultValue); + return defaultValues.map((_defaultValue) => { + return defaultToNative(_defaultValue); }); } } function isEmpty(value) { - if (value === undefined) { + if (value === undefined || value === null) { return true; } - if (value === null) { + if ( + (dataType() === 'string' || dataType() === 'workspaceObjectName') && + value.length === 0 + ) { return true; } - switch (dataType()) { - case 'string': - if (value.length === 0) { - return true; - } - break; - case 'workspaceObjectName': - if (value.length === 0) { - return true; - } - break; - } return false; } @@ -315,12 +267,6 @@ define([], () => { case 'text': switch (fieldType) { case 'text': - return { - required: required(), - defaultValue: defaultValue(), - min: spec.text_options ? spec.text_options.min_length : null, - max: spec.text_options ? spec.text_options.max_length : null, - }; case 'autocomplete': return { required: required(), @@ -342,65 +288,32 @@ define([], () => { throw new Error('Unknown text param field type'); } case 'int': - switch (fieldType) { - case 'text': - return {}; - case 'checkbox': - return {}; - default: - return {}; - } case 'float': return {}; case 'workspaceObjectName': - switch (paramClass()) { - case 'input': - return { - required: required(), - types: spec.text_options.valid_ws_types, - defaultValue: defaultValue(), - }; - case 'output': - return { - required: required(), - types: spec.text_options.valid_ws_types, - defaultValue: defaultValue(), - }; - case 'parameter': - return { - required: required(), - types: spec.text_options.valid_ws_types, - defaultValue: defaultValue(), - }; - default: - throw new Error('Unknown workspaceObjectName ui class'); + if (['input', 'output', 'parameter'].includes(paramClass())) { + return { + required: required(), + types: spec.text_options.valid_ws_types, + defaultValue: defaultValue(), + }; } + throw new Error('Unknown workspaceObjectName ui class'); case '[]workspaceObjectName': - switch (paramClass()) { - case 'input': - return { - required: required(), - types: spec.text_options.valid_ws_types, - defaultValues: defaultValue(), - }; - case 'parameter': - return { - required: required(), - types: spec.text_options.valid_ws_types, - defaultValues: defaultValue(), - }; - default: - throw new Error('Unknown []workspaceObjectName ui class'); + if (['input', 'parameter'].includes(paramClass())) { + return { + required: required(), + types: spec.text_options.valid_ws_types, + defaultValue: defaultValue(), + }; } + throw new Error('Unknown []workspaceObjectName ui class'); case '[]string': case '[]text': switch (fieldType) { case 'dropdown': return {}; case 'text': - return { - required: required(), - }; case 'textarea': return { required: required(), @@ -428,7 +341,6 @@ define([], () => { // Used to generate a description for each item. Becomes the "desc". displayTemplate: spec.subdata_selection.description_template, }; - break; case 'xxinput_property_x': return { defaultValue: defaultValue(), @@ -459,7 +371,6 @@ define([], () => { }, }, }; - break; case 'sample_property': return { required: required(), @@ -471,23 +382,22 @@ define([], () => { map: function (subdata) { const collected = {}; Object.keys(subdata).forEach((key) => { - let id, - name, - column = subdata[key]; + let _id, _name; + const column = subdata[key]; column.forEach((value) => { if ( value.category === 'DataSeries' && value.property_name === 'SeriesID' ) { - id = value.property_value; + _id = value.property_value; } else if ( value.category === 'Property' && value.property_name === 'Name' ) { - name = value.property_value; + _name = value.property_value; } - if (id && name) { - collected[id] = name; + if (_id && _name) { + collected[_id] = _name; } }); }); @@ -508,7 +418,6 @@ define([], () => { }); }, }; - break; case 'unspecified': // a bunch of field types are untyped: switch (fieldType) { @@ -592,8 +501,6 @@ define([], () => { // The ui_class is really just for the man page and app cell ui organization, so it is relatively minor // to override it with is_output_name which is actually functional! if (spec.text_options && spec.text_options.is_output_name) { - // console.error('Parameter ' + spec.id + ' is a parameter type, but has text_options.is_output_name specified', spec); - //throw new Error('Parameter ' + spec.id + ' is a parameter type, but has text_options.is_output_name specified'); paramClassName = 'output'; } break; diff --git a/kbase-extension/static/kbase/js/common/props.js b/kbase-extension/static/kbase/js/common/props.js index 2909cd4d02..df3122c8f3 100644 --- a/kbase-extension/static/kbase/js/common/props.js +++ b/kbase-extension/static/kbase/js/common/props.js @@ -20,8 +20,8 @@ define([], () => { return; } // pop off the last property for setting at the end. - let propKey = path.pop(), - key, + const propKey = path.pop(); + let key, temp = data; // Walk the path, creating empty objects if need be. while (path.length > 0) { @@ -46,8 +46,8 @@ define([], () => { return; } // pop off the last property for setting at the end. - let propKey = path.pop(), - key, + const propKey = path.pop(); + let key, temp = data; // Walk the path, creating empty objects if need be. while (path.length > 0) { @@ -117,14 +117,14 @@ define([], () => { if (!config) { config = {}; } + const updateHandler = config.onUpdate, + historyEnabled = updateHandler ? true : false; + let obj = config.data || {}, lastObj, historyCount = 0, - updateHandler = config.onUpdate, - historyEnabled = updateHandler ? true : false, lastValueSaved = false, - timer, - api; + timer; /* * In enabled by setting an update handler via the onUpdate factory @@ -242,8 +242,8 @@ define([], () => { return; } increment = increment === undefined ? 1 : increment; - let propKey = path.pop(), - key, + const propKey = path.pop(); + let key, temp = obj; while (path.length > 0) { key = path.shift(); @@ -273,8 +273,8 @@ define([], () => { if (path.length === 0) { return; } - let propKey = path.pop(), - key, + const propKey = path.pop(); + let key, temp = obj; while (path.length > 0) { key = path.shift(); @@ -289,23 +289,27 @@ define([], () => { return true; } - api = { - setItem: setItem, - hasItem: hasItem, - getItem: getItem, - copyItem: copyItem, - incrItem: incrItem, - deleteItem: deleteItem, - pushItem: pushItem, - popItem: popItem, - reset: reset, - getRawObject: function () { - return obj; - }, - getLastRawObject: function () { - return lastObj; - }, - getHistoryCount: getHistoryCount, + function getRawObject() { + return obj; + } + + function getLastRawObject() { + return lastObj; + } + + const api = { + setItem, + hasItem, + getItem, + copyItem, + incrItem, + deleteItem, + pushItem, + popItem, + reset, + getRawObject, + getLastRawObject, + getHistoryCount, }; return api; } @@ -314,9 +318,9 @@ define([], () => { make: function (config) { return factory(config); }, - getDataItem: getDataItem, - setDataItem: setDataItem, - pushDataItem: pushDataItem, - popDataItem: popDataItem, + getDataItem, + setDataItem, + pushDataItem, + popDataItem, }; }); diff --git a/kbase-extension/static/kbase/js/common/pythonInterop.js b/kbase-extension/static/kbase/js/common/pythonInterop.js index d80754b5de..38cbc38065 100644 --- a/kbase-extension/static/kbase/js/common/pythonInterop.js +++ b/kbase-extension/static/kbase/js/common/pythonInterop.js @@ -1,5 +1,6 @@ define([], () => { 'use strict'; + const indentString = ' '; function escapeString(stringValue, delimiter) { const delimiterRegex = new RegExp(delimiter, 'g'); @@ -13,7 +14,6 @@ define([], () => { return "'"; } - const indentString = ' '; function makeIndent(level) { let retval = ''; for (let i = 0; i < level; i += 1) { @@ -21,14 +21,12 @@ define([], () => { } return retval; } + function pythonifyValue(value, options, indentLevel) { options = options || {}; indentLevel = indentLevel || 0; switch (typeof value) { case 'number': - if (value === null) { - return 'None'; - } return String(value); case 'string': return '"' + escapeString(value, options.delimiter || autoDelimiter(value)) + '"'; @@ -49,13 +47,12 @@ define([], () => { if (value === null) { return 'None'; } - var prefix = makeIndent(indentLevel + 1); return ( '{\n' + Object.keys(value) .map((key) => { return ( - prefix + + makeIndent(indentLevel + 1) + pythonifyValue(key, options) + ': ' + pythonifyValue(value[key], options, indentLevel + 1) @@ -89,22 +86,32 @@ define([], () => { }); } + /** + * Builds a "nice" list of args by adding indentations and returns between each. + * E.g., turns ["foo", "bar", "baz"] into: + * " + * foo, + * bar, + * baz + * " + * @param {array} args - the array of arguments to make pretty + * @returns + */ function buildNiceArgsList(args) { const indent = indentString; return '\n' + indent + args.join(',\n' + indent) + '\n'; } function buildBatchAppRunner(cellId, runId, app, params) { - let paramSetName = 'batch_params', + const paramSetName = 'batch_params', pythonifiedParams = pythonifyValue(params, { autoIndent: true }), - positionalArgs = [pythonifyValue(app.id), paramSetName], namedArgs = objectToNamedArgs({ tag: app.tag, version: app.version, cell_id: cellId, run_id: runId, }); - positionalArgs = positionalArgs.concat(namedArgs); + const positionalArgs = [pythonifyValue(app.id), paramSetName].concat(namedArgs); return [ paramSetName + ' = ' + pythonifiedParams, 'from biokbase.narrative.jobs.appmanager import AppManager', @@ -244,16 +251,53 @@ define([], () => { return pythonCode; } + /** + * Builds the call to run_app_bulk + * @param {string} cellId the unique id of the cell, for run metadata + * @param {string} runId the unique id of the run, for metadata + * @param {array} appInfo the set of information to send to the function. + * This should have the following format: + * [{ + * app_id: 'MyModule/my_app', + * tag: 'release' (or 'beta' or 'dev'), + * version: '1.2.3' (or a git hash if not released), + * params: [{ + * param1: value1, + * param2: value2, + * }, { + * param1: value3, + * param2: value4 + * }] + * }, ...repeat the above] + * Each app gets its own entry in the list, and each object in the params list + * is an individual run of that app. + * @returns string - the Python code to run + */ + function buildBulkAppRunner(cellId, runId, appInfo) { + const args = [ + pythonifyValue(appInfo, {}, 1), + ...objectToNamedArgs({ + cell_id: cellId, + run_id: runId, + }), + ]; + return [ + 'from biokbase.narrative.jobs.appmanager import AppManager', + `AppManager().run_app_bulk(${buildNiceArgsList(args)})`, + ].join('\n'); + } + return { - objectToNamedArgs: objectToNamedArgs, - pythonifyValue: pythonifyValue, - buildAppRunner: buildAppRunner, - buildBatchAppRunner: buildBatchAppRunner, - buildEditorRunner: buildEditorRunner, - buildViewRunner: buildViewRunner, - buildAdvancedViewRunner: buildAdvancedViewRunner, - buildOutputRunner: buildOutputRunner, - buildCustomWidgetRunner: buildCustomWidgetRunner, - buildDataWidgetRunner: buildDataWidgetRunner, + objectToNamedArgs, + pythonifyValue, + buildAppRunner, + buildBatchAppRunner, + buildEditorRunner, + buildViewRunner, + buildAdvancedViewRunner, + buildOutputRunner, + buildCustomWidgetRunner, + buildDataWidgetRunner, + buildBulkAppRunner, }; }); diff --git a/nbextensions/appCell2/widgets/runClock.js b/kbase-extension/static/kbase/js/common/runClock.js similarity index 66% rename from nbextensions/appCell2/widgets/runClock.js rename to kbase-extension/static/kbase/js/common/runClock.js index 4f629f54f3..da4db8f191 100644 --- a/nbextensions/appCell2/widgets/runClock.js +++ b/kbase-extension/static/kbase/js/common/runClock.js @@ -1,25 +1,23 @@ -define(['bluebird', 'common/runtime', 'kb_common/html', 'common/format'], ( +define(['bluebird', 'common/runtime', 'common/html', 'common/format'], ( Promise, Runtime, html, format ) => { + 'use strict'; const t = html.tag, span = t('span'); - function factory(config) { - var config = config || {}, - container, - runtime = Runtime.make(), + function factory(config = {}) { + const runtime = Runtime.make(), busConnection = runtime.bus().connect(), channel = busConnection.channel('default'), - clockId = html.genId(), - startTime; + clockId = html.genId(); + let container, startTime; function buildLayout() { return span({ id: clockId, - style: {}, }); } @@ -28,9 +26,8 @@ define(['bluebird', 'common/runtime', 'kb_common/html', 'common/format'], ( return; } const now = new Date(), - elapsed = now.getTime() - startTime; - - const clockNode = document.getElementById(clockId); + elapsed = now.getTime() - startTime, + clockNode = document.getElementById(clockId); if (config.on && config.on.tick) { try { @@ -43,18 +40,19 @@ define(['bluebird', 'common/runtime', 'kb_common/html', 'common/format'], ( console.error('Error handling clock tick, closing clock', err); stop(); } - } else { - if (!clockNode) { - console.warn('Could not find clock node at' + clockId, 'Stopping the clock'); - stop(); - return; - } - clockNode.innerHTML = [ - config.prefix || '', - format.niceDuration(elapsed), - config.suffix || '', - ].join(''); + return; + } + + if (!clockNode) { + console.warn(`Could not find clock node at ${clockId}: stopping the clock.`); + stop(); + return; } + clockNode.innerHTML = [ + config.prefix || '', + format.niceDuration(elapsed), + config.suffix || '', + ].join(''); } function start(arg) { diff --git a/kbase-extension/static/kbase/js/common/runtime.js b/kbase-extension/static/kbase/js/common/runtime.js index 8e7d2684c7..efc15b9977 100644 --- a/kbase-extension/static/kbase/js/common/runtime.js +++ b/kbase-extension/static/kbase/js/common/runtime.js @@ -9,27 +9,43 @@ define([ 'use strict'; const narrativeConfig = Props.make({ data: Config.getConfig() }); - function factory(config) { + function factory(config = {}) { + const busArgs = config.bus || {}; + let clock, theBus; + function createRuntime() { - const bus = Bus.make(); + theBus = Bus.make(busArgs); - const clock = Clock.make({ - bus: bus, + clock = Clock.make({ + bus: theBus, resolution: 1000, }); clock.start(); $(document).on('dataUpdated.Narrative', () => { - bus.emit('workspace-changed'); + theBus.emit('workspace-changed'); }); return { created: new Date(), - bus: bus, + bus: theBus, env: Props.make({}), + clock, }; } + function destroy() { + if (clock) { + clock.stop(); + } + $(document).off('dataUpdated.Narrative'); + if (theBus) { + theBus.destroy(); + } + theBus = null; + window.kbaseRuntime = null; + } + /* * The runtime hooks into a window */ @@ -58,12 +74,11 @@ define([ } function getUserSetting(settingKey, defaultValue) { - let settings = Jupyter.notebook.metadata.kbase.userSettings, - setting; + const settings = Jupyter.notebook.metadata.kbase.userSettings; if (!settings) { return defaultValue; } - setting = settings[settingKey]; + const setting = settings[settingKey]; if (setting === undefined) { return defaultValue; } @@ -92,14 +107,15 @@ define([ } return { - authToken: authToken, + authToken, config: getConfig, - bus: bus, - getUserSetting: getUserSetting, - setEnv: setEnv, - getEnv: getEnv, - workspaceId: workspaceId, - userId: userId, + bus, + getUserSetting, + setEnv, + getEnv, + workspaceId, + userId, + destroy, }; } diff --git a/kbase-extension/static/kbase/js/common/sdk.js b/kbase-extension/static/kbase/js/common/sdk.js index 61252d793d..1115847eef 100644 --- a/kbase-extension/static/kbase/js/common/sdk.js +++ b/kbase-extension/static/kbase/js/common/sdk.js @@ -635,14 +635,13 @@ define(['common/props'], (Props) => { let defaultValue; let nullValue; - let zeroValue; nullValue = null; defaultValue = {}; Object.keys(groupParams).forEach((id) => { defaultValue[id] = groupParams[id].data.defaultValue; }); - zeroValue = defaultValue; + const zeroValue = defaultValue; const structSpec = { id: group.id, @@ -745,8 +744,7 @@ define(['common/props'], (Props) => { function convertAppSpec(sdkAppSpec) { // Parameters - let parameterSpecs = {}, - parameterLayout; + const parameterSpecs = {}; // First convert all parameters @@ -770,7 +768,7 @@ define(['common/props'], (Props) => { // first filter out the paramters which have been moved into groups, // and then add the groups in. - parameterLayout = sdkAppSpec.parameters + const parameterLayout = sdkAppSpec.parameters .filter((parameter) => { if (parameterSpecs[parameter.id]) { return true; @@ -823,6 +821,6 @@ define(['common/props'], (Props) => { } return { - convertAppSpec: convertAppSpec, + convertAppSpec, }; }); diff --git a/kbase-extension/static/kbase/js/common/spec.js b/kbase-extension/static/kbase/js/common/spec.js index 9de85d1f13..ef60d0b922 100644 --- a/kbase-extension/static/kbase/js/common/spec.js +++ b/kbase-extension/static/kbase/js/common/spec.js @@ -2,14 +2,12 @@ * Provides app spec functionality. */ -define([ - 'require', - 'bluebird', - 'common/lang', - 'common/sdk', - 'common/specValidation', - 'widgets/appWidgets2/validators/resolver', -], (require, Promise, lang, sdk, Validation, validationResolver) => { +define(['bluebird', 'util/util', 'common/sdk', 'widgets/appWidgets2/validators/resolver'], ( + Promise, + Util, + sdk, + validationResolver +) => { 'use strict'; function factory(config) { @@ -27,82 +25,87 @@ define([ return spec; } - /* - * Make a "shell" model based on the spec. Recursively build an object - * with properties as defined by the spec. - * Effectively this means that only the top level is represented, since + /** + * Makes a 'model' object (not a view model yet, really, because it's just data) from the + * given spec. + * It does this by: + * The top level spec is treated as a struct. + * The default value for each paraemter is simply set as the value for the given parameter + * on a model object. + * If appType = "bulkImport", this is further separated into "filePaths" and "params" + * sub-objects. + * @param {string} appType */ - function makeEmptyModel() { - const model = {}; - spec.parameters.layout.forEach((id) => { - model[id] = - spec.parameters.specs[id].data.defaultValue || - spec.parameters.specs[id].data.nullValue; - }); - return model; - } - - /* - Makes a model (not a view model quite yet, really, because just data) - from the given spec. - It does this by: - The top level spec is treated as a struct. - The default value for each paramater is simply set as the value for the given parameter - on a model object. - One exception is that if a parameter is a - */ - function makeDefaultedModel() { + function makeDefaultedModel(appType) { const model = {}; + if (appType === 'bulkImport') { + model['params'] = {}; + } spec.parameters.layout.forEach((id) => { const paramSpec = spec.parameters.specs[id]; let modelValue; if (paramSpec.data.type === 'struct') { if (paramSpec.data.constraints.required) { - modelValue = lang.copy(paramSpec.data.defaultValue); + modelValue = Util.copy(paramSpec.data.defaultValue); } else { modelValue = paramSpec.data.nullValue; } } else { - modelValue = lang.copy(paramSpec.data.defaultValue); + modelValue = Util.copy(paramSpec.data.defaultValue); + } + if (appType === 'bulkImport') { + model.params[id] = modelValue; + } else { + model[id] = modelValue; } - model[id] = modelValue; }); return model; } - const typeToValidatorModule = { - string: 'text', - int: 'int', - float: 'float', - sequence: 'sequence', - struct: 'struct', - }; - - function getValidatorModule(fieldSpec) { - const moduleName = typeToValidatorModule[fieldSpec.data.type]; - if (!moduleName) { - throw new Error('No validator for type: ' + fieldSpec.data.type); - } - return moduleName; + /** + * Validates the model (object that maps from parameter id to current value) against + * this spec. This returns a Promise that resolves into a map from parameter ids to + * validations. + * @param {object} model the object containing the data model to validate + * should have key-value pairs for each parameter id. + * @returns Promise that resolves into a mapping from parameter id -> validation + * structure + */ + function validateModel(model) { + return validateParams(spec.parameters.layout, model, {}); } - function validateModel(model) { - // TODO: spec at the top level should be a struct... - // return; + /** + * A trimmed version of validateModel that's specific for a few params. + * Given an array of parameter ids and an object with key-value pairs from + * paramId -> value, validate the set. Only the given parameter ids are validated. + * Any others are ignored. + * + * This returns a Promise that resolves into key-value pairs of parameter id -> + * validation response. + * @param {array} paramIds - the array of parameter ids to validate + * @param {object} values - an object where the key is the parameter id, and the value has + * both the value, and arbitrary options to be passed to the specific validator. + * @param {object} options + */ + function validateParams(paramIds, values, options) { const validationMap = {}; - spec.parameters.layout.forEach((id) => { - const fieldValue = model[id]; - const fieldSpec = spec.parameters.specs[id]; - validationMap[id] = validationResolver.validate(fieldValue, fieldSpec); + options = options || {}; + paramIds.forEach((id) => { + validationMap[id] = validationResolver.validate( + values[id], + spec.parameters.specs[id], + options[id] || {} + ); }); return Promise.props(validationMap); } return Object.freeze({ - getSpec: getSpec, - makeEmptyModel: makeEmptyModel, - makeDefaultedModel: makeDefaultedModel, - validateModel: validateModel, + getSpec, + makeDefaultedModel, + validateModel, + validateParams, }); } diff --git a/kbase-extension/static/kbase/js/common/specValidation.js b/kbase-extension/static/kbase/js/common/specValidation.js deleted file mode 100644 index 70c0768319..0000000000 --- a/kbase-extension/static/kbase/js/common/specValidation.js +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Provides app spec functionality. - */ - -define([], () => { - 'use strict'; - - function toInteger(value) { - switch (typeof value) { - case 'number': - if (value !== Math.floor(value)) { - throw new Error('Integer is a non-integer number'); - } - return value; - case 'string': - if (value.match(/^[-+]?[\d]+$/)) { - return parseInt(value, 10); - } - throw new Error('Invalid integer format'); - default: - throw new Error('Type ' + typeof value + ' cannot be converted to integer'); - } - } - - function isEmptyString(value) { - if (value === null) { - return true; - } - if (typeof value === 'string' && value.trim() === '') { - return true; - } - return false; - } - - function validateWorkspaceObjectNameString(spec, value) { - let parsedValue, - messageId, - shortMessage, - errorMessage, - diagnosis = 'valid'; - - if (typeof value !== 'string') { - diagnosis = 'invalid'; - errorMessage = 'value must be a string in workspace object name format'; - } else { - parsedValue = value.trim(); - if (!parsedValue) { - if (spec.data.constraints.required) { - messageId = 'required-missing'; - diagnosis = 'required-missing'; - errorMessage = 'value is required'; - } else { - diagnosis = 'optional-empty'; - } - } else if (/\s/.test(parsedValue)) { - messageId = 'obj-name-no-spaces'; - diagnosis = 'invalid'; - errorMessage = 'an object name may not contain a space'; - } else if (/^[+\-]*\d+$/.test(parsedValue)) { - messageId = 'obj-name-not-integer'; - diagnosis = 'invalid'; - errorMessage = 'an object name may not be in the form of an integer'; - } else if (!/^[A-Za-z0-9|\.|\||_\-]+$/.test(parsedValue)) { - messageId = 'obj-name-invalid-characters'; - diagnosis = 'invalid'; - errorMessage = - 'one or more invalid characters detected; an object name may only include alphabetic characters, numbers, and the symbols "_", "-", ".", and "|"'; - } else if (parsedValue.length > 255) { - messageId = 'obj-name-too-long'; - diagnosis = 'invalid'; - errorMessage = 'an object name may not exceed 255 characters in length'; - } - } - return { - isValid: errorMessage ? false : true, - messageId: messageId, - errorMessage: errorMessage, - shortMessage: shortMessage, - diagnosis: diagnosis, - value: value, - parsedValue: parsedValue, - }; - } - - function validateString(spec, value) { - let parsedValue, - errorMessage, - diagnosis = 'valid', - c = spec.data.constraints, - regexp = c.regexp ? new RegExp(c.regexp) : false; - - if (c.type) { - switch (c.type) { - case 'WorkspaceObjectName': - return validateWorkspaceObjectNameString(spec, value); - } - } - - if (isEmptyString(value)) { - parsedValue = ''; - if (c.required) { - diagnosis = 'required-missing'; - errorMessage = 'value is required'; - } else { - diagnosis = 'optional-empty'; - } - } else if (typeof value !== 'string') { - diagnosis = 'invalid'; - errorMessage = 'value must be a string (it is of type "' + typeof value + '")'; - } else { - // parsedValue = value.trim(); - parsedValue = value; - if (parsedValue.length < c.min) { - diagnosis = 'invalid'; - errorMessage = 'the minimum length for this parameter is ' + c.min; - } else if (parsedValue.length > c.max) { - diagnosis = 'invalid'; - errorMessage = 'the maximum length for this parameter is ' + c.max; - } else if (regexp && !regexp.test(parsedValue)) { - diagnosis = 'invalid'; - errorMessage = - 'The text value did not match the regular expression constraint ' + c.regexp; - } else { - diagnosis = 'valid'; - } - } - - return { - isValid: errorMessage ? false : true, - errorMessage: errorMessage, - diagnosis: diagnosis, - value: value, - parsedValue: parsedValue, - }; - } - - function validateStruct(spec, value) { - let parsedValue, - errorMessage, - diagnosis = 'valid', - c = spec.data.constraints; - - // make sure it is a plain object - - // is it empty? what does it mean for it to be empty? - // each member is empty. - - // use the spec to validate each member - const result = {}; - - spec.parameters.layout.forEach((id) => { - const fieldValue = value[id]; - const fieldSpec = spec.parameters.specs[id]; - result[id] = { - id: id, - result: validateModel(fieldSpec, fieldValue), - }; - }); - - return result; - } - - function validateStructList(spec, value) { - let parsedValue, - errorMessage, - diagnosis = 'valid', - c = spec.data.constraints; - - // make sure it is a plain object - - console.log('validataing struct list', spec, value); - - // is it empty? what does it mean for it to be empty? - // each member is empty. - let empty = false; - if (value === undefined || value === null) { - empty = true; - } else if (value.length === 0) { - empty = true; - } - if (empty) { - if (spec.data.constraints.required) { - return { - isValid: false, - errorMessage: 'Required but empty', - diagnosis: 'required-missing', - value: value, - parsedValue: spec.nullValue, - }; - } else { - return { - isValid: true, - diagnosis: 'empty-optional', - value: value, - parsedValue: spec.nullValue, - }; - } - } - - // use the spec to validate each member - - // validate eacn item in the value. - - const results = value.map((item) => { - // yes, the spec for a struct list is identical (for now) to - // a struct. - // TODO: we need an ordered set type to wrap the struct!!!!!! - return validateStruct(spec.parameters.specs.item, item); - }); - - return results; - } - - function validateTrue(value) { - return { - isValid: true, - errorMessage: null, - diagnosis: 'valid', - value: value, - parsedValue: value, - }; - } - - function resolveValidator(spec) { - let fun; - switch (spec.data.type) { - case 'text': - case 'string': - fun = validateString; - break; - case 'struct': - fun = validateStruct; - break; - case '[]struct': - fun = validateStructList; - break; - default: - console.warn('no validator for ', spec); - fun = validateTrue; - } - - return function (value) { - return fun(spec, value); - }; - } - - function validateModel(spec, value) { - const validator = resolveValidator(spec), - result = validator(value); - - return result; - } - - function factory(config) { - return Object.freeze({ - validateModel: validateModel, - }); - } - - return { - make: function (config) { - return factory(config); - }, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/ui.js b/kbase-extension/static/kbase/js/common/ui.js index 2252836310..14d8f03dda 100644 --- a/kbase-extension/static/kbase/js/common/ui.js +++ b/kbase-extension/static/kbase/js/common/ui.js @@ -2,14 +2,13 @@ define([ // please use jquery with discretion. 'jquery', 'bluebird', - 'kb_common/html', + 'common/html', 'base/js/namespace', './runtime', - './events', 'google-code-prettify/prettify', - 'css!google-code-prettify/prettify.css', + 'css!google-code-prettify/prettify', 'bootstrap', -], ($, Promise, html, Jupyter, Runtime, Events, PR) => { +], ($, Promise, html, Jupyter, Runtime, PR) => { 'use strict'; const t = html.tag, div = t('div'), @@ -25,120 +24,286 @@ define([ tr = t('tr'), th = t('th'), td = t('td'), - i = t('i'); + i = t('i'), + cssClassName = 'kb-ui'; // "static" methods + const staticMethods = { + buildCollapsiblePanel, + buildIcon, + buildPanel, + confirmDialog, + htmlEncode, + loading, + makeCollapsiblePanel, + makePanel, + na, + showConfirmDialog, + showDialog, + showErrorDialog, + showInfoDialog, + cssClassName, + }; + function na() { - return span({ style: { fontStyle: 'italic', color: 'orange' } }, 'NA'); + return span({ class: `${cssClassName}__text--na` }, 'NA'); } - function renderInfoDialog(title, content, okLabel, type) { - let extraClass = ''; - if (type) { - extraClass = ' bg-' + type; - } - return div({ class: 'modal fade', tabindex: '-1', role: 'dialog' }, [ - div({ class: 'modal-dialog' }, [ - div({ class: 'modal-content' }, [ - div({ class: 'modal-header' + extraClass }, [ - button( - { - type: 'button', - class: 'close', - dataDismiss: 'modal', - ariaLabel: okLabel, - }, - [span({ ariaHidden: 'true' }, '×')] - ), - span({ class: 'modal-title' }, title), - ]), - div({ class: 'modal-body' }, [content]), - div({ class: 'modal-footer' }, [ - button( - { - type: 'button', - class: 'btn btn-default', - dataDismiss: 'modal', - dataElement: 'ok', - }, - okLabel - ), - ]), - ]), + function htmlEncode(str) { + return str + .replace(/&/g, '&') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(//g, '>'); + } + + /** + * Make a static (non-collapsing) bootstrap panel with default styling + * @param {string} title - panel title + * @param {string} elementName - name for the data element in the panel body + * + * @returns {string} HTML string to create the panel + */ + function makePanel(title, elementName) { + return div({ class: 'panel panel-primary' }, [ + div({ class: 'panel-heading' }, [div({ class: 'panel-title' }, title)]), + div({ class: 'panel-body' }, [ + div({ dataElement: elementName, class: 'container-fluid' }), ]), ]); } - function showInfoDialog(arg) { - let dialog = renderInfoDialog(arg.title, arg.body, arg.okLabel || 'OK'), - dialogId = html.genId(), - confirmNode = document.createElement('div'), - kbaseNode, - modalNode, - modalDialogNode; - - confirmNode.id = dialogId; - confirmNode.innerHTML = dialog; - - // top level element for kbase usage - kbaseNode = document.querySelector('[data-element="kbase"]'); - if (!kbaseNode) { - kbaseNode = document.createElement('div'); - kbaseNode.setAttribute('data-element', 'kbase'); - document.body.appendChild(kbaseNode); + /** + * Build a static (non-collapsing) bootstrap panel + * @param {object} args with keys + * id - id attribute for the panel (optional) + * type - the type of bootstrap panel (e.g. primary) (opt.) + * classes - extra classes to apply to the panel container div (opt.) + * hidden - if present, the 'hidden' class is applied (opt) + * icon - args to buildIcon; the icon will appear next to the title (opt) + * title - panel title + * name - name of the dataElement in the top div of the panel + * body - panel contents + * + * @returns {string} HTML string to create the panel + */ + function buildPanel(args) { + const type = args.type || 'primary'; + let classes = ['panel', 'panel-' + type], + icon; + if (args.hidden) { + classes.push('hidden'); } - - // a node upon which to place Bootstrap modals. - modalNode = kbaseNode.querySelector('[data-element="modal"]'); - if (!modalNode) { - modalNode = document.createElement('div'); - modalNode.setAttribute('data-element', 'modal'); - kbaseNode.appendChild(modalNode); + if (args.classes) { + classes = classes.concat(args.classes); } + if (args.icon) { + icon = [' ', buildIcon(args.icon)]; + } + return div( + { + class: classes.join(' '), + dataElement: args.name, + }, + [ + (function () { + if (args.title) { + return div({ class: 'panel-heading' }, [ + div({ class: 'panel-title', dataElement: 'title' }, [args.title, icon]), + ]); + } + })(), + div( + { + class: 'panel-body', + dataElement: 'body', + }, + [args.body] + ), + ] + ); + } - modalNode.appendChild(confirmNode); - - modalDialogNode = modalNode.querySelector('.modal'); - $(modalDialogNode).modal('show'); - return new Promise((resolve) => { - modalDialogNode.querySelector('[data-element="ok"]').addEventListener('click', () => { - confirmNode.parentElement.removeChild(confirmNode); - resolve(false); - }); - modalDialogNode.addEventListener('hide.bs.modal', () => { - resolve(false); - }); - }); + /** + * Make a collapsible bootstrap panel with default styling + * @param {string} title - panel title + * @param {string} elementName - name for the data element in the panel body + * + * @returns {string} HTML string to create the panel + */ + function makeCollapsiblePanel(title, elementName) { + const collapseId = html.genId(); + + return div({ class: 'panel panel-default' }, [ + div({ class: 'panel-heading' }, [ + div( + { class: 'panel-title' }, + span( + { + class: 'collapsed', + dataToggle: 'collapse', + dataTarget: '#' + collapseId, + style: { cursor: 'pointer' }, + }, + title + ) + ), + ]), + div( + { id: collapseId, class: 'panel-collapse collapse' }, + div({ class: 'panel-body' }, [ + div({ dataElement: elementName, class: 'container-fluid' }), + ]) + ), + ]); } - function buildError(error) { - return table( + /** + * Build a collapsible bootstrap panel + * @param {object} args with keys + * id - id attribute for the panel (optional) + * type - the type of bootstrap panel (e.g. primary) (opt.) + * classes - extra classes to apply to the panel container div (opt.) + * hidden - if present, the 'hidden' class is applied (opt) + * collapsed - panel starts collapsed (opt) + * icon - args to buildIcon; the icon will appear next to the title (opt) + * title - panel title + * name - name of the dataElement in the top div of the panel + * body - panel contents + * + * @returns {string} HTML string to create the panel + */ + + function buildCollapsiblePanel(args) { + const panelId = args.id || html.genId(), + collapseId = html.genId(), + type = args.type || 'primary', + collapseClasses = ['panel-collapse collapse'], + toggleClasses = ['panel-title-collapse-toggle']; + let icon, + classes = ['panel', 'panel-' + type]; + + if (args.hidden) { + classes.push('hidden'); + } + if (!args.collapsed) { + collapseClasses.push('in'); + } else { + toggleClasses.push('collapsed'); + } + if (args.classes) { + classes = classes.concat(args.classes); + } + if (args.icon) { + icon = [' ', buildIcon(args.icon)]; + } + return div( { - class: 'table table-striped', + id: panelId, + class: classes.join(' '), + dataElement: args.name, }, [ - tr([th('Name'), td(error.name)]), - tr([th('Code'), td(error.code)]), - tr([th('Message'), td(error.message)]), - tr([th('Detail'), td(error.detail)]), - tr([th('Reference'), td(error.reference)]), + div({ class: 'panel-heading' }, [ + div( + { class: 'panel-title' }, + span( + { + dataElement: 'title', + class: toggleClasses.join(' '), + dataToggle: 'collapse', + dataTarget: '#' + collapseId, + style: { cursor: 'pointer' }, + }, + [args.title, icon] + ) + ), + ]), + div( + { id: collapseId, class: collapseClasses.join(' ') }, + div({ class: 'panel-body', dataElement: 'body' }, [args.body]) + ), ] ); } - function showErrorDialog(arg) { - const body = buildError(arg.error); + /** + * Build a Font Awesome icon! + * + * @param {object} arg with keys + * Required keys: + * name: {string} // will be appended to 'fa-' to create the icon name + * + * Optional keys: + * rotate: {number}|{string} // generates class 'fa-rotate-' + * flip: {string} // generates class 'fa-flip-' + * size: {string} // generates class 'fa-' + * or {number} // generates class 'fa-x' + * classes: {array{string}} // more classes to add + * style: {object} // any style params + * color: {string} // icon colour + * + * @returns {string} HTML to generate the icon + * if no icon name is supplied, the string will be '' + */ + function buildIcon(arg = {}) { + if (!arg.name) { + console.error('Cannot create icon: no name supplied'); + return ''; + } - let dialog = renderInfoDialog(arg.title, body, 'OK', 'danger'), - dialogId = html.genId(), - confirmNode = document.createElement('div'), - kbaseNode, - modalNode, - modalDialogNode; + const klasses = ['fa', `${cssClassName}__icon`], + style = {}; - confirmNode.id = dialogId; + klasses.push('fa-' + arg.name); + if (arg.rotate) { + klasses.push('fa-rotate-' + String(arg.rotate)); + } + if (arg.flip) { + klasses.push('fa-flip-' + arg.flip); + } + if (arg.size) { + if (typeof arg.size === 'number') { + klasses.push('fa-' + String(arg.size) + 'x'); + } else { + klasses.push('fa-' + arg.size); + } + } + if (arg.classes) { + arg.classes.forEach((klass) => { + klasses.push(klass); + }); + } + if (arg.style) { + Object.keys(arg.style).forEach((key) => { + style[key] = arg.style[key]; + }); + } + if (arg.color) { + style.color = arg.color; + } + + return span({ + dataElement: 'icon', + style: style, + class: klasses.join(' '), + }); + } + + function confirmDialog(prompt) { + return window.confirm(prompt); + } + + function _generateConfirmNode(dialog) { + const confirmNode = document.createElement('div'); + confirmNode.id = html.genId(); confirmNode.innerHTML = dialog; + return confirmNode; + } + function _setUpModalNodes(confirmNode) { + let kbaseNode, modalNode; // top level element for kbase usage kbaseNode = document.querySelector('[data-element="kbase"]'); if (!kbaseNode) { @@ -154,151 +319,329 @@ define([ modalNode.setAttribute('data-element', 'modal'); kbaseNode.appendChild(modalNode); } - modalNode.appendChild(confirmNode); - - modalDialogNode = modalNode.querySelector('.modal'); - $(modalDialogNode).modal('show'); - return new Promise((resolve) => { - modalDialogNode.querySelector('[data-element="ok"]').addEventListener('click', () => { - confirmNode.parentElement.removeChild(confirmNode); - resolve(false); - }); - modalDialogNode.addEventListener('hide.bs.modal', () => { - resolve(false); - }); - }); + return modalNode.querySelector('.modal'); } - function renderDialog(title, content, cancelLabel, buttons, options) { - const style = {}; - if (options && options.width) { - style.width = options.width; + /** + * Build a bootstrap modal + * @param {object} args with keys + * title - title for the modal + * body - the modal content (should be HTML) + * buttons - an array of buttons to add to the modal + * style - additional style attributes to add to the modal (opt) + * bsClass - the bootstrap theme to use for the modal (opt) + * one of 'primary', 'success', 'info', 'warning', 'danger' + * includeCancel - if present, create a 'Cancel' button (closes the modal) + * the value of the includeCancel key will be used as the button label + * includeOK - if present, create a 'OK' button (closes the modal) + * the value of the includeOK key will be used as the button label + * @returns {string} HTML string to create the panel + */ + + function _renderModal(arg = {}) { + if (!arg.title || !arg.body) { + throw new Error('Missing required arguments "title" and "body"'); + } + + const bgClass = arg.bsClass ? `bg-${arg.bsClass}` : ''; + const titleClass = arg.bsClass + ? arg.bsClass === 'primary' + ? '' + : `text-${arg.bsClass}` + : 'text-primary'; + + const buttons = arg.buttons || []; + if (arg.includeCancel) { + buttons.unshift( + button( + { + type: 'button', + class: 'btn btn-default', + dataDismiss: 'modal', + dataElement: 'cancel', + }, + arg.includeCancel + ) + ); } + + if (arg.includeOK) { + buttons.push( + button( + { + type: 'button', + class: 'btn btn-' + (arg.bsClass || 'primary'), + dataDismiss: 'modal', + dataElement: 'ok', + }, + arg.includeOK + ) + ); + } + return div({ class: 'modal fade', tabindex: '-1', role: 'dialog' }, [ - div({ class: 'modal-dialog', style: style }, [ + div({ class: 'modal-dialog', style: arg.style }, [ div({ class: 'modal-content' }, [ - div({ class: 'modal-header' }, [ + div({ class: `modal-header ${bgClass}` }, [ button( { type: 'button', class: 'close', dataDismiss: 'modal', - ariaLabel: cancelLabel, + ariaLabel: 'close modal', }, [span({ ariaHidden: 'true' }, '×')] ), - span({ class: 'modal-title kb-title' }, title), + span({ class: `modal-title ${titleClass}` }, arg.title), ]), - div({ class: 'modal-body' }, [content]), - div( - { class: 'modal-footer' }, - buttons - .map((btn) => { - return button( - { - type: 'button', - class: 'btn btn-' + (btn.type || 'default'), - dataElement: btn.action, - }, - btn.label - ); - }) - .concat([ - button( - { - type: 'button', - class: 'btn btn-default', - dataDismiss: 'modal', - dataElement: 'cancel', - }, - cancelLabel - ), - ]) - ), + div({ class: 'modal-body' }, [arg.body]), + div({ class: 'modal-footer' }, buttons), ]), ]), ]); } - function showDialog(args) { - args.buttons = args.buttons || []; - let dialog = renderDialog( - args.title, - args.body, - args.cancelLabel || 'Cancel', - args.buttons, - args.options - ), - dialogId = html.genId(), - confirmNode = document.createElement('div'), - kbaseNode, - modalNode, - modalDialogNode; + /** + * Show a generic, configurable dialog + * @param {Object} arg with keys + * {string} title + * {string} body + * {string} type - the type of dialog; 'confirm' dialogs can return true + * or false, whereas others return false + * {string} includeOK (opt) - if defined, an 'OK' button is added with + * the value as the label + * {string} includeCancel (opt) - if defined, a 'Cancel' button is added + * with the value as the label + * {string} bsClass (opt) - the bootstrap theme to use for the modal + * one of 'primary', 'success', 'info', 'warning', 'danger' + * {function} doThisFirst - a function to perform after creating the promise; + * this allows actions to be performed between creating the modal + * and before the modal is dismissed by keyboard or mouse action + * + * @returns {Promise} that resolves to false if the modal is dismissed using the + * cancel button, the close button, by clicking on the modal backdrop, or by pressing + * the escape key. It resolves to true if the ok button is clicked or the user hits + * enter. + */ + function showGenericDialog(args) { + const dialog = _renderModal(args), + confirmNode = _generateConfirmNode(dialog), + modalDialogNode = _setUpModalNodes(confirmNode); + + // this shows the modal + $(modalDialogNode).modal({ keyboard: false }); + + let resolution = false; + + if (args.type === 'confirm') { + modalDialogNode.querySelector('[data-element="ok"]').addEventListener('click', () => { + resolution = true; + }); + } - confirmNode.id = dialogId; - confirmNode.innerHTML = dialog; + modalDialogNode.addEventListener('keyup', (e) => { + // 13 = enter, 27 = escape + // the key is sometimes a string, hence == instead of === + if (e.key == 13 || e.key == 27) { + $(modalDialogNode).modal('hide'); + if (e.key == 13 && args.type === 'confirm') { + resolution = true; + } + } + }); - // top level element for kbase usage - kbaseNode = document.querySelector('[data-element="kbase"]'); - if (!kbaseNode) { - kbaseNode = document.createElement('div'); - kbaseNode.setAttribute('data-element', 'kbase'); - document.body.appendChild(kbaseNode); - } + return new Promise((resolve) => { + $(modalDialogNode).on('hidden.bs.modal', () => { + confirmNode.remove(); + resolve(resolution); + }); + if (args.doThisFirst) { + args.doThisFirst(); + } + }); + } - // a node upon which to place Bootstrap modals. - modalNode = kbaseNode.querySelector('[data-element="modal"]'); - if (!modalNode) { - modalNode = document.createElement('div'); - modalNode.setAttribute('data-element', 'modal'); - kbaseNode.appendChild(modalNode); + /** + * Show a dialog with 'cancel' and 'ok' options + * @param {Object} arg with keys + * {string} title + * {string} body + * {string} okLabel (opt) - the label for the OK button + * {string} cancelLabel (opt) - the label for the Cancel button + * {string} bsClass (opt) - the bootstrap theme to use for the modal + * one of 'primary', 'success', 'info', 'warning', 'danger' + * @returns {Promise} that resolves to false if the modal is dismissed using the + * cancel button, the close button, by clicking on the modal backdrop, or by pressing + * the escape key. It resolves to true if the ok button is clicked or the user hits + * enter. + */ + function showConfirmDialog(arg) { + return showGenericDialog( + Object.assign({}, arg, { + type: 'confirm', + includeOK: arg.okLabel || 'OK', + includeCancel: arg.cancelLabel || 'Cancel', + }) + ); + } + + /** + * Show an information dialog; can be dismissed by clicking the 'OK' button + * @param {Object} arg with keys + * {string} title + * {string} body + * {string} okLabel (opt) - the label for the OK button + * {string} bsClass (opt) - the bootstrap theme to use for the modal + * one of 'primary', 'success', 'info', 'warning', 'danger' + * @returns {Promise} that resolves to false when the modal is dismissed + */ + function showInfoDialog(arg) { + return showGenericDialog( + Object.assign({}, arg, { + type: 'info', + includeOK: arg.okLabel || 'OK', + }) + ); + } + + function _buildErrorTable(error) { + return table( + { + class: 'table table-striped', + }, + [ + tr([th('Name'), td(error.name)]), + tr([th('Code'), td(error.code)]), + tr([th('Message'), td(error.message)]), + tr([th('Detail'), td(error.detail)]), + tr([th('Reference'), td(error.reference)]), + ] + ); + } + + /** + * Show an error dialog; can be dismissed by clicking the 'OK' button + * @param {Object} arg with keys + * {string} title + * {object} error - the error to be displayed + * {string} okLabel (opt) - the label for the OK button + * @returns {Promise} that resolves to false when the modal is dismissed + */ + function showErrorDialog(arg) { + return showGenericDialog( + Object.assign({}, arg, { + type: 'error', + bsClass: 'danger', + body: _buildErrorTable(arg.error), + includeOK: arg.okLabel || 'OK', + }) + ); + } + + function _renderDialog(arg) { + const style = {}; + if (arg.options && arg.options.width) { + style.width = arg.options.width; + } + if (!arg.buttons) { + arg.buttons = []; } - modalNode.appendChild(confirmNode); + const renderedButtons = arg.buttons.map((btn) => { + return button( + { + type: 'button', + class: 'btn btn-' + (btn.type || 'default'), + dataElement: btn.action, + dataDismiss: 'modal', + }, + btn.label + ); + }); + + return _renderModal({ + title: arg.title, + body: arg.body, + includeCancel: arg.cancelLabel || 'Cancel', + buttons: renderedButtons, + style: style, + }); + } + + function showDialog(arg) { + const dialog = _renderDialog(arg), + confirmNode = _generateConfirmNode(dialog), + modalDialogNode = _setUpModalNodes(confirmNode); - modalDialogNode = modalNode.querySelector('.modal'); $(modalDialogNode).modal('show'); - return new Promise((resolve, reject) => { + + let resolution = { action: 'cancel' }; + + arg.buttons.forEach((btn) => { modalDialogNode - .querySelector('[data-element="cancel"]') + .querySelector('[data-element="' + btn.action + '"]') .addEventListener('click', (e) => { - confirmNode.parentElement.removeChild(confirmNode); - resolve({ - action: 'cancel', - }); - }); - args.buttons.forEach((btn) => { - modalDialogNode - .querySelector('[data-element="' + btn.action + '"]') - .addEventListener('click', (e) => { - try { - const result = btn.handler(e); - if (result) { - $(modalDialogNode).modal('hide'); - confirmNode.parentElement.removeChild(confirmNode); - resolve({ - action: btn.action, - result: result, - }); - } - } catch (ex) { - reject(ex); + try { + const result = btn.handler(e); + if (result) { + resolution = { + action: btn.action, + result: result, + }; } - }); - }); - - modalDialogNode.addEventListener('hide.bs.modal', (e) => { - resolve({ - action: 'cancel', + } catch (ex) { + console.error(ex); + } }); + }); + + return new Promise((resolve) => { + $(modalDialogNode).on('hidden.bs.modal', () => { + confirmNode.remove(); + resolve(resolution); }); + if (arg.doThisFirst) { + arg.doThisFirst(); + } }); } + /** + * Creates a spinning icon as a span. Returns the HTML as a string. + * @param {Object} arg should have keys: + * - message {string} - an optional message to add to the spinner + * - size {string} - an optional Font Awesome 4 size modifier (2x, 3x, etc) + * - color {string} - an optional CSS color value + * - class {string} - optional extra class(es) to add to the spinner + */ + function loading(arg) { + arg = arg || {}; + const prompt = arg.message ? `${arg.message}...    ` : ''; + const sizeClass = arg.size ? `fa-${arg.size}` : ''; + const style = arg.color ? { color: arg.color } : ''; + const extraClass = arg.class || ''; + + return span([ + prompt, + i({ + class: [ + 'fa', + 'fa-spinner', + 'fa-pulse', + sizeClass, + extraClass, + 'fa-fw', + 'margin-bottom', + ].join(' '), + style: style, + }), + ]); + } + function factory(config) { const container = config.node, - bus = config.bus, + { bus } = config, runtime = Runtime.make(); /* @@ -393,109 +736,6 @@ define([ return container.querySelector(selector); } - function confirmDialog(prompt, yesLabel, noLabel) { - return window.confirm(prompt); - } - - function renderConfirmDialog(arg) { - const yesLabel = arg.yesLabel || 'Yes', - noLabel = arg.noLabel || 'No'; - const dialog = div({ class: 'modal fade', tabindex: '-1', role: 'dialog' }, [ - div({ class: 'modal-dialog' }, [ - div({ class: 'modal-content' }, [ - div({ class: 'modal-header' }, [ - button( - { - type: 'button', - class: 'close', - dataDismiss: 'modal', - ariaLabel: noLabel, - }, - [span({ ariaHidden: 'true' }, '×')] - ), - span({ class: 'modal-title' }, arg.title), - ]), - div({ class: 'modal-body' }, [arg.body]), - div({ class: 'modal-footer' }, [ - button( - { - type: 'button', - class: 'btn btn-default', - dataDismiss: 'modal', - dataElement: 'no', - }, - noLabel - ), - button( - { type: 'button', class: 'btn btn-primary', dataElement: 'yes' }, - yesLabel - ), - ]), - ]), - ]), - ]); - return dialog; - } - - function showConfirmDialog(arg) { - let dialog = renderConfirmDialog(arg), - dialogId = html.genId(), - confirmNode = document.createElement('div'), - kbaseNode, - modalNode, - modalDialogNode; - - confirmNode.id = dialogId; - confirmNode.innerHTML = dialog; - - // top level element for kbase usage - kbaseNode = document.querySelector('[data-element="kbase"]'); - if (!kbaseNode) { - kbaseNode = document.createElement('div'); - kbaseNode.setAttribute('data-element', 'kbase'); - document.body.appendChild(kbaseNode); - } - - // a node uponwhich to place Bootstrap modals. - modalNode = kbaseNode.querySelector('[data-element="modal"]'); - if (!modalNode) { - modalNode = document.createElement('div'); - modalNode.setAttribute('data-element', 'modal'); - kbaseNode.appendChild(modalNode); - } - - modalNode.appendChild(confirmNode); - - modalDialogNode = modalNode.querySelector('.modal'); - - $(modalDialogNode).modal('show'); - return new Promise((resolve) => { - modalDialogNode - .querySelector('[data-element="yes"]') - .addEventListener('click', () => { - $(modalDialogNode).modal('hide'); - confirmNode.parentElement.removeChild(confirmNode); - resolve(true); - }); - modalDialogNode.addEventListener('keyup', (e) => { - if (e.keyCode === 13) { - $(modalDialogNode).modal('hide'); - confirmNode.parentElement.removeChild(confirmNode); - resolve(true); - } - }); - modalDialogNode - .querySelector('[data-element="no"]') - .addEventListener('click', () => { - confirmNode.parentElement.removeChild(confirmNode); - resolve(false); - }); - modalDialogNode.addEventListener('hide.bs.modal', () => { - resolve(false); - }); - }); - } - function addButtonClickEvent(events, eventName, data) { return events.addEvent({ type: 'click', @@ -518,7 +758,7 @@ define([ function makeButton(label, name, options) { const klass = options.type || 'default', - events = options.events; + { events } = options; return button( { type: 'button', @@ -530,18 +770,38 @@ define([ ); } + /** + * Build a button + * + * @param {object} arg with keys + * Required keys: + * name: {string} // generates attribute data-button="" + * title|tip|label: {string} // button title attribute + * + * Optional keys: + * classes: {array{string}} // more button classes to add + * event: {object} // event data; see addButtonClickEvent for format + * events: {object} // common/events.js object + * features: {array{string}} // generate attributes of the form + * data-feature-{string} = true + * hidden: {boolean} // if the button should be hidden + * icon: {object} // creates an icon; see buildIcon for object format + * label: {string} // button title attribute and button text + * style: {object} // any style params + * tip: {string} // button title attribute + * title: {string} // button title attribute + * type: {string} // bootstrap button class, appended to 'btn-'; + * // defaults to 'default', i.e. 'btn-default' + * + * @returns {string} HTML to generate the button + */ function buildButton(arg) { - let klass = arg.type || 'default', - buttonClasses = ['btn', 'btn-' + klass], - events = arg.events, - icon, - title = arg.title || arg.tip || arg.label, - attribs; + const klass = arg.type || 'default', + { events } = arg; + let buttonClasses = ['btn', 'btn-' + klass], + icon; if (arg.icon) { - if (!arg.icon.classes) { - arg.icon.classes = []; - } icon = buildIcon(arg.icon); } @@ -552,19 +812,23 @@ define([ if (arg.classes) { buttonClasses = buttonClasses.concat(arg.classes); } - if (!arg.event) { - arg.event = {}; - } - attribs = { + const attribs = { type: 'button', class: buttonClasses.join(' '), - title: title, + title: arg.title || arg.tip || arg.label, dataButton: arg.name, - id: addButtonClickEvent(events, arg.event.type || arg.name, arg.event.data), style: arg.style, }; + if (events && arg.event) { + attribs.id = addButtonClickEvent( + events, + arg.event.type || arg.name, + arg.event.data + ); + } + if (arg.features) { arg.features.forEach((feature) => { attribs['data-feature-' + feature] = true; @@ -573,22 +837,23 @@ define([ return button( attribs, - [icon, span({ style: { verticalAlign: 'middle' } }, arg.label)].join(' ') + [icon, span({ class: `${cssClassName}__button_label` }, arg.label)].join(' ') ); } function enableButton(name) { - const button = getButton(name); - button.classList.remove('hidden'); - button.classList.remove('disabled'); - button.removeAttribute('disabled'); + const _button = getButton(name); + _button.classList.remove('hidden'); + _button.classList.remove('disabled'); + _button.removeAttribute('disabled'); } + // note that disabling the button also shows it function disableButton(name) { - const button = getButton(name); - button.classList.remove('hidden'); - button.classList.add('disabled'); - button.setAttribute('disabled', true); + const _button = getButton(name); + _button.classList.remove('hidden'); + _button.classList.add('disabled'); + _button.setAttribute('disabled', true); } function activateButton(name) { @@ -627,136 +892,6 @@ define([ el.classList.remove('hidden'); } - function makePanel(title, elementName) { - return div({ class: 'panel panel-primary' }, [ - div({ class: 'panel-heading' }, [div({ class: 'panel-title' }, title)]), - div({ class: 'panel-body' }, [ - div({ dataElement: elementName, class: 'container-fluid' }), - ]), - ]); - } - - function buildPanel(args) { - let type = args.type || 'primary', - classes = ['panel', 'panel-' + type], - icon; - if (args.hidden) { - classes.push('hidden'); - } - if (args.classes) { - classes = classes.concat(args.classes); - } - if (args.icon) { - icon = [' ', buildIcon(args.icon)]; - } - return div( - { - class: classes.join(' '), - dataElement: args.name, - }, - [ - (function () { - if (args.title) { - return div({ class: 'panel-heading' }, [ - div({ class: 'panel-title', dataElement: 'title' }, [ - args.title, - icon, - ]), - ]); - } - })(), - div( - { - class: 'panel-body', - dataElement: 'body', - }, - [args.body] - ), - ] - ); - } - - function makeCollapsiblePanel(title, elementName) { - const collapseId = html.genId(); - - return div({ class: 'panel panel-default' }, [ - div({ class: 'panel-heading' }, [ - div( - { class: 'panel-title' }, - span( - { - class: 'collapsed', - dataToggle: 'collapse', - dataTarget: '#' + collapseId, - style: { cursor: 'pointer' }, - }, - title - ) - ), - ]), - div( - { id: collapseId, class: 'panel-collapse collapse' }, - div({ class: 'panel-body' }, [ - div({ dataElement: elementName, class: 'container-fluid' }), - ]) - ), - ]); - } - - function buildCollapsiblePanel(args) { - let panelId = args.id || html.genId(), - collapseId = html.genId(), - type = args.type || 'primary', - classes = ['panel', 'panel-' + type], - collapseClasses = ['panel-collapse collapse'], - toggleClasses = [], - icon; - - if (args.hidden) { - classes.push('hidden'); - // style.display = 'none'; - } - if (!args.collapsed) { - collapseClasses.push('in'); - } else { - toggleClasses.push('collapsed'); - } - if (args.classes) { - classes = classes.concat(args.classes); - } - if (args.icon) { - icon = [' ', buildIcon(args.icon)]; - } - return div( - { - id: panelId, - class: classes.join(' '), - dataElement: args.name, - }, - [ - div({ class: 'panel-heading' }, [ - div( - { class: 'panel-title' }, - span( - { - dataElement: 'title', - class: toggleClasses.join(' '), - dataToggle: 'collapse', - dataTarget: '#' + collapseId, - style: { cursor: 'pointer' }, - }, - [args.title, icon] - ) - ), - ]), - div( - { id: collapseId, class: collapseClasses.join(' ') }, - div({ class: 'panel-body', dataElement: 'body' }, [args.body]) - ), - ] - ); - } - function collapsePanel(path) { const node = getElement(path); if (!node) { @@ -802,16 +937,16 @@ define([ } function setContent(path, content) { - const node = getElements(path); - node.forEach((node) => { - node.innerHTML = content; + const nodes = getElements(path); + nodes.forEach((_node) => { + _node.innerHTML = content; }); } function setText(path, text) { const node = getElements(path); - node.forEach((node) => { - node.innerText = text; + node.forEach((_node) => { + _node.innerText = text; }); } @@ -820,8 +955,8 @@ define([ if (!node) { return; } - qsa(node, '[data-toggle="tooltip"]').forEach((node) => { - $(node).tooltip(); + qsa(node, '[data-toggle="tooltip"]').forEach((_node) => { + $(_node).tooltip(); }); } @@ -842,12 +977,11 @@ define([ } function getUserSetting(settingKey, defaultValue) { - let settings = Jupyter.notebook.metadata.kbase.userSettings, - setting; + const settings = Jupyter.notebook.metadata.kbase.userSettings; if (!settings) { return defaultValue; } - setting = settings[settingKey]; + const setting = settings[settingKey]; if (setting === undefined) { return defaultValue; } @@ -855,83 +989,41 @@ define([ } function ifAdvanced(fun) { - const isAdvanced = getUserSetting('advanced', runtime.config('features.advanced')); - if (isAdvanced) { + const userIsAdvanced = getUserSetting('advanced', runtime.config('features.advanced')); + if (userIsAdvanced) { return fun(); } } function ifDeveloper(fun) { - const isDeveloper = getUserSetting('developer', runtime.config('features.developer')); - if (isDeveloper) { + const userIsDeveloper = getUserSetting( + 'developer', + runtime.config('features.developer') + ); + if (userIsDeveloper) { return fun(); } } function isAdvanced() { - const isAdvanced = getUserSetting('advanced', runtime.config('features.advanced')); - if (isAdvanced) { + const userIsAdvanced = getUserSetting('advanced', runtime.config('features.advanced')); + if (userIsAdvanced) { return true; } return false; } - function isDeveloper(fun) { - const isDeveloper = getUserSetting('developer', runtime.config('features.developer')); - if (isDeveloper) { + function isDeveloper() { + const userIsDeveloper = getUserSetting( + 'developer', + runtime.config('features.developer') + ); + if (userIsDeveloper) { return true; } return false; } - function buildIcon(arg) { - const klasses = ['fa'], - style = { verticalAlign: 'middle' }; - klasses.push('fa-' + arg.name); - if (arg.rotate) { - klasses.push('fa-rotate-' + String(arg.rotate)); - } - if (arg.flip) { - klasses.push('fa-flip-' + arg.flip); - } - if (arg.size) { - if (typeof arg.size === 'number') { - klasses.push('fa-' + String(arg.size) + 'x'); - } else { - klasses.push('fa-' + arg.size); - } - } - if (arg.classes) { - arg.classes.forEach((klass) => { - klasses.push(klass); - }); - } - if (arg.style) { - Object.keys(arg.style).forEach((key) => { - style[key] = arg.style[key]; - }); - } - if (arg.color) { - style.color = arg.color; - } - - return span({ - dataElement: 'icon', - style: style, - class: klasses.join(' '), - }); - } - - function reverse(arr) { - let newArray = [], - i, - len = arr.length; - for (i = len - 1; i >= 0; i -= 1) { - newArray.push(arr[i]); - } - return newArray; - } - function updateTab(tabId, tabName, updates) { const node = document.getElementById(tabId); if (!node) { @@ -959,10 +1051,10 @@ define([ const iconNode = tabTab.querySelector('[data-element="icon"]'); if (iconNode) { // remove any icons. - let classList = iconNode.classList; - for (let i = classList.length; classList > 0; classList -= 1) { - if (classList.item[i].substring(0, 3) === 'fa-') { - classList.remove(classList.item[i]); + let { classList } = iconNode; + for (let x = classList.length; classList > 0; classList -= 1) { + if (classList.item[x].substring(0, 3) === 'fa-') { + classList.remove(classList.item[x]); } } iconNode.classList.add('fa-' + updates.icon); @@ -973,27 +1065,22 @@ define([ if (updates.color) { tabTab.style.color = updates.color; } - - // switch to tab - if (updates.select) { - } } function buildTabs(arg) { - let tabsId = arg.id, + const tabsId = arg.id, tabsAttribs = {}, tabClasses = ['nav', 'nav-tabs'], tabStyle = {}, - activeIndex, - tabTabs, tabs = arg.tabs.filter((tab) => { return tab ? true : false; }), events = [], - content, - selectInitialTab = false, tabMap = {}, panelClasses = ['tab-pane']; + let activeIndex, + tabTabs, + selectInitialTab = false; if (arg.fade) { panelClasses.push('fade'); @@ -1025,7 +1112,7 @@ define([ } }); if (arg.alignRight) { - tabTabs = reverse(tabs); + tabTabs = tabs.reverse(); tabStyle.float = 'right'; if (selectInitialTab) { activeIndex = tabs.length - 1 - arg.initialTab; @@ -1036,11 +1123,11 @@ define([ activeIndex = arg.initialTab; } } - content = div(tabsAttribs, [ + const content = div(tabsAttribs, [ ul( { class: tabClasses.join(' '), role: 'tablist' }, tabTabs.map((tab, index) => { - let tabAttribs = { + const tabAttribs = { role: 'presentation', }, linkAttribs = { @@ -1052,8 +1139,8 @@ define([ dataPanelId: tab.panelId, dataToggle: 'tab', }, - icon, label = span({ dataElement: 'label' }, tab.label); + let icon; if (tab.icon) { icon = buildIcon({ name: tab.icon }); } else { @@ -1100,10 +1187,10 @@ define([ // TURN THIS INTO A MINI WIDGET! function jsonBlockWidget() { - function factory(cfg) { - const config = cfg || {}, - indent = config.indent || 3, - fontSize = config.fontSize || 0.8; + function jsonBlockWidgetFactory(cfg) { + const jsonBlockWidgetConfig = cfg || {}, + indent = jsonBlockWidgetConfig.indent || 3, + fontSize = jsonBlockWidgetConfig.fontSize || 0.8; function render(obj) { const specText = JSON.stringify(obj, false, indent), @@ -1136,8 +1223,8 @@ define([ }; } return { - make: function (config) { - return factory(config); + make: function (args) { + return jsonBlockWidgetFactory(args); }, }; } @@ -1156,12 +1243,6 @@ define([ }); } - function camelToHyphen(s) { - return s.replace(/[A-Z]/g, (m) => { - return '-' + m.toLowerCase(); - }); - } - function updateFromViewModel(viewModel, path) { if (!path) { path = []; @@ -1193,8 +1274,8 @@ define([ } break; case 'style': - Object.keys(attribValue).forEach((key) => { - node.style[camelToHyphen(key)] = attribValue[key]; + Object.keys(attribValue).forEach((_key) => { + node.style[html.camelToKebab(_key)] = attribValue[_key]; }); } }); @@ -1240,8 +1321,8 @@ define([ } } - function buildError(err) { - return div({}, [ + function _buildError(err) { + return [ buildPanel({ title: 'Message', body: err.message, @@ -1268,34 +1349,33 @@ define([ classes: ['kb-panel-light'], }) : '', - ]); - } - - function htmlEncode(str) { - return str - .replace(/&/, '&') - .replace(/'/, ''') - .replace(/"/, '"') - .replace(//, '>'); + ].join('\n'); } function buildErrorStacktrace(err) { - return div([ - ol( - {}, - err.stack.split(/\n/).map((item) => { - return li( - { - style: { - marginTop: '6px', - }, - }, - [htmlEncode(item)] - ); - }) - ), - ]); + return div( + { + class: 'kb-error-dialog__stacktrace_container', + }, + [ + ol( + { + class: 'kb-error-dialog__stacktrace_lines', + }, + err.stack + .split(/\n/) + .filter((item) => item.length) + .map((item) => { + return li( + { + class: 'kb-error-dialog__stacktrace_single_line', + }, + [htmlEncode(item)] + ); + }) + ), + ] + ); } function buildErrorTabs(arg) { @@ -1304,138 +1384,102 @@ define([ { label: 'Summary', name: 'summary', - content: div( - { - style: { - marginTop: '10px', + content: div([ + p( + { + class: 'kb-error-dialog__err_preamble', }, - }, - [arg.preamble, p(arg.error.message)] - ), + arg.preamble + ), + p( + { + class: 'kb-error-dialog__err_message', + }, + arg.error.message + ), + ]), }, { label: 'Details', name: 'details', - content: div( - { - style: { - marginTop: '10px', - }, - }, - [buildError(arg.error)] - ), + content: div([_buildError(arg.error)]), }, { label: 'Stack Trace', name: 'stacktrace', - content: div( - { - style: { - marginTop: '10px', - }, - }, - [ - buildPanel({ - title: 'Javascript Stack Trace', - body: buildErrorStacktrace(arg.error), - classes: ['kb-panel-light'], - }), - ] - ), + content: div([ + buildPanel({ + title: 'Javascript Stack Trace', + body: buildErrorStacktrace(arg.error), + classes: ['kb-panel-light'], + }), + ]), }, ], }); } - function loading(arg) { - let prompt; - if (arg.message) { - prompt = arg.message + '...    '; - } - let sizeClass; - if (arg.size) { - sizeClass = 'fa-' + arg.size; - } - const style = {}; - if (arg.color) { - style.color = arg.color; - } - return span([ - prompt, - i({ - class: [ - 'fa', - 'fa-spinner', - 'fa-pulse', - sizeClass, - 'fa-fw', - 'margin-bottom', - ].join(' '), - style: style, - }), - ]); - } - - return Object.freeze({ - getElement: getElement, - getElements: getElements, - getButton: getButton, - // setButton: setButton, - getNode: getNode, - makeButton: makeButton, - buildButton: buildButton, - enableButton: enableButton, - disableButton: disableButton, - activateButton: activateButton, - deactivateButton: deactivateButton, - hideButton: hideButton, - showButton: showButton, - setButtonLabel: setButtonLabel, - confirmDialog: confirmDialog, - hideElement: hideElement, - showElement: showElement, - makePanel: makePanel, - buildPanel: buildPanel, - makeCollapsiblePanel: makeCollapsiblePanel, - buildCollapsiblePanel: buildCollapsiblePanel, - collapsePanel: collapsePanel, - expandPanel: expandPanel, - createNode: createNode, - setContent: setContent, - setText: setText, - na: na, - ifAdvanced: ifAdvanced, - ifDeveloper: ifDeveloper, - isAdvanced: isAdvanced, - isDeveloper: isDeveloper, - showConfirmDialog: showConfirmDialog, - showInfoDialog: showInfoDialog, - showDialog: showDialog, - buildButtonToolbar: buildButtonToolbar, - buildIcon: buildIcon, - addClass: addClass, - removeClass: removeClass, - buildTabs: buildTabs, - jsonBlockWidget: jsonBlockWidget(), - enableTooltips: enableTooltips, - updateTab: updateTab, - buildGridTable: buildGridTable, - updateFromViewModel: updateFromViewModel, - buildPresentableJson: buildPresentableJson, - buildErrorTabs: buildErrorTabs, - htmlEncode: htmlEncode, - loading: loading, - }); + return Object.freeze( + Object.assign({}, staticMethods, { + activateButton, + addClass, + buildButton, + buildButtonToolbar, + buildCollapsiblePanel, + buildErrorTabs, + buildGridTable, + buildIcon, + buildPanel, + buildPresentableJson, + buildTabs, + collapsePanel, + confirmDialog, + createNode, + deactivateButton, + disableButton, + enableButton, + enableTooltips, + expandPanel, + getButton, + getElement, + getElements, + getNode, + hideButton, + hideElement, + htmlEncode, + ifAdvanced, + ifDeveloper, + isAdvanced, + isDeveloper, + jsonBlockWidget: jsonBlockWidget(), + loading, + makeButton, + makeCollapsiblePanel, + makePanel, + na, + removeClass, + setButtonLabel, + setContent, + setText, + showButton, + showConfirmDialog, + showDialog, + showElement, + showErrorDialog, + showInfoDialog, + updateFromViewModel, + updateTab, + }) + ); } - return { - make: function (config) { - return factory(config); + return Object.assign( + {}, + { + make: function (config) { + return factory(config); + }, }, - // "static" methods - na: na, - showInfoDialog: showInfoDialog, - showDialog: showDialog, - showErrorDialog: showErrorDialog, - }; + staticMethods + ); }); diff --git a/kbase-extension/static/kbase/js/common/unodep.js b/kbase-extension/static/kbase/js/common/unodep.js deleted file mode 100644 index 3b901fe4d4..0000000000 --- a/kbase-extension/static/kbase/js/common/unodep.js +++ /dev/null @@ -1,115 +0,0 @@ -define([], () => { - 'use strict'; - - /* - * Show elapsed time in a friendly fashion. - */ - function pad(string, width, char, right) { - if (!char) { - char = '0'; - } - if (typeof string === 'number') { - string = String(string); - } - let padLen = width - string.length, - padding = '', - i; - if (padLen <= 0) { - return string; - } - for (i = 0; i < padLen; i += 1) { - padding += char; - } - if (right) { - return string + padding; - } - return padding + string; - } - function formatElapsedTime(value, defaultValue) { - if (!value) { - return defaultValue; - } - let temp = value; - - const units = [1000, 60, 60, 24].map((unit) => { - const unitValue = temp % unit; - temp = (temp - unitValue) / unit; - return unitValue; - }); - - return [ - [pad(units[3], 2), pad(units[2], 2), pad(units[1], 2)].join(':'), - pad(units[0], 3), - ].join('.'); - } - function formatTime(time) { - if (time) { - return format.niceElapsedTime(time); - } - } - function isEqual(v1, v2) { - const path = []; - function iseq(v1, v2) { - const t1 = typeof v1; - const t2 = typeof v2; - if (t1 !== t2) { - return false; - } - switch (t1) { - case 'string': - case 'number': - case 'boolean': - if (v1 !== v2) { - return false; - } - break; - case 'undefined': - if (t2 !== 'undefined') { - return false; - } - break; - case 'object': - if (v1 instanceof Array) { - if (v1.length !== v2.length) { - return false; - } else { - for (var i = 0; i < v1.length; i++) { - path.push(i); - if (!iseq(v1[i], v2[i])) { - return false; - } - path.pop(); - } - } - } else if (v1 === null) { - if (v2 !== null) { - return false; - } - } else if (v2 === null) { - return false; - } else { - const k1 = Object.keys(v1); - const k2 = Object.keys(v2); - if (k1.length !== k2.length) { - return false; - } - for (var i = 0; i < k1.length; i++) { - path.push(k1[i]); - if (!iseq(v1[k1[i]], v2[k1[i]])) { - return false; - } - path.pop(); - } - } - } - return true; - } - return iseq(v1, v2); - } - - return { - formatElapsedTime: formatElapsedTime, - formatTime: formatTime, - isEqual: isEqual, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/utils.js b/kbase-extension/static/kbase/js/common/utils.js deleted file mode 100644 index 7ea9af2210..0000000000 --- a/kbase-extension/static/kbase/js/common/utils.js +++ /dev/null @@ -1,135 +0,0 @@ -define(['kb_common/html', 'kb_common/format', './props', 'bootstrap'], (html, format, Props) => { - 'use strict'; - const t = html.tag, - div = t('div'); - - function makePanel(title, elementName) { - return div({ class: 'panel panel-primary' }, [ - div({ class: 'panel-heading' }, [div({ class: 'panel-title' }, title)]), - div({ class: 'panel-body' }, [ - div({ dataElement: elementName, class: 'container-fluid' }), - ]), - ]); - } - - function buildPanel(args) { - const style = {}, - type = args.type || 'primary'; - if (args.hidden) { - style.display = 'none'; - } - return div({ class: 'panel panel-' + type, dataElement: args.name, style: style }, [ - div({ class: 'panel-heading' }, [div({ class: 'panel-title' }, args.title)]), - div({ class: 'panel-body' }, [args.body]), - ]); - } - - function getElement(container, names) { - const selector = names - .map((name) => { - return '[data-element="' + name + '"]'; - }) - .join(' '); - - return container.querySelector(selector); - } - function createMeta(cell, initial) { - const meta = cell.metadata; - meta.kbase = initial; - cell.metadata = meta; - } - function getMeta(cell, group, name) { - if (!cell.metadata.kbase) { - return; - } - if (name === undefined) { - return cell.metadata.kbase[group]; - } - if (!cell.metadata.kbase[group]) { - return; - } - return cell.metadata.kbase[group][name]; - } - function setMeta(cell, group, name, value) { - /* - * This funny business is because the trigger on setting the metadata - * property (via setter and getter in core Cell object) is only invoked - * when the metadata preoperty is actually set -- doesn't count if - * properties of it are. - */ - const temp = cell.metadata; - // Handle the case of setting a group to an entire object - if (value === undefined) { - temp.kbase[group] = name; - } else { - if (!temp.kbase[group]) { - temp.kbase[group] = {}; - } - temp.kbase[group][name] = value; - } - cell.metadata = temp; - } - function setCellMeta(cell, path, value, forceRefresh) { - if (!cell.metadata) { - cell.metadata = {}; - } - Props.setDataItem(cell.metadata, path, value); - if (forceRefresh) { - cell.metadata = cell.metadata; - } - } - function getCellMeta(cell, path, defaultValue) { - return Props.getDataItem(cell.metadata, path, defaultValue); - } - function pushMeta(cell, props, value) { - const meta = Props.make(cell.metadata.kbase); - meta.incrItem(props, value); - } - - /* - * Show elapsed time in a friendly fashion. - */ - function formatTime(time) { - if (time) { - return format.niceElapsedTime(time); - } - } - function horribleHackToHideElement(cell, selector, tries) { - const prompt = cell.element.find(selector); - if (prompt.length > 0) { - prompt.css('visibility', 'hidden'); - return; - } - - if (tries > 0) { - tries -= 1; - window.setTimeout(() => { - horribleHackToHideElement(cell, tries); - }, 100); - } else { - console.warn('Could not hide the prompt, sorry'); - } - } - - function toBoolean(value) { - if (value && value !== null) { - return true; - } - return false; - } - - return { - makePanel: makePanel, - buildPanel: buildPanel, - getElement: getElement, - createMeta: createMeta, - getMeta: getMeta, - setMeta: setMeta, - getCellMeta: getCellMeta, - setCellMeta: setCellMeta, - pushMeta: pushMeta, - formatTime: formatTime, - horribleHackToHideElement: horribleHackToHideElement, - toBoolean: toBoolean, - }; -}); diff --git a/kbase-extension/static/kbase/js/common/validation.js b/kbase-extension/static/kbase/js/common/validation.js index ffd3e25bf3..9b77eb7894 100644 --- a/kbase-extension/static/kbase/js/common/validation.js +++ b/kbase-extension/static/kbase/js/common/validation.js @@ -1,54 +1,13 @@ -define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( - Promise, - Workspace, - serviceUtils -) => { +define([ + 'bluebird', + 'kb_service/client/workspace', + 'kb_service/utils', + 'util/util', + 'util/string', +], (Promise, Workspace, serviceUtils, Util, StringUtil) => { 'use strict'; function Validators() { - /** - * Converts a string to an integer. - * If it's not a string (or an integer already), this throws an Error. - * Floats are not reduced into integers, unless the decimal part === 0. - * @param {string|number} value - * @returns {number} the integer version of the value. - * @throws {*} an error if: - * - the value is a non-integer number, - * - if the value is a non-int-parseable string, - * - if value is anything else (like an Array or Object) - */ - function toInteger(value) { - switch (typeof value) { - case 'number': - if (value !== Math.floor(value)) { - throw new Error('Integer is a non-integer number'); - } - return value; - case 'string': - if (value.match(/^[-+]?[\d]+$/)) { - return parseInt(value, 10); - } - throw new Error('Invalid integer format'); - default: - throw new Error('Type ' + typeof value + ' cannot be converted to integer'); - } - } - - /** - * Returns true if the value is an empty string (or entirely whitespace), or null. - * Returns false otherwise. - * @param {*} value - */ - function isEmptyString(value) { - if (value === null) { - return true; - } - if (typeof value === 'string' && value.trim() === '') { - return true; - } - return false; - } - /** * This validates a single value against a set of acceptable values. For example, if * value = 'a', and and options.values = ['a', 'b', 'c'], this will return @@ -290,11 +249,11 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( errorObject, messageId, errorMessage, - diagnosis = 'valid', - min = options.min_int, + diagnosis = 'valid'; + const min = options.min_int, max = options.max_int; - if (isEmptyString(value)) { + if (StringUtil.isEmptyString(value)) { if (options.required) { diagnosis = 'required-missing'; messageId = 'required-missing'; @@ -309,7 +268,7 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( } else { plainValue = value.trim(); try { - parsedValue = toInteger(plainValue); + parsedValue = Util.toInteger(plainValue); errorObject = validateInteger(parsedValue, min, max); if (errorObject) { messageId = errorObject.id; @@ -354,14 +313,11 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( } function validateFloatString(value, options) { - let normalizedValue, - parsedValue, - errorMessage, - diagnosis, - min = options.min_float, + let normalizedValue, parsedValue, errorMessage, diagnosis; + const min = options.min_float, max = options.max_float; - if (isEmptyString(value)) { + if (StringUtil.isEmptyString(value)) { if (options.required) { diagnosis = 'required-missing'; errorMessage = 'value is required'; @@ -416,11 +372,11 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( messageId = 'obj-name-no-spaces'; diagnosis = 'invalid'; errorMessage = 'an object name may not contain a space'; - } else if (/^[\+\-]*\d+$/.test(parsedValue)) { + } else if (/^[+-]*\d+$/.test(parsedValue)) { messageId = 'obj-name-not-integer'; diagnosis = 'invalid'; errorMessage = 'an object name may not be in the form of an integer'; - } else if (!/^[A-Za-z0-9|\.|\||_\-]+$/.test(parsedValue)) { + } else if (!/^[A-Za-z0-9|._-]+$/.test(parsedValue)) { messageId = 'obj-name-invalid-characters'; diagnosis = 'invalid'; errorMessage = @@ -464,8 +420,8 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( function validateText(value, options) { let parsedValue, errorMessage, - diagnosis = 'valid', - minLength = options.min_length, + diagnosis = 'valid'; + const minLength = options.min_length, maxLength = options.max_length, regexp = options.regexp_constraint ? new RegExp(options.regexp_constraint) : false; @@ -476,7 +432,7 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( } } - if (isEmptyString(value)) { + if (StringUtil.isEmptyString(value)) { if (options.required) { diagnosis = 'required-missing'; errorMessage = 'value is required'; @@ -534,7 +490,7 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( errorMessage = 'value must be an array'; } else { parsedSet = value.filter((setValue) => { - return !isEmptyString(setValue); + return !StringUtil.isEmptyString(setValue); }); if (parsedSet.length === 0) { if (options.required) { @@ -600,7 +556,7 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( errorMessage, diagnosis = 'valid'; - if (isEmptyString(value)) { + if (StringUtil.isEmptyString(value)) { if (options.required) { diagnosis = 'required-missing'; errorMessage = 'value is required'; @@ -639,21 +595,21 @@ define(['bluebird', 'kb_service/client/workspace', 'kb_service/utils'], ( } return { - validateWorkspaceDataPaletteRef: validateWorkspaceDataPaletteRef, - validateWorkspaceObjectName: validateWorkspaceObjectName, - validateWorkspaceObjectRef: validateWorkspaceObjectRef, - validateInteger: validateInteger, - validateIntString: validateIntString, + validateWorkspaceDataPaletteRef, + validateWorkspaceObjectName, + validateWorkspaceObjectRef, + validateInteger, + validateIntString, validateIntegerField: validateIntString, - validateFloat: validateFloat, - validateFloatString: validateFloatString, + validateFloat, + validateFloatString, validateTextString: validateText, - validateText: validateText, - validateSet: validateSet, + validateText, + validateSet, validateStringSet: validateTextSet, - validateTextSet: validateTextSet, - validateBoolean: validateBoolean, - validateTrue: validateTrue, + validateTextSet, + validateBoolean, + validateTrue, }; } diff --git a/kbase-extension/static/kbase/js/kbaseNarrative.js b/kbase-extension/static/kbase/js/kbaseNarrative.js index 6d0e25897d..728e04dc82 100644 --- a/kbase-extension/static/kbase/js/kbaseNarrative.js +++ b/kbase-extension/static/kbase/js/kbaseNarrative.js @@ -13,7 +13,7 @@ define([ 'bluebird', 'handlebars', 'narrativeConfig', - 'jobCommChannel', + 'common/jobCommChannel', 'kbaseNarrativeSidePanel', 'kbaseNarrativeOutputCell', 'kbaseNarrativeWorkspace', @@ -40,13 +40,14 @@ define([ 'kb_service/utils', 'widgets/loadingWidget', 'kb_service/client/workspace', + 'util/kbaseApiUtil', 'bootstrap', ], ( $, Promise, Handlebars, Config, - JobCommChannel, + JobComms, KBaseNarrativeSidePanel, KBaseNarrativeOutputCell, KBaseNarrativeWorkspace, @@ -72,7 +73,8 @@ define([ Tour, ServiceUtils, LoadingWidget, - Workspace + Workspace, + APIUtil ) => { 'use strict'; @@ -147,8 +149,6 @@ define([ node: document.querySelector('#kb-loading-blocker'), timeout: 20000, }); - - //Jupyter.keyboard_manager.disable(); return this; }; @@ -331,17 +331,17 @@ define([ * after there's a visible DOM element for it to render in. */ Narrative.prototype.initSharePanel = function () { - let sharePanel = $( + const sharePanel = $( '


' ), - shareWidget = null, shareDialog = new BootstrapDialog({ title: 'Change Share Settings', body: sharePanel, closeButton: true, }); + let shareWidget = null; shareDialog.getElement().one('shown.bs.modal', () => { shareWidget = new KBaseNarrativeSharePanel(sharePanel.empty(), { ws_name_or_id: this.getWorkspaceName(), @@ -711,8 +711,8 @@ define([ open: function () { const that = $(this); // Upon ENTER, click the OK button. - that.find('input[type="text"]').keydown((event) => { - if (event.which === Keyboard.keycodes.enter) { + that.find('input[type="text"]').keydown((_event) => { + if (_event.which === Keyboard.keycodes.enter) { that.find('.btn-primary').first().click(); } }); @@ -804,9 +804,7 @@ define([ this.sidePanel = new KBaseNarrativeSidePanel($('#kb-side-panel'), { autorender: false, }); - this.narrController = new KBaseNarrativeWorkspace($('#notebook_panel'), { - ws_id: this.getWorkspaceName(), - }); + this.narrController = new KBaseNarrativeWorkspace($('#notebook_panel')); // Disable autosave so as not to spam the Workspace. Jupyter.notebook.set_autosave_interval(0); @@ -833,13 +831,11 @@ define([ } this.initSharePanel(); this.initStaticNarrativesPanel(); - this.updateDocumentVersion() - .then(() => this.narrController.render()) - .finally(() => this.sidePanel.render()); + this.updateDocumentVersion().finally(() => this.sidePanel.render()); }); $([Jupyter.events]).on('kernel_connected.Kernel', () => { this.loadingWidget.updateProgress('kernel', true); - this.jobCommChannel = new JobCommChannel(); + this.jobCommChannel = new JobComms.JobCommChannel(); this.jobCommChannel .initCommChannel() .then(() => { @@ -900,7 +896,6 @@ define([ */ Narrative.prototype.saveNarrative = function () { this.stopVersionCheck = true; - this.narrController.saveAllCellStates(); Jupyter.notebook.save_checkpoint(); this.toggleDocumentVersionBtn(false); }; @@ -939,11 +934,9 @@ define([ }).show(); return; } - let cell = Jupyter.notebook.get_selected_cell(), - nearIdx = 0; - if (cell) { - nearIdx = Jupyter.notebook.find_cell_index(cell); - } + const cell = Jupyter.notebook.get_selected_cell(), + nearIdx = cell ? Jupyter.notebook.find_cell_index(cell) : 0; + let objInfo = {}; // If a string, expect a ref, and fetch the info. if (typeof obj === 'string') { @@ -1006,7 +999,7 @@ define([ const newWidget = new KBaseNarrativeMethodCell( $('#' + $(newCell.get_text())[0].id) ); - var updateStateAndRun = function () { + const updateStateAndRun = function () { if (newWidget.$inputWidget) { // if the $inputWidget is not null, we are good to go, so set the parameters newWidget.loadState(parameters); @@ -1139,6 +1132,7 @@ define([ duration: delay, } ); + $('#content-column')[0].classList.add('kb-content-column--expanded'); } else { $('#kb-side-toggle-in').hide(0, () => { $('#left-column').show( @@ -1154,6 +1148,7 @@ define([ { easing: 'swing', duration: delay } ); }); + $('#content-column')[0].classList.remove('kb-content-column--expanded'); } }; @@ -1184,5 +1179,37 @@ define([ delete this.kbaseWidgets[cellId]; }; + /** + * This inserts a new bulk import cell below the currently selected cell. + * Its input is a map from object type to a the files to be uploaded and the app + * used to process them. + * { + * fileType: { + * appId: string, + * files: array of files + * } + * } + * This returns a Promise that resolves into the cell that was created. + * @param {object} bulkInput keys = type ids, values = an object with properties + * - appId - the app id to use for that file type (to be used in fetching the spec) + * - files - array of files to import with that file type + */ + Narrative.prototype.insertBulkImportCell = function (bulkInput) { + const cellType = 'app-bulk-import'; + const cellData = { + type: cellType, + typesToFiles: bulkInput ? bulkInput : {}, + }; + // get a unique array of app ids we need to look up + const appIds = [...new Set(Object.values(bulkInput).map((typeInfo) => typeInfo.appId))]; + return APIUtil.getAppSpecs(appIds).then((appSpecs) => { + cellData.specs = appSpecs.reduce((allSpecs, spec) => { + allSpecs[spec.info.id] = spec; + return allSpecs; + }, {}); + return this.insertAndSelectCellBelow('code', null, cellData); + }); + }; + return Narrative; }); diff --git a/kbase-extension/static/kbase/js/kbaseNarrativePrestart.js b/kbase-extension/static/kbase/js/kbaseNarrativePrestart.js index 17ca198e90..f55aa8139d 100644 --- a/kbase-extension/static/kbase/js/kbaseNarrativePrestart.js +++ b/kbase-extension/static/kbase/js/kbaseNarrativePrestart.js @@ -129,7 +129,7 @@ define(['kbwidget', 'bootstrap', 'jquery', 'narrativeConfig', 'common/runtime', * for the function. */ window.KBError = function (where, what) { - return KBFail(false, where, what); + return window.KBFail(false, where, what); }; /** @@ -141,7 +141,7 @@ define(['kbwidget', 'bootstrap', 'jquery', 'narrativeConfig', 'common/runtime', * @param what (string) What happened */ window.KBFatal = function (where, what) { - const res = KBFail(true, where, what); + window.KBFail(true, where, what); const version = Config.get('version') || 'unknown'; const hash = Config.get('git_commit_hash') || 'unknown'; @@ -153,7 +153,7 @@ define(['kbwidget', 'bootstrap', 'jquery', 'narrativeConfig', 'common/runtime', full_version = version + ' (hash=' + hash + ')'; } } - var $fatal = $( + const $fatal = $( '