diff --git a/.cjs2esm.js b/.cjs2esm.js new file mode 100644 index 000000000..c6333cf2e --- /dev/null +++ b/.cjs2esm.js @@ -0,0 +1,18 @@ +module.exports = { + input: ['server'], + ignore: ['server/node_modules', 'server/public'], + output: 'server-esm', + forceDirectory: null, + modules: [], + extension: { + use: 'js', + ignore: [], + }, + addModuleEntry: false, + addPackageJson: true, + filesWithShebang: [], + codemod: { + path: '', + files: ['cjs', 'exports', 'named-export-generation'], + }, +}; diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..43807b8bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.DS_Store +.env +.idea/ +.github +.vscode +/client/build +/client/node_modules +/db +/dbtest +/node_modules +/server/node_modules +/server/public diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 000000000..e80ef99c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,12 @@ +--- +name: Issue template +about: Default issue template +title: '' +labels: '' +assignees: '' + +--- + +**SQLPad is in maintenance mode**. + +This issue will likely go unaddressed unless critical. For security issues, please email rick.bergfalk@gmail.com. diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh new file mode 100755 index 000000000..12f9159a4 --- /dev/null +++ b/.github/workflows/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +set -x + +apt-get update +apt-get install -y --no-install-recommends g++ make python3 python3-setuptools unixodbc-dev + +bash -x scripts/build.sh + +tar -cavf sqlpad.tar.gz -C server/public . diff --git a/.github/workflows/docker-ci-tags.yml b/.github/workflows/docker-ci-tags.yml new file mode 100644 index 000000000..b8d2643c4 --- /dev/null +++ b/.github/workflows/docker-ci-tags.yml @@ -0,0 +1,52 @@ +# https://github.com/docker/metadata-action +# https://docs.docker.com/build/ci/github-actions/examples/#manage-tags-and-labels +name: docker-ci-tags + +on: + push: + branches: + - 'master' + tags: + - 'v*.*.*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + # list of Docker images to use as base name for tags + images: | + sqlpad/sqlpad + # generate Docker tags based on the following events/attributes + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..bb1ed09b2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: yarn + - run: (cd client && yarn) + - run: (cd server && yarn) + - run: yarn lint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..de2f34a7f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,42 @@ +name: SQLPad release + +on: + push: + tags: + - '*' + +jobs: + build: + name: Build and upload + runs-on: ubuntu-latest + steps: + - name: Checkout code ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Build + uses: docker://node:20-slim + with: + entrypoint: bash + args: .github/workflows/build.sh + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload archive to release + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./sqlpad.tar.gz + asset_name: sqlpad.tar.gz + asset_content_type: application/gzip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..02dfd3d73 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,91 @@ +name: test + +on: [push, pull_request] + +env: + TZ: utc + +jobs: + build: + runs-on: ubuntu-latest + + # We want to run on external PRs, but not on our own internal PRs as they'll be run + # by the push to the branch. + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + + strategy: + matrix: + node-version: [20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Build for ${{ matrix.node-version }} + run: scripts/build.sh + + - name: Store artifacts + uses: actions/upload-artifact@v4 + with: + name: built_node${{ matrix.node-version }} + path: server/public + + test: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + + strategy: + fail-fast: false # if one job fails, others will not be aborted + matrix: + backendtype: ['', 'mssql', 'mysql', 'mariadb', 'postgres'] + node-version: [20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Start redis + run: docker compose -f server/docker-compose.yml up -d redis & + + - name: Start ldap + run: docker compose -f server/docker-compose.yml up -d ldap & + + - name: Start service for backend server + if: ${{ matrix.backendtype }} + run: docker compose -f server/docker-compose.yml up -d ${{ matrix.backendtype }} & + + - name: Install node modules + run: (cd server && yarn) + + - name: Download built artifacts + uses: actions/download-artifact@v4 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + path: server/public + + - name: Verify backend DB service is healthy before starting + if: ${{ matrix.backendtype }} + run: | + until docker ps | fgrep "(healthy)" | grep ${{ matrix.backendtype }} ; do + docker ps + sleep 0.5 + done + + - name: Verify ldap is healthy before starting + run: | + until docker ps | fgrep "(healthy)" | grep test-openldap ; do + docker ps + sleep 0.5 + done + + - name: Test + run: npm run test${{ matrix.backendtype }} --prefix server diff --git a/.gitignore b/.gitignore index 73835b419..57e9807e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,14 @@ -node_modules/ -db/ -coverage/ -build/ -public/javascripts/browserified.js -disc.html .DS_Store .env -.zedstate -tags -zedrem.exe -.zedstate .idea/* - - -jsconfig.json - -public/javascripts/browserified.js - -.vscode/settings.json +client/build/ +server/public +coverage/ +db/ +dbtest/ +node_modules/ +sqlpad.tar.gz +server/drivers/sqlite/sqlpad_test_sqlite.db +server/test/artifacts +server/test/fixtures/sales.sqlite +client/.eslintcache diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..f27575a8e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run precommit diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 9372833d4..000000000 --- a/.npmignore +++ /dev/null @@ -1,15 +0,0 @@ -node_modules/ -db/ -screenshots/ -test/ -coverage/ -docs/ -disc.html -.DS_Store -.env -.zedstate -tags -.zedstate -.idea/ -docker/ -docker-compose.yml diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..9a2a0e219 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..9639f9746 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +*.html +package-lock.json +package.json +client/build +client/node_modules +client/public +db +dbtest +docker-validation +scripts +server/node_modules +server/public +server/test/fixtures/**/*.json +docs/dist +docs/node_modules +docs/.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..836dc702a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "eslint.workingDirectories": [ + { "directory": "./client", "changeProcessCWD": true }, + { "directory": "./server", "changeProcessCWD": true } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2245e10e0..83b2fe517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,972 @@ # Changelog -## 2.1.3 -### January 28, 2017 +## [7.5.7] - 2025-08-21 + +- Revert client dependency updates +- Prep repo for archival + +## [7.5.6] - 2025-07-26 + +- Update Google bigquery driver + +## [7.5.5] - 2025-07-25 + +- Update dependencies (minor and patch) + +## [7.5.4] - 2025-06-09 + +- Update dependencies + +## [7.5.3] - 2025-03-23 + +- Update dependencies + +## [7.5.2] - 2024-12-19 + +- Update dependencies + +## [7.5.1] - 2024-09-31 + +- Update ClickHouse driver: ClickHouse 23.3 or later supported +- Update dependencies + +## [7.5.0] - 2024-08-25 + +- Node 20 or later required +- Update dependencies + +## [7.4.4] - 2024-07-21 + +- Update dependencies + +## [7.4.3] - 2024-05-12 + +- Update dependencies + +## [7.4.2] - 2024-04-24 + +- Update dependencies + +## [7.4.1] - 2024-02-24 + +- Update dependencies + +## [7.4.0] - 2024-02-03 + +- Node 18 required +- Show incomplete on truncated response from BigQuery (#1224) +- Update dependencies + +## [7.3.1] - 2024-01-30 + +- Revert odbc implementation to CJS + +## [7.3.0] - 2024-01-27 + +- Restore arm64 container build +- Convert server code to ESM + +## [7.2.0] - 2023-12-23 + +- Add extra result display formats and copy-to-clipboard (#1209) +- Fix helm chart postgres dependency (#1212) +- Fix BigQuery indexes schema panel (#1214) + +## [7.1.3] - 2023-10-29 + +- Update dependencies + +## [7.1.2] - 2023-08-16 + +- Revert dockerfile node version to node 16 + +## [7.1.1] - 2023-08-09 + +- Update dependencies + +## [7.1.0] - 2023-06-18 + +- [Remove snowflake support](https://github.com/sqlpad/sqlpad/pull/1191) +- Fix docker-entrypoint script to allow setting env vars [#1192](https://github.com/sqlpad/sqlpad/pull/1192) +- Update dependencies + +## [7.0.5] - 2023-05-02 + +- Update dependencies + +## [7.0.4] - 2023-04-22 + +- Update dependencies + +## [7.0.3] - 2023-03-19 + +- Updates clickhouse implementation to [official nodejs driver](https://clickhouse.com/docs/en/integrations/language-clients/nodejs). At time of release, compatible Clickhouse versions are 22.8 - 23.2. +- Updates dependencies + +## [7.0.2] - 2023-03-06 + +- Update dependencies + +## [7.0.1] - 2023-02-21 + +- Add "Trust Server Cert" option for SQL Server connections +- Update dependencies (minor and patch) + +## [7.0.0] - 2022-12-03 + +- Update dependencies - potentially breaking changes for auth due to dependency updates. +- Change in versioning scheme. Semver will no longer be followed. + +## [6.11.4] - 2022-12-02 + +- Update dependencies + +## [6.11.3] - 2022-11-16 + +- Update dependencies + +## [6.11.2] - 2022-09-25 + +- Update dependencies + +## [6.11.1] - 2022-06-07 + +- Remove connection password from connection list API. This matches the behavior for connection detail API. +- Update dependencies + +## [6.11.0] - 2022-04-12 + +- Add max rows override for ODBC, Athena, MySQL, Postgres, Snowflake, and SQL Server drivers + +## [6.10.1] - 2022-03-13 + +- Secure connection template functionality. + This restricts connection template values to fields on user object, preventing the use of arbitrary JavaScript, as it could be leveraged for abuse. (This functionality was, and still is, only available to `admin` accounts.) + +## [6.10.0] - 2022-03-11 + +- Add postgres query timeout config +- Add default role config for Google Auth +- Update dependencies + +## [6.9.0] - 2022-02-05 + +- Add cookie security configuration options +- Limit listed connections based on user access +- Add SAML based role management +- Postgres: Add foreign tables to schema +- Athena: Fix athena region configuration +- Athena: Add support reading schema from glue +- MySQL: Allow base64 for SSL +- Fix migrations to be MySQL 5.7 safe +- Update dependencies + +## [6.8.1] - 2021-09-05 + +- Prevent disclosure of Google OAuth secret +- Fix Athena drop/create table query support + +## [6.8.0] - 2021-08-31 + +- Add support for 0000-00-00 date strings in MySQL +- Add MySQL column descriptions +- Add AWS Athena support +- Add cancellable query support to AWS Athena connection +- Add Azure AD support to OIDC auth +- Allow role to be assigned via OIDC user role +- Add ODBC support to docker container (see `docker-examples/oracle-odbc-image` for example) +- Render markdown hyperlinks in query result columns +- Sort schema and table on name +- Polish schema search and default expanded behaviors +- Add compression to schema route payload +- Fix search history +- Fix OIDC illegal redirects +- Fix `/api/batches` forbidden response when using API key + +## [6.7.1] - 2021-06-15 + +- Add Trino HTTPS support +- Fix ClickHouse authentication +- Fix connection edit form +- Update dependencies + +## [6.7.0] - 2021-06-15 + +_Incomplete release. Use 6.7.1 instead._ + +## [6.6.0] - 2021-04-09 + +- Add Presto authentication and HTTPS support +- Fix ClickHouse queries beginning with comment +- Update Dockerfile node and package dependencies + +## [6.5.0] - 2021-03-18 + +- Add query run history side drawer +- Add support for ClickHouse `LIMIT ,` syntax + +## [6.4.3] - 2021-03-14 + +- Fix chart download button +- Fix 6.4.2 build + +## [6.4.2] - 2021-03-12 + +**BROKEN** - UI development config included in production build, breaking UI. + +## [6.4.1] - 2021-03-13 + +- Fix modal/dialog style + +## [6.4.0] - 2021-03-11 + +- Add Trino support +- Enhance OIDC authentication, supporting discover of endpoint URLs when only providing `SQLPAD_OIDC_ISSUER` endpoint. +- Show connection name if user is editor role and only single connection exists. +- Add context menu to query result cell allowing copy and expanded viewing of cell value. +- Fix ClickHouse support for ClickHouse versions prior to 20.7 +- Fix object and date values in CSV and XLSX export + +## [6.3.0] - 2021-03-08 + +- Postgres: Add self-signed certificate support +- Update queries search to be case-insensitive +- Prevent accidental back navigation when scrolling query results +- Fix query result display on Safari +- Update dependencies + +## [6.2.1] - 2021-02-09 + +- Reduce unknown `SQLPAD_*` environment variable error to warning + +## [6.2.0] - 2021-02-07 + +- Add query status to query history +- Ignore `SQLPAD_SERVICE_` env vars during unknown env var check +- BigQuery: use wrapIntegers to prevent int64 truncation +- Fix schema load for single defaulted connection +- Update dependencies + +## [6.1.2] - 2020-12-03 + +- Fix LDAP auth for profiles without mail attribute. SQLPad's users `email` field is now nullable. +- Fix schema item copy-to-clipboard for non-HTTPS environments +- Fix ClicHouse driver auth for latest ClickHouse release +- Use `cancelled` status for statements following a statement error in multi-statement batch. (These previously were stuck in `queued` status.) +- Return 404 for unfinished statement results. Statement results API is only intended to be used once a statement is `finished`. + +## [6.1.1] - 2020-11-29 + +- Fix query result grid value padding + +## [6.1.0] - 2020-11-28 + +Misc enhancements to support showing multiline content in query results. + +- Text value cells render line breaks. Row height is increased to accommodate the column value with most lines. +- JSON and other object values are JSON pretty-printed for multiline rendering. +- If single column is returned, the initial column width will use full width of result area. +- Changes grid font family to monospace +- Adds shadow to indicate text overflow + +## [6.0.2] - 2020-11-28 + +- Add LDAP trace logging +- Add LDAP bind check at startup if enabled + +## [6.0.1] - 2020-11-26 + +- Fix missing query text from query history +- Fix stale page title on sign in + +## [6.0.0] - 2020-11-22 + +This release removes deprecated functionality and changes some default behaviors, requiring a major version bump. There are some notable enhancements as well however. + +### Updating to 6.0.0 + +Upgrading to version 6 requires existing SQLPad instances be running on a 5.x.x release. If version 4 or earlier is detected, startup and migration will be aborted. + +To upgrade, install and run via your preferred method. Version 6 has no data migrations other than a dummy migration to serve as a version marker. If you'd like to roll back to 5.x.x, you _should_ be able to run 5.8.4 on a 6.0.0 database. + +Database backups are encouraged however. Please follow good practices if your SQLPad use warrants it. + +### Visualization Configuration Sidebar + +Visualization configuration has moved into a sidebar on the right. The existing chart button toggles its visibility. + +visualization sidebar + +### Multiple Result Sets + +Multiple result set support has been added to the UI. When executing multiple SQL statements at once, a summary table will show with the status of each SQL statement. Executing single statements works as it did before. + +Note that query visualization is locked to the last SQL statement in a batch. + +multiple results running + +multiple results error + +multiple results statement drill down + +### Query Dialog & Sharing Enhancments + +Query name, tags, and sharing have been moved to a dialog, appearing on initial save or click on query name in the toolbar. Query sharing has been enhanced, allowing queries to be shared with individual users as well as limit their ability to save changes to the query. + +query dialog and sharing enhancements + +### Schema Context Menu + +A context menu has been added to the schema sidebar. When right-clicking schemas, tables, and columns, a menu will display with values that can be copied to the clipboard. + +schema context menu + +### User Profile Dialog + +Users may now provide a name and change their email as well as password if local authentication is enabled. + +user profile dialog + +### Strict Configuration Checking + +Any environment variables set starting with `SQLPAD_` that are not known by SQLPad will raise an error and prevent SQLPad from starting up. This should help catch config issues early. + +### Breaking Changes + +- Removes password reset UI emails. Password reset URLs can still be generated in UI via an admin account +- Removes Slack webhook. Use generic webhooks to build your own. +- Removes deprecated configuration variables +- Removes .json and .ini configuration file support +- Removes open admin registration. `SQLPAD_ADMIN` should be used for initial account, or auto sign-up should be enabled when using compatible auth strategy. +- Reduces default query result row limit to 10,000 (previously 50,000) +- Server process exits on configuration error or unknown configuration key + +### Maintenance, Fixes, and UX + +- Adds query-not-found dialog, prompting user to start new query. +- Simplifies query list (if too much open issue) +- Increases SQL editor font size to 14 +- Fixes tag input leaving unselected value in input +- Fixes clone/new URL not changing as expected +- Fixes reusing connection clients on query change (See issue #806) +- Updates dependencies + +## [5.8.4] - 2020-11-04 + +- Ensure port config value handled as number + +## [5.8.3] - 2020-11-04 + +_accidentally tagged without changes_ + +## [5.8.2] - 2020-10-30 + +- Fix LDAP role sync. If LDAP role filters are set up, role syncing may be toggled per user on user forms. By default auto-created users will have roles synced. Manually added users will not. +- Update dependencies +- Fix connection client heartbeat race condition (UI) + +## [5.8.1] - 2020-10-23 + +- Fix LDAP editor role search filter +- Allow empty/`denied` value for `SQLPAD_LDAP_DEFAULT_ROLE` to deny users that do not match LDAP role filters. See [LDAP documentation](http://sqlpad.github.io/#/authentication?id=ldap-experimental) for more info. + +## [5.8.0] - 2020-10-22 + +- Add LDAP auto sign-up and dynamic role assignment based on LDAP search +- Fix: Allow LDAP auth without local/userpass auth enabled +- Update dependencies + +## [5.7.0] - 2020-10-19 + +- Add Apache Pinot support +- Enhance SQL editor autocomplete + +## [5.6.0] - 2020-09-04 + +- Fix Presto/ClickHouse error display +- Show line numbers in SQL editor +- Update dependencies + +## [5.5.1] - 2020-08-07 + +- Fix missing `top` strategy addition in 5.5.0 + +## [5.5.0] - 2020-08-07 + +- Add `top` limit strategy for ODBC connections +- Fix SQL editor not clearing on new query +- Update dependencies + +## [5.4.0] - 2020-08-03 + +- Add configurable query result store (memory, database, and redis now an option). See `SQLPAD_QUERY_RESULT_STORE` under [configuration](https://sqlpad.github.io/#/configuration) docs. +- Update MySQL connections to use INTERACTIVE flag to prevent early connection close. + +## [5.3.0] - 2020-07-31 + +- Embolden result column headers +- Add configurable session store (memory, database, and redis now an option). See `SQLPAD_SESSION_STORE` under [configuration](https://sqlpad.github.io/#/configuration) docs. +- Update server dependencies + +## [5.2.1] - 2020-07-27 + +- Fix MySQL2 transaction support +- Fix old query results potentially overwriting new query results when using multi-statement transactions + +## [5.2.0] - 2020-07-20 + +This release introduces new generic webhooks for a variety of events, while deprecating specific communication implementations (SMTP email and Slack). With webhooks, it is up to you to implement communication to your SQLPad users. + +The webhooks added support a larger number of events than previously handled, such as queries being run and results/error received from those queries. + +- Add webhooks [documentation](http://sqlpad.github.io/#/webhooks) +- Deprecate SMTP email and Slack webhook, both to be removed in v6. +- Capture database error message on ODBC driver connection error +- Show service token UI only if enabled via config (#787) +- CrateDB - provide separate user, password, and SSL fields (#793) +- Fix multiline string support in `sql-limiter` +- Fix regular expression constraint in `sql-limiter` + +## [5.1.0] - 2020-07-10 + +- Add OpenID Connect authentication support +- Add .env config file support +- Fix disabled admin on initial SQLPad load when using seed queries +- Deprecate INI and JSON config files support +- Environment variables renamed for better consistency. Old name is deprecated: + - `GOOGLE_CLIENT_ID` ➡ `SQLPAD_GOOGLE_CLIENT_ID` + - `GOOGLE_CLIENT_SECRET` ➡ `SQLPAD_GOOGLE_CLIENT_SECRET` + - `DISABLE_AUTH` ➡ `SQLPAD_AUTH_DISABLED` + - `SQLPAD_DISABLE_AUTH_DEFAULT_ROLE` ➡ `SQLPAD_AUTH_DISABLED_DEFAULT_ROLE` + - `DISABLE_USERPASS_AUTH` ➡ `SQLPAD_USERPASS_AUTH_DISABLED` + - `ENABLE_LDAP_AUTH` ➡ `SQLPAD_LDAP_AUTH_ENABLED` + - `LDAP_URL` ➡ `SQLPAD_LDAP_URL` + - `LDAP_BASE_DN` ➡ `SQLPAD_LDAP_BASE_DN` + - `LDAP_USERNAME` ➡ `SQLPAD_LDAP_USERNAME` + - `LDAP_PASSWORD` ➡ `SQLPAD_LDAP_PASSWORD` + - `CERT_PATH` ➡ `SQLPAD_HTTPS_CERT_PATH` + - `CERT_PASSPHRASE` ➡ `SQLPAD_HTTPS_CERT_PASSPHRASE` + - `KEY_PATH` ➡ `SQLPAD_HTTPS_KEY_PATH` + - `SERVICE_TOKEN_SECRET` ➡ `SQLPAD_SERVICE_TOKEN_SECRET` + +## [5.0.0] - 2020-07-03 + +Version 5 contains many infrastructure and API changes, as well as migrations that are not easy to roll back from. It is highly recommended take a backup of your database (`SQLPAD_DB_PATH` directory and/or your backend db in use). + +This release finishes the migration from an embedded JSON database to an ORM and SQLite (used by default). For new instances of SQLPad, the following alternative backend databases may be used: SQL Server, MySQL, MariaDB, or PostgreSQL via `SQLPAD_BACKEND_DB_URI`. + +Providing a value for `SQLPAD_DB_PATH`/`dbPath` is still required, as the file system is still used for sessions and storage of query results. This requirement will be removed in a later 5.x release. + +Migrations will be run on SQLPad start up. To disable this behavior, set `SQLPAD_DB_AUTOMIGRATE` to `false`. Migrations may be run manually via `node server.js --config path/to/file.ext --migrate`. When using `--migrate` the process will exit after applying migrations. + +Special thanks to @eladeyal-intel, @bruth, @yorek, @dengc367, @murphyke, and @Wizhi, for all their contributions in this release! + +### Enhancements + +- Column descriptions added for Google BigQuery + +- Added MySQL 2 driver + +- Add Redshift driver + +- Add ClickHouse driver + +- Add ActiveDirectory authentication + +- Multi-transaction statement option made available for MySQL/MySQL 2 drivers + +- Connection dropdown hidden if user is non-admin and only 1 connection exists + +- Connection may be pre-selected via URL query parameter `connectionId` or `connectionName` + +- Add default role when using disabling auth (`SQLPAD_DISABLE_AUTH_DEFAULT_ROLE`). `editor` is used by default. + +- Users may be enabled/disabled to restrict access (alternative to deletion) + +- Migrations may be run on their own using `--migration` + +- Automigration may be disabled with `SQLPAD_DB_AUTOMIGRATE` + +- Server side filtering and pagination added for `queries` API + +- ODBC - limit strategies added for configuring method used to enforce query row limits + +- Test connection error shown + +- New `/batches` API for running multi-statement SQL. This replaces `/query-result` API, and is written in a more RESTful approach, removing the need to extend SQLPad timeouts. See [API docs](http://sqlpad.github.io/#/api-batches) for more info. + +- Adds `allowedDomains` config item and deprecates `whitelistedDomains` to be removed in v6. + +### Breaking + +- Only admins may see all query history + +- `denyMultipleStatements` connection option removed. Multiple statements are now attempted to be supported at SQLPad REST API level via `batches` and `statements` API. + +- `/download-results/` API has been removed in favor of `/statement-results/`, which is similar but based on `statementId` instead of `cacheKey`. See [API docs](http://sqlpad.github.io/#/api-batches) for more info. + +- `debug` config option removed. Use `appLogLevel` set to `debug` instead. + +- `tableChartLinksRequireAuth` config option removed. + + All table/chart links require authentication going forward. If unauthenticated access to these URLs is necessary, look into whether an alternate auth solution may be used to passively provide authentication as necessary (like auth proxy for example.) + +- data model changes: + + - `createdDate` fields have been renamed to `createdAt` (`created_at` in table) + - `modifiedDate` fields have been renamed to `updatedAt` (`updated_at` in table) + - `user.signupDate` renamed to `user.signupAt` (`signup_at` in table) + - `query.modifiedBy` renamed to `query.updatedBy` (`updated_by` in table) + - `query.createdBy` and `query.updatedBy` now hold user id as opposed to user email address. + - `query.lastAccessDate` is removed. It was no different from `updatedAt` + - Connection driver-specific fields have been moved to `connection.data`. Fields are still decorated on base of connection object for compatibility. + - `cache` table has changed to use `id` instead of `cacheKey`. + - `cache.expiration` has renamed to `cache.expiryDate` for consistency with other models + - `cache.queryName` has renamed to `cache.name` for more generic use + - `cache.schema` has been renamed to `cache.data` and stores raw JSON for generic use + +- Regex filter for query history no longer supported + +- Config file without an .json or .ini extension will be assumed to be JSON. Specific extensions will be required in a future release. + +- Config file that does not exist will throw an error at startup instead of silently ignoring. + +## [4.4.0] - 2020-04-22 + +- Adds ability to set initial default connection via configuration `defaultConnectionId`. (Only for initial load. User connections are then cached locally) +- Changes default query list filter to "all queries" instead of "my queries". +- Defaults schema sidebar to open, and removes local caching of toggle selection. +- Fixes autocomplete missing when schema sidebar is hidden on load. +- Updates dependencies + +## [4.3.0] - 2020-04-16 + +- Support multiple datasets in BigQuery connection +- Fix sql splitting for strings containing `;` + +## [4.2.0] - 2020-04-06 + +### Features + +- Add Google BigQuery support [documentation](https://sqlpad.github.io/#/connections?id=bigquery) + +- Add SQLite support [documentation](https://sqlpad.github.io/#/connections?id=sqlite) + +- Adds batch query support to ODBC (last statement is shown in UI) + +- Auth: Add option to disable authentication. [documentation](https://sqlpad.github.io/#/authentication?id=no-authentication) + + When auth is disabled, application no longer requires authentication. + +- Auth: Add proxy authentication support. [documentation](https://sqlpad.github.io/#/authentication?id=auth-proxy) + +- Add private/shared query model. + + Going forward queries are _private_ by default. When sharing is enabled, query is shared with all users (and they are given write permissions). Finer-grained access to be added in the future (share with specific user, read vs write) + +- Add connection and query seed data support [documentation](https://sqlpad.github.io/#/seed-data) + +- Add service tokens (api tokens). New menu option is available when logged in as admin. + +- Add application header for application-level administration + +- Adds multi-statement transaction support for Postgres, SQLite, and ODBC. [documentation](https://sqlpad.github.io/#/connections?id=multi-statement-transaction-support) + +- Add config deprecation for following keys: `debug`, `tableChartLinksRequireAuth`, `keyPath`, `certPath`, `certPassphrase` + +- Add connection template support [documentation](https://sqlpad.github.io/#/connection-templates) + +- Adds additional query run logging for queries executed and details surrounding them (logged under `info` level) + +### Fixes + +- Fix: long connection form display to always show save/test buttons + +- Use ISO 8601 timestamps for log messages instead of unix epoch + +### Maintenance / Misc / Dev updates + +- Updated dependencies +- Introduced SQLite as backing store. nedb data (embedded db currently in use) will eventually be migrated to SQLite. +- Added migration framework. Migrations are run at server startup. +- Added mult-stage build +- Lots of refactoring to better organize authentication, addition of new features. + +## [4.1.1] - 2020-03-10 + +- Fix auth for email addresses containing `+` and quoted `@` characters + +## [4.1.0] - 2020-02-25 + +### Features + +- Add Snowflake driver/support (#519) +- Add MS SQL Server readOnly intent option (#520) +- Add logging using pino (#527, #547) + + SQLPad now logs json messages to stdout using the [pino](http://getpino.io/#/) library. Amount of logging can be controlled by setting `appLogLevel` and `webLogLevel` for application and web logs respectively. Pino does not manage log transport, but has an ecosystem of tooling available that does http://getpino.io/#/docs/transports. + +- Allow dynamic user data in connection configuration (#544) + + Connections can have user values dynamically substituted in connection configuration values containing text values. This is a work-in-progress and can be used for adventurous. See PR for details, otherwise more info and corresponding UI will come with future releases. + +- Add GitHub Release builds via build pipeline (#550) +- Add `dbInMemory` setting (#553) + + `dbInMemory` will run the embedded SQLPad db in memory without logging to disk. Enabling this does not remove the need for `dbPath` at this time, as file system access is still required for result caches and express session support. (`dbPath` to become optional in future release) + +### Fixes + +- Fix TypeError: Do not know how to serialize a BigInt (#522) +- Fix tests for non-utc time zones (#524) + +### Maintenance + +- Update server dependencies (#525) +- Maintenance & refactoring (#534, #535, #538, #539, #540, #541, #542, #543, #545, #551, #552, #553) + +## [4.0.0] - 2020-01-18 + +### Breaking changes + +- Node 12 or later required + +### Features + +- Add ability to restrict connections to specific users (#502) +- Merge multiple statement result sets for postgres into 1 result set (#510) +- Add Pre-query statements options to MySQL driver (#511) +- Add Query execution history (#512) +- Add Pre-query statements options to MySQL driver (#515) +- Update odbc dependency to v2 + +## [3.5.3] - 2019-12-30 + +- Revert build script to use sh + +## [3.5.2] - 2019-12-30 + +- Fix admin password from env var +- Fix mysql zero-row result bug +- Fix add user and connection form submit page refresh in Firefox +- Fix schema sidebar height issues in Safari/Chrome +- Set build script to use bash instead of sh +- Let connection username/password decipher errors bubble up + +## [3.5.1] - 2019-12-13 + +- Added support for authentication to Cassandra +- Added SSL support for MySQL/MariaDB +- Added JSON query result downloads alongside XSLX and CSV + +## [3.5.0] - 2019-11-29 + +- Add json download format for query result +- Fix use of SQLPAD_ADMIN_PASSWORD setting + +## [3.4.0] - 2019-11-10 + +- Add `sqlserverMultiSubnetFailover` option for SQL Server + +## [3.3.0] - 2019-11-08 + +- Add `SQLPAD_TIMEOUT_SECONDS` config for setting http server timeout. Useful for supporting long running queries. + +## [3.2.1] - 2019-10-17 + +- Fix SQL Server "port must be type number" error + +## [3.2.0] - 2019-10-12 + +- Adds support for defining connections via configuration. + +## [3.1.1] - 2019-10-10 + +Update all dependencies to latest (client & server). Includes major updates to following modules: + +- SAP Hana +- Cassandra +- SQL Server +- email support +- XLSX and CSV query result downloads +- SAML authentication + +Some integrations are not able to be tested by existing test setup. Please open an issue if any breakage is discovered. + +## [3.1.0] - 2019-09-30 + +- Add cookie name config setting +- Add admin password setting + +## [3.0.2] - 2019-09-01 + +- Removes npm version update check and related config item `disableUpdateCheck` + +## [3.0.1] - 2019-09-01 + +SQLPad v3 is a UI redesign/refresh along with a large file structure change and configuration change. It has been in "beta" for quite some time, and if you are running the latest docker image or running a recent build of master, you've already been using it. + +### Features + +- URLs in query results will be turned into links +- SAML authentication support +- Remember selected connection id / schema toggle +- Configurable session time and secret +- Support for JSON and INI config file added. File should config using `key` fields found in [configItems.js](https://github.com/sqlpad/sqlpad/blob/master/server/lib/config/config-items.js). Config file path default is `$HOME/.sqlpadrc` and may otherwise be specified using `--config` via command line or `SQLPAD_CONFIG` environment variable. + +### Breaking changes + +- CLI flags have been changed to use config item key (#460) +- Default db path is no longer used if db path is not provided in config. Previous default was `$HOME/sqlpad/db`. +- Default config file path no longer used. Previous default was `$HOME/.sqlpadrc`. +- Configuration UI has been removed. See https://github.com/sqlpad/sqlpad/issues/447. +- cli-flags in saved .sqlpadrc JSON are no longer used for config values. These configuration keys should instead be switched the the `key` found in `configItems.js`. For example, instead of `dir` or `db`, use `dbPath`. Instead of `cert-passphrase` use `certPassphrase`, etc. +- `--save` and `--forget` cli flags no longer supported + +### Fixes + +- Fix preview sticking around after query selection +- Fix tag/search input in query list +- Add loading indicator for schema sidebar +- Fix user needing to sign in again after sign up +- Fix app menu for non-admin users +- Fix frozen editor after query error + +## [3.0.0] - 2019-09-01 + +This build is broken. sorry :( + +## [3.0.0-beta.1] - 2019-06-25 + +Beta for version 3 may be installed via latest docker image, or by installing via npm referencing exact version or beta tag. + +SQLPad v3 is backwards-compatible with SQLPad v2 database files, and is mostly a UI redesign/refresh and a large file structure change. Give it a try and if you aren't ready for it roll back to v2 and everything should still work. + +#### Editor-first UI refresh + +UI components previously based on bootstrap UI components are now replaced by custom components. Magenta is embraced as a secondary color. + +Management and listing pages (Queries, connections, users, and configuration) have been moved into side drawers, allowing management and browsing of things without leaving the current query. The query editor is the primary focus of the application. + +Query editor toolbars have been consolidated into a single bar to maximize use of space on the page. + +Unsaved changes to a previously-saved query are now saved, prompting the user to restore on next open. This is not enabled for unsaved changes to "new" queries since it could become an annoyance, but can be added if there is interest. + +Query result chart has been moved to a smaller resizable pane along side the SQL query instead of being placed in a tab. This impacts the size available for the chart, but brings it to the default view, allowing altering of the query without changing tabs. + +The schema sidebar may now be hidden and is now searchable. It has also been rewritten to render large trees efficiently. + +Query result grid no longer has data bars for numeric values since it didn't make sense for all number values. Date value display logic has been altered to only show timestamps if timestamps are detected. When timestamps are shown, the full timestamp from the JavaScript date object is displayed. + +## [2.8.1] - 2019-03-07 + +- Fix Google oauth for Google+ API shutdown + +## [2.8.0] - 2018-10-17 + +- Add postgres column description to schema sidebar +- Log user id and email in debug mode +- Replace memory-based session store with file-based + +## [2.7.1] - 2018-08-02 + +- Fix query editor not responding to input after query result scroll + +## [2.7.0] - 2018-07-01 + +- Add optional odbc support. See [ODBC wiki page](https://github.com/sqlpad/sqlpad/wiki/ODBC) for more detais + +## [2.6.1] - 2018-06-17 + +- Fix query editor loading when connections load slowly + +## [2.6.0] - 2018-05-05 + +- Add Cassandra support +- Sort driver dropdown in connection form + +## [2.5.8] - 2018-05-05 + +- Extend data grid to full width of container + +## [2.5.7] - 2018-05-04 + +- Implement data grid using react-virtualized (fixes resizable columns) +- Fix chart rendering error when columns no longer returned by query are referenced +- Allow case insensitive user lookup by email (fixes case sensitive signup/signin issues) + +## [2.5.6] - 2018-04-25 + +- Revert chart fix from 2.5.5 preventing charts from rendering + +## [2.5.5] - 2018-04-23 + +- Remove frameguard protection (fixes iframe embeds) +- Use CDN for bootstrap font (fixes missing icons when using baseUrl) +- Update dependencies +- Fix 0 values classified as string in query results +- Fix UI chart error when referencing columns no longer returned by query +- Fix SQLPad crash postgres queries exceeding max row limit +- Only show admin registration open message if admin registration is actually open +- Fix baseUrl of undefined error +- A lot of driver refactoring + - Driver implementations now consolidated at /server/drivers + - All drivers now tested +- New docker build process/root-level Dockerfile + +## [2.5.4] - 2018-04-01 + +- Fixed password reset link when using base url + +## [2.5.3] - 2018-03-29 + +- Fix SAP HANA schema not being cached due to dots in column name + +## [2.5.2] - 2018-03-27 + +- Fix error when updating connection +- Fix SQLPAD_BASE_URL / --base-url use + +## [2.5.1] - 2018-02-05 + +- Fix early session expiration / extend session expiration every response + +## [2.5.0] - 2018-02-05 + +- Added support for SAP HANA (ccmehil) +- Many security improvements + - Majority of dependencies updated + - Implemented expressjs security best practices + - Helmet middleware added + - Express-session used instead of cookie-session + - Randomly generated cookie secrets + - Sessions now expire (1 hour) + - Limited amount of config info sent to front end +- Updated styling for User and Connection admin pages (bringing boring tables back. updates to rest of app to follow) +- Schema sidebar updates + - Limits presto schema sidebar to schema if provided in connection info + - Removed (view) label on views + +## [2.4.2] - 2017-12-27 + +- Fixed generic schema info query for case-sensitive collation + +## [2.4.1] - 2017-12-03 + +- Fixed disappearing data table after vis resize + +## [2.4.0] - 2017-12-03 + +- Added resizable panes to query editor +- Added SQL formatter to query editor (KochamCie) +- Added clone query button to query editor +- Added prompt when navigating away from unsaved query edits +- Redesigned bar charts in data grid to a more minimal design +- Redesigned query editor nav bar + - Brings query name input out of modal + - Adds unsaved changes indicator to save button + - Adds shortcut/tip documentation to modal + - Uses nav links instead of buttons for less visual noise +- Updated editor shortcuts + - Running query now `ctrl+return` or `command+return` + - Format query with `shift+return` +- Updated tauCharts to latest version +- Implemented react-router & fix unnecessary page loads on navigation +- Bundled remaining vendor JavaScript libs +- Removed external font-awesome dependency from CDN +- Fixed bigint handling for MySQL +- Fixed date display in charts +- Fixed date display for MySQL +- Fixed cell content not expanding when cell is expanded +- Fixed unintended page refresh on editor sidebar link clicks +- Fixed layout bugs from flexbox +- Lots of misc front-end refactoring + +## [2.3.2] - 2017-10-21 + +- Fix --base-url config use +- Refactored layout styling to use flexbox css + +## [2.3.1] - 2017-10-07 + +- Force no-cache on fetch requests (fixes some odd IE issues) +- Fix docker entry point + +## [2.3.0] - 2017-09-04 + +- New features + - Added systemd socket activation support (epeli) + - Added option to disable update check + - Resizable data grid columns (slightly buggy) +- Fixes + - Fixes MySQL schema sidebar showing extra dbs + - Fixes loss of precision of numbers in UI grid (even if they were text) + - Fixes Presto driver + - Fixes React deprecation warnings + - Fixes incorrect date display in UI + - All dates were being localized. now displayed without localization +- Compatibility notes + - Node v6.x now required at minimum + +## [2.2.0] - 2017-05-29 + +- added SOCKS proxy support for postgres (brysgo) + +## [2.2.0-beta2] - 2017-03-19 + +- fixed version displayed in about modal + +## [2.2.0-beta1] - 2017-03-18 + +- fixed query tag weirdness from previous v1 weirdness +- leading 0s preserved in query results and treated as strings instead of numbers +- support for postgres ssl certs (johicks and nikicat) +- fixed crate v1 schema support (mikethebeer) +- naive autocomplete +- refactored connection admin screen +- changed build system to fork create-react-app + +## [2.1.3] - 2017-01-28 + - Ensure strict db startup order (vweevers) - Improve query editor performance/reduce SQL editor lag -## 2.1.2 -### December 9, 2016 +## [2.1.2] - 2016-12-09 + - Fix chart only view not displaying charts - Fix query editor search - Update dependencies -## 2.1.1 -### November 29, 2016 -- Fix: disabling of links on query details modal (thanks vweevers!) -- Fix: Vis tab loading indicator behaves same as query tab, hiding error on rerun (thanks vweevers!) -- Fix: Charts rendered lazily. Query result grid loads faster, large query results won't lock browser until you try to chart. (thanks vweevers!) +## [2.1.1] - 2016-11-29 + +- Fix: disabling of links on query details modal (vweevers) +- Fix: Vis tab loading indicator behaves same as query tab, hiding error on rerun (vweevers) +- Fix: Charts rendered lazily. Query result grid loads faster, large query results won't lock browser until you try to chart. (vweevers) - Fix: Hide local auth form if DISABLE_USERPASS_AUTH=true -## 2.1.0 -### November 20, 2016 -- run https via sqlpad directly (see additional setting) (thanks jameswinegar!) -- Support non English characters when downloading files (thanks askluyao!) +## [2.1.0] - 2016-11-20 + +- run https via sqlpad directly (see additional setting) (jameswinegar) +- Support non English characters when downloading files (askluyao) - render booleans/null timestamps properly -## 2.0.0 -### October 12, 2016 +## [2.0.0] - 2016-10-12 + - (See beta 1 - 3 release notes) -## 2.0.0-beta3 -### October 11, 2016 +## [2.0.0-beta3] - 2016-10-11 + - Password reset/forogot password functionality added - - Admins may generate reset links manually - - If smtp is set up forgot password link is enabled -- EMAIL! + - Admins may generate reset links manually + - If smtp is set up forgot password link is enabled +- EMAIL - Configuration: - - Checklist added for OAuth and Email - - Item is disabled in UI if value is provided by environment or cli - - sensitive values are only masked if environment variables + - Checklist added for OAuth and Email + - Item is disabled in UI if value is provided by environment or cli + - sensitive values are only masked if environment variables + +## [2.0.0-beta2] - 2016-09-19 -## 2.0.0-beta2 -### September 19, 2016 - Move to single-page-app architecture - New query loading animation - Title and export options added to chart/table only views @@ -50,82 +976,94 @@ - Misc bug fixes - More code cleanup -## 2.0.0-beta1 -### September 1, 2016 -- UI design updates *everywhere* +## [2.0.0-beta1] - 2016-09-01 + +- UI design updates _everywhere_ - Query Listing: - - preview query contents by hovering over query listing - - occassional search/filter weirdness has been fixed + - preview query contents by hovering over query listing + - occasional search/filter weirdness has been fixed - Query Editor: - - Schema sidebar no longer separates views and tables in hierarchy - - New result grid - - inline bar plot rendered for numeric values - - display issues fixed for certain browsers - - New tags widget for cleaner input - - Browser tab name now reflects query name - - Updated taucharts library with stacked bar charts - - Line and Scatterplot charts may have chart filters enabled - - 'show advanced settings' in vis editor now has a few advanced settings depending on chart (y min/max, show trendline, show filter) - - switching between sql/vis tabs won't reset chart series toggles - - table/chart only links may be set to no longer require login (see configuration page) + - Schema sidebar no longer separates views and tables in hierarchy + - New result grid + - inline bar plot rendered for numeric values + - display issues fixed for certain browsers + - New tags widget for cleaner input + - Browser tab name now reflects query name + - Updated taucharts library with stacked bar charts + - Line and Scatterplot charts may have chart filters enabled + - 'show advanced settings' in vis editor now has a few advanced settings depending on chart (y min/max, show trendline, show filter) + - switching between sql/vis tabs won't reset chart series toggles + - table/chart only links may be set to no longer require login (see configuration page) - Configuration: - - Specific config inputs and labels - no more open ended key/value inputs - - Current environment config documented with assistive popovers + - Specific config inputs and labels - no more open ended key/value inputs + - Current environment config documented with assistive popovers - Update notification moved in-app - Under the hood - - updated all the code dependencies - - reworked some foundation code for easier future development -- Known issues / not yet implemented: - - Query tag input does not allow creation - - Query auto-refresh not yet implemented + - updated all the code dependencies + - reworked some foundation code for easier future development +- Known issues / not yet implemented: + - Query tag input does not allow creation + - Query auto-refresh not yet implemented + +## [1.17.0] -## 1.17.0 - empty postgres queries (like executing a comment only) no longer crash sqlpad - materialized views are included in schema sidebar for postgres -## 1.16.0 -- SqlPad may now be mounted under a base url path by providing --base-url cli flag or SQLPAD_BASE_URL env variable +## [1.16.0] + +- SQLPad may now be mounted under a base url path by providing --base-url cli flag or SQLPAD_BASE_URL env variable - Updated taucharts to 0.9.1 - Legends are now included when saving png chart images -## 1.15.0 +## [1.15.0] + - Many client-side and server-side dependencies updated - Add ability to bind to a specific IP address via the --ip flag or the SQLPAD_IP environment variable -- Removed sort inputs for bar charts. (Chart sort may instead be influenced using ORDER BY in SQL query.) +- Removed sort inputs for bar charts. (Chart sort may instead be influenced using ORDER BY in SQL query.) + +## [1.14.0] -## 1.14.0 - Add ability to turn off date localization (add config item "localize" set to "false") -## 1.13.0 -- Add --debug flag to SqlPad cli to enable extra logging +## [1.13.0] + +- Add --debug flag to SQLPad cli to enable extra logging - Port and passphrase may be set via environment variables SQLPAD_PORT and SQLPAD_PASSPHRASE -## 1.12.0 +## [1.12.0] + - Add support for Crate.io -## 1.11.0 +## [1.11.0] + - Auto-refresh query every x seconds - Fix crash when unregistered user tries to log in -## 1.10.0 +## [1.10.0] + - MySQL connections can now old/insecure pre 4.1 auth system - links now available to display just the chart or data grid -## 1.9.0 +## [1.9.0] + - Charting now handled by the very cool tauCharts library. It's a bit faster, has facets, grammar of graphics concepts, handles time series data better, trendlines. -- When changing chart types, SqlPad will remember and reapply the field selections where applicable. -- SqlPad database files compacted every 10 minutes, instead of once a day +- When changing chart types, SQLPad will remember and reapply the field selections where applicable. +- SQLPad database files compacted every 10 minutes, instead of once a day - Signup page styling is fixed. - Schema-item-name copy-to-clipboard buttons now available. Opt in by creating configuration item `showSchemaCopyButton` to `true`. - Query results can now be downloaded as xlsx file. (link will be hidden if csv downloads are disabled) -## 1.8.2 +## [1.8.2] + - Connection password no longer visible on connection screen. -## 1.8.1 +## [1.8.1] + - Duplicate content headers prevented when csv filename contains comma. -## 1.8.0 +## [1.8.0] + - Authentication now managed by Passport.js - Username/Password authenication strategy can be disabled by setting environment variable DISABLE_USERPASS_AUTH - Google OAuth strategy can be enabled by setting GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and PUBLIC_URL environment variables @@ -133,45 +1071,56 @@ - Whitelist domains for username administration by setting environment variable WHITELISTED_DOMAINS - Query connection now selected by default if only one exists -## 1.7.0 +## [1.7.0] + - Tags now look like tags - Typeahead added for easy tag creation -## 1.6.0 +## [1.6.0] + - Code cleanup -## 1.5.1 +## [1.5.1] + - remove console logging used for debugging -## 1.5.0 +## [1.5.0] + - Vertica now supported via Vertica driver - CSVs no longer generated if disabled - optimizations made to schema-info processing -## 1.4.1 +## [1.4.1] + - improved db tree/schema info performance -## 1.4.0 +## [1.4.0] + - Charts can be saved as images -## 1.3.0 +## [1.3.0] + - work-around to handle multiple statements using postgres driver - fix to provide MAX_SAFE_INTEGER if not defined -## 1.2.1 +## [1.2.1] + - query results are limited to 50,000 records. This can be changed by adding a configuration key "queryResultMaxRows" and providing the number of max rows you would like returned. - Minor bugfixes - Text selection enabled on query results - schema information now cached - connection port is optional in UI -## 1.2.0 +## [1.2.0] + - Added port property to connections - Configuration system has been added - CSV downloads can be disabled via configuration. Add new item with key "allowCsvDownload" with value "false" to disable. -## 1.1.0 +## [1.1.0] + - Add initial Vertica support via use of Postgres driver -## 1.0.0 -- SqlPad is released! +## [1.0.0] + +- SQLPad is released diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ad1e19ae4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to SQLPad + +Thank you for taking interest in contributing to SQLPad! + +SQLPad is a legacy project and is in maintenance mode. Please only submit issues or PRs for existing functionality. + +### Security Disclosure + +For the safe disclosure of security bugs please email rick.bergfalk@gmail.com directly. + +## License + +By contributing to SQLPad, you agree that your contributions will be licensed under its MIT license. + +## Getting Started + +Ready to get started? Refer to [DEVELOPER-GUIDE.md](https://github.com/sqlpad/sqlpad/blob/master/DEVELOPER-GUIDE.md) to learn how to build and orient yourself with the project. diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 000000000..58df2a8e0 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,31 @@ +# Dependencies + +SQLPad's an old project, and some dependencies are being left at specific versions for specific reasons. + +Lists of outdated dependencies can be retrieved by doing the following: + +```sh +# from root +cd server # or client +yarn outdated +``` + +## Outdated server dependencies (as of 8/25/2024) + +- `eslint` / `eslint-config-prettier` - airbnb preset doesn't have 9 listed as supported yet. +- `ldapjs` - 3.x brings a [lot of changes and risk of breakage](https://github.com/ldapjs/node-ldapjs/releases/tag/v3.0.0). +- `prettier` - 3.x brings some formatting changes. Avoiding unnecessary noise for the forks out there. +- `sql-formatter` - 3.x onward has performance regressions, changes in functionality. +- `umzug` - 3.x has many (unnecessary) breaking changes and additional dependencies. + +## Outdated client dependencies (as of 8/25/2024) + +There's a lot. React is on the verge of big changes, some of the dependencies used have been abandoned. + +Since client-side deps are used for development and compiled into something... there's less harm in leaving these sit. + +The frontend client can be considered "frozen" as of this point. If this project is revived there's a lot that needs to be addressed here. + +- `d3` - taucharts requires d3@5 +- `history` - react-router-dom@5 requires history@4 +- `react-router-dom` - react-router-dom@6 does not support Prompt component (yet) diff --git a/DEVELOPER-GUIDE.md b/DEVELOPER-GUIDE.md new file mode 100644 index 000000000..7f78d45b2 --- /dev/null +++ b/DEVELOPER-GUIDE.md @@ -0,0 +1,170 @@ +# Developer Guide + +## Prerequisites + +- [Node](https://nodejs.org) installed at v14.0.0+ +- [Yarn 1 (classic yarn)](https://classic.yarnpkg.com/lang/en/) +- Some dependencies may require compilation. On macOS, the Xcode Command Line Tools should be installed. On Ubuntu, `apt-get install build-essential` will install the required packages. Similar commands should work on other Linux distros. Windows will require some additional steps, see the [`node-gyp` installation instructions](https://github.com/nodejs/node-gyp#installation) for details. + +## Style Guide + +SQLPad uses an automatic code formatter called [Prettier](https://prettier.io/) and a linting tool called ESLint. +Run `yarn run lint` after making any changes to code to check formatting and lint. Some formatting and lint may be fixed automatically by running `yarn run fixlint`. + +If using Visual Studio Code, Prettier and ESLint extensions can be installed to assist in detecting and fixing issues during development. + +## Getting Started + +- Clone/download this repo +- Install dependencies and build the UI + + ```sh + scripts/build.sh + ``` + + The gist of this script is: + + ```sh + # install root level dependencies using package-lock.json as reference + yarn + # install front-end dependencies using package-lock.json + cd client + yarn + # build front-end + yarn build + # install back-end dependencies + cd ../server + yarn + cd .. + # copy client build to server directory + mkdir server/public + cp -r client/build/* server/public + ``` + +At this point you can run the SQLPad server with the front-end built for production use: + +```sh +cd server +node server.js --config ./config.dev.env +``` + +If preferred, SQLPad can be installed as a global module using the local files in this repo. This allows running SQLPad via the cli in any directory, just as if you had installed it with `npm install sqlpad -g`. Note that you must build and copy the client prior to this step. + +```sh +cd server +npm install -g + +# Now from somewhere else you can run sqlpad like +cd ~ +sqlpad --config ./config.dev.env +``` + +If sqlpad was installed globally via npm from npm, you may need to run `npm uninstall sqlpad -g`. + +A docker image may be built using the Dockerfile located in `server` directory. See `docker-publish.sh` for example docker build command. + +## Developing + +Open 2 terminal sessions in the root of this repo. + +In one session run the back end in development mode + +```sh +cd server +yarn start +``` + +In the other start the development server for the front end. + +```sh +cd client +yarn start +``` + +At this point you should have both back end and front end development servers running. + +http://localhost:3000 serves React-based frontend in dev-mode +http://localhost:3010 serves frontend compiled for production + +When viewing the front end in development mode, the page will automatically refresh on client file changes. The back end server will auto-restart on back end file changes as well. + +When viewing SQLPad in dev-mode in port 3000, some features may not work correctly (like csv/xlsx downloads, google auth). This is likely due to the port difference in configuration (google auth can only redirect to 1 url/port) or the dev-mode proxy setup (React dev mode is served by a secondary web server that will proxy requests it doesn't understand to the SQLPad server running on 3010 during development.) + +ESLint and Prettier are used to enforce code patterns and formatting in this project. A precommit hook should enforce and warn about these checks. If that is not set up however you may find these terminal commands useful. + +```sh +# To check lint +yarn run lint + +# To fix (some) errors and formatting automatically +yarn run fixlint +``` + +## Developing on Windows + +To run `build/scripts.sh` on Windows, you may want to use an application like [cmder](https://cmder.net/). It will allow you to run `scripts/build.sh` by calling `sh` directly with `sh scripts/build.sh`. + +## Test/Dev Database Connection + +A Node.js script, `server/generate-test-db-fixture.js`, can be used to generate a test sqlite database to be used for tests and local development. This replaces the previous mock driver implementation that was enabled when the `debug` flag was turned on. + +This test database allows for running SQL you likely know, instead of having to remember and use some variables set in SQL comments. + +The script adds a sqlite database under `server/test/fixtures/sales.sqlite`. + +It can be used within SQLPad by creating a connection using the SQLite driver, and setting the filename to `./test/fixtures/sales.sqlite`. + +The data in the database is somewhat nonsensical. It is also not randomized so that the results can be predictable. If you'd like to expand on this database, feel free. + +## Running Tests + +SQLPad's test cases focus on the API server, with majority of tests being integration tests, with some unit tests sprinkled throughout. + +Tests are located under `server/test` directory, and use `mocha` as a test runner. Tests may be run by running `yarn test` within the server directory. For tests to run successfully, dependencies must be installed and the test database fixture generated. + +```sh +cd server +# if dependencies have not yet been installed (build.sh will do this) +yarn + +# If test fixture has not yet been generated (build.sh will do this) +node generate-test-db-fixture.js + +# Start backend services if desired. (-d runs in background) +docker-compose up -d redis +docker-compose up -d ldap + +# Run tests +yarn test + +# If you are aiming to run a specific test case, +# you may do so using npx to call mocha, +# specifying the path to the test file and --exit to exit after tests have run +npx mocha test/path/to/file.js --exit +``` + +## Building local docker images + +You can build and test the docker file by running the following from the root of the directory. Note that you must have Docker installed and running. + +```sh +# build image from docker file +# to build for a different architecture you can do +# docker build -t localsqlpad . --no-cache=true --platform=linux/amd64 +docker build -t localsqlpad . + +# list images built +docker images + +# Run localsqlpad image just built +# https://docs.docker.com/engine/reference/run/ +docker run -p 3000:3000 -e "SQLPAD_ADMIN=admin@sqlpad.com" -e "SQLPAD_ADMIN_PASSWORD=admin" localsqlpad + +# Now in browser you can open sqlpad at http://localhost:3000 +``` + +In the event that a specific architecture needs to be tested, say an amd64 image to run on arm64, you can pull a dockerhub image based on specific sha. + +```sh +docker run -p 3000:3000 -e "SQLPAD_ADMIN=admin@sqlpad.com" -e "SQLPAD_ADMIN_PASSWORD=admin" sqlpad/sqlpad@sha256:b80b4ab2af32ed07479d9233327bf3e5352fc71b41245d8e73b6403361be266a +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..3123c0047 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# Need to remote into this image and debug some flow? +# docker run -it --rm node:20-slim /bin/ash +FROM node:20-slim AS build +ARG ODBC_ENABLED=false +RUN apt-get update && apt-get install -y \ + python3 make g++ python3-dev \ + && ( \ + if [ "$ODBC_ENABLED" = "true" ] ; \ + then \ + echo "Installing ODBC build dependencies." 1>&2 ;\ + apt-get install -y unixodbc-dev ;\ + npm install -g node-gyp ;\ + fi\ + ) \ + && rm -rf /var/lib/apt/lists/* +# RUN npm config set python /usr/bin/python3 + +WORKDIR /sqlpad + +# By copying just the package files and installing node layers, +# we can take advantage of caching +# SQLPad is really 3 node projects though +# * root directory for linting +# * client/ for web front end +# * server/ for server (and what eventually holds built front end) +COPY ./package* ./ +COPY ./client/package* ./client/ +COPY ./server/package* ./server/ +COPY ./yarn* ./ +COPY ./client/yarn* ./client/ +COPY ./server/yarn* ./server/ + +# Install dependencies +# Timeout increase necessary for mdi-react timeout +RUN yarn config set network-timeout 600000 -g +RUN yarn +WORKDIR /sqlpad/client +RUN yarn +WORKDIR /sqlpad/server +RUN yarn --production +WORKDIR /sqlpad + +# Copy rest of the project into docker +COPY . . + +# Build front-end and copy files into server public dir +RUN npm run build --prefix client && \ + rm -rf server/public && \ + mkdir server/public && \ + cp -r client/build/* server/public + +# Start another stage with a fresh node +# Copy the server directory that has all the necessary node modules + front end build +FROM node:20-slim as bundle +ARG ODBC_ENABLED=false + +# Create a directory for the hooks and optionaly install ODBC +RUN mkdir -p /etc/docker-entrypoint.d \ + && apt-get update && apt-get install -y wget \ + && ( \ + if [ "$ODBC_ENABLED" = "true" ] ; \ + then \ + echo "Installing ODBC runtime dependencies." 1>&2 ;\ + apt-get install -y unixodbc libaio1 odbcinst libodbc1 ;\ + touch /etc/odbcinst.ini ;\ + fi\ + ) \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/app +COPY --from=build /sqlpad/docker-entrypoint / +COPY --from=build /sqlpad/server . + +ENV NODE_ENV production +ENV SQLPAD_DB_PATH /var/lib/sqlpad +ENV SQLPAD_PORT 3000 +EXPOSE 3000 +ENTRYPOINT ["/docker-entrypoint"] + +# Things to think about for future docker builds +# Perhaps add a healthcheck? +# Should nginx be used to front sqlpad? << No. you can always add an LB/nginx on top of this with compose or other tools when needed. + +RUN ["chmod", "+x", "/docker-entrypoint"] +WORKDIR /var/lib/sqlpad + +# If you want to use ODBC, use `docker build -t sqlpad/sqlpad-odbc --build-arg ODBC_ENABLED=true .` +# That will create an image with ODBC enabled. +# +# Then add specific ODBC drivers. +# Option 1: extend this Dockerfile in a fork. +# Option 2: create your own that starts `FROM sqlpad/sqlpad-odbc` and add drivers there. +# Note: this is currently not available on dockerhub so you must use the build command to provision it locally. diff --git a/README.md b/README.md index 40f74c318..017928b49 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,34 @@ -# SqlPad +## This Project is Archived -A web app for writing and running SQL queries and visualizing the results. Supports Postgres, MySQL, SQL Server, Crate, Vertica, and Presto. Written in Node.js. +Node and React are unkind to long-lived projects. Libraries and trends are constantly changing. Dependencies will be abandoned. -![SqlPad Query List](http://rickbergfalk.github.io/sqlpad/images/screenshots/queries.png) -![SqlPad Query Editor](http://rickbergfalk.github.io/sqlpad/images/screenshots/query-editor.png) -![SqlPad Chart Editor](http://rickbergfalk.github.io/sqlpad/images/screenshots/chart-line.png) +If you're using this project or interested in it, I recommend forking it, gutting the features you don't want or need, and then incrementally rewriting the bits that are left. Or perhaps start fresh, and borrow the parts of this you like. +Most of this codebase is from the 2016-2020 era of JavaScript. To modernize it without breaking things would be a huge time commitment, and I no longer have that time. -## Installation and Usage +# SQLPad -Visit project page at [http://rickbergfalk.github.io/sqlpad/](http://rickbergfalk.github.io/sqlpad/). +A web app for writing and running SQL queries and visualizing the results. Supports Postgres, MySQL, SQL Server, ClickHouse, Crate, Vertica, Trino, Presto, SAP HANA, Cassandra, Google BigQuery, SQLite, TiDB and many more via [ODBC](https://github.com/sqlpad/sqlpad/wiki/ODBC). -Quickstart: +![SQLPad Query Editor](https://user-images.githubusercontent.com/303966/99915755-32f78e80-2ccb-11eb-9f74-b18846d6108d.png) -```sh -npm install sqlpad -g -sqlpad --help -``` -To update sqlpad installed via npm run -```sh -npm install sqlpad -g -``` +**As of version 7, semver is no longer followed**. Going forward patch updates may require major Node.js version updates, or contain removal of functionality. +## Docker Image -## Development +The docker image runs on port 3000 and uses `/var/lib/sqlpad` for the embedded database directory. -See [wiki](https://github.com/rickbergfalk/sqlpad/wiki/v2-Development-Guide) for development details and project information. +See [docker-examples](https://github.com/sqlpad/sqlpad/tree/master/docker-examples) for docker-compose examples. +## Project Documentation -## Tips +The most recently used documentation site's astro code is located under `/docs` directory. -If you have highlighted just part of your query, only that part will be executed when you click Run Query. +## Development +For instructions on installing/running SQLPad from git repo see [DEVELOPER-GUIDE.md](https://github.com/sqlpad/sqlpad/blob/master/DEVELOPER-GUIDE.md) -## License +## License MIT diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..0e9058086 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +Only the latest release is supported. Fixes are not ported back to prior major versions. + +## Reporting a Vulnerability + +Please report to rick.bergfalk@gmail.com. diff --git a/client-js/App.js b/client-js/App.js deleted file mode 100644 index 87fc37c09..000000000 --- a/client-js/App.js +++ /dev/null @@ -1,160 +0,0 @@ -var React = require('react') -var Alert = require('react-s-alert').default -var fetchJson = require('./utilities/fetch-json.js') -var Navbar = require('react-bootstrap/lib/Navbar') -var Nav = require('react-bootstrap/lib/Nav') -var NavItem = require('react-bootstrap/lib/NavItem') -var NavDropdown = require('react-bootstrap/lib/NavDropdown') -var MenuItem = require('react-bootstrap/lib/MenuItem') -var Modal = require('react-bootstrap/lib/Modal') -var Button = require('react-bootstrap/lib/Button') -var Popover = require('react-bootstrap/lib/Popover') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') -import navigateToClickHandler from './utilities/navigateToClickHandler' -var page = require('page') - -var App = React.createClass({ - getInitialState: function () { - return { - showAboutModal: false, - currentUser: {}, - version: {}, - passport: {}, - config: {} - } - }, - openAboutModal: function () { - this.setState({showAboutModal: true}) - }, - closeAboutModal: function () { - this.setState({showAboutModal: false}) - }, - componentDidMount: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/app') - .then((json) => { - // TODO - would it be good to adopt this all-in-one app route or is this bad? - this.setState({ - currentUser: json.currentUser, - version: json.version, - passport: json.passport, - config: json.config - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - signout: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/signout') - .then((json) => { - page('/') - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Problem signing out') - }) - }, - render: function () { - // do stuff - const popover = ( - - Installed Version: {this.state.version.current} -
- Latest: {this.state.version.latest} -
- ) - const updateNotification = () => { - if (this.state.version.updateAvailable) { - return ( - - - - - ) - } - } - const userMenu = () => { - if (this.props.currentUser.role === 'admin') { - return ( - - Connections - Users - Configuration - - About SqlPad - - Sign Out - - ) - } else { - return ( - - About SqlPad - - Sign Out - - ) - } - } - return ( -
- - - - -
- {this.props.children} -
- - - - About SqlPad - - -

- Version: {this.state.version.latest} -

-

- Project Page: http://rickbergfalk.github.io/sqlpad -

-
- -
- - - -
-
- ) - } -}) - -module.exports = App diff --git a/client-js/ConfigValues.js b/client-js/ConfigValues.js deleted file mode 100644 index 8061a7cc2..000000000 --- a/client-js/ConfigValues.js +++ /dev/null @@ -1,303 +0,0 @@ -var React = require('react') -var _ = window._ -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -var Form = require('react-bootstrap/lib/Form') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var Col = require('react-bootstrap/lib/Col') -var Row = require('react-bootstrap/lib/Row') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var HelpBlock = require('react-bootstrap/lib/HelpBlock') -var Popover = require('react-bootstrap/lib/Popover') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var AutoAffix = require('react-overlays/lib/AutoAffix') - -const CheckListItem = (props) => { - if (!props.configKey || !props.configItems || !props.configItems.length) { - return null - } - var configItem = props.configItems.find((item) => { - return item.key === props.configKey - }) - if (!configItem) { - return ( -
  • - {props.configKey} is not in configItems. -
  • - ) - } - return ( -
  • - {configItem.label || configItem.envVar} -
  • - ) -} - -var ConfigValues = React.createClass({ - loadConfigValuesFromServer: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/config-items') - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({configItems: json.configItems}) - }) - }, - saveConfigValue: function (key, value) { - fetchJson('POST', this.props.config.baseUrl + '/api/config-values/' + key, {value: value}) - .then((json) => { - if (json.error) { - Alert.error('Save failed') - } else { - Alert.success('Value saved') - this.loadConfigValuesFromServer() - } - }) - }, - getInitialState: function () { - return { - configItems: [] - } - }, - componentDidMount: function () { - this.loadConfigValuesFromServer() - this.saveConfigValue = _.debounce(this.saveConfigValue, 500) - }, - render: function () { - var configItemInputNodes = this.state.configItems - .filter(config => config.interface === 'ui') - .map(config => { - return ( - - ) - }) - return ( -
    - -
    -

    Configuration

    -
    -
    - {configItemInputNodes} -
    -
    -

    - Some configuration is only accessible via environment variables - or command-line-interface (CLI) flags. Below are the current values for these - variables. Sensitive values are masked. Hover over input for additional information. -

    -
    - -
    - - - -
    -
    -

    - Feature Checklist -

    -

    - Unlock features by providing the required configuration. -

    -
    - Email -
      - - - - - -
    - Google OAuth -
      - - - -
    -
    -
    -
    - -
    - ) - } -}) -module.exports = ConfigValues - -var ConfigItemInput = React.createClass({ - getInitialState: function () { - return { - value: this.props.config.effectiveValue - } - }, - handleChange: function (e) { - this.setState({ - value: e.target.value - }) - this.props.saveConfigValue(this.props.config.key, e.target.value) - }, - render: function () { - const config = this.props.config - const disabled = (config.effectiveValueSource === 'cli' || config.effectiveValueSource === 'saved cli' || config.effectiveValueSource === 'env') - - const effectiveValueSourceLabels = { - 'cli': 'Command Line', - 'saved cli': 'Saved Command Line', - 'env': 'Environment Varialbe' - } - const overriddenBy = effectiveValueSourceLabels[config.effectiveValueSource] - - const inputNode = () => { - if (config.options) { - var optionNodes = config.options.map(function (option) { - return ( - - ) - }) - return ( - - {optionNodes} - - ) - } else { - return ( - - ) - } - } - - var defaultValue = () => { - if (config.default === '') { - return ( - empty - ) - } - return ( - {config.default.toString()} - ) - } - - var cliFlag = (config.cliFlag && config.cliFlag.pop ? config.cliFlag.pop() : config.cliFlag) - - var helpPopover = ( - - {config.description} - - Default: {defaultValue()} - - {(cliFlag ? ( - - CLI Flag: --{cliFlag} - - ) : null)} - {(config.envVar ? ( - - Environment Variable: {config.envVar} - - ) : null)} - {(disabled ? ( -
    - - Set By: {overriddenBy} - - - When set by command line or environment, item is not configurable via UI. - -
    - ) : null)} -
    - ) - - return ( - - - {config.label} - - - - {inputNode()} - - - - ) - } -}) - -var ConfigEnvDocumentation = React.createClass({ - render: function () { - var configNodes = this.props.configItems - .filter(config => config.interface === 'env') - .map(function (config) { - var defaultValue = () => { - if (config.default === '') { - return ( - empty - ) - } - return ( - {config.default.toString()} - ) - } - var currentValue = (config.value === '' ? '' : config.effectiveValue.toString()) - var cliFlag = (config.cliFlag && config.cliFlag.pop ? config.cliFlag.pop() : config.cliFlag) - var helpPopover = ( - - -

    - {config.description} -

    -

    - Default: {defaultValue()} -

    - {(cliFlag ? ( -

    - CLI Flag: --{cliFlag} -

    - ) : null)} -

    - Environment Variable: {config.envVar} -

    -

    - Set By: {config.effectiveValueSource} -

    -
    -
    - ) - return ( - - - {config.envVar} - - - - - - - - ) - }) - return ( -
    - {configNodes} -
    - ) - } -}) diff --git a/client-js/ConnectionAdmin.js b/client-js/ConnectionAdmin.js deleted file mode 100644 index 906d11973..000000000 --- a/client-js/ConnectionAdmin.js +++ /dev/null @@ -1,398 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -var Panel = require('react-bootstrap/lib/Panel') -var Form = require('react-bootstrap/lib/Form') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var Checkbox = require('react-bootstrap/lib/Checkbox') -var Button = require('react-bootstrap/lib/Button') -var ListGroup = require('react-bootstrap/lib/ListGroup') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var Popover = require('react-bootstrap/lib/Popover') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') -const _ = window._ - -var ConnectionController = React.createClass({ - getInitialState: function () { - return { - connections: [], - selectedConnection: null, - isTesting: false, - isSaving: false - } - }, - componentDidMount: function () { - this.loadConnectionsFromServer() - }, - handleSelect: function (connection) { - this.setState({ - selectedConnection: _.clone(connection) - }) - }, - handleDelete: function (connection) { - fetchJson('DELETE', this.props.config.baseUrl + '/api/connections/' + connection._id) - .then((json) => { - if (json.error) return Alert.error('Delete failed') - Alert.success('Connection deleted') - if (this.state.selectedConnection && connection._id === this.state.selectedConnection._id) { - this.setState({selectedConnection: null}) - } - this.loadConnectionsFromServer() - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - onNewConnectionClick: function () { - this.setState({ - selectedConnection: {} - }) - }, - setConnectionValue: function (attribute, value) { - var selectedConnection = this.state.selectedConnection - if (selectedConnection) { - selectedConnection[attribute] = value - this.setState({selectedConnection: selectedConnection}) - } - }, - loadConnectionsFromServer: function () { - fetchJson('get', this.props.config.baseUrl + '/api/connections') - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({connections: json.connections}) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - testConnection: function () { - this.setState({isTesting: true}) - fetchJson('POST', this.props.config.baseUrl + '/api/test-connection', this.state.selectedConnection) - .then((json) => { - this.setState({isTesting: false}) - if (json.error) return Alert.error('Test Failed') - return Alert.success('Test successful') - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - saveConnection: function () { - this.setState({isSaving: true}) - if (this.state.selectedConnection._id) { - fetchJson('PUT', this.props.config.baseUrl + '/api/connections/' + this.state.selectedConnection._id, this.state.selectedConnection) - .then((json) => { - this.setState({isSaving: false}) - if (json.error) return Alert.error('Save failed') - Alert.success('Connection saved') - this.setState({selectedConnection: null}) - this.loadConnectionsFromServer() - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - } else { - fetchJson('POST', this.props.config.baseUrl + '/api/connections', this.state.selectedConnection) - .then((json) => { - this.setState({ - isSaving: false, - selectedConnection: json.connection || this.state.selectedConnection - }) - if (json.error) return Alert.error('Save failed') - Alert.success('Connection saved') - this.setState({selectedConnection: null}) - this.loadConnectionsFromServer() - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - } - }, - render: function () { - return ( -
    - - -
    - ) - } -}) - -module.exports = ConnectionController - -var ConnectionList = React.createClass({ - style: { - position: 'absolute', - left: 0, - width: '50%', - top: 0, - bottom: 0, - backgroundColor: '#FDFDFD', - overflowY: 'auto', - padding: 10 - }, - render: function () { - var listRows = this.props.connections.map((connection) => { - return ( - - ) - }) - return ( -
    - Connections - - {listRows} - - -
    - ) - } -}) - -var ConnectionListRow = React.createClass({ - onDelete: function (e) { - this.props.handleDelete(this.props.connection) - }, - onSelect: function (e) { - this.props.handleSelect(this.props.connection) - }, - render: function () { - var getClassNames = () => { - if (this.props.selectedConnection && this.props.selectedConnection._id === this.props.connection._id) { - return 'list-group-item ListRow ListRowSelected' - } else { - return 'list-group-item ListRow' - } - } - const popoverClick = ( - - - - ) - return ( -
  • -

    {this.props.connection.name}

    -
    {this.props.connection.driver} {this.props.connection.host}/{this.props.connection.database}
    - - - -
  • - ) - } -}) - -var ConnectionForm = React.createClass({ - style: { - position: 'absolute', - right: 0, - width: '50%', - top: 0, - bottom: 0, - backgroundColor: '#FDFDFD', - overflowY: 'auto', - padding: 10 - }, - onNameChange: function (e) { - this.props.setConnectionValue('name', e.target.value) - }, - onDriverChange: function (e) { - this.props.setConnectionValue('driver', e.target.value) - }, - onHostChange: function (e) { - this.props.setConnectionValue('host', e.target.value) - }, - onPortChange: function (e) { - this.props.setConnectionValue('port', e.target.value) - }, - onDatabaseChange: function (e) { - this.props.setConnectionValue('database', e.target.value) - }, - onUsernameChange: function (e) { - this.props.setConnectionValue('username', e.target.value) - }, - onPasswordChange: function (e) { - this.props.setConnectionValue('password', e.target.value) - }, - onDomainChange: function (e) { - this.props.setConnectionValue('domain', e.target.value) - }, - onSqlserverEncryptChange: function (e) { - this.props.setConnectionValue('sqlserverEncrypt', e.target.checked) - }, - onMysqlInsecureAuthChange: function (e) { - this.props.setConnectionValue('mysqlInsecureAuth', e.target.checked) - }, - onPostgresSslChange: function (e) { - this.props.setConnectionValue('postgresSsl', e.target.checked) - }, - onPrestoCatalogChange: function (e) { - this.props.setConnectionValue('prestoCatalog', e.target.value) - }, - onPrestoSchemaChange: function (e) { - this.props.setConnectionValue('prestoSchema', e.target.value) - }, - render: function () { - if (!this.props.selectedConnection) { - return ( -
    - ) - } - var connection = this.props.selectedConnection - var databaseInput = () => { - if (connection.driver !== 'crate' && connection.driver !== 'presto') { - return ( - - Database - - - ) - } - } - var usernameInput = () => { - if (connection.driver !== 'crate') { - return ( - - Database Username - - - ) - } - } - var passwordInput = () => { - if (connection.driver !== 'crate' && connection.driver !== 'presto') { - return ( - - Database Password - - - ) - } - } - var domainInput = () => { - if (connection.driver === 'sqlserver') { - return ( - - Domain - - - ) - } - } - var sqlserverEncryptInput = () => { - if (connection.driver === 'sqlserver') { - return ( - - - Encrypt (necessary for Azure) - - - ) - } - } - var mysqlInsecureAuthInput = () => { - if (connection.driver === 'mysql') { - return ( - - - Use old/insecure pre 4.1 Auth System - - - ) - } - } - var postgresSslInput = () => { - if (connection.driver === 'postgres') { - return ( - - - Use SSL - - - ) - } - } - return ( -
    - -
    - - Friendly Connection Name - - - - Database Driver - - - - - - - - - - - Host/Server/IP Address - - - - Port (optional) - - - {databaseInput()} - {domainInput()} - {usernameInput()} - {passwordInput()} - {sqlserverEncryptInput()} - {mysqlInsecureAuthInput()} - {postgresSslInput()} - {(connection.driver === 'presto' ? ( -
    - - Catalog - - - - Schema - - -
    - ) : null)} - - {' '} - -
    -
    -
    - ) - } -}) diff --git a/client-js/FilterableQueryList.js b/client-js/FilterableQueryList.js deleted file mode 100644 index f7aedfcd0..000000000 --- a/client-js/FilterableQueryList.js +++ /dev/null @@ -1,379 +0,0 @@ -var React = require('react') -var moment = require('moment') -var _ = window._ -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -import AceEditor from 'react-ace' -import 'brace/mode/sql' -import 'brace/theme/sqlserver' -var chartDefinitions = require('./components/ChartDefinitions.js') -var Label = require('react-bootstrap/lib/Label') -var Form = require('react-bootstrap/lib/Form') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var ListGroup = require('react-bootstrap/lib/ListGroup') -var Button = require('react-bootstrap/lib/Button') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var Popover = require('react-bootstrap/lib/Popover') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') -import navigateToClickHandler from './utilities/navigateToClickHandler' - -var FilterableQueryList = React.createClass({ - getInitialState: function () { - return { - queries: [], - connections: [], - createdBys: [], - tags: [], - searchInput: null, - selectedConnection: null, - selectedTag: null, - selectedCreatedBy: this.props.currentUser ? this.props.currentUser.email : '', - selectedSortBy: null, - selectedQuery: null - } - }, - handleQueryListRowMouseOver: function (query) { - this.setState({selectedQuery: query}) - }, - handleQueryDelete: function (queryId) { - var queries = this.state.queries - var selectedQuery = this.state.selectedQuery - if (selectedQuery._id === queryId) selectedQuery = null - queries = queries.filter((q) => { - return q._id !== queryId - }) - this.setState({ - queries: queries, - selectedQuery: selectedQuery - }) - fetchJson('DELETE', this.props.config.baseUrl + '/api/queries/' + queryId) - .then((json) => { - if (json.error) Alert.error(json.error) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - loadConfigValuesFromServer: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/queries') - .then((json) => { - var createdBys = _.uniq(_.pluck(json.queries, 'createdBy')) - var tags = _.compact(_.uniq(_.flatten(_.pluck(json.queries, 'tags')))) - var selectedCreatedBy = this.state.selectedCreatedBy - if (createdBys.indexOf(this.props.currentUser.email) === -1) { - selectedCreatedBy = '' - } - this.setState({ - queries: json.queries, - createdBys: createdBys, - selectedCreatedBy: selectedCreatedBy, - tags: tags - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - fetchJson('GET', this.props.config.baseUrl + '/api/connections') - .then((json) => { - this.setState({connections: json.connections}) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - onSearchChange: function (searchInput) { - this.setState({ - searchInput: searchInput, - selectedQuery: null - }) - }, - onConnectionChange: function (connectionId) { - this.setState({ - selectedConnection: connectionId, - selectedQuery: null - }) - }, - onTagChange: function (tag) { - this.setState({ - selectedTag: tag, - selectedQuery: null - }) - }, - onCreatedByChange: function (createdBy) { - this.setState({ - selectedCreatedBy: createdBy, - selectedQuery: null - }) - }, - onSortByChange: function (sortBy) { - this.setState({ - selectedSortBy: sortBy - }) - }, - componentDidMount: function () { - this.loadConfigValuesFromServer() - }, - render: function () { - var filteredQueries = this.state.queries.map((q) => q) - if (this.state.selectedTag) { - filteredQueries = filteredQueries.filter((q) => { - return (q.tags && q.tags.length && q.tags.indexOf(this.state.selectedTag) > -1) - }) - } - if (this.state.selectedCreatedBy) { - filteredQueries = filteredQueries.filter((q) => { - return (q.createdBy === this.state.selectedCreatedBy) - }) - } - if (this.state.selectedConnection) { - filteredQueries = filteredQueries.filter((q) => { - return (q.connectionId === this.state.selectedConnection) - }) - } - if (this.state.searchInput) { - var terms = this.state.searchInput.split(' ') - var termCount = terms.length - filteredQueries = filteredQueries.filter((q) => { - var matchedCount = 0 - terms.forEach(function (term) { - term = term.toLowerCase() - if ((q.name && q.name.toLowerCase().search(term) !== -1) || (q.queryText && q.queryText.toLowerCase().search(term) !== -1)) matchedCount++ - }) - return (matchedCount === termCount) - }) - } - if (this.state.selectedSortBy === 'name') { - filteredQueries = _.sortBy(filteredQueries, (query) => query.name.toLowerCase()) - } else { - filteredQueries = _.sortBy(filteredQueries, 'modifiedDate').reverse() - } - - return ( -
    - - - -
    - ) - } -}) - -var QueryListSidebar = React.createClass({ - onSearchChange: function (e) { - this.props.onSearchChange(e.target.value) - }, - onConnectionChange: function (e) { - this.props.onConnectionChange(e.target.value) - }, - onTagChange: function (e) { - this.props.onTagChange(e.target.value) - }, - onCreatedByChange: function (e) { - this.props.onCreatedByChange(e.target.value) - }, - onSortByChange: function (e) { - this.props.onSortByChange(e.target.value) - }, - render: function () { - var connectionSelectOptions = this.props.connections.map(function (conn) { - return ( - - ) - }) - var createdBySelectOptions = this.props.createdBys.map(function (createdBy) { - return ( - - ) - }) - var tagSelectOptions = this.props.tags.map(function (tag) { - return ( - - ) - }) - return ( -
    -
    - - Search - - -
    - - Tag - - - {tagSelectOptions} - - -
    - - Connection - - - {connectionSelectOptions} - - -
    - - Created By - - - {createdBySelectOptions} - - -
    - - Sort By - - - - - -
    -
    - ) - } -}) - -var QueryList = React.createClass({ - render: function () { - var self = this - var QueryListRows = this.props.queries.map((query) => { - return ( - - ) - }) - return ( -
    - Queries - - {QueryListRows} - -
    - ) - } -}) - -var QueryListRow = React.createClass({ - getInitialState: function () { - return { - showPreview: false - } - }, - onMouseOver: function (e) { - this.props.handleQueryListRowMouseOver(this.props.query) - }, - onDelete: function (e) { - this.props.handleQueryDelete(this.props.query._id) - }, - render: function () { - var tagLabels = this.props.query.tags.map((tag) => { - return ( - - ) - }) - var tableUrl = this.props.config.baseUrl + '/query-table/' + this.props.query._id - var chartUrl = this.props.config.baseUrl + '/query-chart/' + this.props.query._id - var selectedStyle = () => { - if (this.props.selectedQuery && this.props.selectedQuery._id === this.props.query._id) { - return 'list-group-item QueryListRow QueryListRowSelected' - } else { - return 'list-group-item QueryListRow' - } - } - const popoverClick = ( - - - - ) - return ( -
  • -

    {this.props.query.name}

    -

    {this.props.query.createdBy} {tagLabels}

    -

    table chart

    - - - -
  • - ) - } -}) - -var QueryPreview = React.createClass({ - render: function () { - if (this.props.selectedQuery) { - if (this.editor && this.props.config.editorWordWrap) { - this.editor.session.setUseWrapMode(true) - } - var query = this.props.selectedQuery - var chartTypeLabel = () => { - var chartType = (query.chartConfiguration && query.chartConfiguration.chartType ? query.chartConfiguration.chartType : null) - var chartDefinition = _.findWhere(chartDefinitions, {chartType: chartType}) - return (chartDefinition ?

    Chart: {chartDefinition.chartLabel}

    : null) - } - return ( -
    - Preview -

    {this.props.selectedQuery.name}

    - { - this.editor = (ref ? ref.editor : null) - }} - /> - {chartTypeLabel()} -

    Modified: {moment(query.modifiedDate).calendar()}

    -

    Created By: {query.createdBy}

    -
    - ) - } else { - return ( -
    - ) - } - } -}) - -module.exports = FilterableQueryList diff --git a/client-js/ForgotPassword.js b/client-js/ForgotPassword.js deleted file mode 100644 index 88fd7c0e6..000000000 --- a/client-js/ForgotPassword.js +++ /dev/null @@ -1,48 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -var page = require('page') - -var ForgotPassword = React.createClass({ - getInitialState: function () { - return { - email: '' - } - }, - onEmailChange: function (e) { - this.setState({email: e.target.value}) - }, - resetPassword: function (e) { - e.preventDefault() - fetchJson('POST', this.props.config.baseUrl + '/api/forgot-password', this.state) - .then((json) => { - if (json.error) return Alert.error(json.error) - page('/password-reset') - }) - .catch((ex) => { - Alert.error('Problem resetting password') - console.error(ex) - }) - }, - render: function () { - return ( -
    -
    -

    SqlPad

    - -
    - -
    - -
    - ) - } -}) - -module.exports = ForgotPassword diff --git a/client-js/FullscreenMessage.js b/client-js/FullscreenMessage.js deleted file mode 100644 index 81b8f2338..000000000 --- a/client-js/FullscreenMessage.js +++ /dev/null @@ -1,20 +0,0 @@ -var React = require('react') - -const style = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - flexDirection: 'column', - left: 0, - right: 0, - top: 0, - bottom: 0, - fontSize: 36 -} - -module.exports = (props) => ( -
    - {props.children} -
    -) diff --git a/client-js/PasswordReset.js b/client-js/PasswordReset.js deleted file mode 100644 index 29bdb8b61..000000000 --- a/client-js/PasswordReset.js +++ /dev/null @@ -1,92 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -var page = require('page') - -var PasswordReset = React.createClass({ - getInitialState: function () { - return { - email: '', - password: '', - passwordConfirmation: '', - notFound: false - } - }, - onEmailChange: function (e) { - this.setState({email: e.target.value}) - }, - onPasswordChange: function (e) { - this.setState({password: e.target.value}) - }, - onPasswordConfirmationChange: function (e) { - this.setState({passwordConfirmation: e.target.value}) - }, - resetPassword: function (e) { - e.preventDefault() - fetchJson('POST', this.props.config.baseUrl + '/api/password-reset/' + this.props.passwordResetId, this.state) - .then((json) => { - if (json.error) return Alert.error(json.error) - page('/') - }) - .catch((ex) => { - Alert.error('Problem resetting password') - console.error(ex) - }) - }, - componentDidMount: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/password-reset/' + this.props.passwordResetId) - .then((json) => { - if (json.error) return Alert.error(json.error) - if (!json.passwordResetId) this.setState({notFound: true}) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - render: function () { - if (this.state.notFound) { - return ( -
    -
    -

    Password Reset
    Not Found

    -
    - -
    - ) - } - return ( -
    -
    -

    SqlPad

    - - - -
    - -
    - -
    - ) - } -}) - -module.exports = PasswordReset diff --git a/client-js/QueryChartOnly.js b/client-js/QueryChartOnly.js deleted file mode 100644 index 17ba3fc47..000000000 --- a/client-js/QueryChartOnly.js +++ /dev/null @@ -1,99 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var SqlpadTauChart = require('./components/SqlpadTauChart.js') -var DropdownButton = require('react-bootstrap/lib/DropdownButton') -var MenuItem = require('react-bootstrap/lib/MenuItem') -import IncompleteDataNotification from './components/IncompleteDataNotification' - -var QueryEditor = React.createClass({ - getInitialState: function () { - return { - isRunning: false, - runQueryStartTime: undefined, - queryResult: undefined - } - }, - runQuery: function (queryId) { - this.setState({ - isRunning: true, - runQueryStartTime: new Date() - }) - fetchJson('GET', this.props.config.baseUrl + '/api/queries/' + queryId) - .then((json) => { - if (json.error) console.error(json.error) - this.setState({ - query: json.query - }) - }) - .then(() => { - return fetchJson('GET', this.props.config.baseUrl + '/api/query-result/' + queryId) - }) - .then((json) => { - if (json.error) console.error(json.error) - this.setState({ - isRunning: false, - queryError: json.error, - queryResult: json.queryResult - }) - }) - .catch((ex) => { - console.error(ex.toString()) - this.setState({ - isRunning: false - }) - }) - }, - componentDidMount: function () { - this.runQuery(this.props.queryId) - }, - onSaveImageClick: function (e) { - if (this.sqlpadTauChart && this.sqlpadTauChart.chart) { - this.sqlpadTauChart.chart.fire('exportTo', 'png') - } - }, - hasRows: function () { - var queryResult = this.state.queryResult - return !!(queryResult && queryResult.rows && queryResult.rows.length) - }, - isChartable: function () { - var pending = this.state.isRunning || this.state.queryError - return !pending && this.hasRows() - }, - render: function () { - var csvDownloadLink - var xlsxDownloadLink - if (this.state.queryResult) { - csvDownloadLink = this.props.config.baseUrl + '/download-results/' + this.state.queryResult.cacheKey + '.csv' - xlsxDownloadLink = this.props.config.baseUrl + '/download-results/' + this.state.queryResult.cacheKey + '.xlsx' - } - return ( -
    -

    {(this.state.query ? this.state.query.name : '')}

    -
    - - {(this.state.queryResult ? ( - - png - {(this.props.config.allowCsvDownload ? (csv) : null)} - {(this.props.config.allowCsvDownload ? (xlsx) : null)} - - ) : null)} -
    -
    - { - this.sqlpadTauChart = ref - }} /> -
    -
    - ) - } -}) - -module.exports = QueryEditor diff --git a/client-js/QueryEditor.js b/client-js/QueryEditor.js deleted file mode 100644 index c987f9d6a..000000000 --- a/client-js/QueryEditor.js +++ /dev/null @@ -1,561 +0,0 @@ -var React = require('react') -var uuid = require('uuid') -var keymaster = require('keymaster') -import { Creatable } from 'react-select' -var SchemaInfo = require('./components/SchemaInfo.js') -var QueryResultDataTable = require('./components/QueryResultDataTable.js') -var QueryResultHeader = require('./components/QueryResultHeader.js') -var ChartInputs = require('./components/ChartInputs.js') -var SqlpadTauChart = require('./components/SqlpadTauChart.js') -var chartDefinitions = require('./components/ChartDefinitions.js') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -import AceEditor from 'react-ace' -import 'brace/mode/sql' -import 'brace/theme/sqlserver' -import 'brace/ext/searchbox' - -var Row = require('react-bootstrap/lib/Row') -var Col = require('react-bootstrap/lib/Col') -var Nav = require('react-bootstrap/lib/Nav') -var NavItem = require('react-bootstrap/lib/NavItem') -var Tab = require('react-bootstrap/lib/Tab') -var Form = require('react-bootstrap/lib/Form') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var Button = require('react-bootstrap/lib/Button') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var Modal = require('react-bootstrap/lib/Modal') -var Tooltip = require('react-bootstrap/lib/Tooltip') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') -var HelpBlock = require('react-bootstrap/lib/HelpBlock') - -var QueryDetailsModal = React.createClass({ - getInitialState: function () { - return { - showModal: false - } - }, - close: function () { - if (this.saveOnClose) { - setTimeout(this.props.saveQuery, 750) - this.saveOnClose = false - } - this.setState({ showModal: false }) - }, - input: undefined, - open: function () { - this.setState({ showModal: true }) - }, - openForSave: function () { - this.saveOnClose = true - this.setState({ showModal: true }) - }, - onSubmit: function (e) { - e.preventDefault() - this.close() - }, - onQueryNameChange: function (e) { - var newName = e.target.value - this.props.onQueryNameChange(newName) - }, - onEntered: function () { - if (this.input) this.input.focus() - }, - render: function () { - var modalNavLink = (href, text) => { - var saved = !!this.props.query._id - if (saved) { - return ( -
  • - - {text} {' '} - -
  • - ) - } else { - var tooltip = Save query to enable table/chart view links - return ( - -
  • - e.preventDefault()} > - {text} {' '} - -
  • -
    - ) - } - } - var validationState = (this.saveOnClose && !this.props.query.name.length ? 'warning' : null) - var validationHelp = (this.saveOnClose && !this.props.query.name.length ? Query name is required to save query. : null) - return ( - - - -
    - - Query Name - { - this.input = ref - }} - type='text' - value={this.props.query.name} - onChange={this.onQueryNameChange} /> - - {validationHelp} - -
    - - Query Tags - - -
    -
      - {modalNavLink('?format=table', 'Link to Table')} - {modalNavLink('?format=chart', 'Link to Chart')} -
    -
    -
    - - - -
    - ) - } -}) - -var QueryEditor = React.createClass({ - loadConnectionsFromServer: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/connections/') - .then((json) => { - if (json.error) Alert.error(json.error) - const connections = json.connections - const query = this.state.query - // if only 1 connection auto-select it - if (connections.length === 1 && this.state.query) { - query.connectionId = connections[0]._id - } - this.setState({ - connections: connections, - query: query - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - loadQueryFromServer: function (queryId) { - fetchJson('GET', this.props.config.baseUrl + '/api/queries/' + queryId) - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({ - query: json.query - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - getInitialState: function () { - return { - cacheKey: uuid.v1(), - connections: [], - availableTags: [], - isSaving: false, - isRunning: false, - isDirty: false, - runQueryStartTime: undefined, - queryResult: undefined, - query: { - _id: '', - name: '', - tags: [], - connectionId: '', - queryText: '', - chartConfiguration: { - chartType: '', - fields: {} // key value for chart - } - } - } - }, - saveQuery: function () { - var query = this.state.query - if (!query.name) { - this.queryDetailsModal.openForSave() - return - } - this.setState({isSaving: true}) - if (query._id) { - fetchJson('PUT', this.props.config.baseUrl + '/api/queries/' + query._id, query) - .then((json) => { - if (json.error) { - Alert.error(json.error) - this.setState({isSaving: false}) - return - } - Alert.success('Query Saved') - this.setState({ - isSaving: false, - query: json.query - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - } else { - fetchJson('POST', this.props.config.baseUrl + '/api/queries', query) - .then((json) => { - if (json.error) { - Alert.error(json.error) - this.setState({isSaving: false}) - return - } - window.history.replaceState({}, json.query.name, this.props.config.baseUrl + '/queries/' + json.query._id) - Alert.success('Query Saved') - this.setState({ - isSaving: false, - query: json.query - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - } - }, - queryDetailsModal: undefined, - openQueryDetailsModal: function () { - this.queryDetailsModal.open() - }, - onConnectionChange: function (connectionId) { - var query = this.state.query - query.connectionId = connectionId - this.setState({ - query: query - }) - }, - onQueryNameChange: function (name) { - var query = this.state.query - query.name = name - this.setState({query: query}) - }, - onQueryTagsChange: function (values) { - var query = this.state.query - query.tags = values.map(v => v.value) - this.setState({query: query}) - }, - onQueryTextChange: function (queryText) { - var query = this.state.query - query.queryText = queryText - this.setState({ - query: query - }) - }, - onChartTypeChange: function (e) { - var chartType = e.target.value - var query = this.state.query - query.chartConfiguration.chartType = chartType - this.setState({query: query}) - }, - runQuery: function () { - var editor = this.editor - var selectedText = editor.session.getTextRange(editor.getSelectionRange()) - var queryToRun = selectedText || this.state.query.queryText - this.setState({ - isRunning: true, - runQueryStartTime: new Date() - }) - setTimeout(this.runningTimer, 60) - var postData = { - connectionId: this.state.query.connectionId, - cacheKey: this.state.cacheKey, - queryName: this.state.query.name, - queryText: queryToRun - } - fetchJson('POST', this.props.config.baseUrl + '/api/query-result', postData) - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({ - isDirty: false, - isRunning: false, - queryError: json.error, - queryResult: json.queryResult - }) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - loadTagsFromServer: function () { - fetchJson('GET', this.props.config.baseUrl + '/api/tags') - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({availableTags: json.tags}) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - componentWillReceiveProps: function (nextProps) { - if (nextProps.queryId !== 'new') this.loadQueryFromServer(nextProps.queryId) - else if (nextProps.queryId === 'new') { - this.setState({ - activeTabKey: 'sql', - queryResult: undefined, - query: { - _id: '', - name: '', - tags: [], - connectionId: '', - queryText: '', - chartConfiguration: { - chartType: '', - fields: {} // key value for chart - } - } - }) - } - }, - componentDidMount: function () { - this.loadConnectionsFromServer() - this.loadTagsFromServer() - if (this.props.queryId !== 'new') this.loadQueryFromServer(this.props.queryId) - - if (this.editor) { - this.editor.focus() - if (this.props.config.editorWordWrap) this.editor.session.setUseWrapMode(true) - } - - /* Shortcuts - ============================================================================== */ - // keymaster doesn't fire on input/textarea events by default - // since we are only using command/ctrl shortcuts, - // we want the event to fire all the time for any element - keymaster.filter = function (event) { - return true - } - keymaster.unbind('ctrl+s, command+s') - keymaster('ctrl+s, command+s', (e) => { - this.saveQuery() - e.preventDefault() - return false - }) - // there should only ever be 1 QueryEditor on the page, - // but just in case there isn't unbind anything previously bound - // rather something previously not run than something run more than once - keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e') - keymaster('ctrl+r, command+r, ctrl+e, command+e', (e) => { - this.runQuery() - e.preventDefault() - return false - }) - }, - componentWillUnmount: function () { - keymaster.unbind('ctrl+s, command+s') - keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e') - }, - onChartConfigurationFieldsChange: function (chartFieldId, queryResultField) { - var query = this.state.query - query.chartConfiguration.fields[chartFieldId] = queryResultField - this.setState({ - query: query - }) - }, - sqlpadTauChart: undefined, - hasRows: function () { - var queryResult = this.state.queryResult - return !!(queryResult && queryResult.rows && queryResult.rows.length) - }, - isChartable: function () { - var pending = this.state.isRunning || this.state.queryError - return !pending && this.state.activeTabKey === 'vis' && this.hasRows() - }, - onVisualizeClick: function (e) { - this.sqlpadTauChart.renderChart(true) - }, - onTabSelect: function (tabkey) { - this.setState({activeTabKey: tabkey}) - }, - onSaveImageClick: function (e) { - if (this.sqlpadTauChart && this.sqlpadTauChart.chart) { - this.sqlpadTauChart.chart.fire('exportTo', 'png') - } - }, - render: function () { - var tabsFormStyle = { - position: 'absolute', - left: '150px' - } - document.title = (this.state.query.name ? this.state.query.name : 'New Query') - var tagOptions = this.state.availableTags.map((t) => { - return {value: t, label: t} - }) - if (this.state.query && this.state.query.tags) { - this.state.query.tags.forEach((t) => { - tagOptions.push({value: t, label: t}) - }) - } - var chartOptions = chartDefinitions.map((d) => { - return ( - - ) - }) - return ( -
    - - - - -
    - - - {(this.state.query.name ? this.state.query.name : '(click to name query)')} - { - this.queryDetailsModal = ref - }} /> - -
    - - - - -
    - -
    -
    -
    - { - this.editor = (ref ? ref.editor : null) - }} /> -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - - - - {chartOptions} - - - -
    -
    - - -
    -
    -
    - { - this.sqlpadTauChart = ref - }} /> -
    -
    -
    - -
    - -
    - -
    - ) - } -}) - -module.exports = QueryEditor diff --git a/client-js/QueryTableOnly.js b/client-js/QueryTableOnly.js deleted file mode 100644 index 7a96ad833..000000000 --- a/client-js/QueryTableOnly.js +++ /dev/null @@ -1,82 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var DropdownButton = require('react-bootstrap/lib/DropdownButton') -var MenuItem = require('react-bootstrap/lib/MenuItem') -import IncompleteDataNotification from './components/IncompleteDataNotification' -var QueryResultDataTable = require('./components/QueryResultDataTable.js') - -var QueryEditor = React.createClass({ - getInitialState: function () { - return { - isRunning: false, - runQueryStartTime: undefined, - queryResult: undefined - } - }, - runQuery: function (queryId) { - this.setState({ - isRunning: true, - runQueryStartTime: new Date() - }) - fetchJson('GET', this.props.config.baseUrl + '/api/queries/' + queryId) - .then((json) => { - if (json.error) console.error(json.error) - this.setState({ - query: json.query - }) - }) - .then(() => { - return fetchJson('GET', this.props.config.baseUrl + '/api/query-result/' + queryId) - }) - .then((json) => { - if (json.error) console.error(json.error) - this.setState({ - isRunning: false, - queryError: json.error, - queryResult: json.queryResult - }) - }) - .catch((ex) => { - console.error(ex.toString()) - this.setState({ - isRunning: false - }) - }) - }, - componentDidMount: function () { - this.runQuery(this.props.queryId) - }, - render: function () { - var csvDownloadLink - var xlsxDownloadLink - if (this.state.queryResult) { - csvDownloadLink = this.props.config.baseUrl + '/download-results/' + this.state.queryResult.cacheKey + '.csv' - xlsxDownloadLink = this.props.config.baseUrl + '/download-results/' + this.state.queryResult.cacheKey + '.xlsx' - } - return ( -
    -

    {(this.state.query ? this.state.query.name : '')}

    -
    - - {(this.state.queryResult && this.props.config.allowCsvDownload ? ( - - csv - xlsx - - ) : null)} -
    -
    - -
    -
    - ) - } -}) - -module.exports = QueryEditor diff --git a/client-js/SignIn.js b/client-js/SignIn.js deleted file mode 100644 index d54945328..000000000 --- a/client-js/SignIn.js +++ /dev/null @@ -1,81 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -import navigateToClickHandler from './utilities/navigateToClickHandler' -var page = require('page') - -var SignIn = React.createClass({ - getInitialState: function () { - return { - email: '', - password: '' - } - }, - onEmailChange: function (e) { - this.setState({email: e.target.value}) - }, - onPasswordChange: function (e) { - this.setState({password: e.target.value}) - }, - signIn: function (e) { - e.preventDefault() - fetchJson('POST', this.props.config.baseUrl + '/api/signin', this.state) - .then((json) => { - if (json.error) return Alert.error('Username or password incorrect') - page('/') - }) - .catch((ex) => { - Alert.error('Username or Password incorrect') - console.error(ex) - }) - }, - render: function () { - const localForm = ( -
    -
    - - -
    - -
    -
    -

    - Sign Up - {(this.props.smtpConfigured ? Forgot Password : null)} -

    -
    -
    - ) - const googleForm = ( - - ) - return ( -
    -

    SqlPad

    - {('local' in this.props.passport.strategies ? localForm : null)} - {('google' in this.props.passport.strategies ? googleForm : null)} - -
    - ) - } -}) - -module.exports = SignIn diff --git a/client-js/SignUp.js b/client-js/SignUp.js deleted file mode 100644 index 9656e3e7c..000000000 --- a/client-js/SignUp.js +++ /dev/null @@ -1,88 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var Alert = require('react-s-alert').default -var page = require('page') - -var SignUp = React.createClass({ - getInitialState: function () { - return { - email: '', - password: '', - passwordConfirmation: '' - } - }, - onEmailChange: function (e) { - this.setState({email: e.target.value}) - }, - onPasswordChange: function (e) { - this.setState({password: e.target.value}) - }, - onPasswordConfirmationChange: function (e) { - this.setState({passwordConfirmation: e.target.value}) - }, - signUp: function (e) { - e.preventDefault() - fetchJson('POST', this.props.config.baseUrl + '/api/signup', this.state) - .then((json) => { - if (json.error) return Alert.error(json.error) - page('/') - }) - .catch((ex) => { - Alert.error('Problem signing up') - console.error(ex) - }) - }, - render: function () { - const adminRegistrationOpenIntro = () => { - if (this.props.adminRegistrationOpen) { - return ( -
    -

    Admin Registration is Open

    -

    - Welcome to SqlPad! - Since there are no admins currently in the system, - registration is open to anyone. By signing up, you will - be granted admin rights, and the system will be locked down. - Only people explicitly invited & whitelisted will be able to join. -

    -
    -
    - ) - } - } - return ( -
    -
    -

    SqlPad

    - {adminRegistrationOpenIntro()} - - - -
    - -
    - -
    - ) - } -}) - -module.exports = SignUp diff --git a/client-js/UserAdmin.js b/client-js/UserAdmin.js deleted file mode 100644 index 7bd937b2e..000000000 --- a/client-js/UserAdmin.js +++ /dev/null @@ -1,322 +0,0 @@ -var React = require('react') -var fetchJson = require('./utilities/fetch-json.js') -var moment = require('moment') -var uuid = require('uuid') -var Alert = require('react-s-alert').default -var Panel = require('react-bootstrap/lib/Panel') -var Form = require('react-bootstrap/lib/Form') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var Button = require('react-bootstrap/lib/Button') -var ListGroup = require('react-bootstrap/lib/ListGroup') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var Popover = require('react-bootstrap/lib/Popover') -var OverlayTrigger = require('react-bootstrap/lib/OverlayTrigger') - -var UserAdmin = React.createClass({ - getInitialState: function () { - return { - users: [], - isSaving: false - } - }, - componentDidMount: function () { - this.loadUsersFromServer() - }, - handleDelete: function (user) { - fetchJson('DELETE', this.props.config.baseUrl + '/api/users/' + user._id) - .then((json) => { - if (json.error) return Alert.error('Delete Failed: ' + json.error.toString()) - Alert.success('User Deleted') - this.loadUsersFromServer() - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - loadUsersFromServer: function () { - fetchJson('get', this.props.config.baseUrl + '/api/users') - .then((json) => { - if (json.error) Alert.error(json.error) - this.setState({users: json.users}) - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - updateUserRole: function (user) { - this.setState({isSaving: true}) - fetchJson('PUT', this.props.config.baseUrl + '/api/users/' + user._id, {role: user.role}) - .then((json) => { - this.loadUsersFromServer() - this.setState({isSaving: false}) - if (json.error) return Alert.error('Update failed: ' + json.error.toString()) - Alert.success('User Updated') - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - generatePasswordResetLink: function (user) { - this.setState({isSaving: true}) - fetchJson('PUT', this.props.config.baseUrl + '/api/users/' + user._id, {passwordResetId: user.passwordResetId}) - .then((json) => { - this.loadUsersFromServer() - this.setState({isSaving: false}) - if (json.error) return Alert.error('Update failed: ' + json.error.toString()) - Alert.success('Password link generated') - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - removePasswordResetLink: function (user) { - this.setState({isSaving: true}) - fetchJson('PUT', this.props.config.baseUrl + '/api/users/' + user._id, {passwordResetId: ''}) - .then((json) => { - this.loadUsersFromServer() - this.setState({isSaving: false}) - if (json.error) return Alert.error('Update failed: ' + json.error.toString()) - Alert.success('Password reset link removed') - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - render: function () { - return ( -
    - - -
    - ) - } -}) - -module.exports = UserAdmin - -var UserList = React.createClass({ - style: { - position: 'absolute', - left: 0, - width: '60%', - top: 0, - bottom: 0, - backgroundColor: '#FDFDFD', - overflowY: 'auto', - padding: 10 - }, - render: function () { - var listRows = this.props.users.map((user) => { - return ( - - ) - }) - return ( -
    - Users - - {listRows} - -
    - ) - } -}) - -var UserListRow = React.createClass({ - onDelete: function (e) { - this.props.handleDelete(this.props.user) - }, - onRoleChange: function (e) { - var user = this.props.user - user.role = e.target.value - this.props.updateUserRole(user) - }, - generatePasswordResetLink: function () { - var user = this.props.user - user.passwordResetId = uuid.v4() - this.props.generatePasswordResetLink(user) - }, - removePasswordResetLink: function () { - this.props.removePasswordResetLink(this.props.user) - }, - formControlStyle: { - minWidth: 200, - marginLeft: 20 - }, - render: function () { - var getClassNames = () => { - return 'list-group-item ListRow' - } - const popoverClick = ( - - - - ) - var signupDate = () => { - if (!this.props.user.signupDate) return (
    Signup Date: not signed up yet
    ) - return (
    Signup Date: {moment(this.props.user.signupDate).calendar()}
    ) - } - return ( -
  • -

    {this.props.user.email}

    - {signupDate()} - -
    - - Role{' '} - - - - - -
    - {(this.props.currentUser._id !== this.props.user._id ? ( - - - - ) : null)} -
  • - ) - } -}) - -const PasswordResetButtonLink = (props) => { - var style = { - fontSize: 14, - marginTop: 10, - marginBottom: 10, - display: 'block' - } - if (props.passwordResetId) { - return ( - - {' '} - Password Reset Link - - ) - } - return ( - - ) -} - -var InviteUserForm = React.createClass({ - style: { - position: 'absolute', - right: 0, - width: '40%', - top: 0, - bottom: 0, - backgroundColor: '#FDFDFD', - overflowY: 'auto', - padding: 10 - }, - getInitialState: function () { - return { - email: null, - role: null, - isInviting: false - } - }, - onEmailChange: function (e) { - this.setState({email: e.target.value}) - }, - onRoleChange: function (e) { - this.setState({ - role: e.target.value - }) - }, - onInviteClick: function (e) { - var user = { - email: this.state.email, - role: this.state.role - } - this.setState({ - isInviting: true - }) - fetchJson('POST', this.props.config.baseUrl + '/api/users', user) - .then((json) => { - this.setState({ - isInviting: false - }) - if (json.error) return Alert.error('Whitelist failed: ' + json.error.toString()) - Alert.success('User Whitelisted') - this.setState({ - email: null, - role: null - }) - this.props.loadUsersFromServer() - }) - .catch((ex) => { - console.error(ex.toString()) - Alert.error('Something is broken') - }) - }, - render: function () { - return ( -
    - Invite User - -
    -

    - Users may only sign up if they have first been whitelisted. - Once whitelisted, invite them to - continue the sign-up process on the signup page. -

    -

    - Admins can add and edit database connections, - as well as whitelist/invite users to join. -

    -
    - - Email - - - - Role - - - - - - -
    -
    -
    - ) - } -}) diff --git a/client-js/components/ChartInputs.js b/client-js/components/ChartInputs.js deleted file mode 100644 index 747698f51..000000000 --- a/client-js/components/ChartInputs.js +++ /dev/null @@ -1,133 +0,0 @@ -var React = require('react') -var _ = window._ -var chartDefinitions = require('./ChartDefinitions.js') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var ControlLabel = require('react-bootstrap/lib/ControlLabel') -var Checkbox = require('react-bootstrap/lib/Checkbox') - -function cleanBoolean (value) { - if (typeof value === 'string') { - if (value.toLowerCase() === 'true') { - value = true - } else if (value.toLowerCase() === 'false') { - value = false - } - } - return value -} - -var ChartInputs = React.createClass({ - getInitialState: function () { - return { - showAdvanced: false - } - }, - toggleAdvanced: function () { - this.setState({ - showAdvanced: !this.state.showAdvanced - }) - }, - changeChartConfigurationField: function (chartFieldId, queryResultField) { - this.props.onChartConfigurationFieldsChange(chartFieldId, queryResultField) - }, - render: function () { - var queryChartConfigurationFields = this.props.queryChartConfigurationFields || {} - var queryResult = this.props.queryResult - var queryResultFields = (queryResult && queryResult.fields ? queryResult.fields : []) - var chartInputDefinition = _.findWhere(chartDefinitions, {chartType: this.props.chartType}) - - if (!chartInputDefinition || !chartInputDefinition.fields) return null - - var regularInputDefinitionFields = chartInputDefinition.fields.filter((field) => { - return (field.advanced == null || field.advanced === false) - }) - - var advancedInputDefinitionFields = chartInputDefinition.fields.filter((field) => { - return field.advanced === true - }) - - var showAdvancedLink = (advancedInputDefinitionFields.length - ? {this.state.showAdvanced ? 'hide advanced settings' : 'show advanced settings'} - : null) - - var formGroupNodes = (inputDefinitionFields) => { - return inputDefinitionFields.map((field) => { - if (field.inputType === 'field-dropdown') { - var optionNodes = queryResultFields.map(function (qrfield) { - return ( - - ) - }) - var selectedQueryResultField = queryChartConfigurationFields[field.fieldId] - if (queryResultFields.indexOf(selectedQueryResultField) === -1) { - optionNodes.push(function () { - return ( - - ) - }()) - } - return ( - - {field.label} - { - this.changeChartConfigurationField(field.fieldId, e.target.value) - }} - componentClass='select' - className='input-small'> - - - ) - } else if (field.inputType === 'checkbox') { - var checked = cleanBoolean(queryChartConfigurationFields[field.fieldId]) || false - return ( - - { - this.changeChartConfigurationField(field.fieldId, e.target.checked) - }}> - {field.label} - - - ) - } else if (field.inputType === 'textbox') { - var value = queryChartConfigurationFields[field.fieldId] - return ( - - {field.label} - { - this.changeChartConfigurationField(field.fieldId, e.target.value) - }} - type='text' - className='input-small' /> - - ) - } else { - throw Error(`field.inputType ${field.inputType} not supported`) - } - }) - } - - var advancedFormGroup = () => { - if (this.state.showAdvanced) return formGroupNodes(advancedInputDefinitionFields) - else return null - } - - return ( -
    - {formGroupNodes(regularInputDefinitionFields)} - {showAdvancedLink} - {advancedFormGroup()} -
    - ) - } -}) - -module.exports = ChartInputs diff --git a/client-js/components/IncompleteDataNotification.js b/client-js/components/IncompleteDataNotification.js deleted file mode 100644 index 8e6627f5e..000000000 --- a/client-js/components/IncompleteDataNotification.js +++ /dev/null @@ -1,33 +0,0 @@ -var React = require('react') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -var Popover = require('react-bootstrap/lib/Popover') -var Overlay = require('react-bootstrap/lib/Overlay') -var ReactDOM = require('react-dom') - -export default React.createClass({ - getInitialState () { - return { show: false } - }, - toggle () { - this.setState({ show: !this.state.show }) - }, - render () { - if (this.props.queryResult && this.props.queryResult.incomplete) { - const sharedProps = { - show: this.state.show, - target: () => ReactDOM.findDOMNode(this.refs.incompleteDataTarget) - } - return ( - - Incomplete - - - Return fewer rows or increase query result max rows in configuration. - - - - ) - } - return null - } -}) diff --git a/client-js/components/QueryResultDataTable.js b/client-js/components/QueryResultDataTable.js deleted file mode 100644 index c5a44f658..000000000 --- a/client-js/components/QueryResultDataTable.js +++ /dev/null @@ -1,138 +0,0 @@ -var React = require('react') -var SpinKitCube = require('./SpinKitCube.js') -var moment = require('moment') -import {Table, Column, Cell} from 'fixed-data-table' // react's fixed data table -var _ = window._ - -var renderValue = (input, fieldMeta) => { - if (input === null || input === undefined) { - return null - } else if (input === true || input === false) { - return input.toString() - } else if (fieldMeta.datatype === 'date') { - return moment(input).format('MM/DD/YYYY HH:mm:ss') - } else if (_.isObject(input)) { - return JSON.stringify(input, null, 2) - } else { - return input - } -} - -// NOTE: PureComponent's shallow compare works for this component -// because the isRunning prop will toggle with each query execution -// It would otherwise not rerender on change of prop.queryResult alone -class QueryResultDataTable extends React.PureComponent { - - constructor (props) { - super(props) - this.state = { - gridWidth: 0, - gridHeight: 0 - } - // This binding is necessary to make `this` work in the callback - this.handleResize = this.handleResize.bind(this) - } - - handleResize (e) { - var resultGrid = document.getElementById('result-grid') - if (resultGrid) { - this.setState({ - gridHeight: resultGrid.clientHeight, - gridWidth: resultGrid.clientWidth - }) - } - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize) - this.handleResize() - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize) - } - - render () { - if (this.props.isRunning) { - return ( -
    - -
    - ) - } else if (this.props.queryError) { - return ( -
    - {this.props.queryError} -
    - ) - } else if (this.props.queryResult && this.props.queryResult.rows) { - var queryResult = this.props.queryResult - var columnNodes = queryResult.fields.map(function (field) { - var fieldMeta = queryResult.meta[field] - var valueLength = fieldMeta.maxValueLength - - if (field.length > valueLength) valueLength = field.length - var columnWidth = valueLength * 20 - if (columnWidth < 200) columnWidth = 200 - else if (columnWidth > 350) columnWidth = 350 - var cellWidth = columnWidth - 10 - - return ( - {field}} - cell={({rowIndex}) => { - var value = queryResult.rows[rowIndex][field] - var barStyle - var numberBar - if (fieldMeta.datatype === 'number') { - value = Number(value) - var range = fieldMeta.max - (fieldMeta.min < 0 ? fieldMeta.min : 0) - var left = 0 - if (fieldMeta.min < 0 && value < 0) { - left = Math.abs(fieldMeta.min - value) / range * 100 + '%' - } else if (fieldMeta.min < 0 && value >= 0) { - left = Math.abs(fieldMeta.min) / range * 100 + '%' - } - barStyle = { - position: 'absolute', - left: left, - top: 0, - bottom: 0, - width: (Math.abs(value) / range) * 100 + '%', - backgroundColor: '#bae6f7' - } - numberBar =
    - } - return ( - - {numberBar} -
    - {renderValue(value, fieldMeta)} -
    -
    - ) - }} - width={columnWidth} - /> - ) - }) - return ( -
    - - {columnNodes} -
    -
    - ) - } else { - return
    - } - } -} - -module.exports = QueryResultDataTable diff --git a/client-js/components/QueryResultHeader.js b/client-js/components/QueryResultHeader.js deleted file mode 100644 index f8a3ce06c..000000000 --- a/client-js/components/QueryResultHeader.js +++ /dev/null @@ -1,57 +0,0 @@ -var React = require('react') -var SecondsTimer = require('./SecondsTimer.js') -import IncompleteDataNotification from './IncompleteDataNotification' - -var QueryResultHeader = React.createClass({ - render: function () { - if (this.props.isRunning || !this.props.queryResult) { - return ( -
    - {(this.props.isRunning ? ( - - Query Run Time: - - sec. - - - ) : null)} -
    - ) - } - var csvDownloadLink = this.props.config.baseUrl + '/download-results/' + this.props.cacheKey + '.csv' - var xlsxDownloadLink = this.props.config.baseUrl + '/download-results/' + this.props.cacheKey + '.xlsx' - var serverSec = (this.props.queryResult ? (this.props.queryResult.queryRunTime / 1000) + ' sec.' : '') - var rowCount = (this.props.queryResult && this.props.queryResult.rows ? this.props.queryResult.rows.length : '') - var downloadLinks = () => { - if (this.props.config.allowCsvDownload) { - return ( - - Download: - .csv - .xlsx - - ) - } - } - return ( -
    - - Query Run Time: - {serverSec} - - - Rows: - {rowCount} - - - {downloadLinks()} - - - - -
    - ) - } -}) - -module.exports = QueryResultHeader diff --git a/client-js/components/SchemaInfo.js b/client-js/components/SchemaInfo.js deleted file mode 100644 index 30ae8e56c..000000000 --- a/client-js/components/SchemaInfo.js +++ /dev/null @@ -1,275 +0,0 @@ -var React = require('react') -var FormGroup = require('react-bootstrap/lib/FormGroup') -var FormControl = require('react-bootstrap/lib/FormControl') -var Glyphicon = require('react-bootstrap/lib/Glyphicon') -import CopyToClipboard from 'react-copy-to-clipboard' -var fetchJson = require('../utilities/fetch-json.js') - -class SchemaInfo extends React.PureComponent { - - constructor (props) { - super(props) - this.state = { - schemaInfo: {}, - loading: false - } - // This binding is necessary to make `this` work in the callback - this.getSchemaInfo = this.getSchemaInfo.bind(this) - this.onConnectionChange = this.onConnectionChange.bind(this) - this.onRefreshClick = this.onRefreshClick.bind(this) - } - - componentDidMount () { - if (this.props.connectionId) this.getSchemaInfo(this.props.connectionId) - } - - componentWillReceiveProps (nextProps) { - if (this.props.connectionId !== nextProps.connectionId) { - this.getSchemaInfo(nextProps.connectionId) - } - } - - getSchemaInfo (connectionId, reload) { - if (connectionId) { - this.setState({ - schemaInfo: {}, - loading: true - }) - var url = this.props.config.baseUrl + '/api/schema-info/' + connectionId - if (reload) url += '?reload=true' - fetchJson('GET', url) - .then((json) => { - if (json.error) console.error(json.error) - this.setState({ - schemaInfo: json.schemaInfo - }) - // sometimes refreshes happen so fast and people don't get to enjoy the animation - setTimeout(() => { - this.setState({loading: false}) - }, 1000) - }) - .catch((ex) => { - console.error(ex.toString()) - }) - } else { - this.setState({ - schemaInfo: {} - }) - } - } - - onConnectionChange (e) { - var connectionId = e.target.value - this.props.onConnectionChange(connectionId) - this.getSchemaInfo(connectionId) - } - - onRefreshClick (e) { - e.preventDefault() - this.getSchemaInfo(this.props.connectionId, true) - } - - render () { - var connectionSelectOptions = this.props.connections.map(function (conn) { - return ( - - ) - }) - var refreshClass = (this.state.loading ? 'spinning' : '') - - var schemaInfo = this.state.schemaInfo - var schemaCount = (schemaInfo ? Object.keys(schemaInfo).length : 0) - var initShowTables = (schemaCount <= 2) - var schemaItemNodes = Object.keys(schemaInfo).map((schema) => { - return ( - - ) - }) - - return ( -
    - - - - {connectionSelectOptions} - - -
    -
    - - - -
    -
      - {schemaItemNodes} -
    -
    -
    -
    - ) - } -} - -var SchemaInfoSchemaItem = React.createClass({ - getInitialState: function () { - return { - showTables: this.props.initShowTables - } - }, - onClick: function (e) { - e.stopPropagation() - e.preventDefault() - this.setState({ - showTables: !this.state.showTables - }) - }, - render: function () { - var tableJsx - if (this.state.showTables) { - tableJsx = Object.keys(this.props.tables).map((table) => { - return ( - - ) - }) - } - return ( -
  • - {this.props.schema} -
      - {tableJsx} -
    -
  • - ) - } -}) - -var SchemaInfoTableItem = React.createClass({ - getInitialState: function () { - return { - showColumns: false, - showCopyButton: false, - copyButtonText: 'copy' - } - }, - onClick: function (e) { - e.stopPropagation() - e.preventDefault() - this.setState({ - showColumns: !this.state.showColumns - }) - }, - onMouseOver: function (e) { - this.setState({ - showCopyButton: true - }) - }, - onMouseOut: function (e) { - this.setState({ - showCopyButton: false - }) - }, - onCopyClick: function (e) { - e.stopPropagation() - }, - onCopy: function () { - this.setState({copyButtonText: 'copied'}) - setTimeout(() => { - this.setState({copyButtonText: 'copy'}) - }, 2000) - }, - render: function () { - var columnJsx - if (this.state.showColumns) { - columnJsx = this.props.columns.map((column) => { - return ( - - ) - }) - } - // this is hacky, but because of the way we're passing the schema info around - // we need to reach down into the columns to get the type of this object - var viewType = () => { - var type = this.props.columns[0].table_type - if (type.toLowerCase().split('')[0] === 'v') return ( (view)) - } - var copyButtonClassName = (this.state.showCopyButton ? 'copy-button label' : 'copy-button label hidden') - var getCopyToClipboard = () => { - if (this.props.config && this.props.config.showSchemaCopyButton) { - return ( - - {this.state.copyButtonText} - - ) - } - } - return ( -
  • - - {this.props.table} {viewType()} - {getCopyToClipboard()} - -
      - {columnJsx} -
    -
  • - ) - } -}) - -var SchemaInfoColumnItem = React.createClass({ - getInitialState: function () { - return { - showCopyButton: false, - copyButtonText: 'copy' - } - }, - onMouseOver: function (e) { - this.setState({ - showCopyButton: true - }) - }, - onMouseOut: function (e) { - this.setState({ - showCopyButton: false - }) - }, - onCopyClick: function (e) { - e.stopPropagation() - e.preventDefault() - }, - onCopy: function () { - this.setState({copyButtonText: 'copied'}) - setTimeout(() => { - this.setState({copyButtonText: 'copy'}) - }, 2000) - }, - render: function () { - var copyButtonClassName = (this.state.showCopyButton ? 'copy-button label label-info' : 'copy-button label label-info hidden') - var getCopyToClipboard = () => { - if (this.props.config && this.props.config.showSchemaCopyButton) { - return ( - - {this.state.copyButtonText} - - ) - } - } - return ( -
  • - - {this.props.column_name} - ({this.props.data_type}) - {getCopyToClipboard()} - -
  • - ) - } -}) - -module.exports = SchemaInfo diff --git a/client-js/components/SecondsTimer.js b/client-js/components/SecondsTimer.js deleted file mode 100644 index aa959bfad..000000000 --- a/client-js/components/SecondsTimer.js +++ /dev/null @@ -1,33 +0,0 @@ -var React = require('react') - -var SecondsTimer = React.createClass({ - _mounted: false, - getInitialState: function () { - return { - runSeconds: 0 - } - }, - timer: function () { - if (this._mounted) { - var now = new Date() - this.setState({ - runSeconds: ((now - this.props.startTime) / 1000).toFixed(0) - }) - setTimeout(this.timer, 33) - } - }, - componentDidMount: function () { - this._mounted = true - this.timer() - }, - componentWillUnmount: function () { - this._mounted = false - }, - render: function () { - return ( - {this.state.runSeconds} - ) - } -}) - -module.exports = SecondsTimer diff --git a/client-js/components/SpinKitCube.js b/client-js/components/SpinKitCube.js deleted file mode 100644 index c6b68f769..000000000 --- a/client-js/components/SpinKitCube.js +++ /dev/null @@ -1,16 +0,0 @@ -// http://tobiasahlin.com/spinkit/ -var React = require('react') - -module.exports = () => ( -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -) diff --git a/client-js/components/SqlpadTauChart.js b/client-js/components/SqlpadTauChart.js deleted file mode 100644 index 6046e9966..000000000 --- a/client-js/components/SqlpadTauChart.js +++ /dev/null @@ -1,250 +0,0 @@ -var React = require('react') -var _ = window._ -var chartDefinitions = require('./ChartDefinitions.js') -var SpinKitCube = require('./SpinKitCube.js') -var tauCharts = window.tauCharts -var Alert = require('react-s-alert').default - -var SqlpadTauChart = React.createClass({ - componentDidUpdate: function (prevProps) { - if (this.props.isRunning || this.props.queryError) { - this.destroyChart() - } else if (this.props.renderChart && !this.chart) { - this.renderChart() - } - }, - chart: undefined, - chartStyle: { - padding: '20px 10px 10px 20px', - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0 - }, - destroyChart () { - if (this.chart) { - this.chart.destroy() - this.chart = null - } - }, - renderChart: function (rerender) { - // This is invoked during following: - // - Vis tab enter - // - Visualize button press (forces rerender) - // - new data arrival - var meta = (this.props.queryResult ? this.props.queryResult.meta : {}) - var dataRows = (this.props.queryResult ? this.props.queryResult.rows : []) - var chartType = this.props.query.chartConfiguration.chartType - var selectedFields = this.props.query.chartConfiguration.fields - var chartDefinition = _.findWhere(chartDefinitions, {chartType: chartType}) - - if (rerender || !dataRows.length || !chartDefinition) { - this.destroyChart() - } - - // If there's no data just exit the chart render - if (!dataRows.length) return - - // if there's no chart definition exit the render - if (!chartDefinition) return - - var chartConfig = { - type: chartDefinition.tauChartsType, - plugins: [ - tauCharts.api.plugins.get('tooltip')(), - tauCharts.api.plugins.get('legend')(), - tauCharts.api.plugins.get('exportTo')({ - cssPaths: [this.props.config.baseUrl + '/javascripts/vendor/tauCharts/tauCharts.min.css'], - fileName: this.props.query.name || 'unnamed query' - }) - ] - } - - var unmetRequiredFields = [] - var definitionFields = chartDefinition.fields.map(function (field) { - if (field.required && !selectedFields[field.fieldId]) { - unmetRequiredFields.push(field) - } - field.val = selectedFields[field.fieldId] - return field - }) - if (unmetRequiredFields.length) { - // if rerender is true, a render was explicitly requested by user clicking the vis button - // TODO - highlight fields that are required but not provided - if (rerender) { - Alert.error('Unmet required fields: ' + unmetRequiredFields.map(f => f.label).join(', ')) - } - return - } - - // loop through data rows and convert types as needed - dataRows = dataRows.map((row) => { - var newRow = {} - Object.keys(row).forEach(col => { - var datatype = this.props.queryResult.meta[col].datatype - if (datatype === 'date') { - newRow[col] = new Date(row[col]) - } else if (datatype === 'number') { - newRow[col] = Number(row[col]) - } else { - newRow[col] = row[col] - } - }) - - // HACK - - // Facets need to be a dimension, not a measure. - // tauCharts auto detects numbers to be measures - // Here we'll convert a number to a string, - // to trick tauCharts into thinking its a dimension - var forceDimensionFields = _.where(definitionFields, {forceDimension: true}) - forceDimensionFields.forEach(function (fieldDefinition) { - var col = fieldDefinition.val - var colDatatype = (meta[col] ? meta[col].datatype : null) - if (col && colDatatype === 'number' && newRow[col]) { - newRow[col] = newRow[col].toString() - } - }) - return newRow - }) - - var definitionFieldsById = _.indexBy(definitionFields, 'fieldId') - switch (chartType) { - case 'line': - chartConfig.x = [definitionFieldsById.x.val] - if (definitionFieldsById.xFacet.val) { - chartConfig.x.unshift(definitionFieldsById.xFacet.val) - } - chartConfig.y = [definitionFieldsById.y.val] - if (definitionFieldsById.yFacet.val) { - chartConfig.y.unshift(definitionFieldsById.yFacet.val) - } - if (definitionFieldsById.filter.val) { - chartConfig.plugins.push(tauCharts.api.plugins.get('quick-filter')()) - } - if (definitionFieldsById.trendline.val) { - chartConfig.plugins.push(tauCharts.api.plugins.get('trendline')()) - } - if (definitionFieldsById.split.val) chartConfig.color = definitionFieldsById.split.val - if (definitionFieldsById.size.val) chartConfig.size = definitionFieldsById.size.val - if (definitionFieldsById.yMin.val || definitionFieldsById.yMax.val) { - chartConfig.guide = { - y: {autoScale: false} - } - if (definitionFieldsById.yMin.val) chartConfig.guide.y.min = Number(definitionFieldsById.yMin.val) - if (definitionFieldsById.yMax.val) chartConfig.guide.y.max = Number(definitionFieldsById.yMax.val) - } - break - - case 'bar': - chartConfig.x = [definitionFieldsById.barvalue.val] - if (definitionFieldsById.valueFacet.val) { - chartConfig.x.unshift(definitionFieldsById.valueFacet.val) - } - chartConfig.y = [definitionFieldsById.barlabel.val] - if (definitionFieldsById.labelFacet.val) { - chartConfig.y.unshift(definitionFieldsById.labelFacet.val) - } - break - - case 'verticalbar': - chartConfig.y = [definitionFieldsById.barvalue.val] - if (definitionFieldsById.valueFacet.val) { - chartConfig.y.unshift(definitionFieldsById.valueFacet.val) - } - chartConfig.x = [definitionFieldsById.barlabel.val] - if (definitionFieldsById.labelFacet.val) { - chartConfig.x.unshift(definitionFieldsById.labelFacet.val) - } - break - - case 'stacked-bar-horizontal': - chartConfig.x = [definitionFieldsById.barvalue.val] - if (definitionFieldsById.valueFacet.val) { - chartConfig.x.unshift(definitionFieldsById.valueFacet.val) - } - chartConfig.y = [definitionFieldsById.barlabel.val] - if (definitionFieldsById.labelFacet.val) { - chartConfig.y.unshift(definitionFieldsById.labelFacet.val) - } - if (definitionFieldsById.color.val) chartConfig.color = definitionFieldsById.color.val - break - - case 'stacked-bar-vertical': - chartConfig.y = [definitionFieldsById.barvalue.val] - if (definitionFieldsById.valueFacet.val) { - chartConfig.y.unshift(definitionFieldsById.valueFacet.val) - } - chartConfig.x = [definitionFieldsById.barlabel.val] - if (definitionFieldsById.labelFacet.val) { - chartConfig.x.unshift(definitionFieldsById.labelFacet.val) - } - if (definitionFieldsById.color.val) chartConfig.color = definitionFieldsById.color.val - break - - case 'bubble': - chartConfig.x = [definitionFieldsById.x.val] - if (definitionFieldsById.xFacet.val) { - chartConfig.x.unshift(definitionFieldsById.xFacet.val) - } - chartConfig.y = [definitionFieldsById.y.val] - if (definitionFieldsById.yFacet.val) { - chartConfig.y.unshift(definitionFieldsById.yFacet.val) - } - if (definitionFieldsById.filter.val) { - chartConfig.plugins.push(tauCharts.api.plugins.get('quick-filter')()) - } - if (definitionFieldsById.trendline.val) { - chartConfig.plugins.push(tauCharts.api.plugins.get('trendline')()) - } - if (definitionFieldsById.size.val) chartConfig.size = definitionFieldsById.size.val - if (definitionFieldsById.color.val) chartConfig.color = definitionFieldsById.color.val - break - - default: - console.error('unknown chart type') - } - - // Add data to chart config - chartConfig.data = dataRows - - if (!this.chart) { - this.chart = new tauCharts.Chart(chartConfig) - this.chart.renderTo('#chart') - } else { - this.chart.setData(dataRows) - } - }, - setData: function (chartData) { - this.chart.setData(chartData) - }, - componentWillUnmount () { - this.destroyChart() - }, - render: function () { - var runResultNotification = () => { - if (this.props.isRunning) { - return ( -
    - -
    - ) - } else if (this.props.queryError) { - return ( -
    - {this.props.queryError} -
    - ) - } else { - return null - } - } - return ( -
    - {runResultNotification()} -
    - ) - } -}) - -module.exports = SqlpadTauChart diff --git a/client-js/index.js b/client-js/index.js deleted file mode 100644 index 455531878..000000000 --- a/client-js/index.js +++ /dev/null @@ -1,231 +0,0 @@ -// This is the client side js entry file to be browserified -var page = require('page') -var React = require('react') -var ReactDOM = require('react-dom') -var fetchJson = require('./utilities/fetch-json.js') - -fetchJson('GET', 'api/app') - .then((json) => { - init(json) - }) - .catch((ex) => { - console.error(ex.toString()) - }) - -function init (appData) { - var config = appData.config - const BASE_URL = config.baseUrl - - // account for baseUrl in client-side routing - page.base(BASE_URL) - - /* client-side routes - ============================================================================== */ - var App = require('./App.js') - var UserAdmin = require('./UserAdmin.js') - var ConnectionAdmin = require('./ConnectionAdmin.js') - var ConfigValues = require('./ConfigValues.js') - var FilterableQueryList = require('./FilterableQueryList.js') - var QueryEditor = require('./QueryEditor.js') - var SignIn = require('./SignIn.js') - var SignUp = require('./SignUp.js') - var ForgotPassword = require('./ForgotPassword.js') - var PasswordReset = require('./PasswordReset.js') - var QueryTableOnly = require('./QueryTableOnly.js') - var QueryChartOnly = require('./QueryChartOnly.js') - var FullscreenMessage = require('./FullscreenMessage.js') - - function getAppData (ctx, next) { - fetchJson('GET', 'api/app') - .then((json) => { - ctx.config = json.config - ctx.smtpConfigured = json.smtpConfigured - ctx.googleAuthConfigured = json.googleAuthConfigured - ctx.currentUser = json.currentUser - ctx.passport = json.passport - ctx.adminRegistrationOpen = json.adminRegistrationOpen - ctx.version = json.version - next() - }) - .catch((ex) => { - console.error(ex.toString()) - }) - } - - function mustBeAuthenticated (ctx, next) { - if (ctx.currentUser) { - next() - } else { - page.redirect('/signin') - } - } - - function mustBeAdmin (ctx, next) { - if (ctx.currentUser && ctx.currentUser.role === 'admin') { - next() - } else { - console.log('must be admin') - } - } - - page('*', getAppData) - - page.redirect('/', '/queries') - - page('/users', mustBeAuthenticated, mustBeAdmin, function (ctx) { - document.title = 'SqlPad - Users' - ReactDOM.render( - - - , - document.getElementById('root') - ) - }) - - page('/connections', mustBeAuthenticated, mustBeAdmin, function (ctx) { - document.title = 'SqlPad - Connections' - ReactDOM.render( - - - , - document.getElementById('root') - ) - }) - - page('/config-values', mustBeAuthenticated, mustBeAdmin, function (ctx) { - document.title = 'SqlPad - Configuration' - ReactDOM.render( - - - , - document.getElementById('root') - ) - }) - - page('/queries', mustBeAuthenticated, function (ctx) { - document.title = 'SqlPad - Queries' - ReactDOM.render( - - - , - document.getElementById('root') - ) - }) - - page('/queries/:queryId', mustBeAuthenticated, function (ctx) { - ReactDOM.render( - - - , - document.getElementById('root') - ) - }) - - page('/signin', function (ctx) { - document.title = 'SqlPad - Sign In' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('/signup', function (ctx) { - document.title = 'SqlPad - Sign Up' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('/forgot-password', function (ctx) { - document.title = 'SqlPad - Forgot Password' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('/password-reset', function (ctx) { - document.title = 'SqlPad - Password Reset' - ReactDOM.render( - -

    - Password reset requested. -

    -

    - An email has been sent with further instruction. -

    -
    , - document.getElementById('root') - ) - }) - - page('/password-reset/:passwordResetId', function (ctx) { - document.title = 'SqlPad - Reset Password' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('/query-table/:queryId', function (ctx) { - document.title = 'SqlPad' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('/query-chart/:queryId', function (ctx) { - document.title = 'SqlPad' - ReactDOM.render( - , - document.getElementById('root') - ) - }) - - page('*', function (ctx) { - document.title = 'SqlPad - Not Found' - var Component - if (ctx.currentUser) { - Component = ( - - - Not Found - - - ) - } else { - Component = ( - - Not Found - - ) - } - ReactDOM.render( - Component, - document.getElementById('root') - ) - }) - - /* init router - ============================================================================== */ - page({click: false}) -} diff --git a/client-js/utilities/fetch-json.js b/client-js/utilities/fetch-json.js deleted file mode 100644 index 360f22de4..000000000 --- a/client-js/utilities/fetch-json.js +++ /dev/null @@ -1,25 +0,0 @@ -import 'whatwg-fetch' - -module.exports = (verb, url, body) => { - var opts = { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - } - if (!verb) throw new Error('You must supply a verb to the fetch wrapper') - opts.method = verb.toUpperCase() - if (body) opts.body = JSON.stringify(body) - return fetch(url, opts).then((response) => { - if (response.status === 200) { - return response.json() - } else { - console.error(response) - return { - error: 'Server responded not ok' - } - } - }) -} diff --git a/client-js/utilities/navigateToClickHandler.js b/client-js/utilities/navigateToClickHandler.js deleted file mode 100644 index a1731c1cb..000000000 --- a/client-js/utilities/navigateToClickHandler.js +++ /dev/null @@ -1,17 +0,0 @@ -var page = require('page') - -/* - navigateTo() returns a function used to navigate to a page via page.js - General usage will be to assign to onClick handler for links. - config.baseUrl does not need to be provided as page.js is aware of the base url. - - example: navigateTo('/somepage') -*/ - -export default function navigateToClickHandler (path) { - return function (e) { - e.preventDefault() - e.stopPropagation() - page(path) - } -} diff --git a/client/.eslintignore b/client/.eslintignore new file mode 100644 index 000000000..a68457dbe --- /dev/null +++ b/client/.eslintignore @@ -0,0 +1,3 @@ +build +node_modules +public \ No newline at end of file diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 000000000..285ced691 --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "env": { + "browser": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "prettier" + ], + "rules": { + "no-console": "off", + "prefer-const": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-function": "off" + } +} diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 000000000..b6e77ca9b --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +bundle-analysis-result.html \ No newline at end of file diff --git a/client/.prettierignore b/client/.prettierignore new file mode 100644 index 000000000..24e39b124 --- /dev/null +++ b/client/.prettierignore @@ -0,0 +1,6 @@ +*.html +package-lock.json +package.json +build +node_modules +public \ No newline at end of file diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 000000000..f024d5005 --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "/home/rickb/code/sqlpad/client/node_modules/typescript/lib" +} diff --git a/client/decs.d.ts b/client/decs.d.ts new file mode 100644 index 000000000..064c5f854 --- /dev/null +++ b/client/decs.d.ts @@ -0,0 +1,5 @@ +declare module 'taucharts/dist/plugins/trendline'; +declare module 'taucharts/dist/plugins/tooltip'; +declare module 'taucharts/dist/plugins/quick-filter'; +declare module 'taucharts/dist/plugins/legend'; +declare module 'taucharts/dist/plugins/export-to'; diff --git a/client/index.html b/client/index.html new file mode 100644 index 000000000..9a4ee7f1b --- /dev/null +++ b/client/index.html @@ -0,0 +1,19 @@ + + + + + + + + SQLPad + + + + + + + +
    + + + \ No newline at end of file diff --git a/client/package.json b/client/package.json new file mode 100644 index 000000000..d629cba68 --- /dev/null +++ b/client/package.json @@ -0,0 +1,75 @@ +{ + "name": "sqlpad-front-end", + "version": "7.5.7", + "private": true, + "dependencies": { + "@reach/dialog": "^0.18.0", + "@reach/menu-button": "^0.18.0", + "@reach/tooltip": "^0.18.0", + "@types/humanize-duration": "^3.27.1", + "@types/keymaster": "^1.6.30", + "@types/lodash": "^4.14.181", + "@types/match-sorter": "^6.0.0", + "@types/node": "^18.0.0", + "@types/react": "^18.2.28", + "@types/react-dom": "18.2.13", + "@types/react-measure": "^2.0.8", + "@types/react-router-dom": "^5.3.3", + "@types/react-window": "^1.8.5", + "@types/react-window-infinite-loader": "^1.0.6", + "@types/uuid": "^9.0.0", + "@vitejs/plugin-react": "^4.1.0", + "d3": "^5.16.0", + "downshift": "^6.1.7", + "history": "^4.10.1", + "humanize-duration": "^3.27.1", + "keymaster": "^1.6.2", + "localforage": "^1.10.0", + "lodash": "^4.17.20", + "match-sorter": "^6.3.1", + "mdi-react": "9.1.0", + "mitt": "^3.0.0", + "prism-react-renderer": "^2.1.0", + "prop-types": "^15.8.1", + "query-string": "^8.1.0", + "react": "^18.2.0", + "react-ace": "^10.1.0", + "react-dom": "18.2.0", + "react-draggable": "^4.4.4", + "react-measure": "^2.5.2", + "react-router-dom": "^5.3.0", + "react-split-pane": "^0.1.92", + "react-window": "^1.8.6", + "react-window-infinite-loader": "^1.0.5", + "swr": "^1.3.0", + "taucharts": "^2.8.0", + "typescript": "5.2.2", + "use-debounce": "^9.0.3", + "uuid": "^9.0.0", + "vite": "^4.1.1", + "whatwg-fetch": "^3.5.0", + "zustand": "^4.3.2" + }, + "scripts": { + "build": "tsc && vite build", + "start": "vite", + "fixlint": "eslint --fix \"**/*.{js,jsx,ts,tsx}\" && prettier --write .", + "lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier --check .", + "dev": "vite", + "serve": "vite preview" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.5", + "@typescript-eslint/parser": "^6.7.5", + "eslint": "^8.13.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-react-hooks": "^4.4.0", + "prettier": "^2.6.2" + } +} diff --git a/docs/images/favicon.ico b/client/public/favicon.ico similarity index 100% rename from docs/images/favicon.ico rename to client/public/favicon.ico diff --git a/client/public/javascripts/vendor/tauCharts/tauCharts.min.css b/client/public/javascripts/vendor/tauCharts/tauCharts.min.css new file mode 100644 index 000000000..142c7487f --- /dev/null +++ b/client/public/javascripts/vendor/tauCharts/tauCharts.min.css @@ -0,0 +1,10174 @@ +/*! + * /* + * taucharts@2.7.1 (2019-03-14) + * Copyright 2019 Targetprocess, Inc. + * Licensed under Apache License 2.0 + * * / + * + */ +.tau-chart__layout { + line-height: 1; + font-family: Helvetica Neue, Segoe UI, Open Sans, Ubuntu, sans-serif; +} +.tau-chart__layout text { + font: normal 13px Helvetica Neue, Segoe UI, Open Sans, Ubuntu, sans-serif; +} +.tau-chart__chart { + font-family: Helvetica Neue, Segoe UI, Open Sans, Ubuntu, sans-serif; + position: absolute; + height: 100%; + width: 100%; + overflow: auto; +} +.tau-chart__layout { + display: -webkit-box; + display: -webkit-flexbox; + display: -ms-flexbox; + display: flex; + -ms-flex-align: stretch; + -webkit-align-items: stretch; + align-items: stretch; + -ms-flex-direction: column; + -webkit-box-orient: vertical; + flex-direction: column; + height: 100%; + width: 100%; + overflow: auto; + background: transparent; + color: #333; +} +.tau-chart__layout__header { + -ms-flex: 0 0.1 auto; + -webkit-box-flex: 0 0.1 auto; + flex: 0 0.1 auto; + position: relative; +} +.tau-chart__layout__container { + display: -webkit-box; + display: -webkit-flexbox; + display: -ms-flexbox; + display: flex; + -ms-flex: 1 1 auto; + -webkit-box-flex: 1 1 auto; + flex: 1 1 auto; + height: 100%; +} +.tau-chart__layout__footer { + -ms-flex: 0 1 auto; + -webkit-box-flex: 0 1 auto; + flex: 0 1 auto; +} +.tau-chart__layout__sidebar { + -ms-flex: 0 1 auto; + -webkit-box-flex: 0 1 auto; + flex: 0 1 auto; +} +.tau-chart__layout__content { + -ms-flex: 1 1 auto; + -webkit-box-flex: 1 1 auto; + flex: 1 1 auto; + overflow: hidden; +} +.tau-chart__layout__sidebar-right { + position: relative; + overflow: hidden; + -ms-flex: 0 0 auto; + -webkit-box-flex: 0 0 auto; + flex: 0 0 auto; +} +.tau-chart__layout__sidebar-right__wrap { + max-height: 100%; + box-sizing: border-box; +} +.tau-chart__layout text { + fill: #333; +} +.tau-chart__layout.tau-chart__layout_rendering-error { + opacity: 0.75; +} +.tau-chart__rendering-timeout-warning { + align-items: center; + background: rgba(255, 255, 255, 0.5); + display: flex; + flex-direction: column; + height: 100%; + position: absolute; + top: 0; + width: 100%; +} +.tau-chart__rendering-timeout-warning svg { + height: 100%; + max-width: 32em; + width: 100%; +} +.tau-chart__rendering-timeout-warning text { + font-weight: 300; +} +.tau-chart__progress { + box-sizing: border-box; + height: 0.25em; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: absolute; + top: 0; + transition: opacity 1s 0.75s; + width: 100%; +} +.tau-chart__progress_active { + opacity: 1; +} +.tau-chart__progress__value { + background: rgba(51, 51, 51, 0.25); + height: 100%; + transition: width 0.75s; +} +.tau-chart { + /* region Select --------------------------------------------------*/ +} +.tau-chart__checkbox { + position: relative; + display: block; +} +.tau-chart__checkbox__input { + position: absolute; + z-index: -1; + opacity: 0; +} +.tau-chart__checkbox__icon { + position: relative; + width: 14px; + height: 14px; + top: 3px; + display: inline-block; + border: 1px solid #c3c3c3; + border-radius: 2px; + background: linear-gradient(to bottom, #fff 0%, #dbdbde 100%); +} +.tau-chart__checkbox__icon:before { + display: none; + content: ''; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAFoTx1HAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MEQ4M0RDOTE4NDQ2MTFFNEE5RTdBRERDQzRBQzNEMTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MEQ4M0RDOTI4NDQ2MTFFNEE5RTdBRERDQzRBQzNEMTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowRDgzREM4Rjg0NDYxMUU0QTlFN0FERENDNEFDM0QxNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowRDgzREM5MDg0NDYxMUU0QTlFN0FERENDNEFDM0QxNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pn2UjdoAAAEGSURBVHjaYvz//z8DGIAYSUlJdwECiBEukpiY/BDEAQggBrgIVBkLjAEDAAHEiMyBywBNOwDmJCYm/cdQBhBAqHrQAUgSojV5P8QtSY+A+D7cPTDdMAUwTQABhNdYJgZ8AF1nRkaGAgjDvQzi/AOCP3+YWX7+/HmXiYlRAcXY37//AEPs511OTg65uXPnPkQxNi0tTTklJUWGaNcCBBj+EMIDmBjIBCwo1jMyYigAul/x79//B4CulwOqODBv3hxHDKcmJycfAHLtgfrvMTExJf/7938xUF4GaOB9FhZmh1mzZj2CqUdNEkAdSUmZSsAgBNrAIAsUAQYlu+O0adMeo0cS/QMHAGJZps83N5ZDAAAAAElFTkSuQmCC'); + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} +.tau-chart__checkbox__text { + margin-left: 5px; +} +.tau-chart__checkbox__input ~ .tau-chart__checkbox__text { + cursor: pointer; +} +.tau-chart__checkbox__input:disabled ~ .tau-chart__checkbox__text { + cursor: default; + opacity: 0.3; +} +.tau-chart__checkbox__input:not(:disabled):focus + .tau-chart__checkbox__icon { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3), 0 0 7px 0 #52a8ec; + outline: none; +} +.tau-chart__checkbox:hover .tau-chart__checkbox__input:not(:disabled) ~ .tau-chart__checkbox__icon { + border-color: #999; +} +.tau-chart__checkbox__input:checked + .tau-chart__checkbox__icon { + background: linear-gradient(to bottom, #fff 0%, #dbdbde 100%); +} +.tau-chart__checkbox__input:checked + .tau-chart__checkbox__icon:before { + display: block; +} +.tau-chart__select { + font-size: 13px; + font-family: inherit; + display: inline-block; + height: 24px; + line-height: 24px; + vertical-align: middle; + padding: 2px; + background-color: #fff; + border: 1px solid #c3c3c3; + border-radius: 2px; + color: #333; +} +.tau-chart__select:focus { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3), 0 0 7px 0 #52a8ec; + outline: none; +} +.tau-chart__select[disabled] { + opacity: 0.3; + cursor: default; +} +.tau-chart__select[multiple] { + height: auto; +} +.tau-chart__select option[disabled] { + opacity: 0.6; +} +.tau-chart__button { + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid currentColor; + border-radius: 4px; + box-shadow: 0 0 1px rgba(0, 0, 0, 0.1); + box-sizing: border-box; + color: #b3b3b3; + cursor: pointer; + font-size: 13px; + padding: 0 6px; + line-height: 1.5em; + height: calc(1.5em + 2px); +} +.tau-chart__button:hover { + border-color: #999999; + color: #333; +} +/* region Generate .color@{n}-@{i} function */ +.tau-chart__svg .color20-1 { + stroke: #6FA1D9; + fill: #6FA1D9; +} +.tau-chart__svg .color20-2 { + stroke: #DF2B59; + fill: #DF2B59; +} +.tau-chart__svg .color20-3 { + stroke: #66DA26; + fill: #66DA26; +} +.tau-chart__svg .color20-4 { + stroke: #4C3862; + fill: #4C3862; +} +.tau-chart__svg .color20-5 { + stroke: #E5B011; + fill: #E5B011; +} +.tau-chart__svg .color20-6 { + stroke: #3A3226; + fill: #3A3226; +} +.tau-chart__svg .color20-7 { + stroke: #CB461A; + fill: #CB461A; +} +.tau-chart__svg .color20-8 { + stroke: #C7CE23; + fill: #C7CE23; +} +.tau-chart__svg .color20-9 { + stroke: #7FCDC2; + fill: #7FCDC2; +} +.tau-chart__svg .color20-10 { + stroke: #CCA1C8; + fill: #CCA1C8; +} +.tau-chart__svg .color20-11 { + stroke: #C84CCE; + fill: #C84CCE; +} +.tau-chart__svg .color20-12 { + stroke: #54762E; + fill: #54762E; +} +.tau-chart__svg .color20-13 { + stroke: #746BC9; + fill: #746BC9; +} +.tau-chart__svg .color20-14 { + stroke: #953441; + fill: #953441; +} +.tau-chart__svg .color20-15 { + stroke: #5C7A76; + fill: #5C7A76; +} +.tau-chart__svg .color20-16 { + stroke: #C8BF87; + fill: #C8BF87; +} +.tau-chart__svg .color20-17 { + stroke: #BFC1C3; + fill: #BFC1C3; +} +.tau-chart__svg .color20-18 { + stroke: #8E5C31; + fill: #8E5C31; +} +.tau-chart__svg .color20-19 { + stroke: #71CE7B; + fill: #71CE7B; +} +.tau-chart__svg .color20-20 { + stroke: #BE478B; + fill: #BE478B; +} +.tau-chart__svg .color-default { + stroke: #6FA1D9; + fill: #6FA1D9; +} +/* endregion */ +/* region Generate .line-params-@{n} function */ +/* Generate .line-size-@{n} */ +.tau-chart__line-width-1 { + stroke-width: 1px; +} +.tau-chart__line-width-2 { + stroke-width: 1.5px; +} +.tau-chart__line-width-3 { + stroke-width: 2px; +} +.tau-chart__line-width-4 { + stroke-width: 2.5px; +} +.tau-chart__line-width-5 { + stroke-width: 3px; +} +/* Generate .line-opacity-@{n} */ +.tau-chart__line-opacity-1 { + stroke-opacity: 1; +} +.tau-chart__line-opacity-2 { + stroke-opacity: 0.95; +} +.tau-chart__line-opacity-3 { + stroke-opacity: 0.9; +} +.tau-chart__line-opacity-4 { + stroke-opacity: 0.85; +} +.tau-chart__line-opacity-5 { + stroke-opacity: 0.8; +} +/* endregion */ +/* endregion */ +.tau-chart { + /* Links */ + /* Axises and Grid */ + /* Scatterplot */ + /* Linechart */ + /* Bar */ + /* TODO: fix to avoid conflict on "stroke" with color brewer */ + /* TODO: remove this when CSS for color brewer is fixed */ + /* PLUGINS */ + /* Highlighter */ +} +.tau-chart a { + color: #3962FF; + border-bottom: 1px solid rgba(57, 98, 255, 0.3); + text-decoration: none; +} +.tau-chart a:hover { + color: #E17152; + border-bottom: 1px solid rgba(225, 113, 82, 0.3); +} +.tau-chart__time-axis-overflow .tick:nth-child(even) { + display: none; +} +.tau-chart__svg { + display: block; + overflow: hidden; +} +.tau-chart__svg .place { + fill: #fff; + stroke: #000; + stroke-opacity: 0.7; + stroke-width: 0.5; +} +.tau-chart__svg .place-label { + opacity: 0.7; + font-size: 11px; + color: #000000; + line-height: 13px; + text-anchor: start; +} +.tau-chart__svg .place-label-countries, +.tau-chart__svg .place-label-subunits, +.tau-chart__svg .place-label-states { + text-anchor: middle; + font-size: 10px; + fill: rgba(51, 51, 51, 0.5); + line-height: 10px; + text-transform: capitalize; +} +.tau-chart__svg .map-contour-level path { + stroke-opacity: 0.5; + stroke-linejoin: 'round'; +} +.tau-chart__svg .map-contour-level-0 path { + stroke: #fff; +} +.tau-chart__svg .map-contour-level-1 path { + stroke: #fff; +} +.tau-chart__svg .map-contour-level-2 path { + stroke: #fff; +} +.tau-chart__svg .map-contour-level-3 path { + stroke: #fff; +} +.tau-chart__svg .map-contour-level-4 path { + stroke: #fff; +} +.tau-chart__svg .map-contour-highlighted, +.tau-chart__svg .map-contour:hover { + fill: #FFBF00; +} +.tau-chart__svg .map-contour-highlighted path, +.tau-chart__svg .map-contour:hover path { + stroke: #fff; +} +.tau-chart__svg .map-contour-highlighted text, +.tau-chart__svg .map-contour:hover text { + fill: #000; +} +.tau-chart__svg .axis line, +.tau-chart__svg .axis path { + stroke-width: 1; + fill: none; + stroke: rgba(189, 195, 205, 0.4); + shape-rendering: crispEdges; +} +.tau-chart__svg .axis.facet-axis .tick line { + opacity: 0; +} +.tau-chart__svg .axis.facet-axis .tick line.label-ref { + opacity: 1; +} +.tau-chart__svg .axis.facet-axis .tick text { + font-size: 12px; + font-weight: 600; +} +.tau-chart__svg .axis.facet-axis path.domain { + opacity: 0; +} +.tau-chart__svg .axis.facet-axis.compact .tick text { + font-weight: normal; +} +.tau-chart__svg .axis.facet-axis.compact .label { + font-weight: normal; +} +.tau-chart__svg .axis.facet-axis.compact .label .label-token { + font-weight: normal; +} +.tau-chart__svg .tick text { + font-size: 11px; +} +.tau-chart__svg .grid .grid-lines path { + shape-rendering: crispEdges; +} +.tau-chart__svg .grid .line path, +.tau-chart__svg .grid path.line, +.tau-chart__svg .grid path.domain { + fill: none; +} +.tau-chart__svg .grid .tick > line, +.tau-chart__svg .grid .extra-tick-line { + fill: none; + stroke: rgba(189, 195, 205, 0.4); + stroke-width: 1px; + shape-rendering: crispEdges; +} +.tau-chart__svg .grid .tick.zero-tick > line { + stroke: rgba(126, 129, 134, 0.505); +} +.tau-chart__svg .grid .line path { + shape-rendering: auto; +} +.tau-chart__svg .grid .cursor-line { + shape-rendering: crispEdges; + stroke: #adadad; + stroke-width: 1px; +} +.tau-chart__svg .label { + font-size: 12px; + font-weight: 600; +} +.tau-chart__svg .label .label-token { + font-size: 12px; + font-weight: 600; + text-transform: capitalize; +} +.tau-chart__svg .label .label-token-1, +.tau-chart__svg .label .label-token-2 { + font-weight: normal; +} +.tau-chart__svg .label .label-token-2 { + fill: gray; +} +.tau-chart__svg .label .label-token-delimiter { + font-weight: normal; + fill: gray; +} +.tau-chart__svg .label.inline .label-token { + font-weight: normal; + fill: gray; + text-transform: none; +} +.tau-chart__svg .brush .selection { + fill-opacity: 0.3; + stroke: #fff; + shape-rendering: crispEdges; +} +.tau-chart__svg .background { + stroke: #f2f2f2; +} +.tau-chart__dot { + opacity: 0.7; + stroke-width: 0; + transition: stroke-width 0.1s ease, opacity 0.2s ease; +} +.tau-chart__line { + fill: none; + transition: stroke-opacity 0.2s ease, stroke-width 0.2s ease; +} +.tau-chart__dot-line { + opacity: 1; + transition: stroke-opacity 0.2s ease; +} +.tau-chart__bar { + opacity: 0.7; + shape-rendering: geometricPrecision; + stroke-opacity: 0.5; + stroke-width: 1; + stroke: #fff; + transition: opacity 0.2s ease; +} +.tau-chart__area { + transition: opacity 0.2s ease; +} +.tau-chart__area path:not(.i-data-anchor), +.tau-chart__area polygon { + opacity: 0.6; + transition: stroke-opacity 0.2s ease, stroke-width 0.2s ease; +} +.tau-chart__svg .tau-chart__bar { + stroke: #fff; +} +.tau-chart__dot.tau-chart__highlighted { + stroke-width: 1; + opacity: 1; +} +.tau-chart__dot.tau-chart__dimmed { + opacity: 0.2; +} +.tau-chart__line.tau-chart__highlighted { + stroke-opacity: 1; + stroke-width: 3; +} +.tau-chart__line.tau-chart__dimmed { + stroke-opacity: 0.2; +} +.i-role-label.tau-chart__highlighted, +.tau-chart__area.tau-chart__highlighted, +.tau-chart__bar.tau-chart__highlighted { + stroke-opacity: 1; + opacity: 1; +} +.i-role-label.tau-chart__dimmed, +.tau-chart__area.tau-chart__dimmed, +.tau-chart__bar.tau-chart__dimmed { + opacity: 0.2; +} +.tau-chart__annotation-line { + stroke-width: 2px; + stroke-dasharray: 1,1; + shape-rendering: crispEdges; +} +.tau-chart__annotation-area.tau-chart__area polygon { + opacity: 0.1; +} +.tau-chart__category-filter { + box-sizing: border-box; + margin-right: 30px; + padding: 20px 0 10px 10px; + width: 160px; +} +.tau-chart__category-filter__category__label { + font-weight: 600; + font-size: 13px; + margin: 0 0 10px 10px; + text-transform: capitalize; +} +.tau-chart__category-filter__category__values { + margin-bottom: 10px; +} +.tau-chart__category-filter__value { + align-items: center; + color: #ccc; + cursor: pointer; + display: flex; + flex-direction: row; + font-size: 13px; + width: 100%; +} +.tau-chart__category-filter__value:hover { + background-color: rgba(189, 195, 205, 0.2); +} +.tau-chart__category-filter__value_checked { + color: #333; +} +.tau-chart__category-filter__value__toggle { + flex: none; + padding: 10px 10px 8px 10px; +} +.tau-chart__category-filter__value__toggle__icon { + background-color: transparent; + border: 1px solid #8694a3; + box-sizing: border-box; + border-radius: 50%; + display: inline-block; + height: 16px; + pointer-events: none; + position: relative; + width: 16px; +} +.tau-chart__category-filter__value__toggle__icon::before, +.tau-chart__category-filter__value__toggle__icon::after { + background-color: #333; + content: ""; + display: block; + opacity: 0; + position: absolute; +} +.tau-chart__category-filter__value__toggle__icon::before { + height: 2px; + left: 3px; + top: 6px; + width: 8px; +} +.tau-chart__category-filter__value__toggle__icon::after { + height: 8px; + left: 6px; + top: 3px; + width: 2px; +} +.tau-chart__category-filter__value__toggle:hover .tau-chart__category-filter__value__toggle__icon::before, +.tau-chart__category-filter__value__toggle:hover .tau-chart__category-filter__value__toggle__icon::after { + opacity: 1; +} +.tau-chart__category-filter__value_checked .tau-chart__category-filter__value__toggle__icon { + background-color: #8694a3; + border-color: transparent; +} +.tau-chart__category-filter__value_checked .tau-chart__category-filter__value__toggle__icon::before, +.tau-chart__category-filter__value_checked .tau-chart__category-filter__value__toggle__icon::after { + background-color: #fff; + transform: rotate(45deg); +} +.tau-chart__category-filter__value__label { + padding-left: 4px; +} +.tau-chart__layout .tau-crosshair__line { + shape-rendering: crispEdges; + stroke-dasharray: 1px 1px; + stroke-width: 1px; +} +.tau-chart__layout .tau-crosshair__label__text { + fill: #fff; + stroke: none; +} +.tau-chart__layout .tau-crosshair__label__text, +.tau-chart__layout .tau-crosshair__label__text-shadow { + font-size: 12px; + font-weight: normal; +} +.tau-chart__layout .tau-crosshair__line-shadow { + shape-rendering: crispEdges; + stroke: #cccccc; + stroke-width: 1px; +} +.tau-chart__layout .tau-crosshair__group.y .tau-crosshair__line-shadow { + transform: translateX(-0.5px); +} +.tau-chart__layout .tau-crosshair__group.x .tau-crosshair__line-shadow { + transform: translateY(0.5px); +} +.tau-chart__layout .tau-crosshair__label__text-shadow { + stroke-linejoin: round; + stroke-width: 3px; + visibility: hidden; +} +.tau-chart__layout .tau-crosshair__label__box { + fill-opacity: 0.85; + rx: 3px; + ry: 3px; + stroke: none; +} +.tau-chart__layout .tau-crosshair__line.color20-1 { + stroke: #6FA1D9; +} +.tau-chart__layout .tau-crosshair__label.color20-1 .tau-crosshair__label__text-shadow { + stroke: #6FA1D9; +} +.tau-chart__layout .tau-crosshair__label.color20-1 .tau-crosshair__label__box { + fill: #6FA1D9; +} +.tau-chart__layout .tau-crosshair__line.color20-2 { + stroke: #DF2B59; +} +.tau-chart__layout .tau-crosshair__label.color20-2 .tau-crosshair__label__text-shadow { + stroke: #DF2B59; +} +.tau-chart__layout .tau-crosshair__label.color20-2 .tau-crosshair__label__box { + fill: #DF2B59; +} +.tau-chart__layout .tau-crosshair__line.color20-3 { + stroke: #66DA26; +} +.tau-chart__layout .tau-crosshair__label.color20-3 .tau-crosshair__label__text-shadow { + stroke: #66DA26; +} +.tau-chart__layout .tau-crosshair__label.color20-3 .tau-crosshair__label__box { + fill: #66DA26; +} +.tau-chart__layout .tau-crosshair__line.color20-4 { + stroke: #4C3862; +} +.tau-chart__layout .tau-crosshair__label.color20-4 .tau-crosshair__label__text-shadow { + stroke: #4C3862; +} +.tau-chart__layout .tau-crosshair__label.color20-4 .tau-crosshair__label__box { + fill: #4C3862; +} +.tau-chart__layout .tau-crosshair__line.color20-5 { + stroke: #E5B011; +} +.tau-chart__layout .tau-crosshair__label.color20-5 .tau-crosshair__label__text-shadow { + stroke: #E5B011; +} +.tau-chart__layout .tau-crosshair__label.color20-5 .tau-crosshair__label__box { + fill: #E5B011; +} +.tau-chart__layout .tau-crosshair__line.color20-6 { + stroke: #3A3226; +} +.tau-chart__layout .tau-crosshair__label.color20-6 .tau-crosshair__label__text-shadow { + stroke: #3A3226; +} +.tau-chart__layout .tau-crosshair__label.color20-6 .tau-crosshair__label__box { + fill: #3A3226; +} +.tau-chart__layout .tau-crosshair__line.color20-7 { + stroke: #CB461A; +} +.tau-chart__layout .tau-crosshair__label.color20-7 .tau-crosshair__label__text-shadow { + stroke: #CB461A; +} +.tau-chart__layout .tau-crosshair__label.color20-7 .tau-crosshair__label__box { + fill: #CB461A; +} +.tau-chart__layout .tau-crosshair__line.color20-8 { + stroke: #C7CE23; +} +.tau-chart__layout .tau-crosshair__label.color20-8 .tau-crosshair__label__text-shadow { + stroke: #C7CE23; +} +.tau-chart__layout .tau-crosshair__label.color20-8 .tau-crosshair__label__box { + fill: #C7CE23; +} +.tau-chart__layout .tau-crosshair__line.color20-9 { + stroke: #7FCDC2; +} +.tau-chart__layout .tau-crosshair__label.color20-9 .tau-crosshair__label__text-shadow { + stroke: #7FCDC2; +} +.tau-chart__layout .tau-crosshair__label.color20-9 .tau-crosshair__label__box { + fill: #7FCDC2; +} +.tau-chart__layout .tau-crosshair__line.color20-10 { + stroke: #CCA1C8; +} +.tau-chart__layout .tau-crosshair__label.color20-10 .tau-crosshair__label__text-shadow { + stroke: #CCA1C8; +} +.tau-chart__layout .tau-crosshair__label.color20-10 .tau-crosshair__label__box { + fill: #CCA1C8; +} +.tau-chart__layout .tau-crosshair__line.color20-11 { + stroke: #C84CCE; +} +.tau-chart__layout .tau-crosshair__label.color20-11 .tau-crosshair__label__text-shadow { + stroke: #C84CCE; +} +.tau-chart__layout .tau-crosshair__label.color20-11 .tau-crosshair__label__box { + fill: #C84CCE; +} +.tau-chart__layout .tau-crosshair__line.color20-12 { + stroke: #54762E; +} +.tau-chart__layout .tau-crosshair__label.color20-12 .tau-crosshair__label__text-shadow { + stroke: #54762E; +} +.tau-chart__layout .tau-crosshair__label.color20-12 .tau-crosshair__label__box { + fill: #54762E; +} +.tau-chart__layout .tau-crosshair__line.color20-13 { + stroke: #746BC9; +} +.tau-chart__layout .tau-crosshair__label.color20-13 .tau-crosshair__label__text-shadow { + stroke: #746BC9; +} +.tau-chart__layout .tau-crosshair__label.color20-13 .tau-crosshair__label__box { + fill: #746BC9; +} +.tau-chart__layout .tau-crosshair__line.color20-14 { + stroke: #953441; +} +.tau-chart__layout .tau-crosshair__label.color20-14 .tau-crosshair__label__text-shadow { + stroke: #953441; +} +.tau-chart__layout .tau-crosshair__label.color20-14 .tau-crosshair__label__box { + fill: #953441; +} +.tau-chart__layout .tau-crosshair__line.color20-15 { + stroke: #5C7A76; +} +.tau-chart__layout .tau-crosshair__label.color20-15 .tau-crosshair__label__text-shadow { + stroke: #5C7A76; +} +.tau-chart__layout .tau-crosshair__label.color20-15 .tau-crosshair__label__box { + fill: #5C7A76; +} +.tau-chart__layout .tau-crosshair__line.color20-16 { + stroke: #C8BF87; +} +.tau-chart__layout .tau-crosshair__label.color20-16 .tau-crosshair__label__text-shadow { + stroke: #C8BF87; +} +.tau-chart__layout .tau-crosshair__label.color20-16 .tau-crosshair__label__box { + fill: #C8BF87; +} +.tau-chart__layout .tau-crosshair__line.color20-17 { + stroke: #BFC1C3; +} +.tau-chart__layout .tau-crosshair__label.color20-17 .tau-crosshair__label__text-shadow { + stroke: #BFC1C3; +} +.tau-chart__layout .tau-crosshair__label.color20-17 .tau-crosshair__label__box { + fill: #BFC1C3; +} +.tau-chart__layout .tau-crosshair__line.color20-18 { + stroke: #8E5C31; +} +.tau-chart__layout .tau-crosshair__label.color20-18 .tau-crosshair__label__text-shadow { + stroke: #8E5C31; +} +.tau-chart__layout .tau-crosshair__label.color20-18 .tau-crosshair__label__box { + fill: #8E5C31; +} +.tau-chart__layout .tau-crosshair__line.color20-19 { + stroke: #71CE7B; +} +.tau-chart__layout .tau-crosshair__label.color20-19 .tau-crosshair__label__text-shadow { + stroke: #71CE7B; +} +.tau-chart__layout .tau-crosshair__label.color20-19 .tau-crosshair__label__box { + fill: #71CE7B; +} +.tau-chart__layout .tau-crosshair__line.color20-20 { + stroke: #BE478B; +} +.tau-chart__layout .tau-crosshair__label.color20-20 .tau-crosshair__label__text-shadow { + stroke: #BE478B; +} +.tau-chart__layout .tau-crosshair__label.color20-20 .tau-crosshair__label__box { + fill: #BE478B; +} +.diff-tooltip__table { + border-top: 1px solid rgba(51, 51, 51, 0.2); + margin-top: 5px; + padding-top: 5px; + width: calc(100% + 15px); +} +.diff-tooltip__header { + align-items: stretch; + display: flex; + font-weight: 600; + justify-content: space-between; + padding: 2px 0px; + position: relative; +} +.diff-tooltip__header__text { + align-items: center; + display: inline-flex; + flex: 1 1 auto; + justify-content: flex-start; + max-width: 120px; +} +.diff-tooltip__header__value { + align-items: center; + display: inline-flex; + flex: 1 1 auto; + justify-content: flex-end; + margin-right: 15px; + max-width: 120px; + padding-left: 10px; + text-align: right; +} +.diff-tooltip__header__updown { + align-items: center; + display: inline-flex; + flex: 1 1 auto; + font-size: 75%; + height: 100%; + justify-content: flex-start; + padding-left: 2px; + position: absolute; + right: 0; + visibility: hidden; +} +.diff-tooltip__body { + max-height: 250px; + overflow: hidden; + padding: 1px; + position: relative; +} +.diff-tooltip__body__content { + padding-bottom: 1px; +} +.diff-tooltip__body_overflow-top::before, +.diff-tooltip__body_overflow-bottom::after { + align-items: center; + color: rgba(51, 51, 51, 0.7); + content: "..."; + display: flex; + flex-direction: column; + height: 26px; + left: 0; + position: absolute; + width: 100%; + z-index: 2; +} +.diff-tooltip__body_overflow-top::before { + background: linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0)); + justify-content: flex-start; + top: 0; +} +.diff-tooltip__body_overflow-bottom::after { + background: linear-gradient(to top, #fff, rgba(255, 255, 255, 0)); + justify-content: flex-end; + bottom: 0; +} +.diff-tooltip__item { + display: flex; + justify-content: space-between; + margin-right: 15px; + min-width: 100px; + position: relative; +} +.diff-tooltip__item_highlighted { + background-color: rgba(241, 233, 255, 0.5); + box-shadow: 0 0 0 1px #877aa1; + z-index: 1; +} +.diff-tooltip__item__bg { + align-items: center; + display: inline-flex; + height: 100%; + justify-content: center; + min-width: 3px; + opacity: 0.6; + position: absolute; + z-index: 0; +} +.diff-tooltip__item__text { + flex: 1 1 auto; + overflow: hidden; + padding: 2px 4px; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + z-index: 1; +} +.diff-tooltip__item__value { + flex: none; + display: table-cell; + padding: 2px 4px 2px 30px; + z-index: 1; +} +.diff-tooltip__item__updown { + align-items: center; + display: inline-flex; + flex: 4; + justify-content: flex-start; + left: 100%; + height: 100%; + padding: 0 4px 0 4px; + position: absolute; +} +.diff-tooltip__item__updown_positive { + color: #4ca383; +} +.diff-tooltip__item__updown_negative { + color: #df6772; +} +.diff-tooltip__field__updown_positive { + color: #4ca383; +} +.diff-tooltip__field__updown_negative { + color: #df6772; +} +.interval-highlight__range { + shape-rendering: crispEdges; +} +.interval-highlight__range-start { + shape-rendering: crispEdges; + stroke: #b8aecb; + stroke-dasharray: 2 1; +} +.interval-highlight__range-end { + shape-rendering: crispEdges; + stroke: #b8aecb; +} +.interval-highlight__gradient-start { + stop-color: #c4b3e6; + stop-opacity: 0.02; +} +.interval-highlight__gradient-end { + stop-color: #c4b3e6; + stop-opacity: 0.2; +} +.diff-tooltip__item__bg.color20-1 { + background-color: #6FA1D9; +} +.diff-tooltip__item__bg.color20-2 { + background-color: #DF2B59; +} +.diff-tooltip__item__bg.color20-3 { + background-color: #66DA26; +} +.diff-tooltip__item__bg.color20-4 { + background-color: #4C3862; +} +.diff-tooltip__item__bg.color20-5 { + background-color: #E5B011; +} +.diff-tooltip__item__bg.color20-6 { + background-color: #3A3226; +} +.diff-tooltip__item__bg.color20-7 { + background-color: #CB461A; +} +.diff-tooltip__item__bg.color20-8 { + background-color: #C7CE23; +} +.diff-tooltip__item__bg.color20-9 { + background-color: #7FCDC2; +} +.diff-tooltip__item__bg.color20-10 { + background-color: #CCA1C8; +} +.diff-tooltip__item__bg.color20-11 { + background-color: #C84CCE; +} +.diff-tooltip__item__bg.color20-12 { + background-color: #54762E; +} +.diff-tooltip__item__bg.color20-13 { + background-color: #746BC9; +} +.diff-tooltip__item__bg.color20-14 { + background-color: #953441; +} +.diff-tooltip__item__bg.color20-15 { + background-color: #5C7A76; +} +.diff-tooltip__item__bg.color20-16 { + background-color: #C8BF87; +} +.diff-tooltip__item__bg.color20-17 { + background-color: #BFC1C3; +} +.diff-tooltip__item__bg.color20-18 { + background-color: #8E5C31; +} +.diff-tooltip__item__bg.color20-19 { + background-color: #71CE7B; +} +.diff-tooltip__item__bg.color20-20 { + background-color: #BE478B; +} +.tau-chart__print-block { + display: none; +} +.tau-chart__export { + float: right; + margin: 0 20px 0 0; + display: block; + text-indent: 20px; + overflow: hidden; + background-repeat: no-repeat; + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+ZXhwb3J0PC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiMwMDAiPjxwYXRoIGQ9Ik0xNyAxLjY3bC04LjMyOCA4LjM2Nkw4IDkuNSAxNi4zNTMgMUgxMlYwaDZ2NmgtMVYxLjY3eiIgb3BhY2l0eT0iLjgiLz48cGF0aCBkPSJNMCA1LjAxQzAgMy4zNDYgMS4zMzcgMiAzLjAxIDJIMTZ2MTIuOTljMCAxLjY2My0xLjMzNyAzLjAxLTMuMDEgMy4wMUgzLjAxQzEuMzQ2IDE4IDAgMTYuNjYzIDAgMTQuOTlWNS4wMXpNMTUgMTVDMTUgMTYuMTA1IDE0LjEwMyAxNyAxMi45OTQgMTdIMy4wMDZDMS44OTggMTcgMSAxNi4xMDMgMSAxNC45OTRWNS4wMDZDMSAzLjg5OCAxLjg4NyAzIDIuOTk4IDNIOVYyaDd2N2gtMXY2LjAwMnoiIG9wYWNpdHk9Ii40Ii8+PC9nPjwvZz48L3N2Zz4=); + width: 20px; + height: 20px; + color: transparent; + opacity: 0.6; + cursor: pointer; + text-decoration: none; + position: relative; + z-index: 2; +} +.tau-chart__export:hover { + opacity: 1; + text-decoration: none; +} +.tau-chart__export__list { + font-size: 11px; + margin: 0; + padding: 0; +} +.tau-chart__export__item { + overflow: hidden; + box-sizing: border-box; +} +.tau-chart__export__item > a { + display: block; + padding: 7px 15px; + color: inherit; + text-decoration: none; + cursor: pointer; +} +.tau-chart__export__item > a:hover, +.tau-chart__export__item > a:focus { + background: #EAF2FC; + outline: none; + box-shadow: none; +} +.tau-chart__legend { + padding: 20px 0 10px 10px; + position: relative; + margin-right: 30px; + width: 160px; + box-sizing: border-box; +} +.tau-chart__legend__wrap { + margin-bottom: 30px; + position: relative; +} +.tau-chart__legend__wrap:last-child { + margin-bottom: 0; +} +.tau-chart__legend__title { + margin: 0 0 10px 10px; + text-transform: capitalize; + font-weight: 600; + font-size: 13px; +} +.tau-chart__legend__reset { + margin-top: -4px; + position: absolute; + right: -25px; + top: 0; + z-index: 1; +} +.tau-chart__legend__reset.disabled { + display: none; +} +.tau-chart__legend__reset + .tau-chart__legend__title { + margin-right: 1.7em; +} +.tau-chart__legend__item { + padding: 10px 20px 8px 40px; + position: relative; + font-size: 13px; + line-height: 1.2em; + cursor: pointer; +} +.tau-chart__legend__item:hover { + background-color: rgba(189, 195, 205, 0.2); +} +.tau-chart__legend__item--size { + cursor: default; +} +.tau-chart__legend__item--size:hover { + background: none; +} +.tau-chart__legend__item .color-default { + background: #6FA1D9; + border-color: #6FA1D9; +} +.tau-chart__legend__item:disabled, +.tau-chart__legend__item.disabled { + color: #ccc; +} +.tau-chart__legend__item.disabled .tau-chart__legend__guide { + background: transparent; +} +.tau-chart__legend__guide { + position: absolute; + box-sizing: border-box; + width: 100%; + height: 100%; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 1px solid transparent; + border-radius: 50%; +} +.tau-chart__legend__guide__wrap { + position: absolute; + top: calc((10px - 8px) + 0.6em); + left: 10px; + width: 16px; + height: 16px; +} +.tau-chart__legend__guide--size { + stroke: #6FA1D9; + fill: #6FA1D9; +} +.tau-chart__legend__guide--color__overlay { + background-color: transparent; + height: 36px; + left: -12px; + position: absolute; + top: -12px; + width: 36px; +} +.tau-chart__legend__guide--color::before { + content: ""; + display: none; + height: 2px; + left: 3px; + pointer-events: none; + position: absolute; + top: 6px; + width: 8px; +} +.tau-chart__legend__guide--color::after { + content: ""; + display: none; + height: 8px; + left: 6px; + pointer-events: none; + position: absolute; + top: 3px; + width: 2px; +} +.tau-chart__legend__item .tau-chart__legend__guide--color:hover::before, +.tau-chart__legend__item .tau-chart__legend__guide--color:hover::after { + background-color: #fff; + display: inline-block; + transform: rotate(45deg); +} +.tau-chart__legend__item.disabled .tau-chart__legend__guide--color:hover { + background: #fff; +} +.tau-chart__legend__item.disabled .tau-chart__legend__guide--color:hover::before, +.tau-chart__legend__item.disabled .tau-chart__legend__guide--color:hover::after { + background-color: #333; + transform: none; +} +.tau-chart__legend__size-wrapper, +.tau-chart__legend__gradient-wrapper { + box-sizing: border-box; + margin: 10px; + overflow: visible; + width: 100%; +} +.tau-chart__legend__size, +.tau-chart__legend__gradient { + overflow: visible; +} +.tau-chart__legend__size__item__circle.color-definite { + stroke: #cacaca; + fill: #cacaca; +} +.tau-chart__legend__size__item__circle.color-default-size { + stroke: #6FA1D9; + fill: #6FA1D9; +} +.tau-chart__legend__gradient__bar { + rx: 4px; + ry: 4px; +} +.tau-chart__legend__item .color20-1 { + background: #6FA1D9; + border: 1px solid #6FA1D9; +} +.tau-chart__legend__item.disabled .color20-1 { + background-color: transparent; +} +.tau-chart__legend__item .color20-2 { + background: #DF2B59; + border: 1px solid #DF2B59; +} +.tau-chart__legend__item.disabled .color20-2 { + background-color: transparent; +} +.tau-chart__legend__item .color20-3 { + background: #66DA26; + border: 1px solid #66DA26; +} +.tau-chart__legend__item.disabled .color20-3 { + background-color: transparent; +} +.tau-chart__legend__item .color20-4 { + background: #4C3862; + border: 1px solid #4C3862; +} +.tau-chart__legend__item.disabled .color20-4 { + background-color: transparent; +} +.tau-chart__legend__item .color20-5 { + background: #E5B011; + border: 1px solid #E5B011; +} +.tau-chart__legend__item.disabled .color20-5 { + background-color: transparent; +} +.tau-chart__legend__item .color20-6 { + background: #3A3226; + border: 1px solid #3A3226; +} +.tau-chart__legend__item.disabled .color20-6 { + background-color: transparent; +} +.tau-chart__legend__item .color20-7 { + background: #CB461A; + border: 1px solid #CB461A; +} +.tau-chart__legend__item.disabled .color20-7 { + background-color: transparent; +} +.tau-chart__legend__item .color20-8 { + background: #C7CE23; + border: 1px solid #C7CE23; +} +.tau-chart__legend__item.disabled .color20-8 { + background-color: transparent; +} +.tau-chart__legend__item .color20-9 { + background: #7FCDC2; + border: 1px solid #7FCDC2; +} +.tau-chart__legend__item.disabled .color20-9 { + background-color: transparent; +} +.tau-chart__legend__item .color20-10 { + background: #CCA1C8; + border: 1px solid #CCA1C8; +} +.tau-chart__legend__item.disabled .color20-10 { + background-color: transparent; +} +.tau-chart__legend__item .color20-11 { + background: #C84CCE; + border: 1px solid #C84CCE; +} +.tau-chart__legend__item.disabled .color20-11 { + background-color: transparent; +} +.tau-chart__legend__item .color20-12 { + background: #54762E; + border: 1px solid #54762E; +} +.tau-chart__legend__item.disabled .color20-12 { + background-color: transparent; +} +.tau-chart__legend__item .color20-13 { + background: #746BC9; + border: 1px solid #746BC9; +} +.tau-chart__legend__item.disabled .color20-13 { + background-color: transparent; +} +.tau-chart__legend__item .color20-14 { + background: #953441; + border: 1px solid #953441; +} +.tau-chart__legend__item.disabled .color20-14 { + background-color: transparent; +} +.tau-chart__legend__item .color20-15 { + background: #5C7A76; + border: 1px solid #5C7A76; +} +.tau-chart__legend__item.disabled .color20-15 { + background-color: transparent; +} +.tau-chart__legend__item .color20-16 { + background: #C8BF87; + border: 1px solid #C8BF87; +} +.tau-chart__legend__item.disabled .color20-16 { + background-color: transparent; +} +.tau-chart__legend__item .color20-17 { + background: #BFC1C3; + border: 1px solid #BFC1C3; +} +.tau-chart__legend__item.disabled .color20-17 { + background-color: transparent; +} +.tau-chart__legend__item .color20-18 { + background: #8E5C31; + border: 1px solid #8E5C31; +} +.tau-chart__legend__item.disabled .color20-18 { + background-color: transparent; +} +.tau-chart__legend__item .color20-19 { + background: #71CE7B; + border: 1px solid #71CE7B; +} +.tau-chart__legend__item.disabled .color20-19 { + background-color: transparent; +} +.tau-chart__legend__item .color20-20 { + background: #BE478B; + border: 1px solid #BE478B; +} +.tau-chart__legend__item.disabled .color20-20 { + background-color: transparent; +} +.tau-chart__filter__wrap { + padding: 20px 0 10px 10px; + margin-right: 30px; + width: 160px; + box-sizing: border-box; +} +.tau-chart__filter__wrap__title { + margin: 0 0 10px 10px; + text-transform: capitalize; + font-weight: 600; + font-size: 13px; +} +.tau-chart__filter__wrap rect { + fill: rgba(0, 0, 0, 0.2); +} +.tau-chart__filter__wrap .brush .overlay, +.tau-chart__filter__wrap .brush .handle { + opacity: 0; +} +.tau-chart__filter__wrap .brush .selection { + shape-rendering: crispEdges; + fill-opacity: 0.4; + fill: #0074FF; +} +.tau-chart__filter__wrap text.date-label { + text-anchor: middle; + font-size: 12px; +} +.tau-chart__filter__wrap text.date-label .common { + font-weight: 600; +} +.tau-chart__filter__wrap .resize line { + stroke: #000; + stroke-width: 1px; + shape-rendering: crispEdges; +} +.tau-chart__filter__wrap .resize.e text { + text-anchor: middle; + font-size: 12px; +} +.tau-chart__filter__wrap .resize.w text { + text-anchor: middle; + font-size: 12px; +} +.tau-chart__tooltip { + background: rgba(255, 255, 255, 0.9); + position: absolute; + top: 0; + left: 0; + max-width: none; + z-index: 900; + align-items: stretch; + display: flex; + flex-direction: column; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.005); + font-size: 11px; + font-family: Helvetica Neue, Segoe UI, Open Sans, Ubuntu, sans-serif; + /* Fade */ +} +.tau-chart__tooltip.fade { + opacity: 0; + transition: opacity 200ms ease-out; +} +.tau-chart__tooltip.fade.in { + opacity: 1; + transition-duration: 500ms; +} +.tau-chart__tooltip.top-right, +.tau-chart__tooltip.bottom-right { + margin-left: 8px; +} +.tau-chart__tooltip.top-left, +.tau-chart__tooltip.bottom-left { + margin-left: -8px; +} +.tau-chart__tooltip.top, +.tau-chart__tooltip.top-right, +.tau-chart__tooltip.top-left { + margin-top: 8px; +} +.tau-chart__tooltip__content { + box-sizing: border-box; + max-width: 500px; + min-width: 100px; + overflow: hidden; + padding: 15px 15px 10px 15px; +} +.tau-chart__tooltip__buttons { + background: #EBEEF1; + bottom: 100%; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.005); + display: flex; + flex-direction: row; + flex-wrap: wrap; + max-width: 500px; + min-width: 86px; + position: absolute; + width: 100%; + z-index: -1; +} +.tau-chart__tooltip__buttons::after { + background: linear-gradient(to bottom, #fff 50%, rgba(255, 255, 255, 0)); + content: ""; + display: block; + height: 8px; + left: 0; + pointer-events: none; + position: absolute; + top: 100%; + width: 100%; +} +.tau-chart__tooltip__button { + color: #65717F; + cursor: pointer; + display: inline-flex; + flex: 1 0 auto; + height: 0; + overflow: hidden; + transition: height 500ms; +} +.tau-chart__tooltip__button__wrap { + line-height: 26px; + padding: 0 15px; +} +.tau-chart__tooltip__button:hover { + background: #f5f7f8; + color: #333; +} +.tau-chart__tooltip__button .tau-icon-close-gray { + background-image: url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIzMHB4IiBoZWlnaHQ9IjMwcHgiIHZpZXdCb3g9IjAgMCAzMCAzMCI+PHBhdGggZmlsbD0iIzg0OTZBNyIgZD0iTTEwLDAuNzE1TDkuMjg1LDBMNSw0LjI4NUwwLjcxNSwwTDAsMC43MTVMNC4yODUsNUwwLDkuMjg1TDAuNzE1LDEwTDUsNS43MTVMOS4yODUsMTBMMTAsOS4yODVMNS43MTUsNUwxMCwwLjcxNXoiLz48L3N2Zz4=); + display: inline-block; + width: 12px; + height: 12px; + position: relative; + top: 3px; + margin-right: 5px; +} +.tau-chart__tooltip.stuck .tau-chart__tooltip__button { + height: 26px; +} +.tau-chart__tooltip.top .tau-chart__tooltip__buttons, +.tau-chart__tooltip.top-right .tau-chart__tooltip__buttons, +.tau-chart__tooltip.top-left .tau-chart__tooltip__buttons { + bottom: initial; + top: 100%; +} +.tau-chart__tooltip.top .tau-chart__tooltip__buttons__wrap, +.tau-chart__tooltip.top-right .tau-chart__tooltip__buttons__wrap, +.tau-chart__tooltip.top-left .tau-chart__tooltip__buttons__wrap { + position: relative; + top: calc(100% - 26px); +} +.tau-chart__tooltip.top .tau-chart__tooltip__buttons::after, +.tau-chart__tooltip.top-right .tau-chart__tooltip__buttons::after, +.tau-chart__tooltip.top-left .tau-chart__tooltip__buttons::after { + background: linear-gradient(to top, #fff 50%, rgba(255, 255, 255, 0)); + bottom: 100%; + top: initial; +} +.tau-chart__tooltip.top-right .tau-chart__tooltip__button__wrap, +.tau-chart__tooltip.top-left .tau-chart__tooltip__button__wrap { + position: relative; + top: calc(100% - 26px); +} +.tau-chart__tooltip__list { + display: table; +} +.tau-chart__tooltip__list__item { + display: table-row; +} +.tau-chart__tooltip__list__elem { + display: table-cell; + padding-bottom: 4px; + line-height: 1.3; + color: #000; +} +.tau-chart__tooltip__list__elem:not(:first-child) { + padding-left: 15px; +} +.tau-chart__tooltip__list__elem:first-child { + color: #8e8e8e; +} +.tau-chart__tooltip__gray-text { + color: #8e8e8e; +} +.tau-chart__tooltip-target { + cursor: pointer; +} +.tau-chart__tooltip-target .tau-chart__dot.tau-chart__highlighted, +.tau-chart__tooltip-target .tau-chart__bar.tau-chart__highlighted, +.tau-chart__tooltip-target .i-data-anchor.tau-chart__highlighted { + stroke: #333; + stroke-width: 1; +} +.tau-chart__tooltip-target .tau-chart__bar.tau-chart__highlighted { + shape-rendering: crispEdges; +} +.tau-chart__svg .tau-chart__trendline.color20-1 { + stroke: #357ac7; +} +.tau-chart__svg .tau-chart__trendline.color20-2 { + stroke: #a5193d; +} +.tau-chart__svg .tau-chart__trendline.color20-3 { + stroke: #47991a; +} +.tau-chart__svg .tau-chart__trendline.color20-4 { + stroke: #261c31; +} +.tau-chart__svg .tau-chart__trendline.color20-5 { + stroke: #9e790c; +} +.tau-chart__svg .tau-chart__trendline.color20-6 { + stroke: #0c0a08; +} +.tau-chart__svg .tau-chart__trendline.color20-7 { + stroke: #872f11; +} +.tau-chart__svg .tau-chart__trendline.color20-8 { + stroke: #888d18; +} +.tau-chart__svg .tau-chart__trendline.color20-9 { + stroke: #48b8a8; +} +.tau-chart__svg .tau-chart__trendline.color20-10 { + stroke: #b16fab; +} +.tau-chart__svg .tau-chart__trendline.color20-11 { + stroke: #9c2ca1; +} +.tau-chart__svg .tau-chart__trendline.color20-12 { + stroke: #2d3f19; +} +.tau-chart__svg .tau-chart__trendline.color20-13 { + stroke: #483eaa; +} +.tau-chart__svg .tau-chart__trendline.color20-14 { + stroke: #5c2028; +} +.tau-chart__svg .tau-chart__trendline.color20-15 { + stroke: #3b4e4c; +} +.tau-chart__svg .tau-chart__trendline.color20-16 { + stroke: #b0a353; +} +.tau-chart__svg .tau-chart__trendline.color20-17 { + stroke: #989b9e; +} +.tau-chart__svg .tau-chart__trendline.color20-18 { + stroke: #55371d; +} +.tau-chart__svg .tau-chart__trendline.color20-19 { + stroke: #3eb44b; +} +.tau-chart__svg .tau-chart__trendline.color20-20 { + stroke: #883063; +} +.tau-chart__svg .tau-chart__trendline.color-default { + stroke: #357ac7; +} +.tau-chart { + /* TrendLine */ +} +.tau-chart__trendlinepanel { + padding: 20px 0 20px 20px; + margin-right: 20px; + width: 160px; + box-sizing: border-box; +} +.tau-chart__trendlinepanel__title { + margin: 0 0 10px 0; + text-transform: capitalize; + font-weight: 600; + font-size: 13px; +} +.tau-chart__trendlinepanel__control { + width: 100%; +} +.tau-chart__trendlinepanel__error-message { + font-size: 11px; + line-height: 16px; + margin-left: 5px; +} +.tau-chart__trendlinepanel.applicable-false.hide-trendline-error, +.tau-chart__trendlinepanel.applicable-false .tau-chart__checkbox__input, +.tau-chart__trendlinepanel.applicable-false .tau-chart__trendlinepanel__control, +.tau-chart__trendlinepanel.applicable-false .tau-chart__checkbox__icon { + display: none; +} +.tau-chart__trendline { + stroke-dasharray: 4, 4; +} +/* This product includes color specifications and designs developed by Cynthia Brewer (http://colorbrewer.org/). */ +/* + generate from addons color-brewer.js + copy(_.flatten(_.map(res, function(value, hue){ + return _.map(value, function(value, number) { + return _.map(value,function(value,index) { + return ['.', hue, '.', 'q', index, '-', number, '{fill:', value, ';stroke:', value, ';}'].join(''); + }) + }) +})).join('')) +*/ +.YlGn.q0-3 { + fill: #f7fcb9; + background: #f7fcb9; + stroke: #f7fcb9; +} +.YlGn.q1-3 { + fill: #addd8e; + background: #addd8e; + stroke: #addd8e; +} +.YlGn.q2-3 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.YlGn.q0-4 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGn.q1-4 { + fill: #c2e699; + background: #c2e699; + stroke: #c2e699; +} +.YlGn.q2-4 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q3-4 { + fill: #238443; + background: #238443; + stroke: #238443; +} +.YlGn.q0-5 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGn.q1-5 { + fill: #c2e699; + background: #c2e699; + stroke: #c2e699; +} +.YlGn.q2-5 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q3-5 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.YlGn.q4-5 { + fill: #006837; + background: #006837; + stroke: #006837; +} +.YlGn.q0-6 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGn.q1-6 { + fill: #d9f0a3; + background: #d9f0a3; + stroke: #d9f0a3; +} +.YlGn.q2-6 { + fill: #addd8e; + background: #addd8e; + stroke: #addd8e; +} +.YlGn.q3-6 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q4-6 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.YlGn.q5-6 { + fill: #006837; + background: #006837; + stroke: #006837; +} +.YlGn.q0-7 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGn.q1-7 { + fill: #d9f0a3; + background: #d9f0a3; + stroke: #d9f0a3; +} +.YlGn.q2-7 { + fill: #addd8e; + background: #addd8e; + stroke: #addd8e; +} +.YlGn.q3-7 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q4-7 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.YlGn.q5-7 { + fill: #238443; + background: #238443; + stroke: #238443; +} +.YlGn.q6-7 { + fill: #005a32; + background: #005a32; + stroke: #005a32; +} +.YlGn.q0-8 { + fill: #ffffe5; + background: #ffffe5; + stroke: #ffffe5; +} +.YlGn.q1-8 { + fill: #f7fcb9; + background: #f7fcb9; + stroke: #f7fcb9; +} +.YlGn.q2-8 { + fill: #d9f0a3; + background: #d9f0a3; + stroke: #d9f0a3; +} +.YlGn.q3-8 { + fill: #addd8e; + background: #addd8e; + stroke: #addd8e; +} +.YlGn.q4-8 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q5-8 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.YlGn.q6-8 { + fill: #238443; + background: #238443; + stroke: #238443; +} +.YlGn.q7-8 { + fill: #005a32; + background: #005a32; + stroke: #005a32; +} +.YlGn.q0-9 { + fill: #ffffe5; + background: #ffffe5; + stroke: #ffffe5; +} +.YlGn.q1-9 { + fill: #f7fcb9; + background: #f7fcb9; + stroke: #f7fcb9; +} +.YlGn.q2-9 { + fill: #d9f0a3; + background: #d9f0a3; + stroke: #d9f0a3; +} +.YlGn.q3-9 { + fill: #addd8e; + background: #addd8e; + stroke: #addd8e; +} +.YlGn.q4-9 { + fill: #78c679; + background: #78c679; + stroke: #78c679; +} +.YlGn.q5-9 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.YlGn.q6-9 { + fill: #238443; + background: #238443; + stroke: #238443; +} +.YlGn.q7-9 { + fill: #006837; + background: #006837; + stroke: #006837; +} +.YlGn.q8-9 { + fill: #004529; + background: #004529; + stroke: #004529; +} +.YlGnBu.q0-3 { + fill: #edf8b1; + background: #edf8b1; + stroke: #edf8b1; +} +.YlGnBu.q1-3 { + fill: #7fcdbb; + background: #7fcdbb; + stroke: #7fcdbb; +} +.YlGnBu.q2-3 { + fill: #2c7fb8; + background: #2c7fb8; + stroke: #2c7fb8; +} +.YlGnBu.q0-4 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGnBu.q1-4 { + fill: #a1dab4; + background: #a1dab4; + stroke: #a1dab4; +} +.YlGnBu.q2-4 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q3-4 { + fill: #225ea8; + background: #225ea8; + stroke: #225ea8; +} +.YlGnBu.q0-5 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGnBu.q1-5 { + fill: #a1dab4; + background: #a1dab4; + stroke: #a1dab4; +} +.YlGnBu.q2-5 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q3-5 { + fill: #2c7fb8; + background: #2c7fb8; + stroke: #2c7fb8; +} +.YlGnBu.q4-5 { + fill: #253494; + background: #253494; + stroke: #253494; +} +.YlGnBu.q0-6 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGnBu.q1-6 { + fill: #c7e9b4; + background: #c7e9b4; + stroke: #c7e9b4; +} +.YlGnBu.q2-6 { + fill: #7fcdbb; + background: #7fcdbb; + stroke: #7fcdbb; +} +.YlGnBu.q3-6 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q4-6 { + fill: #2c7fb8; + background: #2c7fb8; + stroke: #2c7fb8; +} +.YlGnBu.q5-6 { + fill: #253494; + background: #253494; + stroke: #253494; +} +.YlGnBu.q0-7 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlGnBu.q1-7 { + fill: #c7e9b4; + background: #c7e9b4; + stroke: #c7e9b4; +} +.YlGnBu.q2-7 { + fill: #7fcdbb; + background: #7fcdbb; + stroke: #7fcdbb; +} +.YlGnBu.q3-7 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q4-7 { + fill: #1d91c0; + background: #1d91c0; + stroke: #1d91c0; +} +.YlGnBu.q5-7 { + fill: #225ea8; + background: #225ea8; + stroke: #225ea8; +} +.YlGnBu.q6-7 { + fill: #0c2c84; + background: #0c2c84; + stroke: #0c2c84; +} +.YlGnBu.q0-8 { + fill: #ffffd9; + background: #ffffd9; + stroke: #ffffd9; +} +.YlGnBu.q1-8 { + fill: #edf8b1; + background: #edf8b1; + stroke: #edf8b1; +} +.YlGnBu.q2-8 { + fill: #c7e9b4; + background: #c7e9b4; + stroke: #c7e9b4; +} +.YlGnBu.q3-8 { + fill: #7fcdbb; + background: #7fcdbb; + stroke: #7fcdbb; +} +.YlGnBu.q4-8 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q5-8 { + fill: #1d91c0; + background: #1d91c0; + stroke: #1d91c0; +} +.YlGnBu.q6-8 { + fill: #225ea8; + background: #225ea8; + stroke: #225ea8; +} +.YlGnBu.q7-8 { + fill: #0c2c84; + background: #0c2c84; + stroke: #0c2c84; +} +.YlGnBu.q0-9 { + fill: #ffffd9; + background: #ffffd9; + stroke: #ffffd9; +} +.YlGnBu.q1-9 { + fill: #edf8b1; + background: #edf8b1; + stroke: #edf8b1; +} +.YlGnBu.q2-9 { + fill: #c7e9b4; + background: #c7e9b4; + stroke: #c7e9b4; +} +.YlGnBu.q3-9 { + fill: #7fcdbb; + background: #7fcdbb; + stroke: #7fcdbb; +} +.YlGnBu.q4-9 { + fill: #41b6c4; + background: #41b6c4; + stroke: #41b6c4; +} +.YlGnBu.q5-9 { + fill: #1d91c0; + background: #1d91c0; + stroke: #1d91c0; +} +.YlGnBu.q6-9 { + fill: #225ea8; + background: #225ea8; + stroke: #225ea8; +} +.YlGnBu.q7-9 { + fill: #253494; + background: #253494; + stroke: #253494; +} +.YlGnBu.q8-9 { + fill: #081d58; + background: #081d58; + stroke: #081d58; +} +.GnBu.q0-3 { + fill: #e0f3db; + background: #e0f3db; + stroke: #e0f3db; +} +.GnBu.q1-3 { + fill: #a8ddb5; + background: #a8ddb5; + stroke: #a8ddb5; +} +.GnBu.q2-3 { + fill: #43a2ca; + background: #43a2ca; + stroke: #43a2ca; +} +.GnBu.q0-4 { + fill: #f0f9e8; + background: #f0f9e8; + stroke: #f0f9e8; +} +.GnBu.q1-4 { + fill: #bae4bc; + background: #bae4bc; + stroke: #bae4bc; +} +.GnBu.q2-4 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q3-4 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.GnBu.q0-5 { + fill: #f0f9e8; + background: #f0f9e8; + stroke: #f0f9e8; +} +.GnBu.q1-5 { + fill: #bae4bc; + background: #bae4bc; + stroke: #bae4bc; +} +.GnBu.q2-5 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q3-5 { + fill: #43a2ca; + background: #43a2ca; + stroke: #43a2ca; +} +.GnBu.q4-5 { + fill: #0868ac; + background: #0868ac; + stroke: #0868ac; +} +.GnBu.q0-6 { + fill: #f0f9e8; + background: #f0f9e8; + stroke: #f0f9e8; +} +.GnBu.q1-6 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.GnBu.q2-6 { + fill: #a8ddb5; + background: #a8ddb5; + stroke: #a8ddb5; +} +.GnBu.q3-6 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q4-6 { + fill: #43a2ca; + background: #43a2ca; + stroke: #43a2ca; +} +.GnBu.q5-6 { + fill: #0868ac; + background: #0868ac; + stroke: #0868ac; +} +.GnBu.q0-7 { + fill: #f0f9e8; + background: #f0f9e8; + stroke: #f0f9e8; +} +.GnBu.q1-7 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.GnBu.q2-7 { + fill: #a8ddb5; + background: #a8ddb5; + stroke: #a8ddb5; +} +.GnBu.q3-7 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q4-7 { + fill: #4eb3d3; + background: #4eb3d3; + stroke: #4eb3d3; +} +.GnBu.q5-7 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.GnBu.q6-7 { + fill: #08589e; + background: #08589e; + stroke: #08589e; +} +.GnBu.q0-8 { + fill: #f7fcf0; + background: #f7fcf0; + stroke: #f7fcf0; +} +.GnBu.q1-8 { + fill: #e0f3db; + background: #e0f3db; + stroke: #e0f3db; +} +.GnBu.q2-8 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.GnBu.q3-8 { + fill: #a8ddb5; + background: #a8ddb5; + stroke: #a8ddb5; +} +.GnBu.q4-8 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q5-8 { + fill: #4eb3d3; + background: #4eb3d3; + stroke: #4eb3d3; +} +.GnBu.q6-8 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.GnBu.q7-8 { + fill: #08589e; + background: #08589e; + stroke: #08589e; +} +.GnBu.q0-9 { + fill: #f7fcf0; + background: #f7fcf0; + stroke: #f7fcf0; +} +.GnBu.q1-9 { + fill: #e0f3db; + background: #e0f3db; + stroke: #e0f3db; +} +.GnBu.q2-9 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.GnBu.q3-9 { + fill: #a8ddb5; + background: #a8ddb5; + stroke: #a8ddb5; +} +.GnBu.q4-9 { + fill: #7bccc4; + background: #7bccc4; + stroke: #7bccc4; +} +.GnBu.q5-9 { + fill: #4eb3d3; + background: #4eb3d3; + stroke: #4eb3d3; +} +.GnBu.q6-9 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.GnBu.q7-9 { + fill: #0868ac; + background: #0868ac; + stroke: #0868ac; +} +.GnBu.q8-9 { + fill: #084081; + background: #084081; + stroke: #084081; +} +.BuGn.q0-3 { + fill: #e5f5f9; + background: #e5f5f9; + stroke: #e5f5f9; +} +.BuGn.q1-3 { + fill: #99d8c9; + background: #99d8c9; + stroke: #99d8c9; +} +.BuGn.q2-3 { + fill: #2ca25f; + background: #2ca25f; + stroke: #2ca25f; +} +.BuGn.q0-4 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuGn.q1-4 { + fill: #b2e2e2; + background: #b2e2e2; + stroke: #b2e2e2; +} +.BuGn.q2-4 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q3-4 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.BuGn.q0-5 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuGn.q1-5 { + fill: #b2e2e2; + background: #b2e2e2; + stroke: #b2e2e2; +} +.BuGn.q2-5 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q3-5 { + fill: #2ca25f; + background: #2ca25f; + stroke: #2ca25f; +} +.BuGn.q4-5 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.BuGn.q0-6 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuGn.q1-6 { + fill: #ccece6; + background: #ccece6; + stroke: #ccece6; +} +.BuGn.q2-6 { + fill: #99d8c9; + background: #99d8c9; + stroke: #99d8c9; +} +.BuGn.q3-6 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q4-6 { + fill: #2ca25f; + background: #2ca25f; + stroke: #2ca25f; +} +.BuGn.q5-6 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.BuGn.q0-7 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuGn.q1-7 { + fill: #ccece6; + background: #ccece6; + stroke: #ccece6; +} +.BuGn.q2-7 { + fill: #99d8c9; + background: #99d8c9; + stroke: #99d8c9; +} +.BuGn.q3-7 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q4-7 { + fill: #41ae76; + background: #41ae76; + stroke: #41ae76; +} +.BuGn.q5-7 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.BuGn.q6-7 { + fill: #005824; + background: #005824; + stroke: #005824; +} +.BuGn.q0-8 { + fill: #f7fcfd; + background: #f7fcfd; + stroke: #f7fcfd; +} +.BuGn.q1-8 { + fill: #e5f5f9; + background: #e5f5f9; + stroke: #e5f5f9; +} +.BuGn.q2-8 { + fill: #ccece6; + background: #ccece6; + stroke: #ccece6; +} +.BuGn.q3-8 { + fill: #99d8c9; + background: #99d8c9; + stroke: #99d8c9; +} +.BuGn.q4-8 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q5-8 { + fill: #41ae76; + background: #41ae76; + stroke: #41ae76; +} +.BuGn.q6-8 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.BuGn.q7-8 { + fill: #005824; + background: #005824; + stroke: #005824; +} +.BuGn.q0-9 { + fill: #f7fcfd; + background: #f7fcfd; + stroke: #f7fcfd; +} +.BuGn.q1-9 { + fill: #e5f5f9; + background: #e5f5f9; + stroke: #e5f5f9; +} +.BuGn.q2-9 { + fill: #ccece6; + background: #ccece6; + stroke: #ccece6; +} +.BuGn.q3-9 { + fill: #99d8c9; + background: #99d8c9; + stroke: #99d8c9; +} +.BuGn.q4-9 { + fill: #66c2a4; + background: #66c2a4; + stroke: #66c2a4; +} +.BuGn.q5-9 { + fill: #41ae76; + background: #41ae76; + stroke: #41ae76; +} +.BuGn.q6-9 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.BuGn.q7-9 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.BuGn.q8-9 { + fill: #00441b; + background: #00441b; + stroke: #00441b; +} +.PuBuGn.q0-3 { + fill: #ece2f0; + background: #ece2f0; + stroke: #ece2f0; +} +.PuBuGn.q1-3 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBuGn.q2-3 { + fill: #1c9099; + background: #1c9099; + stroke: #1c9099; +} +.PuBuGn.q0-4 { + fill: #f6eff7; + background: #f6eff7; + stroke: #f6eff7; +} +.PuBuGn.q1-4 { + fill: #bdc9e1; + background: #bdc9e1; + stroke: #bdc9e1; +} +.PuBuGn.q2-4 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q3-4 { + fill: #02818a; + background: #02818a; + stroke: #02818a; +} +.PuBuGn.q0-5 { + fill: #f6eff7; + background: #f6eff7; + stroke: #f6eff7; +} +.PuBuGn.q1-5 { + fill: #bdc9e1; + background: #bdc9e1; + stroke: #bdc9e1; +} +.PuBuGn.q2-5 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q3-5 { + fill: #1c9099; + background: #1c9099; + stroke: #1c9099; +} +.PuBuGn.q4-5 { + fill: #016c59; + background: #016c59; + stroke: #016c59; +} +.PuBuGn.q0-6 { + fill: #f6eff7; + background: #f6eff7; + stroke: #f6eff7; +} +.PuBuGn.q1-6 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBuGn.q2-6 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBuGn.q3-6 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q4-6 { + fill: #1c9099; + background: #1c9099; + stroke: #1c9099; +} +.PuBuGn.q5-6 { + fill: #016c59; + background: #016c59; + stroke: #016c59; +} +.PuBuGn.q0-7 { + fill: #f6eff7; + background: #f6eff7; + stroke: #f6eff7; +} +.PuBuGn.q1-7 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBuGn.q2-7 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBuGn.q3-7 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q4-7 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBuGn.q5-7 { + fill: #02818a; + background: #02818a; + stroke: #02818a; +} +.PuBuGn.q6-7 { + fill: #016450; + background: #016450; + stroke: #016450; +} +.PuBuGn.q0-8 { + fill: #fff7fb; + background: #fff7fb; + stroke: #fff7fb; +} +.PuBuGn.q1-8 { + fill: #ece2f0; + background: #ece2f0; + stroke: #ece2f0; +} +.PuBuGn.q2-8 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBuGn.q3-8 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBuGn.q4-8 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q5-8 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBuGn.q6-8 { + fill: #02818a; + background: #02818a; + stroke: #02818a; +} +.PuBuGn.q7-8 { + fill: #016450; + background: #016450; + stroke: #016450; +} +.PuBuGn.q0-9 { + fill: #fff7fb; + background: #fff7fb; + stroke: #fff7fb; +} +.PuBuGn.q1-9 { + fill: #ece2f0; + background: #ece2f0; + stroke: #ece2f0; +} +.PuBuGn.q2-9 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBuGn.q3-9 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBuGn.q4-9 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.PuBuGn.q5-9 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBuGn.q6-9 { + fill: #02818a; + background: #02818a; + stroke: #02818a; +} +.PuBuGn.q7-9 { + fill: #016c59; + background: #016c59; + stroke: #016c59; +} +.PuBuGn.q8-9 { + fill: #014636; + background: #014636; + stroke: #014636; +} +.PuBu.q0-3 { + fill: #ece7f2; + background: #ece7f2; + stroke: #ece7f2; +} +.PuBu.q1-3 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBu.q2-3 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.PuBu.q0-4 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuBu.q1-4 { + fill: #bdc9e1; + background: #bdc9e1; + stroke: #bdc9e1; +} +.PuBu.q2-4 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q3-4 { + fill: #0570b0; + background: #0570b0; + stroke: #0570b0; +} +.PuBu.q0-5 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuBu.q1-5 { + fill: #bdc9e1; + background: #bdc9e1; + stroke: #bdc9e1; +} +.PuBu.q2-5 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q3-5 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.PuBu.q4-5 { + fill: #045a8d; + background: #045a8d; + stroke: #045a8d; +} +.PuBu.q0-6 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuBu.q1-6 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBu.q2-6 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBu.q3-6 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q4-6 { + fill: #2b8cbe; + background: #2b8cbe; + stroke: #2b8cbe; +} +.PuBu.q5-6 { + fill: #045a8d; + background: #045a8d; + stroke: #045a8d; +} +.PuBu.q0-7 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuBu.q1-7 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBu.q2-7 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBu.q3-7 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q4-7 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBu.q5-7 { + fill: #0570b0; + background: #0570b0; + stroke: #0570b0; +} +.PuBu.q6-7 { + fill: #034e7b; + background: #034e7b; + stroke: #034e7b; +} +.PuBu.q0-8 { + fill: #fff7fb; + background: #fff7fb; + stroke: #fff7fb; +} +.PuBu.q1-8 { + fill: #ece7f2; + background: #ece7f2; + stroke: #ece7f2; +} +.PuBu.q2-8 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBu.q3-8 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBu.q4-8 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q5-8 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBu.q6-8 { + fill: #0570b0; + background: #0570b0; + stroke: #0570b0; +} +.PuBu.q7-8 { + fill: #034e7b; + background: #034e7b; + stroke: #034e7b; +} +.PuBu.q0-9 { + fill: #fff7fb; + background: #fff7fb; + stroke: #fff7fb; +} +.PuBu.q1-9 { + fill: #ece7f2; + background: #ece7f2; + stroke: #ece7f2; +} +.PuBu.q2-9 { + fill: #d0d1e6; + background: #d0d1e6; + stroke: #d0d1e6; +} +.PuBu.q3-9 { + fill: #a6bddb; + background: #a6bddb; + stroke: #a6bddb; +} +.PuBu.q4-9 { + fill: #74a9cf; + background: #74a9cf; + stroke: #74a9cf; +} +.PuBu.q5-9 { + fill: #3690c0; + background: #3690c0; + stroke: #3690c0; +} +.PuBu.q6-9 { + fill: #0570b0; + background: #0570b0; + stroke: #0570b0; +} +.PuBu.q7-9 { + fill: #045a8d; + background: #045a8d; + stroke: #045a8d; +} +.PuBu.q8-9 { + fill: #023858; + background: #023858; + stroke: #023858; +} +.BuPu.q0-3 { + fill: #e0ecf4; + background: #e0ecf4; + stroke: #e0ecf4; +} +.BuPu.q1-3 { + fill: #9ebcda; + background: #9ebcda; + stroke: #9ebcda; +} +.BuPu.q2-3 { + fill: #8856a7; + background: #8856a7; + stroke: #8856a7; +} +.BuPu.q0-4 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuPu.q1-4 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.BuPu.q2-4 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q3-4 { + fill: #88419d; + background: #88419d; + stroke: #88419d; +} +.BuPu.q0-5 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuPu.q1-5 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.BuPu.q2-5 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q3-5 { + fill: #8856a7; + background: #8856a7; + stroke: #8856a7; +} +.BuPu.q4-5 { + fill: #810f7c; + background: #810f7c; + stroke: #810f7c; +} +.BuPu.q0-6 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuPu.q1-6 { + fill: #bfd3e6; + background: #bfd3e6; + stroke: #bfd3e6; +} +.BuPu.q2-6 { + fill: #9ebcda; + background: #9ebcda; + stroke: #9ebcda; +} +.BuPu.q3-6 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q4-6 { + fill: #8856a7; + background: #8856a7; + stroke: #8856a7; +} +.BuPu.q5-6 { + fill: #810f7c; + background: #810f7c; + stroke: #810f7c; +} +.BuPu.q0-7 { + fill: #edf8fb; + background: #edf8fb; + stroke: #edf8fb; +} +.BuPu.q1-7 { + fill: #bfd3e6; + background: #bfd3e6; + stroke: #bfd3e6; +} +.BuPu.q2-7 { + fill: #9ebcda; + background: #9ebcda; + stroke: #9ebcda; +} +.BuPu.q3-7 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q4-7 { + fill: #8c6bb1; + background: #8c6bb1; + stroke: #8c6bb1; +} +.BuPu.q5-7 { + fill: #88419d; + background: #88419d; + stroke: #88419d; +} +.BuPu.q6-7 { + fill: #6e016b; + background: #6e016b; + stroke: #6e016b; +} +.BuPu.q0-8 { + fill: #f7fcfd; + background: #f7fcfd; + stroke: #f7fcfd; +} +.BuPu.q1-8 { + fill: #e0ecf4; + background: #e0ecf4; + stroke: #e0ecf4; +} +.BuPu.q2-8 { + fill: #bfd3e6; + background: #bfd3e6; + stroke: #bfd3e6; +} +.BuPu.q3-8 { + fill: #9ebcda; + background: #9ebcda; + stroke: #9ebcda; +} +.BuPu.q4-8 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q5-8 { + fill: #8c6bb1; + background: #8c6bb1; + stroke: #8c6bb1; +} +.BuPu.q6-8 { + fill: #88419d; + background: #88419d; + stroke: #88419d; +} +.BuPu.q7-8 { + fill: #6e016b; + background: #6e016b; + stroke: #6e016b; +} +.BuPu.q0-9 { + fill: #f7fcfd; + background: #f7fcfd; + stroke: #f7fcfd; +} +.BuPu.q1-9 { + fill: #e0ecf4; + background: #e0ecf4; + stroke: #e0ecf4; +} +.BuPu.q2-9 { + fill: #bfd3e6; + background: #bfd3e6; + stroke: #bfd3e6; +} +.BuPu.q3-9 { + fill: #9ebcda; + background: #9ebcda; + stroke: #9ebcda; +} +.BuPu.q4-9 { + fill: #8c96c6; + background: #8c96c6; + stroke: #8c96c6; +} +.BuPu.q5-9 { + fill: #8c6bb1; + background: #8c6bb1; + stroke: #8c6bb1; +} +.BuPu.q6-9 { + fill: #88419d; + background: #88419d; + stroke: #88419d; +} +.BuPu.q7-9 { + fill: #810f7c; + background: #810f7c; + stroke: #810f7c; +} +.BuPu.q8-9 { + fill: #4d004b; + background: #4d004b; + stroke: #4d004b; +} +.RdPu.q0-3 { + fill: #fde0dd; + background: #fde0dd; + stroke: #fde0dd; +} +.RdPu.q1-3 { + fill: #fa9fb5; + background: #fa9fb5; + stroke: #fa9fb5; +} +.RdPu.q2-3 { + fill: #c51b8a; + background: #c51b8a; + stroke: #c51b8a; +} +.RdPu.q0-4 { + fill: #feebe2; + background: #feebe2; + stroke: #feebe2; +} +.RdPu.q1-4 { + fill: #fbb4b9; + background: #fbb4b9; + stroke: #fbb4b9; +} +.RdPu.q2-4 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q3-4 { + fill: #ae017e; + background: #ae017e; + stroke: #ae017e; +} +.RdPu.q0-5 { + fill: #feebe2; + background: #feebe2; + stroke: #feebe2; +} +.RdPu.q1-5 { + fill: #fbb4b9; + background: #fbb4b9; + stroke: #fbb4b9; +} +.RdPu.q2-5 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q3-5 { + fill: #c51b8a; + background: #c51b8a; + stroke: #c51b8a; +} +.RdPu.q4-5 { + fill: #7a0177; + background: #7a0177; + stroke: #7a0177; +} +.RdPu.q0-6 { + fill: #feebe2; + background: #feebe2; + stroke: #feebe2; +} +.RdPu.q1-6 { + fill: #fcc5c0; + background: #fcc5c0; + stroke: #fcc5c0; +} +.RdPu.q2-6 { + fill: #fa9fb5; + background: #fa9fb5; + stroke: #fa9fb5; +} +.RdPu.q3-6 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q4-6 { + fill: #c51b8a; + background: #c51b8a; + stroke: #c51b8a; +} +.RdPu.q5-6 { + fill: #7a0177; + background: #7a0177; + stroke: #7a0177; +} +.RdPu.q0-7 { + fill: #feebe2; + background: #feebe2; + stroke: #feebe2; +} +.RdPu.q1-7 { + fill: #fcc5c0; + background: #fcc5c0; + stroke: #fcc5c0; +} +.RdPu.q2-7 { + fill: #fa9fb5; + background: #fa9fb5; + stroke: #fa9fb5; +} +.RdPu.q3-7 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q4-7 { + fill: #dd3497; + background: #dd3497; + stroke: #dd3497; +} +.RdPu.q5-7 { + fill: #ae017e; + background: #ae017e; + stroke: #ae017e; +} +.RdPu.q6-7 { + fill: #7a0177; + background: #7a0177; + stroke: #7a0177; +} +.RdPu.q0-8 { + fill: #fff7f3; + background: #fff7f3; + stroke: #fff7f3; +} +.RdPu.q1-8 { + fill: #fde0dd; + background: #fde0dd; + stroke: #fde0dd; +} +.RdPu.q2-8 { + fill: #fcc5c0; + background: #fcc5c0; + stroke: #fcc5c0; +} +.RdPu.q3-8 { + fill: #fa9fb5; + background: #fa9fb5; + stroke: #fa9fb5; +} +.RdPu.q4-8 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q5-8 { + fill: #dd3497; + background: #dd3497; + stroke: #dd3497; +} +.RdPu.q6-8 { + fill: #ae017e; + background: #ae017e; + stroke: #ae017e; +} +.RdPu.q7-8 { + fill: #7a0177; + background: #7a0177; + stroke: #7a0177; +} +.RdPu.q0-9 { + fill: #fff7f3; + background: #fff7f3; + stroke: #fff7f3; +} +.RdPu.q1-9 { + fill: #fde0dd; + background: #fde0dd; + stroke: #fde0dd; +} +.RdPu.q2-9 { + fill: #fcc5c0; + background: #fcc5c0; + stroke: #fcc5c0; +} +.RdPu.q3-9 { + fill: #fa9fb5; + background: #fa9fb5; + stroke: #fa9fb5; +} +.RdPu.q4-9 { + fill: #f768a1; + background: #f768a1; + stroke: #f768a1; +} +.RdPu.q5-9 { + fill: #dd3497; + background: #dd3497; + stroke: #dd3497; +} +.RdPu.q6-9 { + fill: #ae017e; + background: #ae017e; + stroke: #ae017e; +} +.RdPu.q7-9 { + fill: #7a0177; + background: #7a0177; + stroke: #7a0177; +} +.RdPu.q8-9 { + fill: #49006a; + background: #49006a; + stroke: #49006a; +} +.PuRd.q0-3 { + fill: #e7e1ef; + background: #e7e1ef; + stroke: #e7e1ef; +} +.PuRd.q1-3 { + fill: #c994c7; + background: #c994c7; + stroke: #c994c7; +} +.PuRd.q2-3 { + fill: #dd1c77; + background: #dd1c77; + stroke: #dd1c77; +} +.PuRd.q0-4 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuRd.q1-4 { + fill: #d7b5d8; + background: #d7b5d8; + stroke: #d7b5d8; +} +.PuRd.q2-4 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q3-4 { + fill: #ce1256; + background: #ce1256; + stroke: #ce1256; +} +.PuRd.q0-5 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuRd.q1-5 { + fill: #d7b5d8; + background: #d7b5d8; + stroke: #d7b5d8; +} +.PuRd.q2-5 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q3-5 { + fill: #dd1c77; + background: #dd1c77; + stroke: #dd1c77; +} +.PuRd.q4-5 { + fill: #980043; + background: #980043; + stroke: #980043; +} +.PuRd.q0-6 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuRd.q1-6 { + fill: #d4b9da; + background: #d4b9da; + stroke: #d4b9da; +} +.PuRd.q2-6 { + fill: #c994c7; + background: #c994c7; + stroke: #c994c7; +} +.PuRd.q3-6 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q4-6 { + fill: #dd1c77; + background: #dd1c77; + stroke: #dd1c77; +} +.PuRd.q5-6 { + fill: #980043; + background: #980043; + stroke: #980043; +} +.PuRd.q0-7 { + fill: #f1eef6; + background: #f1eef6; + stroke: #f1eef6; +} +.PuRd.q1-7 { + fill: #d4b9da; + background: #d4b9da; + stroke: #d4b9da; +} +.PuRd.q2-7 { + fill: #c994c7; + background: #c994c7; + stroke: #c994c7; +} +.PuRd.q3-7 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q4-7 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.PuRd.q5-7 { + fill: #ce1256; + background: #ce1256; + stroke: #ce1256; +} +.PuRd.q6-7 { + fill: #91003f; + background: #91003f; + stroke: #91003f; +} +.PuRd.q0-8 { + fill: #f7f4f9; + background: #f7f4f9; + stroke: #f7f4f9; +} +.PuRd.q1-8 { + fill: #e7e1ef; + background: #e7e1ef; + stroke: #e7e1ef; +} +.PuRd.q2-8 { + fill: #d4b9da; + background: #d4b9da; + stroke: #d4b9da; +} +.PuRd.q3-8 { + fill: #c994c7; + background: #c994c7; + stroke: #c994c7; +} +.PuRd.q4-8 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q5-8 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.PuRd.q6-8 { + fill: #ce1256; + background: #ce1256; + stroke: #ce1256; +} +.PuRd.q7-8 { + fill: #91003f; + background: #91003f; + stroke: #91003f; +} +.PuRd.q0-9 { + fill: #f7f4f9; + background: #f7f4f9; + stroke: #f7f4f9; +} +.PuRd.q1-9 { + fill: #e7e1ef; + background: #e7e1ef; + stroke: #e7e1ef; +} +.PuRd.q2-9 { + fill: #d4b9da; + background: #d4b9da; + stroke: #d4b9da; +} +.PuRd.q3-9 { + fill: #c994c7; + background: #c994c7; + stroke: #c994c7; +} +.PuRd.q4-9 { + fill: #df65b0; + background: #df65b0; + stroke: #df65b0; +} +.PuRd.q5-9 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.PuRd.q6-9 { + fill: #ce1256; + background: #ce1256; + stroke: #ce1256; +} +.PuRd.q7-9 { + fill: #980043; + background: #980043; + stroke: #980043; +} +.PuRd.q8-9 { + fill: #67001f; + background: #67001f; + stroke: #67001f; +} +.OrRd.q0-3 { + fill: #fee8c8; + background: #fee8c8; + stroke: #fee8c8; +} +.OrRd.q1-3 { + fill: #fdbb84; + background: #fdbb84; + stroke: #fdbb84; +} +.OrRd.q2-3 { + fill: #e34a33; + background: #e34a33; + stroke: #e34a33; +} +.OrRd.q0-4 { + fill: #fef0d9; + background: #fef0d9; + stroke: #fef0d9; +} +.OrRd.q1-4 { + fill: #fdcc8a; + background: #fdcc8a; + stroke: #fdcc8a; +} +.OrRd.q2-4 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q3-4 { + fill: #d7301f; + background: #d7301f; + stroke: #d7301f; +} +.OrRd.q0-5 { + fill: #fef0d9; + background: #fef0d9; + stroke: #fef0d9; +} +.OrRd.q1-5 { + fill: #fdcc8a; + background: #fdcc8a; + stroke: #fdcc8a; +} +.OrRd.q2-5 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q3-5 { + fill: #e34a33; + background: #e34a33; + stroke: #e34a33; +} +.OrRd.q4-5 { + fill: #b30000; + background: #b30000; + stroke: #b30000; +} +.OrRd.q0-6 { + fill: #fef0d9; + background: #fef0d9; + stroke: #fef0d9; +} +.OrRd.q1-6 { + fill: #fdd49e; + background: #fdd49e; + stroke: #fdd49e; +} +.OrRd.q2-6 { + fill: #fdbb84; + background: #fdbb84; + stroke: #fdbb84; +} +.OrRd.q3-6 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q4-6 { + fill: #e34a33; + background: #e34a33; + stroke: #e34a33; +} +.OrRd.q5-6 { + fill: #b30000; + background: #b30000; + stroke: #b30000; +} +.OrRd.q0-7 { + fill: #fef0d9; + background: #fef0d9; + stroke: #fef0d9; +} +.OrRd.q1-7 { + fill: #fdd49e; + background: #fdd49e; + stroke: #fdd49e; +} +.OrRd.q2-7 { + fill: #fdbb84; + background: #fdbb84; + stroke: #fdbb84; +} +.OrRd.q3-7 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q4-7 { + fill: #ef6548; + background: #ef6548; + stroke: #ef6548; +} +.OrRd.q5-7 { + fill: #d7301f; + background: #d7301f; + stroke: #d7301f; +} +.OrRd.q6-7 { + fill: #990000; + background: #990000; + stroke: #990000; +} +.OrRd.q0-8 { + fill: #fff7ec; + background: #fff7ec; + stroke: #fff7ec; +} +.OrRd.q1-8 { + fill: #fee8c8; + background: #fee8c8; + stroke: #fee8c8; +} +.OrRd.q2-8 { + fill: #fdd49e; + background: #fdd49e; + stroke: #fdd49e; +} +.OrRd.q3-8 { + fill: #fdbb84; + background: #fdbb84; + stroke: #fdbb84; +} +.OrRd.q4-8 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q5-8 { + fill: #ef6548; + background: #ef6548; + stroke: #ef6548; +} +.OrRd.q6-8 { + fill: #d7301f; + background: #d7301f; + stroke: #d7301f; +} +.OrRd.q7-8 { + fill: #990000; + background: #990000; + stroke: #990000; +} +.OrRd.q0-9 { + fill: #fff7ec; + background: #fff7ec; + stroke: #fff7ec; +} +.OrRd.q1-9 { + fill: #fee8c8; + background: #fee8c8; + stroke: #fee8c8; +} +.OrRd.q2-9 { + fill: #fdd49e; + background: #fdd49e; + stroke: #fdd49e; +} +.OrRd.q3-9 { + fill: #fdbb84; + background: #fdbb84; + stroke: #fdbb84; +} +.OrRd.q4-9 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.OrRd.q5-9 { + fill: #ef6548; + background: #ef6548; + stroke: #ef6548; +} +.OrRd.q6-9 { + fill: #d7301f; + background: #d7301f; + stroke: #d7301f; +} +.OrRd.q7-9 { + fill: #b30000; + background: #b30000; + stroke: #b30000; +} +.OrRd.q8-9 { + fill: #7f0000; + background: #7f0000; + stroke: #7f0000; +} +.YlOrRd.q0-3 { + fill: #ffeda0; + background: #ffeda0; + stroke: #ffeda0; +} +.YlOrRd.q1-3 { + fill: #feb24c; + background: #feb24c; + stroke: #feb24c; +} +.YlOrRd.q2-3 { + fill: #f03b20; + background: #f03b20; + stroke: #f03b20; +} +.YlOrRd.q0-4 { + fill: #ffffb2; + background: #ffffb2; + stroke: #ffffb2; +} +.YlOrRd.q1-4 { + fill: #fecc5c; + background: #fecc5c; + stroke: #fecc5c; +} +.YlOrRd.q2-4 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q3-4 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.YlOrRd.q0-5 { + fill: #ffffb2; + background: #ffffb2; + stroke: #ffffb2; +} +.YlOrRd.q1-5 { + fill: #fecc5c; + background: #fecc5c; + stroke: #fecc5c; +} +.YlOrRd.q2-5 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q3-5 { + fill: #f03b20; + background: #f03b20; + stroke: #f03b20; +} +.YlOrRd.q4-5 { + fill: #bd0026; + background: #bd0026; + stroke: #bd0026; +} +.YlOrRd.q0-6 { + fill: #ffffb2; + background: #ffffb2; + stroke: #ffffb2; +} +.YlOrRd.q1-6 { + fill: #fed976; + background: #fed976; + stroke: #fed976; +} +.YlOrRd.q2-6 { + fill: #feb24c; + background: #feb24c; + stroke: #feb24c; +} +.YlOrRd.q3-6 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q4-6 { + fill: #f03b20; + background: #f03b20; + stroke: #f03b20; +} +.YlOrRd.q5-6 { + fill: #bd0026; + background: #bd0026; + stroke: #bd0026; +} +.YlOrRd.q0-7 { + fill: #ffffb2; + background: #ffffb2; + stroke: #ffffb2; +} +.YlOrRd.q1-7 { + fill: #fed976; + background: #fed976; + stroke: #fed976; +} +.YlOrRd.q2-7 { + fill: #feb24c; + background: #feb24c; + stroke: #feb24c; +} +.YlOrRd.q3-7 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q4-7 { + fill: #fc4e2a; + background: #fc4e2a; + stroke: #fc4e2a; +} +.YlOrRd.q5-7 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.YlOrRd.q6-7 { + fill: #b10026; + background: #b10026; + stroke: #b10026; +} +.YlOrRd.q0-8 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlOrRd.q1-8 { + fill: #ffeda0; + background: #ffeda0; + stroke: #ffeda0; +} +.YlOrRd.q2-8 { + fill: #fed976; + background: #fed976; + stroke: #fed976; +} +.YlOrRd.q3-8 { + fill: #feb24c; + background: #feb24c; + stroke: #feb24c; +} +.YlOrRd.q4-8 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q5-8 { + fill: #fc4e2a; + background: #fc4e2a; + stroke: #fc4e2a; +} +.YlOrRd.q6-8 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.YlOrRd.q7-8 { + fill: #b10026; + background: #b10026; + stroke: #b10026; +} +.YlOrRd.q0-9 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.YlOrRd.q1-9 { + fill: #ffeda0; + background: #ffeda0; + stroke: #ffeda0; +} +.YlOrRd.q2-9 { + fill: #fed976; + background: #fed976; + stroke: #fed976; +} +.YlOrRd.q3-9 { + fill: #feb24c; + background: #feb24c; + stroke: #feb24c; +} +.YlOrRd.q4-9 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.YlOrRd.q5-9 { + fill: #fc4e2a; + background: #fc4e2a; + stroke: #fc4e2a; +} +.YlOrRd.q6-9 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.YlOrRd.q7-9 { + fill: #bd0026; + background: #bd0026; + stroke: #bd0026; +} +.YlOrRd.q8-9 { + fill: #800026; + background: #800026; + stroke: #800026; +} +.YlOrBr.q0-3 { + fill: #fff7bc; + background: #fff7bc; + stroke: #fff7bc; +} +.YlOrBr.q1-3 { + fill: #fec44f; + background: #fec44f; + stroke: #fec44f; +} +.YlOrBr.q2-3 { + fill: #d95f0e; + background: #d95f0e; + stroke: #d95f0e; +} +.YlOrBr.q0-4 { + fill: #ffffd4; + background: #ffffd4; + stroke: #ffffd4; +} +.YlOrBr.q1-4 { + fill: #fed98e; + background: #fed98e; + stroke: #fed98e; +} +.YlOrBr.q2-4 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q3-4 { + fill: #cc4c02; + background: #cc4c02; + stroke: #cc4c02; +} +.YlOrBr.q0-5 { + fill: #ffffd4; + background: #ffffd4; + stroke: #ffffd4; +} +.YlOrBr.q1-5 { + fill: #fed98e; + background: #fed98e; + stroke: #fed98e; +} +.YlOrBr.q2-5 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q3-5 { + fill: #d95f0e; + background: #d95f0e; + stroke: #d95f0e; +} +.YlOrBr.q4-5 { + fill: #993404; + background: #993404; + stroke: #993404; +} +.YlOrBr.q0-6 { + fill: #ffffd4; + background: #ffffd4; + stroke: #ffffd4; +} +.YlOrBr.q1-6 { + fill: #fee391; + background: #fee391; + stroke: #fee391; +} +.YlOrBr.q2-6 { + fill: #fec44f; + background: #fec44f; + stroke: #fec44f; +} +.YlOrBr.q3-6 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q4-6 { + fill: #d95f0e; + background: #d95f0e; + stroke: #d95f0e; +} +.YlOrBr.q5-6 { + fill: #993404; + background: #993404; + stroke: #993404; +} +.YlOrBr.q0-7 { + fill: #ffffd4; + background: #ffffd4; + stroke: #ffffd4; +} +.YlOrBr.q1-7 { + fill: #fee391; + background: #fee391; + stroke: #fee391; +} +.YlOrBr.q2-7 { + fill: #fec44f; + background: #fec44f; + stroke: #fec44f; +} +.YlOrBr.q3-7 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q4-7 { + fill: #ec7014; + background: #ec7014; + stroke: #ec7014; +} +.YlOrBr.q5-7 { + fill: #cc4c02; + background: #cc4c02; + stroke: #cc4c02; +} +.YlOrBr.q6-7 { + fill: #8c2d04; + background: #8c2d04; + stroke: #8c2d04; +} +.YlOrBr.q0-8 { + fill: #ffffe5; + background: #ffffe5; + stroke: #ffffe5; +} +.YlOrBr.q1-8 { + fill: #fff7bc; + background: #fff7bc; + stroke: #fff7bc; +} +.YlOrBr.q2-8 { + fill: #fee391; + background: #fee391; + stroke: #fee391; +} +.YlOrBr.q3-8 { + fill: #fec44f; + background: #fec44f; + stroke: #fec44f; +} +.YlOrBr.q4-8 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q5-8 { + fill: #ec7014; + background: #ec7014; + stroke: #ec7014; +} +.YlOrBr.q6-8 { + fill: #cc4c02; + background: #cc4c02; + stroke: #cc4c02; +} +.YlOrBr.q7-8 { + fill: #8c2d04; + background: #8c2d04; + stroke: #8c2d04; +} +.YlOrBr.q0-9 { + fill: #ffffe5; + background: #ffffe5; + stroke: #ffffe5; +} +.YlOrBr.q1-9 { + fill: #fff7bc; + background: #fff7bc; + stroke: #fff7bc; +} +.YlOrBr.q2-9 { + fill: #fee391; + background: #fee391; + stroke: #fee391; +} +.YlOrBr.q3-9 { + fill: #fec44f; + background: #fec44f; + stroke: #fec44f; +} +.YlOrBr.q4-9 { + fill: #fe9929; + background: #fe9929; + stroke: #fe9929; +} +.YlOrBr.q5-9 { + fill: #ec7014; + background: #ec7014; + stroke: #ec7014; +} +.YlOrBr.q6-9 { + fill: #cc4c02; + background: #cc4c02; + stroke: #cc4c02; +} +.YlOrBr.q7-9 { + fill: #993404; + background: #993404; + stroke: #993404; +} +.YlOrBr.q8-9 { + fill: #662506; + background: #662506; + stroke: #662506; +} +.Purples.q0-3 { + fill: #efedf5; + background: #efedf5; + stroke: #efedf5; +} +.Purples.q1-3 { + fill: #bcbddc; + background: #bcbddc; + stroke: #bcbddc; +} +.Purples.q2-3 { + fill: #756bb1; + background: #756bb1; + stroke: #756bb1; +} +.Purples.q0-4 { + fill: #f2f0f7; + background: #f2f0f7; + stroke: #f2f0f7; +} +.Purples.q1-4 { + fill: #cbc9e2; + background: #cbc9e2; + stroke: #cbc9e2; +} +.Purples.q2-4 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q3-4 { + fill: #6a51a3; + background: #6a51a3; + stroke: #6a51a3; +} +.Purples.q0-5 { + fill: #f2f0f7; + background: #f2f0f7; + stroke: #f2f0f7; +} +.Purples.q1-5 { + fill: #cbc9e2; + background: #cbc9e2; + stroke: #cbc9e2; +} +.Purples.q2-5 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q3-5 { + fill: #756bb1; + background: #756bb1; + stroke: #756bb1; +} +.Purples.q4-5 { + fill: #54278f; + background: #54278f; + stroke: #54278f; +} +.Purples.q0-6 { + fill: #f2f0f7; + background: #f2f0f7; + stroke: #f2f0f7; +} +.Purples.q1-6 { + fill: #dadaeb; + background: #dadaeb; + stroke: #dadaeb; +} +.Purples.q2-6 { + fill: #bcbddc; + background: #bcbddc; + stroke: #bcbddc; +} +.Purples.q3-6 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q4-6 { + fill: #756bb1; + background: #756bb1; + stroke: #756bb1; +} +.Purples.q5-6 { + fill: #54278f; + background: #54278f; + stroke: #54278f; +} +.Purples.q0-7 { + fill: #f2f0f7; + background: #f2f0f7; + stroke: #f2f0f7; +} +.Purples.q1-7 { + fill: #dadaeb; + background: #dadaeb; + stroke: #dadaeb; +} +.Purples.q2-7 { + fill: #bcbddc; + background: #bcbddc; + stroke: #bcbddc; +} +.Purples.q3-7 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q4-7 { + fill: #807dba; + background: #807dba; + stroke: #807dba; +} +.Purples.q5-7 { + fill: #6a51a3; + background: #6a51a3; + stroke: #6a51a3; +} +.Purples.q6-7 { + fill: #4a1486; + background: #4a1486; + stroke: #4a1486; +} +.Purples.q0-8 { + fill: #fcfbfd; + background: #fcfbfd; + stroke: #fcfbfd; +} +.Purples.q1-8 { + fill: #efedf5; + background: #efedf5; + stroke: #efedf5; +} +.Purples.q2-8 { + fill: #dadaeb; + background: #dadaeb; + stroke: #dadaeb; +} +.Purples.q3-8 { + fill: #bcbddc; + background: #bcbddc; + stroke: #bcbddc; +} +.Purples.q4-8 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q5-8 { + fill: #807dba; + background: #807dba; + stroke: #807dba; +} +.Purples.q6-8 { + fill: #6a51a3; + background: #6a51a3; + stroke: #6a51a3; +} +.Purples.q7-8 { + fill: #4a1486; + background: #4a1486; + stroke: #4a1486; +} +.Purples.q0-9 { + fill: #fcfbfd; + background: #fcfbfd; + stroke: #fcfbfd; +} +.Purples.q1-9 { + fill: #efedf5; + background: #efedf5; + stroke: #efedf5; +} +.Purples.q2-9 { + fill: #dadaeb; + background: #dadaeb; + stroke: #dadaeb; +} +.Purples.q3-9 { + fill: #bcbddc; + background: #bcbddc; + stroke: #bcbddc; +} +.Purples.q4-9 { + fill: #9e9ac8; + background: #9e9ac8; + stroke: #9e9ac8; +} +.Purples.q5-9 { + fill: #807dba; + background: #807dba; + stroke: #807dba; +} +.Purples.q6-9 { + fill: #6a51a3; + background: #6a51a3; + stroke: #6a51a3; +} +.Purples.q7-9 { + fill: #54278f; + background: #54278f; + stroke: #54278f; +} +.Purples.q8-9 { + fill: #3f007d; + background: #3f007d; + stroke: #3f007d; +} +.Blues.q0-3 { + fill: #deebf7; + background: #deebf7; + stroke: #deebf7; +} +.Blues.q1-3 { + fill: #9ecae1; + background: #9ecae1; + stroke: #9ecae1; +} +.Blues.q2-3 { + fill: #3182bd; + background: #3182bd; + stroke: #3182bd; +} +.Blues.q0-4 { + fill: #eff3ff; + background: #eff3ff; + stroke: #eff3ff; +} +.Blues.q1-4 { + fill: #bdd7e7; + background: #bdd7e7; + stroke: #bdd7e7; +} +.Blues.q2-4 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q3-4 { + fill: #2171b5; + background: #2171b5; + stroke: #2171b5; +} +.Blues.q0-5 { + fill: #eff3ff; + background: #eff3ff; + stroke: #eff3ff; +} +.Blues.q1-5 { + fill: #bdd7e7; + background: #bdd7e7; + stroke: #bdd7e7; +} +.Blues.q2-5 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q3-5 { + fill: #3182bd; + background: #3182bd; + stroke: #3182bd; +} +.Blues.q4-5 { + fill: #08519c; + background: #08519c; + stroke: #08519c; +} +.Blues.q0-6 { + fill: #eff3ff; + background: #eff3ff; + stroke: #eff3ff; +} +.Blues.q1-6 { + fill: #c6dbef; + background: #c6dbef; + stroke: #c6dbef; +} +.Blues.q2-6 { + fill: #9ecae1; + background: #9ecae1; + stroke: #9ecae1; +} +.Blues.q3-6 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q4-6 { + fill: #3182bd; + background: #3182bd; + stroke: #3182bd; +} +.Blues.q5-6 { + fill: #08519c; + background: #08519c; + stroke: #08519c; +} +.Blues.q0-7 { + fill: #eff3ff; + background: #eff3ff; + stroke: #eff3ff; +} +.Blues.q1-7 { + fill: #c6dbef; + background: #c6dbef; + stroke: #c6dbef; +} +.Blues.q2-7 { + fill: #9ecae1; + background: #9ecae1; + stroke: #9ecae1; +} +.Blues.q3-7 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q4-7 { + fill: #4292c6; + background: #4292c6; + stroke: #4292c6; +} +.Blues.q5-7 { + fill: #2171b5; + background: #2171b5; + stroke: #2171b5; +} +.Blues.q6-7 { + fill: #084594; + background: #084594; + stroke: #084594; +} +.Blues.q0-8 { + fill: #f7fbff; + background: #f7fbff; + stroke: #f7fbff; +} +.Blues.q1-8 { + fill: #deebf7; + background: #deebf7; + stroke: #deebf7; +} +.Blues.q2-8 { + fill: #c6dbef; + background: #c6dbef; + stroke: #c6dbef; +} +.Blues.q3-8 { + fill: #9ecae1; + background: #9ecae1; + stroke: #9ecae1; +} +.Blues.q4-8 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q5-8 { + fill: #4292c6; + background: #4292c6; + stroke: #4292c6; +} +.Blues.q6-8 { + fill: #2171b5; + background: #2171b5; + stroke: #2171b5; +} +.Blues.q7-8 { + fill: #084594; + background: #084594; + stroke: #084594; +} +.Blues.q0-9 { + fill: #f7fbff; + background: #f7fbff; + stroke: #f7fbff; +} +.Blues.q1-9 { + fill: #deebf7; + background: #deebf7; + stroke: #deebf7; +} +.Blues.q2-9 { + fill: #c6dbef; + background: #c6dbef; + stroke: #c6dbef; +} +.Blues.q3-9 { + fill: #9ecae1; + background: #9ecae1; + stroke: #9ecae1; +} +.Blues.q4-9 { + fill: #6baed6; + background: #6baed6; + stroke: #6baed6; +} +.Blues.q5-9 { + fill: #4292c6; + background: #4292c6; + stroke: #4292c6; +} +.Blues.q6-9 { + fill: #2171b5; + background: #2171b5; + stroke: #2171b5; +} +.Blues.q7-9 { + fill: #08519c; + background: #08519c; + stroke: #08519c; +} +.Blues.q8-9 { + fill: #08306b; + background: #08306b; + stroke: #08306b; +} +.Greens.q0-3 { + fill: #e5f5e0; + background: #e5f5e0; + stroke: #e5f5e0; +} +.Greens.q1-3 { + fill: #a1d99b; + background: #a1d99b; + stroke: #a1d99b; +} +.Greens.q2-3 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.Greens.q0-4 { + fill: #edf8e9; + background: #edf8e9; + stroke: #edf8e9; +} +.Greens.q1-4 { + fill: #bae4b3; + background: #bae4b3; + stroke: #bae4b3; +} +.Greens.q2-4 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q3-4 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.Greens.q0-5 { + fill: #edf8e9; + background: #edf8e9; + stroke: #edf8e9; +} +.Greens.q1-5 { + fill: #bae4b3; + background: #bae4b3; + stroke: #bae4b3; +} +.Greens.q2-5 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q3-5 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.Greens.q4-5 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.Greens.q0-6 { + fill: #edf8e9; + background: #edf8e9; + stroke: #edf8e9; +} +.Greens.q1-6 { + fill: #c7e9c0; + background: #c7e9c0; + stroke: #c7e9c0; +} +.Greens.q2-6 { + fill: #a1d99b; + background: #a1d99b; + stroke: #a1d99b; +} +.Greens.q3-6 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q4-6 { + fill: #31a354; + background: #31a354; + stroke: #31a354; +} +.Greens.q5-6 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.Greens.q0-7 { + fill: #edf8e9; + background: #edf8e9; + stroke: #edf8e9; +} +.Greens.q1-7 { + fill: #c7e9c0; + background: #c7e9c0; + stroke: #c7e9c0; +} +.Greens.q2-7 { + fill: #a1d99b; + background: #a1d99b; + stroke: #a1d99b; +} +.Greens.q3-7 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q4-7 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.Greens.q5-7 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.Greens.q6-7 { + fill: #005a32; + background: #005a32; + stroke: #005a32; +} +.Greens.q0-8 { + fill: #f7fcf5; + background: #f7fcf5; + stroke: #f7fcf5; +} +.Greens.q1-8 { + fill: #e5f5e0; + background: #e5f5e0; + stroke: #e5f5e0; +} +.Greens.q2-8 { + fill: #c7e9c0; + background: #c7e9c0; + stroke: #c7e9c0; +} +.Greens.q3-8 { + fill: #a1d99b; + background: #a1d99b; + stroke: #a1d99b; +} +.Greens.q4-8 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q5-8 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.Greens.q6-8 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.Greens.q7-8 { + fill: #005a32; + background: #005a32; + stroke: #005a32; +} +.Greens.q0-9 { + fill: #f7fcf5; + background: #f7fcf5; + stroke: #f7fcf5; +} +.Greens.q1-9 { + fill: #e5f5e0; + background: #e5f5e0; + stroke: #e5f5e0; +} +.Greens.q2-9 { + fill: #c7e9c0; + background: #c7e9c0; + stroke: #c7e9c0; +} +.Greens.q3-9 { + fill: #a1d99b; + background: #a1d99b; + stroke: #a1d99b; +} +.Greens.q4-9 { + fill: #74c476; + background: #74c476; + stroke: #74c476; +} +.Greens.q5-9 { + fill: #41ab5d; + background: #41ab5d; + stroke: #41ab5d; +} +.Greens.q6-9 { + fill: #238b45; + background: #238b45; + stroke: #238b45; +} +.Greens.q7-9 { + fill: #006d2c; + background: #006d2c; + stroke: #006d2c; +} +.Greens.q8-9 { + fill: #00441b; + background: #00441b; + stroke: #00441b; +} +.Oranges.q0-3 { + fill: #fee6ce; + background: #fee6ce; + stroke: #fee6ce; +} +.Oranges.q1-3 { + fill: #fdae6b; + background: #fdae6b; + stroke: #fdae6b; +} +.Oranges.q2-3 { + fill: #e6550d; + background: #e6550d; + stroke: #e6550d; +} +.Oranges.q0-4 { + fill: #feedde; + background: #feedde; + stroke: #feedde; +} +.Oranges.q1-4 { + fill: #fdbe85; + background: #fdbe85; + stroke: #fdbe85; +} +.Oranges.q2-4 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q3-4 { + fill: #d94701; + background: #d94701; + stroke: #d94701; +} +.Oranges.q0-5 { + fill: #feedde; + background: #feedde; + stroke: #feedde; +} +.Oranges.q1-5 { + fill: #fdbe85; + background: #fdbe85; + stroke: #fdbe85; +} +.Oranges.q2-5 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q3-5 { + fill: #e6550d; + background: #e6550d; + stroke: #e6550d; +} +.Oranges.q4-5 { + fill: #a63603; + background: #a63603; + stroke: #a63603; +} +.Oranges.q0-6 { + fill: #feedde; + background: #feedde; + stroke: #feedde; +} +.Oranges.q1-6 { + fill: #fdd0a2; + background: #fdd0a2; + stroke: #fdd0a2; +} +.Oranges.q2-6 { + fill: #fdae6b; + background: #fdae6b; + stroke: #fdae6b; +} +.Oranges.q3-6 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q4-6 { + fill: #e6550d; + background: #e6550d; + stroke: #e6550d; +} +.Oranges.q5-6 { + fill: #a63603; + background: #a63603; + stroke: #a63603; +} +.Oranges.q0-7 { + fill: #feedde; + background: #feedde; + stroke: #feedde; +} +.Oranges.q1-7 { + fill: #fdd0a2; + background: #fdd0a2; + stroke: #fdd0a2; +} +.Oranges.q2-7 { + fill: #fdae6b; + background: #fdae6b; + stroke: #fdae6b; +} +.Oranges.q3-7 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q4-7 { + fill: #f16913; + background: #f16913; + stroke: #f16913; +} +.Oranges.q5-7 { + fill: #d94801; + background: #d94801; + stroke: #d94801; +} +.Oranges.q6-7 { + fill: #8c2d04; + background: #8c2d04; + stroke: #8c2d04; +} +.Oranges.q0-8 { + fill: #fff5eb; + background: #fff5eb; + stroke: #fff5eb; +} +.Oranges.q1-8 { + fill: #fee6ce; + background: #fee6ce; + stroke: #fee6ce; +} +.Oranges.q2-8 { + fill: #fdd0a2; + background: #fdd0a2; + stroke: #fdd0a2; +} +.Oranges.q3-8 { + fill: #fdae6b; + background: #fdae6b; + stroke: #fdae6b; +} +.Oranges.q4-8 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q5-8 { + fill: #f16913; + background: #f16913; + stroke: #f16913; +} +.Oranges.q6-8 { + fill: #d94801; + background: #d94801; + stroke: #d94801; +} +.Oranges.q7-8 { + fill: #8c2d04; + background: #8c2d04; + stroke: #8c2d04; +} +.Oranges.q0-9 { + fill: #fff5eb; + background: #fff5eb; + stroke: #fff5eb; +} +.Oranges.q1-9 { + fill: #fee6ce; + background: #fee6ce; + stroke: #fee6ce; +} +.Oranges.q2-9 { + fill: #fdd0a2; + background: #fdd0a2; + stroke: #fdd0a2; +} +.Oranges.q3-9 { + fill: #fdae6b; + background: #fdae6b; + stroke: #fdae6b; +} +.Oranges.q4-9 { + fill: #fd8d3c; + background: #fd8d3c; + stroke: #fd8d3c; +} +.Oranges.q5-9 { + fill: #f16913; + background: #f16913; + stroke: #f16913; +} +.Oranges.q6-9 { + fill: #d94801; + background: #d94801; + stroke: #d94801; +} +.Oranges.q7-9 { + fill: #a63603; + background: #a63603; + stroke: #a63603; +} +.Oranges.q8-9 { + fill: #7f2704; + background: #7f2704; + stroke: #7f2704; +} +.Reds.q0-3 { + fill: #fee0d2; + background: #fee0d2; + stroke: #fee0d2; +} +.Reds.q1-3 { + fill: #fc9272; + background: #fc9272; + stroke: #fc9272; +} +.Reds.q2-3 { + fill: #de2d26; + background: #de2d26; + stroke: #de2d26; +} +.Reds.q0-4 { + fill: #fee5d9; + background: #fee5d9; + stroke: #fee5d9; +} +.Reds.q1-4 { + fill: #fcae91; + background: #fcae91; + stroke: #fcae91; +} +.Reds.q2-4 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q3-4 { + fill: #cb181d; + background: #cb181d; + stroke: #cb181d; +} +.Reds.q0-5 { + fill: #fee5d9; + background: #fee5d9; + stroke: #fee5d9; +} +.Reds.q1-5 { + fill: #fcae91; + background: #fcae91; + stroke: #fcae91; +} +.Reds.q2-5 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q3-5 { + fill: #de2d26; + background: #de2d26; + stroke: #de2d26; +} +.Reds.q4-5 { + fill: #a50f15; + background: #a50f15; + stroke: #a50f15; +} +.Reds.q0-6 { + fill: #fee5d9; + background: #fee5d9; + stroke: #fee5d9; +} +.Reds.q1-6 { + fill: #fcbba1; + background: #fcbba1; + stroke: #fcbba1; +} +.Reds.q2-6 { + fill: #fc9272; + background: #fc9272; + stroke: #fc9272; +} +.Reds.q3-6 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q4-6 { + fill: #de2d26; + background: #de2d26; + stroke: #de2d26; +} +.Reds.q5-6 { + fill: #a50f15; + background: #a50f15; + stroke: #a50f15; +} +.Reds.q0-7 { + fill: #fee5d9; + background: #fee5d9; + stroke: #fee5d9; +} +.Reds.q1-7 { + fill: #fcbba1; + background: #fcbba1; + stroke: #fcbba1; +} +.Reds.q2-7 { + fill: #fc9272; + background: #fc9272; + stroke: #fc9272; +} +.Reds.q3-7 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q4-7 { + fill: #ef3b2c; + background: #ef3b2c; + stroke: #ef3b2c; +} +.Reds.q5-7 { + fill: #cb181d; + background: #cb181d; + stroke: #cb181d; +} +.Reds.q6-7 { + fill: #99000d; + background: #99000d; + stroke: #99000d; +} +.Reds.q0-8 { + fill: #fff5f0; + background: #fff5f0; + stroke: #fff5f0; +} +.Reds.q1-8 { + fill: #fee0d2; + background: #fee0d2; + stroke: #fee0d2; +} +.Reds.q2-8 { + fill: #fcbba1; + background: #fcbba1; + stroke: #fcbba1; +} +.Reds.q3-8 { + fill: #fc9272; + background: #fc9272; + stroke: #fc9272; +} +.Reds.q4-8 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q5-8 { + fill: #ef3b2c; + background: #ef3b2c; + stroke: #ef3b2c; +} +.Reds.q6-8 { + fill: #cb181d; + background: #cb181d; + stroke: #cb181d; +} +.Reds.q7-8 { + fill: #99000d; + background: #99000d; + stroke: #99000d; +} +.Reds.q0-9 { + fill: #fff5f0; + background: #fff5f0; + stroke: #fff5f0; +} +.Reds.q1-9 { + fill: #fee0d2; + background: #fee0d2; + stroke: #fee0d2; +} +.Reds.q2-9 { + fill: #fcbba1; + background: #fcbba1; + stroke: #fcbba1; +} +.Reds.q3-9 { + fill: #fc9272; + background: #fc9272; + stroke: #fc9272; +} +.Reds.q4-9 { + fill: #fb6a4a; + background: #fb6a4a; + stroke: #fb6a4a; +} +.Reds.q5-9 { + fill: #ef3b2c; + background: #ef3b2c; + stroke: #ef3b2c; +} +.Reds.q6-9 { + fill: #cb181d; + background: #cb181d; + stroke: #cb181d; +} +.Reds.q7-9 { + fill: #a50f15; + background: #a50f15; + stroke: #a50f15; +} +.Reds.q8-9 { + fill: #67000d; + background: #67000d; + stroke: #67000d; +} +.Greys.q0-3 { + fill: #f0f0f0; + background: #f0f0f0; + stroke: #f0f0f0; +} +.Greys.q1-3 { + fill: #bdbdbd; + background: #bdbdbd; + stroke: #bdbdbd; +} +.Greys.q2-3 { + fill: #636363; + background: #636363; + stroke: #636363; +} +.Greys.q0-4 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.Greys.q1-4 { + fill: #cccccc; + background: #cccccc; + stroke: #cccccc; +} +.Greys.q2-4 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q3-4 { + fill: #525252; + background: #525252; + stroke: #525252; +} +.Greys.q0-5 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.Greys.q1-5 { + fill: #cccccc; + background: #cccccc; + stroke: #cccccc; +} +.Greys.q2-5 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q3-5 { + fill: #636363; + background: #636363; + stroke: #636363; +} +.Greys.q4-5 { + fill: #252525; + background: #252525; + stroke: #252525; +} +.Greys.q0-6 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.Greys.q1-6 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Greys.q2-6 { + fill: #bdbdbd; + background: #bdbdbd; + stroke: #bdbdbd; +} +.Greys.q3-6 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q4-6 { + fill: #636363; + background: #636363; + stroke: #636363; +} +.Greys.q5-6 { + fill: #252525; + background: #252525; + stroke: #252525; +} +.Greys.q0-7 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.Greys.q1-7 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Greys.q2-7 { + fill: #bdbdbd; + background: #bdbdbd; + stroke: #bdbdbd; +} +.Greys.q3-7 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q4-7 { + fill: #737373; + background: #737373; + stroke: #737373; +} +.Greys.q5-7 { + fill: #525252; + background: #525252; + stroke: #525252; +} +.Greys.q6-7 { + fill: #252525; + background: #252525; + stroke: #252525; +} +.Greys.q0-8 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.Greys.q1-8 { + fill: #f0f0f0; + background: #f0f0f0; + stroke: #f0f0f0; +} +.Greys.q2-8 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Greys.q3-8 { + fill: #bdbdbd; + background: #bdbdbd; + stroke: #bdbdbd; +} +.Greys.q4-8 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q5-8 { + fill: #737373; + background: #737373; + stroke: #737373; +} +.Greys.q6-8 { + fill: #525252; + background: #525252; + stroke: #525252; +} +.Greys.q7-8 { + fill: #252525; + background: #252525; + stroke: #252525; +} +.Greys.q0-9 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.Greys.q1-9 { + fill: #f0f0f0; + background: #f0f0f0; + stroke: #f0f0f0; +} +.Greys.q2-9 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Greys.q3-9 { + fill: #bdbdbd; + background: #bdbdbd; + stroke: #bdbdbd; +} +.Greys.q4-9 { + fill: #969696; + background: #969696; + stroke: #969696; +} +.Greys.q5-9 { + fill: #737373; + background: #737373; + stroke: #737373; +} +.Greys.q6-9 { + fill: #525252; + background: #525252; + stroke: #525252; +} +.Greys.q7-9 { + fill: #252525; + background: #252525; + stroke: #252525; +} +.Greys.q8-9 { + fill: #000000; + background: #000000; + stroke: #000000; +} +.PuOr.q0-3 { + fill: #f1a340; + background: #f1a340; + stroke: #f1a340; +} +.PuOr.q1-3 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PuOr.q2-3 { + fill: #998ec3; + background: #998ec3; + stroke: #998ec3; +} +.PuOr.q0-4 { + fill: #e66101; + background: #e66101; + stroke: #e66101; +} +.PuOr.q1-4 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q2-4 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q3-4 { + fill: #5e3c99; + background: #5e3c99; + stroke: #5e3c99; +} +.PuOr.q0-5 { + fill: #e66101; + background: #e66101; + stroke: #e66101; +} +.PuOr.q1-5 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q2-5 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PuOr.q3-5 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q4-5 { + fill: #5e3c99; + background: #5e3c99; + stroke: #5e3c99; +} +.PuOr.q0-6 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q1-6 { + fill: #f1a340; + background: #f1a340; + stroke: #f1a340; +} +.PuOr.q2-6 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q3-6 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q4-6 { + fill: #998ec3; + background: #998ec3; + stroke: #998ec3; +} +.PuOr.q5-6 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q0-7 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q1-7 { + fill: #f1a340; + background: #f1a340; + stroke: #f1a340; +} +.PuOr.q2-7 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q3-7 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PuOr.q4-7 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q5-7 { + fill: #998ec3; + background: #998ec3; + stroke: #998ec3; +} +.PuOr.q6-7 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q0-8 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q1-8 { + fill: #e08214; + background: #e08214; + stroke: #e08214; +} +.PuOr.q2-8 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q3-8 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q4-8 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q5-8 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q6-8 { + fill: #8073ac; + background: #8073ac; + stroke: #8073ac; +} +.PuOr.q7-8 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q0-9 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q1-9 { + fill: #e08214; + background: #e08214; + stroke: #e08214; +} +.PuOr.q2-9 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q3-9 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q4-9 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PuOr.q5-9 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q6-9 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q7-9 { + fill: #8073ac; + background: #8073ac; + stroke: #8073ac; +} +.PuOr.q8-9 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q0-10 { + fill: #7f3b08; + background: #7f3b08; + stroke: #7f3b08; +} +.PuOr.q1-10 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q2-10 { + fill: #e08214; + background: #e08214; + stroke: #e08214; +} +.PuOr.q3-10 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q4-10 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q5-10 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q6-10 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q7-10 { + fill: #8073ac; + background: #8073ac; + stroke: #8073ac; +} +.PuOr.q8-10 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q9-10 { + fill: #2d004b; + background: #2d004b; + stroke: #2d004b; +} +.PuOr.q0-11 { + fill: #7f3b08; + background: #7f3b08; + stroke: #7f3b08; +} +.PuOr.q1-11 { + fill: #b35806; + background: #b35806; + stroke: #b35806; +} +.PuOr.q2-11 { + fill: #e08214; + background: #e08214; + stroke: #e08214; +} +.PuOr.q3-11 { + fill: #fdb863; + background: #fdb863; + stroke: #fdb863; +} +.PuOr.q4-11 { + fill: #fee0b6; + background: #fee0b6; + stroke: #fee0b6; +} +.PuOr.q5-11 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PuOr.q6-11 { + fill: #d8daeb; + background: #d8daeb; + stroke: #d8daeb; +} +.PuOr.q7-11 { + fill: #b2abd2; + background: #b2abd2; + stroke: #b2abd2; +} +.PuOr.q8-11 { + fill: #8073ac; + background: #8073ac; + stroke: #8073ac; +} +.PuOr.q9-11 { + fill: #542788; + background: #542788; + stroke: #542788; +} +.PuOr.q10-11 { + fill: #2d004b; + background: #2d004b; + stroke: #2d004b; +} +.BrBG.q0-3 { + fill: #d8b365; + background: #d8b365; + stroke: #d8b365; +} +.BrBG.q1-3 { + fill: #f5f5f5; + background: #f5f5f5; + stroke: #f5f5f5; +} +.BrBG.q2-3 { + fill: #5ab4ac; + background: #5ab4ac; + stroke: #5ab4ac; +} +.BrBG.q0-4 { + fill: #a6611a; + background: #a6611a; + stroke: #a6611a; +} +.BrBG.q1-4 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q2-4 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q3-4 { + fill: #018571; + background: #018571; + stroke: #018571; +} +.BrBG.q0-5 { + fill: #a6611a; + background: #a6611a; + stroke: #a6611a; +} +.BrBG.q1-5 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q2-5 { + fill: #f5f5f5; + background: #f5f5f5; + stroke: #f5f5f5; +} +.BrBG.q3-5 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q4-5 { + fill: #018571; + background: #018571; + stroke: #018571; +} +.BrBG.q0-6 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q1-6 { + fill: #d8b365; + background: #d8b365; + stroke: #d8b365; +} +.BrBG.q2-6 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q3-6 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q4-6 { + fill: #5ab4ac; + background: #5ab4ac; + stroke: #5ab4ac; +} +.BrBG.q5-6 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q0-7 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q1-7 { + fill: #d8b365; + background: #d8b365; + stroke: #d8b365; +} +.BrBG.q2-7 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q3-7 { + fill: #f5f5f5; + background: #f5f5f5; + stroke: #f5f5f5; +} +.BrBG.q4-7 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q5-7 { + fill: #5ab4ac; + background: #5ab4ac; + stroke: #5ab4ac; +} +.BrBG.q6-7 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q0-8 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q1-8 { + fill: #bf812d; + background: #bf812d; + stroke: #bf812d; +} +.BrBG.q2-8 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q3-8 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q4-8 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q5-8 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q6-8 { + fill: #35978f; + background: #35978f; + stroke: #35978f; +} +.BrBG.q7-8 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q0-9 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q1-9 { + fill: #bf812d; + background: #bf812d; + stroke: #bf812d; +} +.BrBG.q2-9 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q3-9 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q4-9 { + fill: #f5f5f5; + background: #f5f5f5; + stroke: #f5f5f5; +} +.BrBG.q5-9 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q6-9 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q7-9 { + fill: #35978f; + background: #35978f; + stroke: #35978f; +} +.BrBG.q8-9 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q0-10 { + fill: #543005; + background: #543005; + stroke: #543005; +} +.BrBG.q1-10 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q2-10 { + fill: #bf812d; + background: #bf812d; + stroke: #bf812d; +} +.BrBG.q3-10 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q4-10 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q5-10 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q6-10 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q7-10 { + fill: #35978f; + background: #35978f; + stroke: #35978f; +} +.BrBG.q8-10 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q9-10 { + fill: #003c30; + background: #003c30; + stroke: #003c30; +} +.BrBG.q0-11 { + fill: #543005; + background: #543005; + stroke: #543005; +} +.BrBG.q1-11 { + fill: #8c510a; + background: #8c510a; + stroke: #8c510a; +} +.BrBG.q2-11 { + fill: #bf812d; + background: #bf812d; + stroke: #bf812d; +} +.BrBG.q3-11 { + fill: #dfc27d; + background: #dfc27d; + stroke: #dfc27d; +} +.BrBG.q4-11 { + fill: #f6e8c3; + background: #f6e8c3; + stroke: #f6e8c3; +} +.BrBG.q5-11 { + fill: #f5f5f5; + background: #f5f5f5; + stroke: #f5f5f5; +} +.BrBG.q6-11 { + fill: #c7eae5; + background: #c7eae5; + stroke: #c7eae5; +} +.BrBG.q7-11 { + fill: #80cdc1; + background: #80cdc1; + stroke: #80cdc1; +} +.BrBG.q8-11 { + fill: #35978f; + background: #35978f; + stroke: #35978f; +} +.BrBG.q9-11 { + fill: #01665e; + background: #01665e; + stroke: #01665e; +} +.BrBG.q10-11 { + fill: #003c30; + background: #003c30; + stroke: #003c30; +} +.PRGn.q0-3 { + fill: #af8dc3; + background: #af8dc3; + stroke: #af8dc3; +} +.PRGn.q1-3 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PRGn.q2-3 { + fill: #7fbf7b; + background: #7fbf7b; + stroke: #7fbf7b; +} +.PRGn.q0-4 { + fill: #7b3294; + background: #7b3294; + stroke: #7b3294; +} +.PRGn.q1-4 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q2-4 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q3-4 { + fill: #008837; + background: #008837; + stroke: #008837; +} +.PRGn.q0-5 { + fill: #7b3294; + background: #7b3294; + stroke: #7b3294; +} +.PRGn.q1-5 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q2-5 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PRGn.q3-5 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q4-5 { + fill: #008837; + background: #008837; + stroke: #008837; +} +.PRGn.q0-6 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q1-6 { + fill: #af8dc3; + background: #af8dc3; + stroke: #af8dc3; +} +.PRGn.q2-6 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q3-6 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q4-6 { + fill: #7fbf7b; + background: #7fbf7b; + stroke: #7fbf7b; +} +.PRGn.q5-6 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q0-7 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q1-7 { + fill: #af8dc3; + background: #af8dc3; + stroke: #af8dc3; +} +.PRGn.q2-7 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q3-7 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PRGn.q4-7 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q5-7 { + fill: #7fbf7b; + background: #7fbf7b; + stroke: #7fbf7b; +} +.PRGn.q6-7 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q0-8 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q1-8 { + fill: #9970ab; + background: #9970ab; + stroke: #9970ab; +} +.PRGn.q2-8 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q3-8 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q4-8 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q5-8 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q6-8 { + fill: #5aae61; + background: #5aae61; + stroke: #5aae61; +} +.PRGn.q7-8 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q0-9 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q1-9 { + fill: #9970ab; + background: #9970ab; + stroke: #9970ab; +} +.PRGn.q2-9 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q3-9 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q4-9 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PRGn.q5-9 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q6-9 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q7-9 { + fill: #5aae61; + background: #5aae61; + stroke: #5aae61; +} +.PRGn.q8-9 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q0-10 { + fill: #40004b; + background: #40004b; + stroke: #40004b; +} +.PRGn.q1-10 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q2-10 { + fill: #9970ab; + background: #9970ab; + stroke: #9970ab; +} +.PRGn.q3-10 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q4-10 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q5-10 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q6-10 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q7-10 { + fill: #5aae61; + background: #5aae61; + stroke: #5aae61; +} +.PRGn.q8-10 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q9-10 { + fill: #00441b; + background: #00441b; + stroke: #00441b; +} +.PRGn.q0-11 { + fill: #40004b; + background: #40004b; + stroke: #40004b; +} +.PRGn.q1-11 { + fill: #762a83; + background: #762a83; + stroke: #762a83; +} +.PRGn.q2-11 { + fill: #9970ab; + background: #9970ab; + stroke: #9970ab; +} +.PRGn.q3-11 { + fill: #c2a5cf; + background: #c2a5cf; + stroke: #c2a5cf; +} +.PRGn.q4-11 { + fill: #e7d4e8; + background: #e7d4e8; + stroke: #e7d4e8; +} +.PRGn.q5-11 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PRGn.q6-11 { + fill: #d9f0d3; + background: #d9f0d3; + stroke: #d9f0d3; +} +.PRGn.q7-11 { + fill: #a6dba0; + background: #a6dba0; + stroke: #a6dba0; +} +.PRGn.q8-11 { + fill: #5aae61; + background: #5aae61; + stroke: #5aae61; +} +.PRGn.q9-11 { + fill: #1b7837; + background: #1b7837; + stroke: #1b7837; +} +.PRGn.q10-11 { + fill: #00441b; + background: #00441b; + stroke: #00441b; +} +.PiYG.q0-3 { + fill: #e9a3c9; + background: #e9a3c9; + stroke: #e9a3c9; +} +.PiYG.q1-3 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PiYG.q2-3 { + fill: #a1d76a; + background: #a1d76a; + stroke: #a1d76a; +} +.PiYG.q0-4 { + fill: #d01c8b; + background: #d01c8b; + stroke: #d01c8b; +} +.PiYG.q1-4 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q2-4 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q3-4 { + fill: #4dac26; + background: #4dac26; + stroke: #4dac26; +} +.PiYG.q0-5 { + fill: #d01c8b; + background: #d01c8b; + stroke: #d01c8b; +} +.PiYG.q1-5 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q2-5 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PiYG.q3-5 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q4-5 { + fill: #4dac26; + background: #4dac26; + stroke: #4dac26; +} +.PiYG.q0-6 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q1-6 { + fill: #e9a3c9; + background: #e9a3c9; + stroke: #e9a3c9; +} +.PiYG.q2-6 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q3-6 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q4-6 { + fill: #a1d76a; + background: #a1d76a; + stroke: #a1d76a; +} +.PiYG.q5-6 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q0-7 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q1-7 { + fill: #e9a3c9; + background: #e9a3c9; + stroke: #e9a3c9; +} +.PiYG.q2-7 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q3-7 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PiYG.q4-7 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q5-7 { + fill: #a1d76a; + background: #a1d76a; + stroke: #a1d76a; +} +.PiYG.q6-7 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q0-8 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q1-8 { + fill: #de77ae; + background: #de77ae; + stroke: #de77ae; +} +.PiYG.q2-8 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q3-8 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q4-8 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q5-8 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q6-8 { + fill: #7fbc41; + background: #7fbc41; + stroke: #7fbc41; +} +.PiYG.q7-8 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q0-9 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q1-9 { + fill: #de77ae; + background: #de77ae; + stroke: #de77ae; +} +.PiYG.q2-9 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q3-9 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q4-9 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PiYG.q5-9 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q6-9 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q7-9 { + fill: #7fbc41; + background: #7fbc41; + stroke: #7fbc41; +} +.PiYG.q8-9 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q0-10 { + fill: #8e0152; + background: #8e0152; + stroke: #8e0152; +} +.PiYG.q1-10 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q2-10 { + fill: #de77ae; + background: #de77ae; + stroke: #de77ae; +} +.PiYG.q3-10 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q4-10 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q5-10 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q6-10 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q7-10 { + fill: #7fbc41; + background: #7fbc41; + stroke: #7fbc41; +} +.PiYG.q8-10 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q9-10 { + fill: #276419; + background: #276419; + stroke: #276419; +} +.PiYG.q0-11 { + fill: #8e0152; + background: #8e0152; + stroke: #8e0152; +} +.PiYG.q1-11 { + fill: #c51b7d; + background: #c51b7d; + stroke: #c51b7d; +} +.PiYG.q2-11 { + fill: #de77ae; + background: #de77ae; + stroke: #de77ae; +} +.PiYG.q3-11 { + fill: #f1b6da; + background: #f1b6da; + stroke: #f1b6da; +} +.PiYG.q4-11 { + fill: #fde0ef; + background: #fde0ef; + stroke: #fde0ef; +} +.PiYG.q5-11 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.PiYG.q6-11 { + fill: #e6f5d0; + background: #e6f5d0; + stroke: #e6f5d0; +} +.PiYG.q7-11 { + fill: #b8e186; + background: #b8e186; + stroke: #b8e186; +} +.PiYG.q8-11 { + fill: #7fbc41; + background: #7fbc41; + stroke: #7fbc41; +} +.PiYG.q9-11 { + fill: #4d9221; + background: #4d9221; + stroke: #4d9221; +} +.PiYG.q10-11 { + fill: #276419; + background: #276419; + stroke: #276419; +} +.RdBu.q0-3 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdBu.q1-3 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.RdBu.q2-3 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.RdBu.q0-4 { + fill: #ca0020; + background: #ca0020; + stroke: #ca0020; +} +.RdBu.q1-4 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q2-4 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q3-4 { + fill: #0571b0; + background: #0571b0; + stroke: #0571b0; +} +.RdBu.q0-5 { + fill: #ca0020; + background: #ca0020; + stroke: #ca0020; +} +.RdBu.q1-5 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q2-5 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.RdBu.q3-5 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q4-5 { + fill: #0571b0; + background: #0571b0; + stroke: #0571b0; +} +.RdBu.q0-6 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q1-6 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdBu.q2-6 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q3-6 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q4-6 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.RdBu.q5-6 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q0-7 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q1-7 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdBu.q2-7 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q3-7 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.RdBu.q4-7 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q5-7 { + fill: #67a9cf; + background: #67a9cf; + stroke: #67a9cf; +} +.RdBu.q6-7 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q0-8 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q1-8 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdBu.q2-8 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q3-8 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q4-8 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q5-8 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q6-8 { + fill: #4393c3; + background: #4393c3; + stroke: #4393c3; +} +.RdBu.q7-8 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q0-9 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q1-9 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdBu.q2-9 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q3-9 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q4-9 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.RdBu.q5-9 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q6-9 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q7-9 { + fill: #4393c3; + background: #4393c3; + stroke: #4393c3; +} +.RdBu.q8-9 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q0-10 { + fill: #67001f; + background: #67001f; + stroke: #67001f; +} +.RdBu.q1-10 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q2-10 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdBu.q3-10 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q4-10 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q5-10 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q6-10 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q7-10 { + fill: #4393c3; + background: #4393c3; + stroke: #4393c3; +} +.RdBu.q8-10 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q9-10 { + fill: #053061; + background: #053061; + stroke: #053061; +} +.RdBu.q0-11 { + fill: #67001f; + background: #67001f; + stroke: #67001f; +} +.RdBu.q1-11 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdBu.q2-11 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdBu.q3-11 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdBu.q4-11 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdBu.q5-11 { + fill: #f7f7f7; + background: #f7f7f7; + stroke: #f7f7f7; +} +.RdBu.q6-11 { + fill: #d1e5f0; + background: #d1e5f0; + stroke: #d1e5f0; +} +.RdBu.q7-11 { + fill: #92c5de; + background: #92c5de; + stroke: #92c5de; +} +.RdBu.q8-11 { + fill: #4393c3; + background: #4393c3; + stroke: #4393c3; +} +.RdBu.q9-11 { + fill: #2166ac; + background: #2166ac; + stroke: #2166ac; +} +.RdBu.q10-11 { + fill: #053061; + background: #053061; + stroke: #053061; +} +.RdGy.q0-3 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdGy.q1-3 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.RdGy.q2-3 { + fill: #999999; + background: #999999; + stroke: #999999; +} +.RdGy.q0-4 { + fill: #ca0020; + background: #ca0020; + stroke: #ca0020; +} +.RdGy.q1-4 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q2-4 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q3-4 { + fill: #404040; + background: #404040; + stroke: #404040; +} +.RdGy.q0-5 { + fill: #ca0020; + background: #ca0020; + stroke: #ca0020; +} +.RdGy.q1-5 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q2-5 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.RdGy.q3-5 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q4-5 { + fill: #404040; + background: #404040; + stroke: #404040; +} +.RdGy.q0-6 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q1-6 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdGy.q2-6 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q3-6 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q4-6 { + fill: #999999; + background: #999999; + stroke: #999999; +} +.RdGy.q5-6 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q0-7 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q1-7 { + fill: #ef8a62; + background: #ef8a62; + stroke: #ef8a62; +} +.RdGy.q2-7 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q3-7 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.RdGy.q4-7 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q5-7 { + fill: #999999; + background: #999999; + stroke: #999999; +} +.RdGy.q6-7 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q0-8 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q1-8 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdGy.q2-8 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q3-8 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q4-8 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q5-8 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q6-8 { + fill: #878787; + background: #878787; + stroke: #878787; +} +.RdGy.q7-8 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q0-9 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q1-9 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdGy.q2-9 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q3-9 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q4-9 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.RdGy.q5-9 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q6-9 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q7-9 { + fill: #878787; + background: #878787; + stroke: #878787; +} +.RdGy.q8-9 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q0-10 { + fill: #67001f; + background: #67001f; + stroke: #67001f; +} +.RdGy.q1-10 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q2-10 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdGy.q3-10 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q4-10 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q5-10 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q6-10 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q7-10 { + fill: #878787; + background: #878787; + stroke: #878787; +} +.RdGy.q8-10 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q9-10 { + fill: #1a1a1a; + background: #1a1a1a; + stroke: #1a1a1a; +} +.RdGy.q0-11 { + fill: #67001f; + background: #67001f; + stroke: #67001f; +} +.RdGy.q1-11 { + fill: #b2182b; + background: #b2182b; + stroke: #b2182b; +} +.RdGy.q2-11 { + fill: #d6604d; + background: #d6604d; + stroke: #d6604d; +} +.RdGy.q3-11 { + fill: #f4a582; + background: #f4a582; + stroke: #f4a582; +} +.RdGy.q4-11 { + fill: #fddbc7; + background: #fddbc7; + stroke: #fddbc7; +} +.RdGy.q5-11 { + fill: #ffffff; + background: #ffffff; + stroke: #ffffff; +} +.RdGy.q6-11 { + fill: #e0e0e0; + background: #e0e0e0; + stroke: #e0e0e0; +} +.RdGy.q7-11 { + fill: #bababa; + background: #bababa; + stroke: #bababa; +} +.RdGy.q8-11 { + fill: #878787; + background: #878787; + stroke: #878787; +} +.RdGy.q9-11 { + fill: #4d4d4d; + background: #4d4d4d; + stroke: #4d4d4d; +} +.RdGy.q10-11 { + fill: #1a1a1a; + background: #1a1a1a; + stroke: #1a1a1a; +} +.RdYlBu.q0-3 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlBu.q1-3 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlBu.q2-3 { + fill: #91bfdb; + background: #91bfdb; + stroke: #91bfdb; +} +.RdYlBu.q0-4 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.RdYlBu.q1-4 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q2-4 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q3-4 { + fill: #2c7bb6; + background: #2c7bb6; + stroke: #2c7bb6; +} +.RdYlBu.q0-5 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.RdYlBu.q1-5 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q2-5 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlBu.q3-5 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q4-5 { + fill: #2c7bb6; + background: #2c7bb6; + stroke: #2c7bb6; +} +.RdYlBu.q0-6 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q1-6 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlBu.q2-6 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q3-6 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q4-6 { + fill: #91bfdb; + background: #91bfdb; + stroke: #91bfdb; +} +.RdYlBu.q5-6 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q0-7 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q1-7 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlBu.q2-7 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q3-7 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlBu.q4-7 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q5-7 { + fill: #91bfdb; + background: #91bfdb; + stroke: #91bfdb; +} +.RdYlBu.q6-7 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q0-8 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q1-8 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlBu.q2-8 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q3-8 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q4-8 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q5-8 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q6-8 { + fill: #74add1; + background: #74add1; + stroke: #74add1; +} +.RdYlBu.q7-8 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q0-9 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q1-9 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlBu.q2-9 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q3-9 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q4-9 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlBu.q5-9 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q6-9 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q7-9 { + fill: #74add1; + background: #74add1; + stroke: #74add1; +} +.RdYlBu.q8-9 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q0-10 { + fill: #a50026; + background: #a50026; + stroke: #a50026; +} +.RdYlBu.q1-10 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q2-10 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlBu.q3-10 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q4-10 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q5-10 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q6-10 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q7-10 { + fill: #74add1; + background: #74add1; + stroke: #74add1; +} +.RdYlBu.q8-10 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q9-10 { + fill: #313695; + background: #313695; + stroke: #313695; +} +.RdYlBu.q0-11 { + fill: #a50026; + background: #a50026; + stroke: #a50026; +} +.RdYlBu.q1-11 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlBu.q2-11 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlBu.q3-11 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlBu.q4-11 { + fill: #fee090; + background: #fee090; + stroke: #fee090; +} +.RdYlBu.q5-11 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlBu.q6-11 { + fill: #e0f3f8; + background: #e0f3f8; + stroke: #e0f3f8; +} +.RdYlBu.q7-11 { + fill: #abd9e9; + background: #abd9e9; + stroke: #abd9e9; +} +.RdYlBu.q8-11 { + fill: #74add1; + background: #74add1; + stroke: #74add1; +} +.RdYlBu.q9-11 { + fill: #4575b4; + background: #4575b4; + stroke: #4575b4; +} +.RdYlBu.q10-11 { + fill: #313695; + background: #313695; + stroke: #313695; +} +.Spectral.q0-3 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.Spectral.q1-3 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.Spectral.q2-3 { + fill: #99d594; + background: #99d594; + stroke: #99d594; +} +.Spectral.q0-4 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.Spectral.q1-4 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q2-4 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q3-4 { + fill: #2b83ba; + background: #2b83ba; + stroke: #2b83ba; +} +.Spectral.q0-5 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.Spectral.q1-5 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q2-5 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.Spectral.q3-5 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q4-5 { + fill: #2b83ba; + background: #2b83ba; + stroke: #2b83ba; +} +.Spectral.q0-6 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q1-6 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.Spectral.q2-6 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q3-6 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q4-6 { + fill: #99d594; + background: #99d594; + stroke: #99d594; +} +.Spectral.q5-6 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q0-7 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q1-7 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.Spectral.q2-7 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q3-7 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.Spectral.q4-7 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q5-7 { + fill: #99d594; + background: #99d594; + stroke: #99d594; +} +.Spectral.q6-7 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q0-8 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q1-8 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.Spectral.q2-8 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q3-8 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q4-8 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q5-8 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q6-8 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Spectral.q7-8 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q0-9 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q1-9 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.Spectral.q2-9 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q3-9 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q4-9 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.Spectral.q5-9 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q6-9 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q7-9 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Spectral.q8-9 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q0-10 { + fill: #9e0142; + background: #9e0142; + stroke: #9e0142; +} +.Spectral.q1-10 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q2-10 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.Spectral.q3-10 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q4-10 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q5-10 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q6-10 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q7-10 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Spectral.q8-10 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q9-10 { + fill: #5e4fa2; + background: #5e4fa2; + stroke: #5e4fa2; +} +.Spectral.q0-11 { + fill: #9e0142; + background: #9e0142; + stroke: #9e0142; +} +.Spectral.q1-11 { + fill: #d53e4f; + background: #d53e4f; + stroke: #d53e4f; +} +.Spectral.q2-11 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.Spectral.q3-11 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.Spectral.q4-11 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.Spectral.q5-11 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.Spectral.q6-11 { + fill: #e6f598; + background: #e6f598; + stroke: #e6f598; +} +.Spectral.q7-11 { + fill: #abdda4; + background: #abdda4; + stroke: #abdda4; +} +.Spectral.q8-11 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Spectral.q9-11 { + fill: #3288bd; + background: #3288bd; + stroke: #3288bd; +} +.Spectral.q10-11 { + fill: #5e4fa2; + background: #5e4fa2; + stroke: #5e4fa2; +} +.RdYlGn.q0-3 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlGn.q1-3 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlGn.q2-3 { + fill: #91cf60; + background: #91cf60; + stroke: #91cf60; +} +.RdYlGn.q0-4 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.RdYlGn.q1-4 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q2-4 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q3-4 { + fill: #1a9641; + background: #1a9641; + stroke: #1a9641; +} +.RdYlGn.q0-5 { + fill: #d7191c; + background: #d7191c; + stroke: #d7191c; +} +.RdYlGn.q1-5 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q2-5 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlGn.q3-5 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q4-5 { + fill: #1a9641; + background: #1a9641; + stroke: #1a9641; +} +.RdYlGn.q0-6 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q1-6 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlGn.q2-6 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q3-6 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q4-6 { + fill: #91cf60; + background: #91cf60; + stroke: #91cf60; +} +.RdYlGn.q5-6 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q0-7 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q1-7 { + fill: #fc8d59; + background: #fc8d59; + stroke: #fc8d59; +} +.RdYlGn.q2-7 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q3-7 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlGn.q4-7 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q5-7 { + fill: #91cf60; + background: #91cf60; + stroke: #91cf60; +} +.RdYlGn.q6-7 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q0-8 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q1-8 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlGn.q2-8 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q3-8 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q4-8 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q5-8 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q6-8 { + fill: #66bd63; + background: #66bd63; + stroke: #66bd63; +} +.RdYlGn.q7-8 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q0-9 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q1-9 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlGn.q2-9 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q3-9 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q4-9 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlGn.q5-9 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q6-9 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q7-9 { + fill: #66bd63; + background: #66bd63; + stroke: #66bd63; +} +.RdYlGn.q8-9 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q0-10 { + fill: #a50026; + background: #a50026; + stroke: #a50026; +} +.RdYlGn.q1-10 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q2-10 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlGn.q3-10 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q4-10 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q5-10 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q6-10 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q7-10 { + fill: #66bd63; + background: #66bd63; + stroke: #66bd63; +} +.RdYlGn.q8-10 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q9-10 { + fill: #006837; + background: #006837; + stroke: #006837; +} +.RdYlGn.q0-11 { + fill: #a50026; + background: #a50026; + stroke: #a50026; +} +.RdYlGn.q1-11 { + fill: #d73027; + background: #d73027; + stroke: #d73027; +} +.RdYlGn.q2-11 { + fill: #f46d43; + background: #f46d43; + stroke: #f46d43; +} +.RdYlGn.q3-11 { + fill: #fdae61; + background: #fdae61; + stroke: #fdae61; +} +.RdYlGn.q4-11 { + fill: #fee08b; + background: #fee08b; + stroke: #fee08b; +} +.RdYlGn.q5-11 { + fill: #ffffbf; + background: #ffffbf; + stroke: #ffffbf; +} +.RdYlGn.q6-11 { + fill: #d9ef8b; + background: #d9ef8b; + stroke: #d9ef8b; +} +.RdYlGn.q7-11 { + fill: #a6d96a; + background: #a6d96a; + stroke: #a6d96a; +} +.RdYlGn.q8-11 { + fill: #66bd63; + background: #66bd63; + stroke: #66bd63; +} +.RdYlGn.q9-11 { + fill: #1a9850; + background: #1a9850; + stroke: #1a9850; +} +.RdYlGn.q10-11 { + fill: #006837; + background: #006837; + stroke: #006837; +} +.Accent.q0-3 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-3 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-3 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q0-4 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-4 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-4 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q3-4 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Accent.q0-5 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-5 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-5 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q3-5 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Accent.q4-5 { + fill: #386cb0; + background: #386cb0; + stroke: #386cb0; +} +.Accent.q0-6 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-6 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-6 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q3-6 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Accent.q4-6 { + fill: #386cb0; + background: #386cb0; + stroke: #386cb0; +} +.Accent.q5-6 { + fill: #f0027f; + background: #f0027f; + stroke: #f0027f; +} +.Accent.q0-7 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-7 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-7 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q3-7 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Accent.q4-7 { + fill: #386cb0; + background: #386cb0; + stroke: #386cb0; +} +.Accent.q5-7 { + fill: #f0027f; + background: #f0027f; + stroke: #f0027f; +} +.Accent.q6-7 { + fill: #bf5b17; + background: #bf5b17; + stroke: #bf5b17; +} +.Accent.q0-8 { + fill: #7fc97f; + background: #7fc97f; + stroke: #7fc97f; +} +.Accent.q1-8 { + fill: #beaed4; + background: #beaed4; + stroke: #beaed4; +} +.Accent.q2-8 { + fill: #fdc086; + background: #fdc086; + stroke: #fdc086; +} +.Accent.q3-8 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Accent.q4-8 { + fill: #386cb0; + background: #386cb0; + stroke: #386cb0; +} +.Accent.q5-8 { + fill: #f0027f; + background: #f0027f; + stroke: #f0027f; +} +.Accent.q6-8 { + fill: #bf5b17; + background: #bf5b17; + stroke: #bf5b17; +} +.Accent.q7-8 { + fill: #666666; + background: #666666; + stroke: #666666; +} +.Dark2.q0-3 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-3 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-3 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q0-4 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-4 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-4 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q3-4 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.Dark2.q0-5 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-5 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-5 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q3-5 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.Dark2.q4-5 { + fill: #66a61e; + background: #66a61e; + stroke: #66a61e; +} +.Dark2.q0-6 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-6 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-6 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q3-6 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.Dark2.q4-6 { + fill: #66a61e; + background: #66a61e; + stroke: #66a61e; +} +.Dark2.q5-6 { + fill: #e6ab02; + background: #e6ab02; + stroke: #e6ab02; +} +.Dark2.q0-7 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-7 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-7 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q3-7 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.Dark2.q4-7 { + fill: #66a61e; + background: #66a61e; + stroke: #66a61e; +} +.Dark2.q5-7 { + fill: #e6ab02; + background: #e6ab02; + stroke: #e6ab02; +} +.Dark2.q6-7 { + fill: #a6761d; + background: #a6761d; + stroke: #a6761d; +} +.Dark2.q0-8 { + fill: #1b9e77; + background: #1b9e77; + stroke: #1b9e77; +} +.Dark2.q1-8 { + fill: #d95f02; + background: #d95f02; + stroke: #d95f02; +} +.Dark2.q2-8 { + fill: #7570b3; + background: #7570b3; + stroke: #7570b3; +} +.Dark2.q3-8 { + fill: #e7298a; + background: #e7298a; + stroke: #e7298a; +} +.Dark2.q4-8 { + fill: #66a61e; + background: #66a61e; + stroke: #66a61e; +} +.Dark2.q5-8 { + fill: #e6ab02; + background: #e6ab02; + stroke: #e6ab02; +} +.Dark2.q6-8 { + fill: #a6761d; + background: #a6761d; + stroke: #a6761d; +} +.Dark2.q7-8 { + fill: #666666; + background: #666666; + stroke: #666666; +} +.Paired.q0-3 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-3 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-3 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q0-4 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-4 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-4 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-4 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q0-5 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-5 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-5 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-5 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-5 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q0-6 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-6 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-6 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-6 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-6 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-6 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q0-7 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-7 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-7 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-7 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-7 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-7 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-7 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q0-8 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-8 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-8 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-8 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-8 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-8 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-8 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q7-8 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Paired.q0-9 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-9 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-9 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-9 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-9 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-9 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-9 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q7-9 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Paired.q8-9 { + fill: #cab2d6; + background: #cab2d6; + stroke: #cab2d6; +} +.Paired.q0-10 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-10 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-10 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-10 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-10 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-10 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-10 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q7-10 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Paired.q8-10 { + fill: #cab2d6; + background: #cab2d6; + stroke: #cab2d6; +} +.Paired.q9-10 { + fill: #6a3d9a; + background: #6a3d9a; + stroke: #6a3d9a; +} +.Paired.q0-11 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-11 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-11 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-11 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-11 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-11 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-11 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q7-11 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Paired.q8-11 { + fill: #cab2d6; + background: #cab2d6; + stroke: #cab2d6; +} +.Paired.q9-11 { + fill: #6a3d9a; + background: #6a3d9a; + stroke: #6a3d9a; +} +.Paired.q10-11 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Paired.q0-12 { + fill: #a6cee3; + background: #a6cee3; + stroke: #a6cee3; +} +.Paired.q1-12 { + fill: #1f78b4; + background: #1f78b4; + stroke: #1f78b4; +} +.Paired.q2-12 { + fill: #b2df8a; + background: #b2df8a; + stroke: #b2df8a; +} +.Paired.q3-12 { + fill: #33a02c; + background: #33a02c; + stroke: #33a02c; +} +.Paired.q4-12 { + fill: #fb9a99; + background: #fb9a99; + stroke: #fb9a99; +} +.Paired.q5-12 { + fill: #e31a1c; + background: #e31a1c; + stroke: #e31a1c; +} +.Paired.q6-12 { + fill: #fdbf6f; + background: #fdbf6f; + stroke: #fdbf6f; +} +.Paired.q7-12 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Paired.q8-12 { + fill: #cab2d6; + background: #cab2d6; + stroke: #cab2d6; +} +.Paired.q9-12 { + fill: #6a3d9a; + background: #6a3d9a; + stroke: #6a3d9a; +} +.Paired.q10-12 { + fill: #ffff99; + background: #ffff99; + stroke: #ffff99; +} +.Paired.q11-12 { + fill: #b15928; + background: #b15928; + stroke: #b15928; +} +.Pastel1.q0-3 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-3 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-3 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q0-4 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-4 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-4 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-4 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q0-5 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-5 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-5 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-5 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q4-5 { + fill: #fed9a6; + background: #fed9a6; + stroke: #fed9a6; +} +.Pastel1.q0-6 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-6 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-6 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-6 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q4-6 { + fill: #fed9a6; + background: #fed9a6; + stroke: #fed9a6; +} +.Pastel1.q5-6 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.Pastel1.q0-7 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-7 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-7 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-7 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q4-7 { + fill: #fed9a6; + background: #fed9a6; + stroke: #fed9a6; +} +.Pastel1.q5-7 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.Pastel1.q6-7 { + fill: #e5d8bd; + background: #e5d8bd; + stroke: #e5d8bd; +} +.Pastel1.q0-8 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-8 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-8 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-8 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q4-8 { + fill: #fed9a6; + background: #fed9a6; + stroke: #fed9a6; +} +.Pastel1.q5-8 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.Pastel1.q6-8 { + fill: #e5d8bd; + background: #e5d8bd; + stroke: #e5d8bd; +} +.Pastel1.q7-8 { + fill: #fddaec; + background: #fddaec; + stroke: #fddaec; +} +.Pastel1.q0-9 { + fill: #fbb4ae; + background: #fbb4ae; + stroke: #fbb4ae; +} +.Pastel1.q1-9 { + fill: #b3cde3; + background: #b3cde3; + stroke: #b3cde3; +} +.Pastel1.q2-9 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Pastel1.q3-9 { + fill: #decbe4; + background: #decbe4; + stroke: #decbe4; +} +.Pastel1.q4-9 { + fill: #fed9a6; + background: #fed9a6; + stroke: #fed9a6; +} +.Pastel1.q5-9 { + fill: #ffffcc; + background: #ffffcc; + stroke: #ffffcc; +} +.Pastel1.q6-9 { + fill: #e5d8bd; + background: #e5d8bd; + stroke: #e5d8bd; +} +.Pastel1.q7-9 { + fill: #fddaec; + background: #fddaec; + stroke: #fddaec; +} +.Pastel1.q8-9 { + fill: #f2f2f2; + background: #f2f2f2; + stroke: #f2f2f2; +} +.Pastel2.q0-3 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-3 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-3 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q0-4 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-4 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-4 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q3-4 { + fill: #f4cae4; + background: #f4cae4; + stroke: #f4cae4; +} +.Pastel2.q0-5 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-5 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-5 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q3-5 { + fill: #f4cae4; + background: #f4cae4; + stroke: #f4cae4; +} +.Pastel2.q4-5 { + fill: #e6f5c9; + background: #e6f5c9; + stroke: #e6f5c9; +} +.Pastel2.q0-6 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-6 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-6 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q3-6 { + fill: #f4cae4; + background: #f4cae4; + stroke: #f4cae4; +} +.Pastel2.q4-6 { + fill: #e6f5c9; + background: #e6f5c9; + stroke: #e6f5c9; +} +.Pastel2.q5-6 { + fill: #fff2ae; + background: #fff2ae; + stroke: #fff2ae; +} +.Pastel2.q0-7 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-7 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-7 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q3-7 { + fill: #f4cae4; + background: #f4cae4; + stroke: #f4cae4; +} +.Pastel2.q4-7 { + fill: #e6f5c9; + background: #e6f5c9; + stroke: #e6f5c9; +} +.Pastel2.q5-7 { + fill: #fff2ae; + background: #fff2ae; + stroke: #fff2ae; +} +.Pastel2.q6-7 { + fill: #f1e2cc; + background: #f1e2cc; + stroke: #f1e2cc; +} +.Pastel2.q0-8 { + fill: #b3e2cd; + background: #b3e2cd; + stroke: #b3e2cd; +} +.Pastel2.q1-8 { + fill: #fdcdac; + background: #fdcdac; + stroke: #fdcdac; +} +.Pastel2.q2-8 { + fill: #cbd5e8; + background: #cbd5e8; + stroke: #cbd5e8; +} +.Pastel2.q3-8 { + fill: #f4cae4; + background: #f4cae4; + stroke: #f4cae4; +} +.Pastel2.q4-8 { + fill: #e6f5c9; + background: #e6f5c9; + stroke: #e6f5c9; +} +.Pastel2.q5-8 { + fill: #fff2ae; + background: #fff2ae; + stroke: #fff2ae; +} +.Pastel2.q6-8 { + fill: #f1e2cc; + background: #f1e2cc; + stroke: #f1e2cc; +} +.Pastel2.q7-8 { + fill: #cccccc; + background: #cccccc; + stroke: #cccccc; +} +.Set1.q0-3 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-3 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-3 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q0-4 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-4 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-4 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-4 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q0-5 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-5 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-5 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-5 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q4-5 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Set1.q0-6 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-6 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-6 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-6 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q4-6 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Set1.q5-6 { + fill: #ffff33; + background: #ffff33; + stroke: #ffff33; +} +.Set1.q0-7 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-7 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-7 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-7 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q4-7 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Set1.q5-7 { + fill: #ffff33; + background: #ffff33; + stroke: #ffff33; +} +.Set1.q6-7 { + fill: #a65628; + background: #a65628; + stroke: #a65628; +} +.Set1.q0-8 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-8 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-8 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-8 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q4-8 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Set1.q5-8 { + fill: #ffff33; + background: #ffff33; + stroke: #ffff33; +} +.Set1.q6-8 { + fill: #a65628; + background: #a65628; + stroke: #a65628; +} +.Set1.q7-8 { + fill: #f781bf; + background: #f781bf; + stroke: #f781bf; +} +.Set1.q0-9 { + fill: #e41a1c; + background: #e41a1c; + stroke: #e41a1c; +} +.Set1.q1-9 { + fill: #377eb8; + background: #377eb8; + stroke: #377eb8; +} +.Set1.q2-9 { + fill: #4daf4a; + background: #4daf4a; + stroke: #4daf4a; +} +.Set1.q3-9 { + fill: #984ea3; + background: #984ea3; + stroke: #984ea3; +} +.Set1.q4-9 { + fill: #ff7f00; + background: #ff7f00; + stroke: #ff7f00; +} +.Set1.q5-9 { + fill: #ffff33; + background: #ffff33; + stroke: #ffff33; +} +.Set1.q6-9 { + fill: #a65628; + background: #a65628; + stroke: #a65628; +} +.Set1.q7-9 { + fill: #f781bf; + background: #f781bf; + stroke: #f781bf; +} +.Set1.q8-9 { + fill: #999999; + background: #999999; + stroke: #999999; +} +.Set2.q0-3 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-3 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-3 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q0-4 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-4 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-4 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q3-4 { + fill: #e78ac3; + background: #e78ac3; + stroke: #e78ac3; +} +.Set2.q0-5 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-5 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-5 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q3-5 { + fill: #e78ac3; + background: #e78ac3; + stroke: #e78ac3; +} +.Set2.q4-5 { + fill: #a6d854; + background: #a6d854; + stroke: #a6d854; +} +.Set2.q0-6 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-6 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-6 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q3-6 { + fill: #e78ac3; + background: #e78ac3; + stroke: #e78ac3; +} +.Set2.q4-6 { + fill: #a6d854; + background: #a6d854; + stroke: #a6d854; +} +.Set2.q5-6 { + fill: #ffd92f; + background: #ffd92f; + stroke: #ffd92f; +} +.Set2.q0-7 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-7 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-7 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q3-7 { + fill: #e78ac3; + background: #e78ac3; + stroke: #e78ac3; +} +.Set2.q4-7 { + fill: #a6d854; + background: #a6d854; + stroke: #a6d854; +} +.Set2.q5-7 { + fill: #ffd92f; + background: #ffd92f; + stroke: #ffd92f; +} +.Set2.q6-7 { + fill: #e5c494; + background: #e5c494; + stroke: #e5c494; +} +.Set2.q0-8 { + fill: #66c2a5; + background: #66c2a5; + stroke: #66c2a5; +} +.Set2.q1-8 { + fill: #fc8d62; + background: #fc8d62; + stroke: #fc8d62; +} +.Set2.q2-8 { + fill: #8da0cb; + background: #8da0cb; + stroke: #8da0cb; +} +.Set2.q3-8 { + fill: #e78ac3; + background: #e78ac3; + stroke: #e78ac3; +} +.Set2.q4-8 { + fill: #a6d854; + background: #a6d854; + stroke: #a6d854; +} +.Set2.q5-8 { + fill: #ffd92f; + background: #ffd92f; + stroke: #ffd92f; +} +.Set2.q6-8 { + fill: #e5c494; + background: #e5c494; + stroke: #e5c494; +} +.Set2.q7-8 { + fill: #b3b3b3; + background: #b3b3b3; + stroke: #b3b3b3; +} +.Set3.q0-3 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-3 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-3 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q0-4 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-4 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-4 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-4 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q0-5 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-5 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-5 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-5 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-5 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q0-6 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-6 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-6 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-6 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-6 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-6 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q0-7 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-7 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-7 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-7 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-7 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-7 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-7 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q0-8 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-8 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-8 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-8 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-8 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-8 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-8 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q7-8 { + fill: #fccde5; + background: #fccde5; + stroke: #fccde5; +} +.Set3.q0-9 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-9 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-9 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-9 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-9 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-9 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-9 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q7-9 { + fill: #fccde5; + background: #fccde5; + stroke: #fccde5; +} +.Set3.q8-9 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Set3.q0-10 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-10 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-10 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-10 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-10 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-10 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-10 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q7-10 { + fill: #fccde5; + background: #fccde5; + stroke: #fccde5; +} +.Set3.q8-10 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Set3.q9-10 { + fill: #bc80bd; + background: #bc80bd; + stroke: #bc80bd; +} +.Set3.q0-11 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-11 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-11 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-11 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-11 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-11 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-11 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q7-11 { + fill: #fccde5; + background: #fccde5; + stroke: #fccde5; +} +.Set3.q8-11 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Set3.q9-11 { + fill: #bc80bd; + background: #bc80bd; + stroke: #bc80bd; +} +.Set3.q10-11 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Set3.q0-12 { + fill: #8dd3c7; + background: #8dd3c7; + stroke: #8dd3c7; +} +.Set3.q1-12 { + fill: #ffffb3; + background: #ffffb3; + stroke: #ffffb3; +} +.Set3.q2-12 { + fill: #bebada; + background: #bebada; + stroke: #bebada; +} +.Set3.q3-12 { + fill: #fb8072; + background: #fb8072; + stroke: #fb8072; +} +.Set3.q4-12 { + fill: #80b1d3; + background: #80b1d3; + stroke: #80b1d3; +} +.Set3.q5-12 { + fill: #fdb462; + background: #fdb462; + stroke: #fdb462; +} +.Set3.q6-12 { + fill: #b3de69; + background: #b3de69; + stroke: #b3de69; +} +.Set3.q7-12 { + fill: #fccde5; + background: #fccde5; + stroke: #fccde5; +} +.Set3.q8-12 { + fill: #d9d9d9; + background: #d9d9d9; + stroke: #d9d9d9; +} +.Set3.q9-12 { + fill: #bc80bd; + background: #bc80bd; + stroke: #bc80bd; +} +.Set3.q10-12 { + fill: #ccebc5; + background: #ccebc5; + stroke: #ccebc5; +} +.Set3.q11-12 { + fill: #ffed6f; + background: #ffed6f; + stroke: #ffed6f; +} + diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 000000000..eb5ce5b03 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "SQLPad", + "name": "SQLPad", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/client/src/Routes.tsx b/client/src/Routes.tsx new file mode 100644 index 000000000..a582e90af --- /dev/null +++ b/client/src/Routes.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { + BrowserRouter as Router, + Redirect, + Route, + Switch, +} from 'react-router-dom'; +import PasswordReset from './pages/PasswordReset'; +import PasswordResetRequested from './pages/PasswordResetRequested'; +import QueryChartOnly from './pages/QueryChartOnly'; +import QueryEditorWrapper from './pages/QueryEditorWrapper'; +import QueryTableOnly from './pages/QueryTableOnly'; +import SignIn from './pages/SignIn'; +import SignUp from './pages/SignUp'; +import { RegisterHistory } from './utilities/history'; +import useAppContext from './utilities/use-app-context'; + +function Routes() { + const { config, currentUser } = useAppContext(); + + // If no config yet return null + // Config is needed in order to mount app at proper basename + if (!config) { + return null; + } + + // If not signed in only allow auth related routes + if (!currentUser) { + return ( + + + } /> + } /> + } + /> + } + /> + {/* If nothing matches redirect to signin */} + } /> + + + + ); + } + + return ( + + + {/* For /queries/new prevent a queryId from being captured via params */} + } /> + + } + /> + + } + /> + + } + /> + + {/* If nothing matches redirect to new query */} + } /> + + + + ); +} + +export default Routes; diff --git a/client/src/app-header/AboutContent.tsx b/client/src/app-header/AboutContent.tsx new file mode 100644 index 000000000..e424db9a0 --- /dev/null +++ b/client/src/app-header/AboutContent.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import OpenInNewIcon from 'mdi-react/OpenInNewIcon'; + +const LINK_STYLE = { display: 'inline-flex', alignItems: 'center' }; + +type Props = { + version?: string; +}; + +function AboutContent({ version = '' }: Props) { + return ( +
    +

    + Version: {version} +

    + + + +

    + Shortcuts +

    +
      +
    • + ctrl+s / command+s : Save +
    • +
    • + ctrl+return / command+return : Run +
    • +
    • + shift+return : Format +
    • +
    + +

    + Tip +

    +

    Run only a portion of a query by highlighting it first.

    +
    + ); +} + +export default AboutContent; diff --git a/client/src/app-header/AboutModal.tsx b/client/src/app-header/AboutModal.tsx new file mode 100644 index 000000000..f471544c2 --- /dev/null +++ b/client/src/app-header/AboutModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Modal from '../common/Modal'; +import useAppContext from '../utilities/use-app-context'; +import AboutContent from './AboutContent'; + +interface Props { + visible: boolean; + onClose: () => void; +} + +function AboutModal({ visible, onClose }: Props) { + const { version } = useAppContext(); + return ( + <> + + + + + ); +} + +export default React.memo(AboutModal); diff --git a/client/src/app-header/AppHeader.tsx b/client/src/app-header/AppHeader.tsx new file mode 100644 index 000000000..a1cdbceab --- /dev/null +++ b/client/src/app-header/AppHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import AppHeaderAdminSection from './AppHeaderAdminSection'; +import AppMenu from './AppMenu'; +import HistoryButton from './HistoryButton'; +import Logo from './Logo'; +import QueryListButton from './QueryListButton'; +import ToolbarNewQueryButton from './ToolbarNewQueryButton'; +import AppHeaderSpacer from './AppHeaderSpacer'; +import AppHeaderUser from './AppHeaderUser'; + +function Appheader() { + return ( +
    +
    + + + + + + + + +
    +
    + ); +} + +export default React.memo(Appheader); diff --git a/client/src/app-header/AppHeaderAdminSection.tsx b/client/src/app-header/AppHeaderAdminSection.tsx new file mode 100644 index 000000000..a56d64bc3 --- /dev/null +++ b/client/src/app-header/AppHeaderAdminSection.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import Button from '../common/Button'; +import Drawer from '../common/Drawer'; +import ConnectionAccessListDrawer from '../connectionAccesses/ConnectionAccessListDrawer'; +import ConnectionListDrawer from '../connections/ConnectionListDrawer'; +import ServiceTokenListDrawer from '../serviceTokens/ServiceTokenListDrawer'; +import UserList from '../users/UserList'; +import useAppContext from '../utilities/use-app-context'; +import AppHeaderDivider from './AppHeaderDivider'; + +function AppHeaderAdminSection() { + const { config, currentUser } = useAppContext(); + const [showConnections, setShowConnections] = useState(false); + const [showConnectionAccesses, setShowConnectionAccesses] = useState(false); + const [showUsers, setShowUsers] = useState(false); + const [showServiceTokens, setShowServiceTokens] = useState(false); + + if (!currentUser || currentUser.role !== 'admin') { + return null; + } + + let hideUsersButton = false; + if (currentUser.id === 'noauth') { + hideUsersButton = true; + } + const showServiceTokensButton = config?.showServiceTokensUI; + + return ( + <> + + + setShowConnections(false)} + /> + + setShowConnectionAccesses(false)} + /> + + setShowUsers(false)} + placement="left" + > + + + + setShowServiceTokens(false)} + /> + + ); +} + +export default React.memo(AppHeaderAdminSection); diff --git a/client/src/app-header/AppHeaderDivider.tsx b/client/src/app-header/AppHeaderDivider.tsx new file mode 100644 index 000000000..305cc25e1 --- /dev/null +++ b/client/src/app-header/AppHeaderDivider.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +function AppHeaderDivider() { + return ( +
    + ); +} + +export default AppHeaderDivider; diff --git a/client/src/app-header/AppHeaderSpacer.tsx b/client/src/app-header/AppHeaderSpacer.tsx new file mode 100644 index 000000000..211c20046 --- /dev/null +++ b/client/src/app-header/AppHeaderSpacer.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const growSpacerStyle = { flexShrink: 0, flexGrow: 1, width: 8 }; +const spacerStyle = { flexShrink: 0, width: 8 }; + +type Props = { + grow?: boolean; +}; + +function AppHeaderSpacer({ grow }: Props) { + if (grow) { + return
    ; + } + return
    ; +} + +export default AppHeaderSpacer; diff --git a/client/src/app-header/AppHeaderUser.module.css b/client/src/app-header/AppHeaderUser.module.css new file mode 100644 index 000000000..03c2c5996 --- /dev/null +++ b/client/src/app-header/AppHeaderUser.module.css @@ -0,0 +1,26 @@ +/* + this style derived from Button + Ghost. + User is not a clickable element at this time however +*/ +.style { + line-height: 1.499; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + border: 1px solid transparent; + user-select: none; + touch-action: manipulation; + height: 32px; + padding: 0 10px; + font-size: 14px; + border-radius: 2px; + color: rgba(255, 255, 255, 0.8); + box-shadow: 0 1px 0 transparent; + border-color: transparent; + background-color: transparent; +} diff --git a/client/src/app-header/AppHeaderUser.tsx b/client/src/app-header/AppHeaderUser.tsx new file mode 100644 index 000000000..c053e3f7d --- /dev/null +++ b/client/src/app-header/AppHeaderUser.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import useAppContext from '../utilities/use-app-context'; +import styles from './AppHeaderUser.module.css'; + +function UserButton() { + const { currentUser } = useAppContext(); + if (!currentUser || currentUser.id === 'noauth') { + return null; + } + return ( +
    + {' '} + {currentUser?.name || currentUser.email || currentUser.ldapId} +
    + ); +} + +export default React.memo(UserButton); diff --git a/client/src/app-header/AppMenu.tsx b/client/src/app-header/AppMenu.tsx new file mode 100644 index 000000000..a36bf31ec --- /dev/null +++ b/client/src/app-header/AppMenu.tsx @@ -0,0 +1,51 @@ +import { MenuItem } from '@reach/menu-button'; +import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import IconMenu from '../common/IconMenu'; +import { resetState } from '../stores/editor-actions'; +import UserProfileModal from '../users/UserProfileModal'; +import { api } from '../utilities/api'; +import useAppContext from '../utilities/use-app-context'; +import AboutModal from './AboutModal'; + +function AppMenu() { + const { currentUser } = useAppContext(); + const [showAbout, setShowAbout] = useState(false); + const [showProfile, setShowProfile] = useState(false); + const history = useHistory(); + + let hideUserItems = false; + if (!currentUser || currentUser.id === 'noauth') { + hideUserItems = true; + } + + return ( +
    + }> + + setShowAbout(true)}>About + + + + setShowAbout(false)} /> + setShowProfile(false)} + /> +
    + ); +} + +export default React.memo(AppMenu); diff --git a/client/src/app-header/HistoryButton.tsx b/client/src/app-header/HistoryButton.tsx new file mode 100644 index 000000000..03cbd46ee --- /dev/null +++ b/client/src/app-header/HistoryButton.tsx @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import Button from '../common/Button'; +import QueryHistoryModal from '../queryHistory/QueryHistoryModal'; +import useAppContext from '../utilities/use-app-context'; + +function HistoryButton() { + const { currentUser } = useAppContext(); + const [showQueryHistory, setShowQueryHistory] = useState(false); + + if (!currentUser) { + return null; + } + + // If an editor has no identity (e.g., logged in without authentication), query history is not available because it can not be distinguished from others'. + if (currentUser.id === 'noauth' && currentUser.role === 'editor') return null; + + return ( +
    + + setShowQueryHistory(false)} + /> +
    + ); +} + +export default React.memo(HistoryButton); diff --git a/client/src/app-header/Logo.module.css b/client/src/app-header/Logo.module.css new file mode 100644 index 000000000..fad6d62c5 --- /dev/null +++ b/client/src/app-header/Logo.module.css @@ -0,0 +1,26 @@ +.logo { + line-height: 1.499; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + user-select: none; + touch-action: manipulation; + padding: 0 10px; + font-size: 14px; + margin-left: -6px; + margin-top: -6px; + height: 44px; + margin-bottom: -6px; + margin-right: 6px; + box-shadow: none; + border-radius: 0; + color: #fff; + background-color: var(--primary-darkest-color); + border-color: var(--primary-color); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); +} diff --git a/client/src/app-header/Logo.tsx b/client/src/app-header/Logo.tsx new file mode 100644 index 000000000..211b3ff82 --- /dev/null +++ b/client/src/app-header/Logo.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './Logo.module.css'; + +/** + * A blue block that stretches beyond the padding in AppHeader + * It could show a logo someday, or maybe be a primary button if it did some action + */ +function Logo() { + return
    SQLPad
    ; +} + +export default Logo; diff --git a/client/src/app-header/QueryListButton.tsx b/client/src/app-header/QueryListButton.tsx new file mode 100644 index 000000000..f6fb4439d --- /dev/null +++ b/client/src/app-header/QueryListButton.tsx @@ -0,0 +1,21 @@ +import Button from '../common/Button'; +import React, { useState } from 'react'; +import QueryListDrawer from '../queries/QueryListDrawer'; + +function QueryListButton() { + const [showQueries, setShowQueries] = useState(false); + + return ( + <> + + setShowQueries(false)} + /> + + ); +} + +export default React.memo(QueryListButton); diff --git a/client/src/app-header/ToolbarNewQueryButton.tsx b/client/src/app-header/ToolbarNewQueryButton.tsx new file mode 100644 index 000000000..9aa4afe26 --- /dev/null +++ b/client/src/app-header/ToolbarNewQueryButton.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import ButtonLink from '../common/ButtonLink'; +import { resetNewQuery } from '../stores/editor-actions'; + +/** + * This link leverages the redirect to generate a new sessionId + */ +function ToolbarNewQueryButton() { + return ( + { + // If user is trying to open another tab, let the browser do that + if ( + e.ctrlKey || + e.metaKey || + e.altKey || + e.shiftKey || + e.button || + e.defaultPrevented + ) { + return; + } + + // Otherwise init a new query + // User could be going from new to new query, and it needs to be reset via function call + // We cannot rely on router to do this for us + resetNewQuery(); + }} + > + New + + ); +} + +export default React.memo(ToolbarNewQueryButton); diff --git a/client/src/common/Button.module.css b/client/src/common/Button.module.css new file mode 100644 index 000000000..ed0bbf22b --- /dev/null +++ b/client/src/common/Button.module.css @@ -0,0 +1,179 @@ +.btn { + line-height: 1.499; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + border: 1px solid transparent; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.065); + cursor: pointer; + user-select: none; + touch-action: manipulation; + height: 32px; + padding: 0 10px; + font-size: 14px; + border-radius: 2px; + color: rgba(0, 0, 0, 0.65); + background-color: #fff; + border-color: rgb(217, 217, 217); + border-top-color: rgb(217, 217, 217, 0.5); + border-left-color: rgb(217, 217, 217, 0.5); +} + +.btn:hover, +.btn:focus { + color: var(--primary-color); + background-color: #fff; + box-shadow: 0 1px 0 var(--primary-color-30); + border: 2px solid var(--primary-color); + /* padding adjustment prevents added border from shifting text */ + padding: 0 9px; +} + +.btn:active { + box-shadow: none; + background-color: #ebebeb; + color: var(--primary-dark-color); +} + +.btn:focus { + outline: 0; +} + +.primary { + color: #fff; + background-color: var(--primary-color); + border-color: var(--primary-color); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 1px 0 rgb(9, 100, 185, 0.5); +} + +.primary:focus { + color: #fff; + background-color: var(--primary-color); + border-color: var(--primary-color); + box-shadow: 0 0 0 2px var(--primary-focus); + z-index: 99999; +} + +.primary:hover { + color: #fff; + background-color: var(--primary-light-color); + border-color: var(--primary-light-color); + z-index: 99999; +} + +.primary:active { + color: #fff; + background-color: var(--primary-dark-color); + border-color: var(--primary-dark-color); +} + +.danger { + color: var(--secondary-color); + background-color: #f5f5f5; + border-color: #d9d9d9; +} + +.danger:hover { + color: #fff; + background-color: var(--secondary-color); + border-color: var(--secondary-color); +} + +.danger:focus { + color: var(--secondary-color); + background-color: #fff; + border-color: var(--secondary-color); +} + +.danger:active { + color: #fff; + background-color: var(--secondary-color); + border-color: var(--secondary-color); +} + +.btn:disabled, +.btn[disabled] { + color: rgba(0, 0, 0, 0.25); + background-color: #eee; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.065); + border: 1px solid transparent; + padding: 0 10px; + border-color: #d9d9d9; +} + +.ghost { + color: rgba(255, 255, 255, 0.8); + box-shadow: 0 1px 0 transparent; + border-color: transparent; + background-color: transparent; +} + +.ghost:hover, +.ghost:focus { + color: rgba(255, 255, 255, 0.85); + background-color: transparent; +} + +.ghost:active { + color: rgba(255, 255, 255, 1); + background-color: transparent; +} + +.ghost:disabled, +.ghost[disabled] { + border: none; + color: rgba(255, 255, 255, 0.55); +} + +.primaryGhost { + color: var(--primary-color); + box-shadow: 0 1px 0 transparent; + border-color: transparent; + background-color: transparent; +} + +.primaryGhost:hover, +.primaryGhost:focus { + color: var(--primary-dark-color); + background-color: transparent; +} + +.primaryGhost:active { + color: var(--primary-dark-color); + background-color: transparent; +} + +.primaryGhost:disabled, +.primaryGhost[disabled] { + border: none; + color: rgba(255, 255, 255, 0.55); +} + +.leftWithMenu { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-right: 1px solid rgba(255, 255, 255, 0.55); +} + +.menuButton { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + padding: 0 4px; +} + +.menuButton:hover, +.menuButton:focus { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + padding: 0 3px; +} + +.menuButton:disabled { + padding: 0 4px; +} diff --git a/client/src/common/Button.tsx b/client/src/common/Button.tsx new file mode 100644 index 000000000..4ff095a8d --- /dev/null +++ b/client/src/common/Button.tsx @@ -0,0 +1,83 @@ +import { Menu, MenuButton, MenuList } from '@reach/menu-button'; +import ChevronDownIcon from 'mdi-react/ChevronDownIcon'; +import React, { ReactNode } from 'react'; +import styles from './Button.module.css'; + +const ICON_SIZE = 18; + +export interface Props extends React.ButtonHTMLAttributes { + icon?: any; + variant?: string; + htmlType?: 'button' | 'submit' | 'reset' | undefined; + menuItems?: ReactNode[]; +} + +export type Ref = HTMLButtonElement; + +const Button = React.forwardRef( + ( + { + children, + icon, + variant, + htmlType, + disabled, + className, + menuItems, + ...rest + }, + ref + ) => { + const classNames = [styles.btn]; + + if (variant === 'primary') { + classNames.push(styles.primary); + } else if (variant === 'danger') { + classNames.push(styles.danger); + } else if (variant === 'ghost') { + classNames.push(styles.ghost); + } else if (variant === 'primary-ghost') { + classNames.push(styles.primaryGhost); + } + + if (className) { + classNames.push(className); + } + + const leftClassNames = [...classNames]; + const rightClassNames = [...classNames, styles.menuButton]; + + if (menuItems) { + leftClassNames.push(styles.leftWithMenu); + } + + return ( + <> + + {menuItems && menuItems.length > 0 && ( + + + + + {menuItems} + + )} + + ); + } +); + +export default Button; diff --git a/client/src/common/ButtonLink.module.css b/client/src/common/ButtonLink.module.css new file mode 100644 index 000000000..4befeecf7 --- /dev/null +++ b/client/src/common/ButtonLink.module.css @@ -0,0 +1,109 @@ +.btnLink { + line-height: 1.499; + position: relative; + display: inline-flex; + align-items: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + border: 1px solid transparent; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.065); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + user-select: none; + touch-action: manipulation; + height: 32px; + padding: 0 10px; + font-size: 14px; + border-radius: 2px; + color: rgba(0, 0, 0, 0.65); + background-color: #fff; + border-color: rgb(217, 217, 217); + border-top-color: rgb(217, 217, 217, 0.5); + border-left-color: rgb(217, 217, 217, 0.5); + outline: 0; +} + +.btnLink:hover, +.btnLink:focus { + color: #40a9ff; + background-color: #fff; + border-color: #40a9ff; + box-shadow: 0 1px 0 rgba(64, 169, 255, 0.5); + outline: 0; +} + +.btnLink:hover, +.btnLink:focus, +.btnLink:active { + text-decoration: none; + background: #fff; + outline: 0; +} + +.btnLink:focus { + outline: 0; +} + +.btnLink:disabled, +.btnLink[disabled] { + color: rgba(0, 0, 0, 0.25); + background-color: #eee; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.065); + border: 1px solid transparent; + border-color: #d9d9d9; +} + +.primary { + color: #fff; + background-color: var(--primary-color); + border-color: var(--primary-color); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 1px 0 rgb(9, 100, 185, 0.5); +} + +.primary:focus { + color: #fff; + background-color: var(--primary-color); + border-color: var(--primary-color); + box-shadow: 0 0 0 2px var(--primary-focus); + z-index: 99999; +} + +.primary:hover { + color: #fff; + background-color: var(--primary-light-color); + border-color: var(--primary-light-color); + z-index: 99999; +} + +.primary:active { + color: #fff; + background-color: var(--primary-dark-color); + border-color: var(--primary-dark-color); +} + +.ghost { + color: rgba(255, 255, 255, 0.8); + box-shadow: 0 1px 0 transparent; + border-color: transparent; + background-color: transparent; +} + +.ghost:hover, +.ghost:focus { + color: rgba(255, 255, 255, 0.85); + background-color: transparent; +} + +.ghost:active { + color: rgba(255, 255, 255, 1); + background-color: transparent; +} + +.ghost:disabled, +.ghost[disabled] { + border: none; + color: rgba(255, 255, 255, 0.55); +} diff --git a/client/src/common/ButtonLink.tsx b/client/src/common/ButtonLink.tsx new file mode 100644 index 000000000..52e76e555 --- /dev/null +++ b/client/src/common/ButtonLink.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import styles from './ButtonLink.module.css'; +import Tooltip from './Tooltip'; + +const ICON_SIZE = 20; + +export interface Props extends React.AnchorHTMLAttributes { + icon?: any; + variant?: string; + tooltip?: string; + to?: string; +} + +const ButtonLink = ({ + className, + children, + icon, + tooltip, + to, + href, + variant, + ...rest +}: Props) => { + const classNames = [styles.btnLink]; + + if (variant === 'primary') { + classNames.push(styles.primary); + } else if (variant === 'ghost') { + classNames.push(styles.ghost); + } + + if (className) { + classNames.push(className); + } + + let link; + if (href) { + link = ( + + {icon && React.cloneElement(icon, { size: ICON_SIZE }, null)} + {children && icon && } + {children} + + ); + } else { + link = ( + + {icon && React.cloneElement(icon, { size: ICON_SIZE }, null)} + {children && icon && } + {children} + + ); + } + + if (tooltip) { + return ( + + {link} + + ); + } + + return link; +}; + +export default ButtonLink; diff --git a/client/src/common/ClipboardButton.tsx b/client/src/common/ClipboardButton.tsx new file mode 100644 index 000000000..4f4d5ef9e --- /dev/null +++ b/client/src/common/ClipboardButton.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import ClipboardIcon from 'mdi-react/ClipboardIcon'; +import IconButton from './IconButton'; + +async function copyToClipboardModern(text: string) { + try { + await navigator.clipboard.writeText(text); + console.log('Text copied to clipboard'); + } catch (err) { + console.error('Failed to copy: ', err); + } +} + +export default function ClipboardButton({ + onCopyStart, +}: { + onCopyStart: () => string; +}) { + return ( + { + const text = onCopyStart(); + copyToClipboardModern(text); + }} + > + + + ); +} diff --git a/client/src/common/Code.module.css b/client/src/common/Code.module.css new file mode 100644 index 000000000..4d91428fa --- /dev/null +++ b/client/src/common/Code.module.css @@ -0,0 +1,21 @@ +.pre { + font-family: 'Courier 10 Pitch', Courier, monospace; + font-size: 95%; + line-height: 140%; + display: inline-block; + word-wrap: break-word; +} + +.code { + font-family: Monaco, Consolas, 'Andale Mono', 'DejaVu Sans Mono', monospace; + font-size: 95%; + line-height: 140%; + background: #faf8f0; + -ms-word-break: break-all; + word-break: break-all; + white-space: normal; +} + +.danger { + color: var(--secondary-color); +} diff --git a/client/src/common/Code.tsx b/client/src/common/Code.tsx new file mode 100644 index 000000000..8b8bb6029 --- /dev/null +++ b/client/src/common/Code.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styles from './Code.module.css'; + +export interface Props extends React.HTMLAttributes { + type?: string; +} + +const Code = ({ children, className, type, ...rest }: Props) => { + const cs = [styles.code]; + + if (className) { + cs.push(className); + } + + if (type === 'danger') { + cs.push(styles.danger); + } + + return ( +
    +      
    +        {children}
    +      
    +    
    + ); +}; + +export default Code; diff --git a/client/src/common/DataNotification.module.css b/client/src/common/DataNotification.module.css new file mode 100644 index 000000000..c0774fe89 --- /dev/null +++ b/client/src/common/DataNotification.module.css @@ -0,0 +1,10 @@ +.text { + margin-right: 0.5rem; + white-space: nowrap; +} + +.AlertIcon { + margin-right: 0.5rem; + position: relative; + top: 3px; +} diff --git a/client/src/common/DataNotification.tsx b/client/src/common/DataNotification.tsx new file mode 100644 index 000000000..90d684ac2 --- /dev/null +++ b/client/src/common/DataNotification.tsx @@ -0,0 +1,25 @@ +import AlertIcon from 'mdi-react/AlertIcon'; +import React from 'react'; +import Text from './Text'; +import Tooltip from './Tooltip'; +import styles from './DataNotification.module.css'; + +export interface Props extends React.HTMLAttributes { + tooltip: string; +} + +function DataNotification({ tooltip, children }: Props) { + return ( + + {/* span use in place of wrapping Text with forwardRef needed by Tooltip */} + + + + {children} + + + + ); +} + +export default DataNotification; diff --git a/client/src/common/DeleteConfirmButton.module.css b/client/src/common/DeleteConfirmButton.module.css new file mode 100644 index 000000000..b795fad36 --- /dev/null +++ b/client/src/common/DeleteConfirmButton.module.css @@ -0,0 +1,21 @@ +.Dialog { + width: 500px; + border-radius: 2px; + box-shadow: var(--box-shadow-2); +} + +.dialogContent { + display: flex; + justify-content: space-between; + font-size: 1.5rem; + margin-bottom: 16px; +} + +.buttonWrapper { + display: flex; + justify-content: space-evenly; +} + +.button { + width: 200px; +} diff --git a/client/src/common/DeleteConfirmButton.tsx b/client/src/common/DeleteConfirmButton.tsx new file mode 100644 index 000000000..7e6775ead --- /dev/null +++ b/client/src/common/DeleteConfirmButton.tsx @@ -0,0 +1,89 @@ +import { Dialog } from '@reach/dialog'; +import DeleteIcon from 'mdi-react/DeleteIcon'; +import React, { useRef, useState } from 'react'; +import Button from './Button'; +import styles from './DeleteConfirmButton.module.css'; +import IconButton from './IconButton'; + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + onConfirm: () => void; + confirmMessage: string; + icon?: boolean; +} + +export type Ref = HTMLButtonElement; + +const DeleteConfirmButton = React.forwardRef( + ( + { + children, + confirmMessage, + onConfirm, + className, + icon, + ...rest + }: ButtonProps, + ref + ) => { + const [visible, setVisible] = useState(false); + const cancelEl = useRef(null); + + return ( + <> + {icon ? ( + setVisible(true)} + ref={ref} + variant="danger" + {...rest} + > + + + ) : ( + + )} + {visible && ( + setVisible(false)} + className={styles.Dialog} + initialFocusRef={cancelEl} + > +
    + {confirmMessage} +
    +
    + + +
    +
    + )} + + ); + } +); + +export default DeleteConfirmButton; diff --git a/client/src/common/Divider.tsx b/client/src/common/Divider.tsx new file mode 100644 index 000000000..d44f6ae5a --- /dev/null +++ b/client/src/common/Divider.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const Divider = ({ style, ...rest }: React.HTMLAttributes) => { + const s = Object.assign( + { + height: 16, + }, + style + ); + + return ( +
    +
    +
    + ); +}; + +export default Divider; diff --git a/client/src/common/Drawer.module.css b/client/src/common/Drawer.module.css new file mode 100644 index 000000000..df5fbda3d --- /dev/null +++ b/client/src/common/Drawer.module.css @@ -0,0 +1,33 @@ +.Dialog { + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; + position: absolute; + box-shadow: var(--box-shadow-2); +} + +.titleWrapper { + box-sizing: border-box; + height: 44px; + font-size: 1.3rem; + border-bottom: var(--border); + display: flex; + padding: 5px 16px; + line-height: 1.5; + align-items: center; + justify-content: space-between; + background-color: rgba(0, 0, 0, 0.03); +} + +.dialogBody { + height: calc(100vh - 44px); + /* + TODO move this padding into a separate component for dialog body, + used as needed for each specific drawer + Another component can be introduced for an "Actions" component that sits outside the body, + with body being scrollable and grows + */ + padding: 16px; + overflow-y: auto; +} diff --git a/client/src/common/Drawer.tsx b/client/src/common/Drawer.tsx new file mode 100644 index 000000000..74e14045e --- /dev/null +++ b/client/src/common/Drawer.tsx @@ -0,0 +1,52 @@ +import { Dialog } from '@reach/dialog'; +import CloseIcon from 'mdi-react/CloseIcon'; +import React from 'react'; +import styles from './Drawer.module.css'; +import IconButton from './IconButton'; + +export interface Props extends React.HTMLAttributes { + title: string; + visible?: boolean; + placement: 'right' | 'left'; + width: number | string; + onClose: () => void; +} + +function Drawer({ + title, + visible, + onClose, + width, + placement, + children, +}: Props) { + const style: React.CSSProperties = { + width, + }; + + if (placement === 'right') { + style.right = 0; + } + + if (visible) { + return ( + +
    + {title} + + + +
    +
    {children}
    +
    + ); + } + return null; +} + +export default Drawer; diff --git a/client/src/common/ErrorBlock.tsx b/client/src/common/ErrorBlock.tsx new file mode 100644 index 000000000..bec813e32 --- /dev/null +++ b/client/src/common/ErrorBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function ErrorBlock({ children }: React.HTMLAttributes) { + return
    {children}
    ; +} + +export default ErrorBlock; diff --git a/client/src/common/ExportButton.tsx b/client/src/common/ExportButton.tsx new file mode 100644 index 000000000..a6eed134a --- /dev/null +++ b/client/src/common/ExportButton.tsx @@ -0,0 +1,97 @@ +import { MenuItem, MenuLink } from '@reach/menu-button'; +import DownloadIcon from 'mdi-react/DownloadIcon'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import useAppContext from '../utilities/use-app-context'; +import IconMenu from './IconMenu'; + +type Ref = HTMLAnchorElement; + +interface NavigationLinkProps + extends React.AnchorHTMLAttributes { + to: string; +} + +const NavigationLink = React.forwardRef( + (props, ref) => { + return ; + } +); + +function ExportButton({ + statementId, + onSaveImageClick, +}: { + statementId?: string; + onSaveImageClick?: () => void; +}) { + const { config } = useAppContext(); + + if (!config || !statementId) { + return null; + } + + const { allowCsvDownload } = config; + + const items = []; + + if (onSaveImageClick) { + items.push( + + png + + ); + } + + if (allowCsvDownload) { + items.push( + + csv + + ); + items.push( + + xlsx + + ); + items.push( + + json + + ); + } + + if (items.length === 0) { + return null; + } + + return ( + }>{items} + ); +} + +ExportButton.propTypes = { + links: PropTypes.object, + onSaveImageClick: PropTypes.func, +}; + +export default ExportButton; diff --git a/client/src/common/FormExplain.module.css b/client/src/common/FormExplain.module.css new file mode 100644 index 000000000..a29059c2c --- /dev/null +++ b/client/src/common/FormExplain.module.css @@ -0,0 +1,9 @@ +.formExplain { + clear: both; + display: inline-block; + min-height: 22px; + margin-top: 4px; + color: rgba(0, 0, 0, 0.45); + font-size: 12px; + line-height: 1.5; +} diff --git a/client/src/common/FormExplain.tsx b/client/src/common/FormExplain.tsx new file mode 100644 index 000000000..6f9d99407 --- /dev/null +++ b/client/src/common/FormExplain.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import styles from './FormExplain.module.css'; + +export default function FormExplain({ + children, +}: React.HTMLAttributes) { + return {children}; +} diff --git a/client/src/common/FullscreenMessage.tsx b/client/src/common/FullscreenMessage.tsx new file mode 100644 index 000000000..5060f6ad6 --- /dev/null +++ b/client/src/common/FullscreenMessage.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +export default function FullscreenMessage({ + children, +}: React.HTMLAttributes) { + return ( +
    + {children} +
    + ); +} diff --git a/client/src/common/HSpacer.tsx b/client/src/common/HSpacer.tsx new file mode 100644 index 000000000..f5882460a --- /dev/null +++ b/client/src/common/HSpacer.tsx @@ -0,0 +1,23 @@ +import React, { CSSProperties } from 'react'; + +export interface Props { + size?: number; + grow?: boolean; +} + +function HSpacer({ size = 1, grow = false }: Props) { + const sizes = [8, 16, 32, 64]; + const width = sizes[size - 1] || 64; + + const style: CSSProperties = { + width, + }; + + if (grow) { + style.flexGrow = 1; + } + + return
    ; +} + +export default HSpacer; diff --git a/client/src/common/HorizontalFormItem.tsx b/client/src/common/HorizontalFormItem.tsx new file mode 100644 index 000000000..a292515a2 --- /dev/null +++ b/client/src/common/HorizontalFormItem.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +export interface Props extends React.HTMLAttributes { + leftWidth?: string | number; + rightWidth?: string | number; + label?: string; +} + +export default function HorizontalFormItem({ + leftWidth = '35%', + rightWidth = '65%', + label, + children, +}: Props) { + return ( +
    +
    + +
    +
    {children}
    +
    + ); +} diff --git a/client/src/common/IconButton.module.css b/client/src/common/IconButton.module.css new file mode 100644 index 000000000..db3990938 --- /dev/null +++ b/client/src/common/IconButton.module.css @@ -0,0 +1,86 @@ +.btn { + line-height: 1.499; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + cursor: pointer; + user-select: none; + touch-action: manipulation; + padding: 4px; + font-size: 14px; + border-radius: 2px; + color: rgba(0, 0, 0, 0.65); + background-color: transparent; + border: none; + border-color: rgb(217, 217, 217); + flex: 0 0 auto; + width: 32px; + height: 32px; +} + +.btn:active, +.btn:hover, +.btn:focus { + text-decoration: none; + outline: none; + border: 2px solid var(--primary-color); + background-color: #f3f3f3; +} + +.btn:hover { + color: var(--primary-light-color); +} + +.btn:active { + color: var(--primary-dark-color); +} + +.btn:disabled, +.btn[disabled] { + color: rgba(0, 0, 0, 0.25); + background-color: #eee; +} + +.btn[disabled]:hover { + border: none; + color: rgba(0, 0, 0, 0.25); + background-color: #eee; +} + +.danger:hover, +.danger:focus { + color: var(--secondary-color); + border: 2px solid var(--secondary-color); +} + +.danger:active { + color: var(--secondary-dark-color); + border: 2px solid var(--secondary-dark-color); +} + +.ghost { + color: rgba(255, 255, 255, 0.8); + background-color: transparent; +} + +.ghost:hover, +.ghost:focus { + color: rgba(255, 255, 255, 0.85); + background-color: transparent; +} + +.ghost:active { + color: rgba(255, 255, 255, 1); + background-color: transparent; +} + +.ghost:disabled, +.ghost[disabled] { + border: none; + color: rgba(255, 255, 255, 0.55); +} diff --git a/client/src/common/IconButton.tsx b/client/src/common/IconButton.tsx new file mode 100644 index 000000000..62ce444da --- /dev/null +++ b/client/src/common/IconButton.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import styles from './IconButton.module.css'; +import Tooltip from './Tooltip'; + +const ICON_SIZE = 18; + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + variant?: string; + to?: string; + tooltip?: string; +} + +export interface LinkProps extends React.HTMLAttributes { + variant?: string; + to?: string; + target?: string; + rel?: string; +} + +export type Ref = HTMLButtonElement; + +const IconButton = React.forwardRef( + ( + { + children, + variant, + to, + tooltip, + disabled, + className, + onClick, + type, + ...rest + }, + ref + ) => { + const classNames = [styles.btn]; + + if (className) { + classNames.push(className); + } + + if (variant === 'danger') { + classNames.push(styles.danger); + } else if (variant === 'ghost') { + classNames.push(styles.ghost); + } + + let button; + + if (!children) { + return null; + } + + // If to is supplied this is a link + // IMPORTANT: Link is wrapped in
    to handle tooltip ref passing + // lineHeight set to initial to fix div/Link being slightly higher than buttons + if (to && !disabled && !onClick) { + button = ( +
    + {/* @ts-expect-error anchor and buttons types are fighting */} + + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + // @ts-expect-error size is not assignable to unknown, unsure how to type this currently + return React.cloneElement(child, { size: ICON_SIZE }, null); + } + })} + +
    + ); + } else { + button = ( + + ); + } + + // If the button is disabled the tooltip gets weird on hover + if (!tooltip || disabled) { + return button; + } + + return {button}; + } +); + +export default IconButton; diff --git a/client/src/common/IconMenu.tsx b/client/src/common/IconMenu.tsx new file mode 100644 index 000000000..03179e49a --- /dev/null +++ b/client/src/common/IconMenu.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Menu, MenuButton, MenuList } from '@reach/menu-button'; +import Tooltip from './Tooltip'; +import iconButtonStyles from './IconButton.module.css'; + +const ICON_SIZE = 18; + +export interface Props extends React.ButtonHTMLAttributes { + icon?: any; + variant?: string; + tooltip?: string; +} + +const IconMenu = ({ children, icon, tooltip, variant, ...rest }: Props) => { + const className = + variant === 'ghost' + ? `${iconButtonStyles.btn} ${iconButtonStyles.ghost}` + : iconButtonStyles.btn; + + const menu = ( + + + {icon && + React.cloneElement( + icon, + { + size: ICON_SIZE, + }, + null + )} + + {children} + + ); + + if (tooltip) { + return ( + + {menu} + + ); + } + + return menu; +}; + +export default IconMenu; diff --git a/client/src/common/IncompleteDataNotification.tsx b/client/src/common/IncompleteDataNotification.tsx new file mode 100644 index 000000000..e9ea2254d --- /dev/null +++ b/client/src/common/IncompleteDataNotification.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import DataNotification from './DataNotification'; + +function IncompleteDataNotification() { + return ( + + Incomplete + + ); +} + +export default IncompleteDataNotification; diff --git a/client/src/common/InfoBlock.tsx b/client/src/common/InfoBlock.tsx new file mode 100644 index 000000000..df2bf4c91 --- /dev/null +++ b/client/src/common/InfoBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function InfoBlock({ children }: React.HTMLAttributes) { + return
    {children}
    ; +} + +export default InfoBlock; diff --git a/client/src/common/Input.module.css b/client/src/common/Input.module.css new file mode 100644 index 000000000..7e0f8edc2 --- /dev/null +++ b/client/src/common/Input.module.css @@ -0,0 +1,68 @@ +.input { + box-sizing: border-box; + margin: 0; + padding: 0; + font-variant: tabular-nums; + list-style: none; + font-feature-settings: 'tnum'; + position: relative; + display: inline-block; + width: 100%; + height: 32px; + padding: 4px 11px; + color: rgba(0, 0, 0, 0.65); + font-size: 14px; + line-height: 1.5; + background-color: #fff; + background-image: none; + border: 1px solid #d9d9d9; + border-radius: 2px; +} + +.input:focus { + border: 2px solid var(--primary-color); + /* padding adjustment prevents added border from shifting text */ + padding-left: 10px; + padding-right: 10px; + outline: 0; +} + +.input:hover { + border: 2px solid var(--primary-color); + /* padding adjustment prevents added border from shifting text */ + padding-left: 10px; + padding-right: 10px; +} + +.input:disabled, +.input[aria-disabled='true'] { + color: graytext; + border-color: #aaa; + background-color: #eee; +} + +.input:disabled:hover, +.input[aria-disabled='true'] { + border: 1px solid #aaa; + /* override hover padding changes */ + padding-left: 11px; + padding-right: 11px; +} + +.danger { + border-color: var(--secondary-color); + box-shadow: inset 0 1px 1px var(--secondary-color-30); +} + +.danger:focus { + border: 2px solid var(--secondary-color); + outline: 0; +} + +.danger:hover { + border-color: var(--secondary-color); +} + +.danger::placeholder { + color: var(--secondary-color); +} diff --git a/client/src/common/Input.tsx b/client/src/common/Input.tsx new file mode 100644 index 000000000..5ef9f6d41 --- /dev/null +++ b/client/src/common/Input.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import styles from './Input.module.css'; + +export interface Props extends React.HTMLProps { + error?: boolean; + className?: string; + name?: string; + type?: string; + placeholder?: string; + required?: boolean; +} + +export type Ref = HTMLInputElement | null; + +const Input = React.forwardRef((props, ref) => { + const { children, error, className, ...rest } = props; + const classNames = [styles.input]; + + if (error) { + classNames.push(styles.danger); + } + + if (className) { + classNames.push(className); + } + + return ( + + {children} + + ); +}); + +export default Input; diff --git a/client/src/common/ListItem.module.css b/client/src/common/ListItem.module.css new file mode 100644 index 000000000..495ee521d --- /dev/null +++ b/client/src/common/ListItem.module.css @@ -0,0 +1,8 @@ +.ListItem { + display: flex; + flex-shrink: 0; + align-items: center; + width: 100%; + min-height: 48px; + border-bottom: var(--border); +} diff --git a/client/src/common/ListItem.tsx b/client/src/common/ListItem.tsx new file mode 100644 index 000000000..d4ebb78cb --- /dev/null +++ b/client/src/common/ListItem.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './ListItem.module.css'; + +const ListItem = ({ + children, + className, + ...rest +}: React.HTMLAttributes) => { + const classNames = [styles.ListItem]; + if (className) { + classNames.push(className); + } + + return ( +
    + {children} +
    + ); +}; + +export default ListItem; diff --git a/client/src/common/Modal.module.css b/client/src/common/Modal.module.css new file mode 100644 index 000000000..8606e10ba --- /dev/null +++ b/client/src/common/Modal.module.css @@ -0,0 +1,27 @@ +.Dialog { + box-shadow: var(--box-shadow-2); + padding: 0; +} + +.titleWrapper { + border-bottom: var(--border); + display: flex; + justify-content: space-between; + box-sizing: border-box; + flex: 0 0 45px; + font-size: 1.2rem; + padding: 8px 16px; + line-height: 1.5; + align-items: center; + background-color: rgba(0, 0, 0, 0.03); +} + +.title { + margin-right: 32px; +} + +.dialogBody { + padding: 16px; + max-width: 90vw; + overflow-x: auto; +} diff --git a/client/src/common/Modal.tsx b/client/src/common/Modal.tsx new file mode 100644 index 000000000..305f2f0cf --- /dev/null +++ b/client/src/common/Modal.tsx @@ -0,0 +1,49 @@ +import { Dialog } from '@reach/dialog'; +import CloseIcon from 'mdi-react/CloseIcon'; +import React from 'react'; +import styles from './Modal.module.css'; +import IconButton from './IconButton'; + +export interface Props extends React.HTMLAttributes { + title: string; + visible?: boolean; + onClose?: () => void; + width: string | number; + initialFocusRef?: React.RefObject; +} + +function Modal({ + title, + visible, + onClose, + width, + initialFocusRef, + children, +}: Props) { + if (visible) { + return ( + +
    + {title} + {onClose && ( + + + + )} +
    +
    {children}
    +
    + ); + } + return null; +} + +export default Modal; diff --git a/client/src/common/MultiSelect.module.css b/client/src/common/MultiSelect.module.css new file mode 100644 index 000000000..dd0731029 --- /dev/null +++ b/client/src/common/MultiSelect.module.css @@ -0,0 +1,94 @@ +.container { + box-sizing: border-box; + margin: 0; + padding: 0; + font-variant: tabular-nums; + list-style: none; + font-feature-settings: 'tnum'; + position: relative; + /* display: inline-block; */ + width: 100%; + min-height: 32px; + /* padding: 4px 11px; */ + color: rgba(0, 0, 0, 0.65); + font-size: 14px; + line-height: 1.5; + background-color: #fff; + background-image: none; + border: 1px solid #d9d9d9; + border-radius: 2px; + + /* this was in an inner container maybe needs to be moved out */ + display: inline-flex; + flex-wrap: wrap; + align-items: center; +} + +.input { + border: none; + flex-grow: 1; + + box-sizing: border-box; + margin: 2px; + padding: 0; + font-variant: tabular-nums; + list-style: none; + font-feature-settings: 'tnum'; + position: relative; + border-radius: 2px; + + /* width: 100%; */ + height: 30px; + padding: 4px 11px; + color: rgba(0, 0, 0, 0.65); + font-size: 14px; + line-height: 14px; +} + +.input:focus { + border: 2px solid var(--primary-color); + /* padding adjustment prevents added border from shifting text */ + padding-left: 9px; + padding-right: 9px; + outline: 0; +} + +.item { + position: relative; + cursor: pointer; + display: block; + border: none; + height: auto; + text-align: left; + line-height: 1em; + /* color: rgba(0, 0, 0, 0.95); */ + /* font-size: 1rem; */ + text-transform: none; + /* font-weight: 400; */ + box-shadow: none; + /* padding: 0.8rem 1.1rem; */ + padding: 8px; + white-space: normal; + overflow-wrap: normal; +} + +.itemActive { + background: #1890ff; + color: white; +} + +.menu { + padding: 0px; + margin-top: 0px; + position: absolute; + background-color: white; + width: 100%; + max-height: 10rem; + overflow: hidden auto; + outline: 0px; + transition: opacity 0.1s ease 0s; + border-radius: 0px 0px 2px 2px; + box-shadow: rgba(34, 36, 38, 0.15) 0px 2px 3px 0px; + z-index: 99999; + border: none; +} diff --git a/client/src/common/MultiSelect.tsx b/client/src/common/MultiSelect.tsx new file mode 100644 index 000000000..de4d36147 --- /dev/null +++ b/client/src/common/MultiSelect.tsx @@ -0,0 +1,252 @@ +import { useCombobox, useMultipleSelection } from 'downshift'; +import React, { useState } from 'react'; +import styles from './MultiSelect.module.css'; +import Tag from './Tag'; +import { matchSorter } from 'match-sorter'; + +export interface MultiSelectItem { + id: string; + name: string; +} + +/** + * Menu - a ul element to contain the options when open + */ +interface MenuProps extends React.HTMLProps { + isOpen?: boolean; +} + +type MenuRef = HTMLUListElement; + +const Menu = React.forwardRef( + ({ isOpen, ...rest }, ref) => { + const classNames = [styles.menu]; + const style: React.CSSProperties = {}; + if (!isOpen) { + style.border = 'none'; + } + return ( +
      + ); + } +); + +/** + * MenuItem - an li element for a specific option + */ +interface MenuItemProps extends React.HTMLProps { + isActive?: boolean; +} + +type MenuItemRef = HTMLLIElement; + +const MenuItem = React.forwardRef( + ({ isActive, ...rest }, ref) => { + const classNames = [styles.item]; + if (isActive) { + classNames.push(styles.itemActive); + } + return
    • ; + } +); + +/** + * helper function to get items using matchSorter library + * @param allItems + * @param selectedItems + * @param inputValue + */ +function getMatchSorterItems( + allItems: MultiSelectItem[], + selectedItems: MultiSelectItem[], + inputValue: string +) { + const selectedById: { [key: string]: MultiSelectItem } = {}; + selectedItems.forEach((item) => (selectedById[item.id] = item)); + + const unselectedItems = allItems.filter((item) => !selectedById[item.id]); + + return inputValue + ? matchSorter(unselectedItems, inputValue, { + keys: ['name'], + }) + : unselectedItems; +} + +export interface Props { + selectedItems: MultiSelectItem[]; + options: MultiSelectItem[]; + onChange: (items: MultiSelectItem[]) => void; + placeholder?: string; + allowNew?: boolean; +} + +/** + * If anyone out there more familiar with downshift wants to clean this up by all means feel free + * options should consist of `{ id, name }`. + */ +function MultiSelect(props: Props) { + const { + options, + onChange, + placeholder, + allowNew, + selectedItems: propSelectedItems, + } = props; + + const [inputValue, setInputValue] = useState(''); + + const { + getSelectedItemProps, + getDropdownProps, + addSelectedItem, + removeSelectedItem, + selectedItems, + } = useMultipleSelection({ + initialSelectedItems: propSelectedItems, + onSelectedItemsChange: (changes) => { + if (changes.selectedItems) { + onChange(changes.selectedItems); + } else { + onChange([]); + } + }, + }); + + const getFilteredItems = () => { + const filteredItems = getMatchSorterItems( + options, + propSelectedItems, + inputValue + ); + + // If input isn't in options or selected items, add it as an item + const existingOption = options.find( + (option) => + option.name?.trim().toLowerCase() === inputValue.trim().toLowerCase() + ); + const existingSelection = propSelectedItems.find( + (item) => + item.name?.trim().toLowerCase() === inputValue.trim().toLowerCase() + ); + + if ( + allowNew && + !existingOption && + !existingSelection && + inputValue.trim() !== '' + ) { + const userOption: MultiSelectItem = { id: inputValue, name: inputValue }; + return [userOption].concat(filteredItems); + } + + return filteredItems; + }; + + const { + isOpen, + getMenuProps, + getInputProps, + getComboboxProps, + highlightedIndex, + getItemProps, + } = useCombobox({ + inputValue, + defaultHighlightedIndex: 0, // after selection, highlight the first item. + selectedItem: null, + items: getFilteredItems(), + stateReducer: (state, actionAndChanges) => { + const { changes, type } = actionAndChanges; + switch (type) { + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + return { + ...changes, + isOpen: false, // close the menu open after selection. + }; + } + return changes; + }, + onStateChange: ({ inputValue, type, selectedItem, ...rest }) => { + switch (type) { + case useCombobox.stateChangeTypes.InputChange: + setInputValue(inputValue || ''); + break; + case useCombobox.stateChangeTypes.InputKeyDownEnter: + case useCombobox.stateChangeTypes.ItemClick: + case useCombobox.stateChangeTypes.InputBlur: + if (selectedItem) { + addSelectedItem(selectedItem); + } + setInputValue(''); + break; + default: + break; + } + }, + }); + + return ( +
      +
      + {selectedItems.map((selectedItem, index) => ( + removeSelectedItem(selectedItem)} + > + {selectedItem.name} + + ))} + + +
      + + {isOpen && + getFilteredItems().map((item, index) => ( + + {item.name} + + ))} + +
      + ); +} + +export default MultiSelect; diff --git a/client/src/common/QueryResultContainer.tsx b/client/src/common/QueryResultContainer.tsx new file mode 100644 index 000000000..b6eb08dfd --- /dev/null +++ b/client/src/common/QueryResultContainer.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { + useQueryResultFormat, + useSessionQueryError, + useStatementColumns, + useStatementRowCount, + useStatementStatus, +} from '../stores/editor-store'; +import { api } from '../utilities/api'; +import ErrorBlock from './ErrorBlock'; +import InfoBlock from './InfoBlock'; +import QueryResultDataTable from './QueryResultDataTable'; +import QueryResultRunning from './QueryResultRunning'; + +export interface Props { + statementId?: string; +} + +function QueryResultContainer({ statementId }: Props) { + const columns = useStatementColumns(statementId) || []; + const rowCount = useStatementRowCount(statementId); + const status = useStatementStatus(statementId); + const queryResultFormat = useQueryResultFormat(); + const queryError = useSessionQueryError(); + const { data, error } = api.useStatementResults(statementId, status); + + if ( + status === 'queued' || + status === 'started' || + (status === 'finished' && !data) + ) { + return ; + } + if (queryError) { + return {queryError}; + } + if (error) { + return Error getting query results; + } + if (rowCount === undefined) { + return null; + } + if (status === 'finished' && rowCount === 0) { + return ( + + Query finished +
      + No rows returned +
      + ); + } + return ( + + ); +} + +export default React.memo(QueryResultContainer); diff --git a/client/src/common/QueryResultDataTable.module.css b/client/src/common/QueryResultDataTable.module.css new file mode 100644 index 000000000..c32e1928a --- /dev/null +++ b/client/src/common/QueryResultDataTable.module.css @@ -0,0 +1,81 @@ +/* + Fancy CSS to apply a shadow when content is overflowing + Approach modified from https://codepen.io/matthewbeta/pen/fzoHI + + There's an issue with this approach in that the content needs to overflow by 10 pixels + so that the "covering" shadow is moved and reveals the actual shadow. + + This can be fixed if 10 pixels of content can be added to the right hand side of a result grid cell. + Prior attempt using flex and a hidden value resulted in strange padding behavior. + + If this cannot be accomplished with CSS it might make sense to render cell values off screen and measure them? + That could get expensive. + + We might not need to measure much though if we map measurements to number of characters, + since we're using a monospace font. + + Open to other ideas if anyone has them! +*/ +.scrollboxEven { + background-image: + /* Shadows */ linear-gradient( + to right, + rgb(250, 250, 250), + rgb(250, 250, 250) + ), + linear-gradient(to right, rgb(250, 250, 250), rgb(250, 250, 250)), + /* Shadow covers */ + linear-gradient(to right, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0)), + linear-gradient(to left, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0)); + + background-position: left center, right center, left center, right center; + background-repeat: no-repeat; + background-color: rgb(250, 250, 250); + background-size: 10px 100%, 10px 100%, 10px 100%, 10px 100%; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +.scrollboxOdd { + background-image: + /* Shadows */ linear-gradient(to right, white, white), + linear-gradient(to right, white, white), + /* Shadow covers */ + linear-gradient(to right, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0)), + linear-gradient(to left, rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0)); + + background-position: left center, right center, left center, right center; + background-repeat: no-repeat; + background-color: white; + background-size: 10px 100%, 10px 100%, 10px 100%, 10px 100%; + + /* Opera doesn't support this in the shorthand */ + background-attachment: local, local, scroll, scroll; +} + +.faderEven { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 16px; + background-image: linear-gradient( + to right, + rgba(255, 255, 255, 0), + rgb(250, 250, 250) + ); +} + +.faderOdd { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 16px; + background-image: linear-gradient( + to right, + rgba(255, 255, 255, 0), + rgb(255, 255, 255) + ); +} diff --git a/client/src/common/QueryResultDataTable.tsx b/client/src/common/QueryResultDataTable.tsx new file mode 100644 index 000000000..51a2bfb9b --- /dev/null +++ b/client/src/common/QueryResultDataTable.tsx @@ -0,0 +1,610 @@ +import { + Menu, + MenuButton, + MenuItem, + MenuItems, + MenuPopover, +} from '@reach/menu-button'; +import React from 'react'; +import { VariableSizeGrid } from 'react-window'; +import throttle from 'lodash/throttle'; +import Draggable from 'react-draggable'; +import Measure from 'react-measure'; +import { QueryResultFormat, StatementColumn, StatementResults } from '../types'; +import styles from './QueryResultDataTable.module.css'; +import Modal from './Modal'; + +// https://davidwalsh.name/detect-scrollbar-width +const scrollbarWidth = () => { + const scrollDiv = document.createElement('div'); + scrollDiv.setAttribute( + 'style', + 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;' + ); + document.body.appendChild(scrollDiv); + const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; +}; + +const renderValue = (input: any, column: StatementColumn) => { + if (input === null || input === undefined) { + return null; + } else if (input === true || input === false) { + return input.toString(); + } else if (column.datatype === 'datetime') { + // Remove the letters from ISO string and present as is + return input.replace('T', ' ').replace('Z', ''); + } else if (column.datatype === 'date') { + // Formats ISO string to YYYY-MM-DD + return input.substring(0, 10); + } else if (typeof input === 'object') { + return JSON.stringify(input, null, 2); + } else if (typeof input === 'string' && input.match('^https?://')) { + return ( + + {input} + + ); + } else { + // Format Markdown hyperlinks + if (typeof input === 'string') { + const mdMatch = input.match('^\\[(.*)\\]\\((https?://.*)\\)$'); + if (mdMatch) { + return ( + + {mdMatch[1]} + + ); + } + } + return input; + } +}; + +/** + * Input fields for clipboard copy value sourcing + * The input must be visible/displayed somewhere, in our case offscreen + * @param props + */ +function OffScreenInput({ id, value }: { id: string; value: string }) { + return ( + + ); +} diff --git a/client/src/common/Tooltip.tsx b/client/src/common/Tooltip.tsx new file mode 100644 index 000000000..3f09f4fd8 --- /dev/null +++ b/client/src/common/Tooltip.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Tooltip as ReachTooltip } from '@reach/tooltip'; +import '@reach/tooltip/styles.css'; + +export interface Props extends React.HTMLAttributes { + label: string; +} + +export default function Tooltip({ children, ...otherProps }: Props) { + return {children}; +} diff --git a/client/src/common/getTauChartConfig.tsx b/client/src/common/getTauChartConfig.tsx new file mode 100644 index 000000000..8f145c56d --- /dev/null +++ b/client/src/common/getTauChartConfig.tsx @@ -0,0 +1,309 @@ +import chartDefinitions from '../utilities/chartDefinitions'; +import exportTo from 'taucharts/dist/plugins/export-to'; +import legend from 'taucharts/dist/plugins/legend'; +import quickFilter from 'taucharts/dist/plugins/quick-filter'; +import tooltip from 'taucharts/dist/plugins/tooltip'; +import tcTrendline from 'taucharts/dist/plugins/trendline'; +import baseUrl from '../utilities/baseUrl'; +import { StatementColumn } from '../types'; + +/** + * Caution this uses any type for a few things + * + * The chart config is a large object and flexible. + * TauCharts does not have types defined and likely never will. + * Additionally data rows are object maps of string keys with any data value. + * */ + +interface StringMap { + [key: string]: string; +} + +interface Field { + fieldId: string; + required: boolean; + label: string; + inputType: string; +} + +interface ChartConfiguration { + chartType: string; + fields: StringMap; +} + +interface DataRow { + /** + * Data values for data row are not typed and actually could be any value + */ + [key: string]: any; +} + +const getUnmetFields = (chartType: string, selectedFieldMap: StringMap) => { + const chartDefinition = chartDefinitions.find( + (def) => def.chartType === chartType + ); + if (!chartDefinition) { + throw new Error(`Unknown chartType ${chartType}`); + } + const unmetRequiredFields: Field[] = []; + + chartDefinition.fields.forEach((field) => { + if (field.required && !selectedFieldMap[field.fieldId]) { + unmetRequiredFields.push(field); + } + }); + + return unmetRequiredFields; +}; + +export default function getTauChartConfig( + chartConfiguration: ChartConfiguration, + columns: StatementColumn[] = [], + rows: any[] = [] +) { + if (!chartConfiguration) { + return null; + } + + let dataRows = rows || []; + const chartType = chartConfiguration.chartType; + const selectedFields = chartConfiguration.fields; + + if (!chartType || !selectedFields) { + return null; + } + + const chartDefinition = chartDefinitions.find( + (def) => def.chartType === chartType + ); + + if (!dataRows.length || !chartDefinition) { + return null; + } + + const chartConfig: any = { + type: chartDefinition.tauChartsType, + plugins: [ + tooltip(), + legend(), + exportTo({ + cssPaths: [ + // NOTE: We must ref the file in vendor dir for export images to work + // (we don't know what the webpack bundle css path will be) + baseUrl() + '/javascripts/vendor/tauCharts/tauCharts.min.css', + ], + }), + ], + settings: { + asyncRendering: true, + renderingTimeout: 10000, + syncRenderingInterval: 50, + handleRenderingErrors: true, + utcTime: true, + }, + }; + + // loop through data rows and convert types as needed + dataRows = dataRows.map((row: DataRow) => { + const newRow: DataRow = {}; + columns.forEach((col: StatementColumn) => { + const { datatype, name } = col; + if (datatype === 'date' || datatype === 'datetime') { + newRow[name] = new Date(row[name]); + } else if (datatype === 'number') { + newRow[name] = Number(row[name]); + } else { + newRow[name] = row[name]; + } + }); + + // HACK - + // Facets need to be a dimension, not a measure. + // tauCharts auto detects numbers to be measures + // Here we'll convert a number to a string, + // to trick tauCharts into thinking its a dimension + const forceDimensionFields = chartDefinition.fields.filter( + (field) => field.forceDimension === true + ); + forceDimensionFields.forEach((fieldDefinition) => { + const columnName = selectedFields[fieldDefinition.fieldId]; + const column = columns.find( + (column: StatementColumn) => column.name === columnName + ); + const colDatatype = column ? column.datatype : null; + if (columnName && colDatatype === 'number' && newRow[columnName]) { + newRow[columnName] = newRow[columnName].toString(); + } + }); + return newRow; + }); + + // Some chartConfiguration.fields may reference columns that no longer exist + // Remove them from a copy of chartConfigurationFields + // Unless they aren't column mapping fields (like trendline, quickfilter) + const cleanedChartConfigurationFields = Object.keys( + chartConfiguration.fields + ).reduce((fieldsMap: StringMap, fieldColName) => { + const fieldDefinition = chartDefinition.fields.find( + (f) => f.fieldId === fieldColName + ); + const columnName = chartConfiguration.fields[fieldColName]; + + if (fieldDefinition && fieldDefinition.inputType !== 'field-dropdown') { + fieldsMap[fieldColName] = columnName; + } else if (columns.find((c: StatementColumn) => c.name === columnName)) { + fieldsMap[fieldColName] = columnName; + } + return fieldsMap; + }, {}); + + // Now that non-existing columns are removed from the configuration fields + // Validate that the chart required fields are provided + const unmetFields = getUnmetFields( + chartType, + cleanedChartConfigurationFields + ); + + if (unmetFields.length) { + // TODO - highlight fields that are required but not provided or clear values no longer relevant + // message.error( + // 'Unmet required fields: ' + unmetFields.map(f => f.label).join(', ') + // ); + return null; + } + + const { + x, + xFacet, + y, + yFacet, + filter, + trendline, + split, + size, + yMin, + yMax, + barvalue, + valueFacet, + barlabel, + labelFacet, + color, + } = cleanedChartConfigurationFields; + + switch (chartType) { + case 'line': + chartConfig.x = [x]; + if (xFacet) { + chartConfig.x.unshift(xFacet); + } + chartConfig.y = [y]; + if (yFacet) { + chartConfig.y.unshift(yFacet); + } + if (filter) { + chartConfig.plugins.push(quickFilter()); + } + if (trendline) { + chartConfig.plugins.push(tcTrendline()); + } + if (split) { + chartConfig.color = split; + } + if (size) { + chartConfig.size = size; + } + if (yMin || yMax) { + chartConfig.guide = { + y: { autoScale: false }, + }; + if (yMin) { + chartConfig.guide.y.min = Number(yMin); + } + if (yMax) { + chartConfig.guide.y.max = Number(yMax); + } + } + break; + + case 'bar': + chartConfig.x = [barvalue]; + if (valueFacet) { + chartConfig.x.unshift(valueFacet); + } + chartConfig.y = [barlabel]; + if (labelFacet) { + chartConfig.y.unshift(labelFacet); + } + break; + + case 'verticalbar': + chartConfig.y = [barvalue]; + if (valueFacet) { + chartConfig.y.unshift(valueFacet); + } + chartConfig.x = [barlabel]; + if (labelFacet) { + chartConfig.x.unshift(labelFacet); + } + break; + + case 'stacked-bar-horizontal': + chartConfig.x = [barvalue]; + if (valueFacet) { + chartConfig.x.unshift(valueFacet); + } + chartConfig.y = [barlabel]; + if (labelFacet) { + chartConfig.y.unshift(labelFacet); + } + if (color) { + chartConfig.color = color; + } + break; + + case 'stacked-bar-vertical': + chartConfig.y = [barvalue]; + if (valueFacet) { + chartConfig.y.unshift(valueFacet); + } + chartConfig.x = [barlabel]; + if (labelFacet) { + chartConfig.x.unshift(labelFacet); + } + if (color) { + chartConfig.color = color; + } + break; + + case 'bubble': + chartConfig.x = [x]; + if (xFacet) { + chartConfig.x.unshift(xFacet); + } + chartConfig.y = [y]; + if (yFacet) { + chartConfig.y.unshift(yFacet); + } + if (filter) { + chartConfig.plugins.push(quickFilter()); + } + if (trendline) { + chartConfig.plugins.push(tcTrendline()); + } + if (size) { + chartConfig.size = size; + } + if (color) { + chartConfig.color = color; + } + break; + + default: + console.error('unknown chart type'); + } + + // Add data to chart chartConfig + chartConfig.data = dataRows; + + return chartConfig; +} diff --git a/client/src/common/message.module.css b/client/src/common/message.module.css new file mode 100644 index 000000000..f29330faa --- /dev/null +++ b/client/src/common/message.module.css @@ -0,0 +1,16 @@ +.message { + box-shadow: var(--box-shadow-1); + position: fixed; + color: #fff; + background-color: var(--primary-color); + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); + padding: 8px; + top: 16px; + right: 16px; + width: 200px; + z-index: 9001 !important; +} + +.error { + background-color: hsl(323, 100%, 42%); +} diff --git a/client/src/common/message.tsx b/client/src/common/message.tsx new file mode 100644 index 000000000..fb8b73e95 --- /dev/null +++ b/client/src/common/message.tsx @@ -0,0 +1,55 @@ +import mitt from 'mitt'; +import React, { useEffect, useState } from 'react'; +import styles from './message.module.css'; + +interface Message { + message: string; + type: 'success' | 'error'; +} + +type Events = { + message: Message; +}; + +const emitter = mitt(); + +export function MessageDisplayer() { + const [messages, setMessages] = useState([]); + + function onMessage(message: Message | undefined) { + setMessages((messages) => { + if (message) { + return [...messages, message]; + } + return messages; + }); + setTimeout(() => setMessages((messages) => messages.slice(1)), 3000); + } + + useEffect(() => { + emitter.on('message', onMessage); + return () => emitter.off('message', onMessage); + }, []); + + if (messages && messages.length > 0) { + const msg = messages[messages.length - 1]; + const classNames = [styles.message]; + if (msg.type === 'error') { + classNames.push(styles.error); + } + return
      {msg.message}
      ; + } + + return null; +} + +const message = { + error: function (message: string) { + emitter.emit('message', { type: 'error', message }); + }, + success: function (message: string) { + emitter.emit('message', { type: 'success', message }); + }, +}; + +export default message; diff --git a/client/src/common/tauChartRef.ts b/client/src/common/tauChartRef.ts new file mode 100644 index 000000000..489f81020 --- /dev/null +++ b/client/src/common/tauChartRef.ts @@ -0,0 +1,27 @@ +import debounce from 'lodash/debounce'; + +const chartRefs: { [key: string]: any } = {}; + +export function setFakeChartRef(queryId: string, chart: any) { + chartRefs[queryId] = chart; +} + +export function delFakeChartRef(queryId: string) { + delete chartRefs[queryId]; +} + +export function exportPng(queryId: string, fileName: string) { + const chart = chartRefs[queryId]; + if (chart && chart.fire) { + chart.fire('export-to', { type: 'png', fileName }); + } +} + +export function resizeChart(queryId: string) { + const chart = chartRefs[queryId]; + if (chart && chart.resize) { + chart.resize(); + } +} + +export const debouncedResizeChart = debounce(resizeChart, 700); diff --git a/client/src/connectionAccesses/ConnectionAccessCreateDrawer.tsx b/client/src/connectionAccesses/ConnectionAccessCreateDrawer.tsx new file mode 100644 index 000000000..afee349f6 --- /dev/null +++ b/client/src/connectionAccesses/ConnectionAccessCreateDrawer.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Drawer from '../common/Drawer'; +import ConnectionAccessForm from './ConnectionAccessForm'; + +function ConnectionAccessCreateDrawer({ + connectionId, + visible, + onClose, + onConnectionAccessSaved, + placement, +}: any) { + return ( + + + + ); +} + +export default ConnectionAccessCreateDrawer; diff --git a/client/src/connectionAccesses/ConnectionAccessForm.tsx b/client/src/connectionAccesses/ConnectionAccessForm.tsx new file mode 100644 index 000000000..ab72e023e --- /dev/null +++ b/client/src/connectionAccesses/ConnectionAccessForm.tsx @@ -0,0 +1,193 @@ +import sortBy from 'lodash/sortBy'; +import React, { ChangeEvent, useState } from 'react'; +import Button from '../common/Button'; +import HorizontalFormItem from '../common/HorizontalFormItem'; +import Input from '../common/Input'; +import message from '../common/message'; +import Select from '../common/Select'; +import { ConnectionAccess } from '../types'; +import { api } from '../utilities/api'; + +type Edits = { + connectionId?: string; + userId?: string; + duration?: string; +}; + +interface UserSummary { + id: string; + name?: string; + email?: string; + ldapId?: string; +} + +interface Props { + onConnectionAccessSaved: (connectionAccess: ConnectionAccess) => void; +} + +function ConnectionAccessForm({ onConnectionAccessSaved }: Props) { + const [connectionAccessEdits, setConnectionAccessEdits] = useState({}); + const [creating, setCreating] = useState(false); + + let { data: apiConnections } = api.useConnections(); + const connections = [ + { + id: '__EVERY_CONNECTION__', + name: 'Every Connection', + }, + ].concat(apiConnections || []); + + let { data: apiUsers } = api.useUsers(); + + const users: UserSummary[] = [ + { + id: '__EVERYONE__', + email: 'Everyone', + }, + ]; + + if (apiUsers) { + apiUsers.forEach((user) => { + users.push({ + id: user.id, + email: user.email, + name: user.name, + ldapId: user.ldapId, + }); + }); + } + + const setConnectionAccessValue = (key: keyof Edits, value: string) => { + setConnectionAccessEdits((prev) => ({ ...prev, [key]: value })); + }; + + const createConnectionAccess = async (event: any) => { + // prevent the browsers default handling of submit. + // otherwise it will bake a get request to the url of the form with the fields as params. + event.preventDefault(); + + if (creating) { + return; + } + + setCreating(true); + const json = await api.post( + '/api/connection-accesses', + connectionAccessEdits + ); + if (json.error) { + setCreating(false); + return message.error(json.error); + } + return onConnectionAccessSaved(json.data); + }; + + const { + connectionId = '', + userId = '', + duration = '', + } = connectionAccessEdits; + + const connectionSelectOptions = [ + ); + } else { + sortBy(connections, ['name']).forEach((connection) => + connectionSelectOptions.push( + + ) + ); + } + + const userSelectOptions = [ + ); + } else { + sortBy(users, ['email']).forEach((user) => + userSelectOptions.push( + + ) + ); + } + + return ( +
      +
      + + + + + + + + ) => + setConnectionAccessValue('duration', e.target.value) + } + /> + +
      +
      + {' '} +
      +
      +
      + ); +} + +export default ConnectionAccessForm; diff --git a/client/src/connectionAccesses/ConnectionAccessList.tsx b/client/src/connectionAccesses/ConnectionAccessList.tsx new file mode 100644 index 000000000..2e1378001 --- /dev/null +++ b/client/src/connectionAccesses/ConnectionAccessList.tsx @@ -0,0 +1,135 @@ +import humanizeDuration from 'humanize-duration'; +import React, { useState } from 'react'; +import Button from '../common/Button'; +import DeleteConfirmButton from '../common/DeleteConfirmButton'; +import ListItem from '../common/ListItem'; +import message from '../common/message'; +import Text from '../common/Text'; +import { ConnectionAccess } from '../types'; +import { api } from '../utilities/api'; +import useAppContext from '../utilities/use-app-context'; +import ConnectionAccessCreateDrawer from './ConnectionAccessCreateDrawer'; + +function ConnectionAccessList() { + const { currentUser } = useAppContext(); + const [showInactives, setShowInactives] = useState(false); + const [showAccessCreate, setShowAccessCreate] = useState(false); + + let { data: caData, mutate } = api.useConnectionAccesses(showInactives); + const connectionAccesses = caData || []; + + const toggleShowInactives = () => { + setShowInactives(!showInactives); + }; + + const newConnectionAccess = () => { + setShowAccessCreate(true); + }; + + const expireConnectionAccess = async (connectionAccessId: any) => { + const json = await api.put( + `/api/connection-accesses/${connectionAccessId}/expire`, + {} + ); + const updated = showInactives + ? connectionAccesses.map((item: any) => { + if (item.id === connectionAccessId) { + return json.data; + } else { + return item; + } + }) + : connectionAccesses.filter( + (item: any) => item.id !== connectionAccessId + ); + mutate(updated); + if (json.error) { + return message.error('Expire Failed: ' + json.error.toString()); + } + }; + + const handleCreateDrawerClose = () => { + setShowAccessCreate(false); + }; + + const handleConnectionAccessSaved = (connectionAccess: ConnectionAccess) => { + const updated = [connectionAccess].concat(connectionAccesses); + mutate(updated); + setShowAccessCreate(false); + }; + + return ( + <> +
      + + +
      + {connectionAccesses.map((item) => { + const actions = []; + const timeToExpire = new Date(item.expiryDate).valueOf() - Date.now(); + const expired = timeToExpire < 0; + + if (currentUser?.role === 'admin' && !expired) { + actions.push( + { + expireConnectionAccess(item.id); + }} + style={{ marginLeft: 8 }} + > + Expire + + ); + } + + return ( + +
      + Connection: {item.connectionName} +
      + User: {item.userEmail} +
      + + Duration: {item.duration || 'Indefinite'}{' '} + {!item.duration || 'seconds'} +    -    Expiry Date:{' '} + {new Date(item.expiryDate).toLocaleString()} + +
      + {!expired ? ( + + Time to Expire:{' '} + {humanizeDuration(timeToExpire, { + units: ['d', 'h', 'm'], + round: true, + })} + + ) : ( + Expired + )} +
      + {actions} +
      + ); + })} + + + ); +} + +export default ConnectionAccessList; diff --git a/client/src/connectionAccesses/ConnectionAccessListDrawer.tsx b/client/src/connectionAccesses/ConnectionAccessListDrawer.tsx new file mode 100644 index 000000000..79b474ebc --- /dev/null +++ b/client/src/connectionAccesses/ConnectionAccessListDrawer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Drawer from '../common/Drawer'; +import ConnectionAccessList from './ConnectionAccessList'; + +function ConnectionAccessListDrawer({ visible, onClose }: any) { + return ( + + + + ); +} + +export default ConnectionAccessListDrawer; diff --git a/client/src/connections/ConnectionEditDrawer.tsx b/client/src/connections/ConnectionEditDrawer.tsx new file mode 100644 index 000000000..157a17fd5 --- /dev/null +++ b/client/src/connections/ConnectionEditDrawer.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Drawer from '../common/Drawer'; +import ConnectionForm from './ConnectionForm'; + +function ConnectionEditDrawer({ + connectionId, + visible, + onClose, + onConnectionSaved, + placement, +}: any) { + const title = connectionId ? 'Edit connection' : 'New connection'; + return ( + + + + ); +} + +export default ConnectionEditDrawer; diff --git a/client/src/connections/ConnectionForm.tsx b/client/src/connections/ConnectionForm.tsx new file mode 100644 index 000000000..04abf2b83 --- /dev/null +++ b/client/src/connections/ConnectionForm.tsx @@ -0,0 +1,424 @@ +import SuccessIcon from 'mdi-react/CheckboxMarkedCircleOutlineIcon'; +import CloseCircleOutlineIcon from 'mdi-react/CloseCircleOutlineIcon'; +import React, { ChangeEvent, ReactNode, useEffect, useState } from 'react'; +import Button from '../common/Button'; +import ErrorBlock from '../common/ErrorBlock'; +import FormExplain from '../common/FormExplain'; +import HorizontalFormItem from '../common/HorizontalFormItem'; +import Input from '../common/Input'; +import message from '../common/message'; +import Select from '../common/Select'; +import SpinKitCube from '../common/SpinKitCube'; +import TextArea from '../common/TextArea'; +import { api } from '../utilities/api'; +import { setAsynchronousDriver } from '../stores/editor-actions'; + +const TEXT = 'TEXT'; +const PASSWORD = 'PASSWORD'; +const CHECKBOX = 'CHECKBOX'; +const TEXTAREA = 'TEXTAREA'; + +interface ValueMap { + [key: string]: string | number | boolean | null | undefined | ValueMap; +} + +interface Edits extends ValueMap { + name?: string; + driver?: string; + idleTimeoutMinutes?: string; + multiStatementTransactionEnabled?: boolean; + data?: ValueMap; +} + +function ConnectionForm({ connectionId, onConnectionSaved }: any) { + const [connectionEdits, setConnectionEdits] = useState({}); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [tested, setTested] = useState(false); + const [testError, setTestError] = useState(null); + const [loading, setLoading] = useState(false); + + const { data: drivers } = api.useDrivers(); + + async function getConnection(connectionId: string) { + if (connectionId) { + setLoading(true); + const json = await api.getConnection(connectionId); + setLoading(false); + + if (json.error) { + return message.error(json.error); + } + + const conn = json.data; + // Convert seconds to minutes for a more user-friendly experience + const idleTimeoutSeconds = conn?.idleTimeoutSeconds; + setConnectionEdits({ + name: conn?.name, + driver: conn?.driver, + idleTimeoutMinutes: idleTimeoutSeconds + ? Math.round(idleTimeoutSeconds / 60).toString() + : '', + multiStatementTransactionEnabled: + conn?.multiStatementTransactionEnabled, + data: conn?.data, + }); + } + } + + useEffect(() => { + getConnection(connectionId); + }, [connectionId]); + + const setConnectionValue = (key: any, value: any) => { + setConnectionEdits((prev) => ({ ...prev, [key]: value })); + }; + + const setConnectionDataValue = (key: any, value: any) => { + setConnectionEdits((prev) => { + const data = prev.data || {}; + return { + ...prev, + data: { ...data, [key]: value }, + }; + }); + }; + + const testConnection = async () => { + setTesting(true); + const data = connectionEdits.data || {}; + const json = await api.post('/api/test-connection', { + ...connectionEdits, + ...data, + }); + setTesting(false); + setTested(true); + setTestError(json.error); + }; + + const saveConnection = async (event: any) => { + event.preventDefault(); + event.stopPropagation(); + + if (saving) { + return; + } + + setSaving(true); + + // connectionEdits.idleTimeoutMinutes needs to be converted to seconds + const idleTimeoutMinutes = parseInt( + connectionEdits.idleTimeoutMinutes || '0', + 10 + ); + if (idleTimeoutMinutes) { + connectionEdits.idleTimeoutSeconds = idleTimeoutMinutes * 60; + } + + let json; + if (connectionId) { + json = await api.put(`/api/connections/${connectionId}`, connectionEdits); + } else { + json = await api.post('/api/connections', connectionEdits); + } + + if (json.error) { + setSaving(false); + return message.error(json.error); + } + if (json.data) { + setAsynchronousDriver(json.data.isAsynchronous); + } + api.reloadConnections(); + return onConnectionSaved(json.data); + }; + + const renderDriverFields = () => { + if (connectionEdits.driver && drivers?.length) { + // NOTE connection.driver is driverId + const driver = drivers.find( + (driver: any) => driver.id === connectionEdits.driver + ); + + if (!driver) { + console.error(`Driver ${connectionEdits.driver} not found`); + return null; + } + + const fieldsJsx: ReactNode[] = []; + if (driver.supportsConnectionClient) { + const mstKey = 'multiStatementTransactionEnabled'; + fieldsJsx.push( + + + setConnectionValue(e.target.name, e.target.checked) + } + /> + + + When enabled a persistent database connection will be opened and + used for query executions, allowing things like opening + transactions and creating temp tables across query executions. + + + ); + + fieldsJsx.push( + + ) => + setConnectionValue(e.target.name, e.target.value) + } + /> + + Number of minutes to allow a connection to be idle before closing. + + + ); + } + + const { fields } = driver; + const connectionEditsData = connectionEdits.data || {}; + const driverInputs = fields.map((field: any) => { + if (field.formType === TEXT) { + const value = connectionEditsData[field.key] || ''; + return ( + + ) => + setConnectionDataValue(e.target.name, e.target.value) + } + /> + {field.description && ( + +
      + + )} + + ); + } else if (field.formType === PASSWORD) { + const value = connectionEditsData[field.key] || ''; + // autoComplete='new-password' used to prevent browsers from autofilling username and password + // Because we dont return a password, Chrome goes ahead and autofills + return ( + + ) => + setConnectionDataValue(e.target.name, e.target.value) + } + /> + {field.description && ( + +
      + + )} + + ); + } else if (field.formType === CHECKBOX) { + const checked = Boolean(connectionEditsData[field.key]) || false; + return ( + + + setConnectionDataValue(e.target.name, e.target.checked) + } + /> + + {field.description && ( + +
      + + )} + + ); + } else if (field.formType === TEXTAREA) { + const value = connectionEditsData[field.key] || ''; + return ( + +