├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CODING_GUIDELINES.md ├── CONTRIBUTING.md ├── Dockerfile ├── EXAMPLES.md ├── LICENSE ├── NEW_CONNECTION.md ├── NEW_RELEASE.md ├── ORACLE.md ├── Procfile ├── README.md ├── app.json ├── app ├── actions │ └── sessions.js ├── background.tiff ├── components │ ├── Configuration.react.js │ ├── Link.react.js │ ├── Login.react.js │ ├── Oauth.react.js │ ├── Settings │ │ ├── ConnectButton │ │ │ └── ConnectButton.react.js │ │ ├── DialectSelector │ │ │ └── DialectSelector.react.js │ │ ├── Logger │ │ │ ├── Logger.react.js │ │ │ └── LoggerController.react.js │ │ ├── OptionsDropdown │ │ │ ├── OptionsDropdown.react.js │ │ │ ├── es-docs-options.jsx │ │ │ ├── es-indices-options.jsx │ │ │ └── sql-options.jsx │ │ ├── Preview │ │ │ ├── ApacheDrillPreview.js │ │ │ ├── Preview.react.js │ │ │ ├── S3Preview.js │ │ │ ├── TableTree.react.js │ │ │ ├── chart-editor.jsx │ │ │ ├── code-editor.css │ │ │ ├── code-editor.jsx │ │ │ ├── sql-table.css │ │ │ └── sql-table.jsx │ │ ├── Settings.react.js │ │ ├── Tabs │ │ │ ├── Tab.react.js │ │ │ └── Tabs.react.js │ │ ├── UserConnections │ │ │ ├── UserConnections.react.js │ │ │ └── filedrop.jsx │ │ ├── cron-picker │ │ │ ├── cron-helpers.js │ │ │ ├── cron-picker.css │ │ │ ├── cron-picker.jsx │ │ │ ├── details.jsx │ │ │ ├── modes │ │ │ │ ├── daily.jsx │ │ │ │ ├── frequently.js │ │ │ │ ├── hourly.jsx │ │ │ │ ├── index.js │ │ │ │ ├── minute.js │ │ │ │ ├── monthly.jsx │ │ │ │ └── weekly.jsx │ │ │ └── time-pickers.jsx │ │ └── scheduler │ │ │ ├── modals │ │ │ ├── create-modal │ │ │ │ ├── create-modal.css │ │ │ │ └── create-modal.jsx │ │ │ ├── login-modal │ │ │ │ ├── login-modal.css │ │ │ │ └── login-modal.jsx │ │ │ ├── preview-modal.jsx │ │ │ └── tags-modal │ │ │ │ ├── tags-modal.css │ │ │ │ └── tags-modal.jsx │ │ │ ├── pickers │ │ │ ├── color-picker.jsx │ │ │ └── tag-picker.jsx │ │ │ ├── presentational │ │ │ ├── api-call-counts.jsx │ │ │ ├── execution-details.jsx │ │ │ ├── request-error.jsx │ │ │ ├── sql.jsx │ │ │ ├── status.jsx │ │ │ ├── tag.jsx │ │ │ ├── timed-message.jsx │ │ │ └── timestamp.jsx │ │ │ ├── scheduler.css │ │ │ └── scheduler.jsx │ ├── error.jsx │ ├── layout.jsx │ ├── modal.jsx │ └── success.jsx ├── constants │ └── constants.js ├── containers │ └── DevTools.js ├── icons │ ├── app.icns │ ├── app.ico │ ├── app_1024x1024.png │ ├── app_16x16.png │ ├── app_256x256.png │ ├── app_32x32.png │ └── app_512x512.png ├── images │ ├── add.png │ ├── apache_drill-logo.png │ ├── athena-logo.LICENSE │ ├── athena-logo.png │ ├── bigquery-logo.LICENSE │ ├── bigquery-logo.png │ ├── checkmark.png │ ├── clickhouse-logo.png │ ├── csv-logo.png │ ├── dataworld-logo.png │ ├── delete.png │ ├── elastic-logo.png │ ├── favicon.ico │ ├── green-dot.svg │ ├── ibmdb2-logo.png │ ├── impala-logo.png │ ├── import-modal-http.png │ ├── import-modal-https.png │ ├── livy-logo.LICENSE │ ├── livy-logo.png │ ├── mariadb-logo-small.png │ ├── mariadb-logo.png │ ├── mssql-logo-small.png │ ├── mssql-logo.png │ ├── mysql-logo-small.png │ ├── mysql-logo.png │ ├── oracle-logo.png │ ├── plotly-connector-logo.svg │ ├── plotly-logo-no-name.png │ ├── plotly-logo.png │ ├── postgres-logo-small.png │ ├── postgres-logo.png │ ├── red-dot.svg │ ├── redshift-logo.png │ ├── running.png │ ├── s3-logo.png │ ├── spark-logo.LICENSE │ ├── spark-logo.png │ ├── sqlite-logo-small.png │ ├── sqlite-logo.png │ ├── unfold.png │ ├── x.png │ └── yellow-dot.svg ├── index.js ├── reducers │ └── index.js ├── store │ ├── configureStore.development.js │ ├── configureStore.js │ └── configureStore.production.js ├── utils │ ├── codemirror │ │ └── clickhouse.js │ ├── queryUtils.js │ └── utils.js └── yarn.lock ├── appveyor.yml ├── backend ├── certificates.js ├── constants.js ├── headless.development.js ├── init.js ├── logger.js ├── main.development.js ├── menus.js ├── parse.js ├── persistent │ ├── Connections.js │ ├── Queries.js │ ├── QueryScheduler.js │ ├── Tags.js │ ├── datastores │ │ ├── ApacheDrill.js │ │ ├── Datastores.js │ │ ├── Elasticsearch.js │ │ ├── S3.js │ │ ├── athena.js │ │ ├── bigquery.js │ │ ├── clickhouse.js │ │ ├── csv.js │ │ ├── datastoremock.js │ │ ├── dataworld.js │ │ ├── drivers │ │ │ └── athena.js │ │ ├── ibmdb2.js │ │ ├── impala.js │ │ ├── livy.js │ │ ├── oracle.js │ │ ├── pool.js │ │ └── sql.js │ └── plotly-api.js ├── plugins │ └── authorization.js ├── preload.js ├── routes.js ├── settings.js └── utils │ ├── authUtils.js │ ├── cronUtils.js │ ├── gridUtils.js │ ├── homeFiles.js │ └── persistenceUtils.js ├── circle.yml ├── mock-storage └── connections.yaml ├── package.json ├── sample-storage └── connections.yaml ├── scripts ├── fix-module-ibmdb.js └── rebuild-modules.js ├── shared └── constants.js ├── static ├── browserconfig.xml ├── css-vendor │ ├── Resizer.css │ ├── Treeview.css │ ├── normalize.css │ ├── react-dd-menu.css │ ├── react-select-custom.css │ ├── react-select.min.css │ ├── react-tabs.css │ ├── show-hint.css │ └── skeleton.css ├── css │ ├── Configuration.css │ ├── ConnectButton.css │ ├── DialectSelector.css │ ├── Link.css │ ├── Logger.css │ ├── Preview.css │ ├── Settings.css │ ├── TableDropdown.css │ ├── Tabs.css │ ├── UserConnections.css │ ├── app.global.css │ ├── fixed-data-table-custom.css │ └── react-select.css ├── fonts │ ├── dosis-v7-latin-ext_latin-regular.woff2 │ ├── dosis.LICENSE │ ├── open-sans-v15-vietnamese_greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 │ ├── open-sans.LICENSE │ ├── ubuntu-mono-v7-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 │ ├── ubuntu-v11-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 │ └── ubuntu.LICENCE ├── images │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── falcon-logo-by-plotly-stripe.png │ ├── falcon_hero.gif │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── index.html └── oauth.html ├── test ├── .eslintrc ├── app │ ├── components │ │ └── Settings │ │ │ ├── ConnectButton │ │ │ └── ConnectButton.test.js │ │ │ ├── DialectSelector │ │ │ └── DialectSelector.test.js │ │ │ ├── Preview │ │ │ ├── chart-editor.test.jsx │ │ │ └── sql-table.test.jsx │ │ │ ├── UserConnections │ │ │ └── filedrop.test.jsx │ │ │ └── scheduler │ │ │ ├── color-picker.test.jsx │ │ │ ├── create-modal.test.jsx │ │ │ ├── login-modal.test.jsx │ │ │ ├── preview-modal.test.jsx │ │ │ ├── scheduler.test.jsx │ │ │ ├── tag-picker.test.jsx │ │ │ └── tags-modal.test.jsx │ └── utils.js ├── athena │ ├── SETUP_ATHENA.md │ └── athena-sample.csv ├── backend │ ├── Connections.spec.js │ ├── Datastores.spec.js │ ├── Queries.spec.js │ ├── QueryScheduler.spec.js │ ├── Tags.spec.js │ ├── certificates.spec.js │ ├── datastores.athena.spec.js │ ├── datastores.clickhouse.spec.js │ ├── datastores.csv.spec.js │ ├── datastores.dataworld.spec.js │ ├── datastores.elasticsearch-v2.spec.js │ ├── datastores.elasticsearch-v5.spec.js │ ├── datastores.ibmdb.spec.js │ ├── datastores.impala.spec.js │ ├── datastores.livy.spec.js │ ├── datastores.mock.spec.js │ ├── datastores.oracle.spec.js │ ├── parse.spec.js │ ├── plotly-api.spec.js │ ├── plotly_datasets.db │ ├── routes.oauth2.spec.js │ ├── routes.queries.spec.js │ ├── routes.spec.js │ ├── routes.tags.spec.js │ ├── settings.spec.js │ └── utils.js ├── docker │ ├── README.md │ ├── clickhouse │ │ ├── Dockerfile │ │ ├── README.md │ │ └── setup.sh │ ├── deployment │ │ ├── db2-deployment.yaml │ │ ├── db2-service.yaml │ │ ├── impala-deployment.yaml │ │ ├── impala-service.yaml │ │ ├── spark-deployment.yaml │ │ └── spark-service.yaml │ ├── elasticsearch │ │ ├── Dockerfile-v5 │ │ ├── sample-data.test-type.ndjson │ │ ├── setup-v5.bash │ │ └── test-types.elastic-2.4-types.ndjson │ ├── ibmdb2 │ │ ├── Dockerfile │ │ ├── README.md │ │ └── build.sh │ ├── impala │ │ ├── Dockerfile │ │ ├── README.md │ │ └── bin │ │ │ ├── setup_database.sh │ │ │ └── setup_test_db.sql │ ├── oracle │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── setup.ctl │ │ ├── setup.sh │ │ └── setup.sql │ └── spark │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── bin │ │ ├── create_hive_tables.py │ │ ├── livy_python_requirements.txt │ │ ├── setup_database.sh │ │ ├── setup_java.sh │ │ └── setup_livy.sh │ │ ├── conf │ │ ├── hive-env.sh │ │ ├── hive-log4j2.properties │ │ ├── hive-site.xml │ │ ├── livy.conf │ │ └── my.cnf │ │ ├── create_hive_tables.py │ │ ├── setup_database.sh │ │ ├── setup_livy.sh │ │ └── setup_mysql.sh └── integration_test.js ├── webpack.config.base.js ├── webpack.config.electron.js ├── webpack.config.headless.js ├── webpack.config.production.js ├── webpack.config.web.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react", "jest"], 3 | "plugins": ["transform-decorators-legacy", "add-module-exports"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | }, 8 | "test": { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | node_modules 27 | 28 | # OSX 29 | .DS_Store 30 | 31 | # App packaged 32 | dist 33 | release 34 | main.js 35 | main.js.map 36 | static/*.js* 37 | 38 | # Local tests 39 | test/set_creds.sh 40 | 41 | *.pem 42 | 43 | *.srl 44 | 45 | .srl 46 | 47 | backend/headlessBundle.js 48 | 49 | backend/headlessBundle.js.map 50 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css,yml}] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | main.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://plot.ly/products/consulting-and-oem/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | 3 | # Tern 4 | .tern-port 5 | 6 | # Vim 7 | *.sw? 8 | 9 | # IntelliJ Idea 10 | .idea 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | sample-storage/log.log 17 | *.log 18 | *.log.* 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directory 36 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 37 | node_modules 38 | 39 | # OSX 40 | .DS_Store 41 | 42 | # App packaged 43 | dist 44 | release 45 | main.js 46 | main.js.map 47 | static/*.js* 48 | static/style.css 49 | 50 | # Local tests 51 | test/set_creds.sh 52 | *.pem 53 | 54 | # bundles 55 | backend/headlessBundle.js 56 | backend/headlessBundle.js.map 57 | 58 | *.srl 59 | 60 | .srl 61 | 62 | backend/headlessBundle.js 63 | 64 | backend/headlessBundle.js.map 65 | 66 | npm-shrinkwrap.unsafe.json 67 | 68 | #AWS Credentials 69 | aws.json 70 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: osx 4 | osx_image: xcode9.0 5 | language: node_js 6 | node_js: "8" 7 | env: 8 | - ELECTRON_CACHE=$HOME/.cache/electron 9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 10 | 11 | cache: 12 | directories: 13 | - node_modules 14 | - $HOME/.cache/electron 15 | - $HOME/.cache/electron-builder 16 | yarn: true 17 | 18 | install: 19 | - yarn install 20 | 21 | script: 22 | - yarn run build 23 | - yarn run pack 24 | - zip --junk-paths mac-falcon-v$(node -p "require('./package.json').version").zip release/*.dmg 25 | 26 | before_cache: 27 | - rm -rf $HOME/.cache/electron-builder/wine 28 | 29 | addons: 30 | artifacts: 31 | s3_region: us-east-1 32 | paths: $(ls mac-falcon-v*.zip | tr "\n" ":") 33 | debug: true 34 | -------------------------------------------------------------------------------- /CODING_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Coding Guidelines 2 | 3 | This is a live document that collects those coding guidelines we intend new code 4 | to follow. 5 | 6 | 7 | ## ESLint 8 | 9 | - Run `yarn lint` to validate your code. 10 | 11 | - Don't commit code with ESLint errors (it'll make the build in CircleCI fail). 12 | 13 | - Code with ESLint warnings won't make the build in CircleCI fail, but reviewers 14 | will likely request they're fixed before merging a contribution. 15 | 16 | 17 | ## Naming Conventions 18 | 19 | ### Path and Filenames 20 | 21 | - Use lower case names and only alphanumeric characters (unless required 22 | otherwise). 23 | 24 | - Avoid stuttering: 25 | - bad: `my-connector/my-connector-driver.js` 26 | - good: `my-connector/driver.js` 27 | 28 | - Use the extension `.jsx` for files that contain JSX and `.js` for those that 29 | only contain JavaScript. 30 | 31 | ### Variable and Function Names 32 | 33 | - Use JavaScript convention, i.e. camelCase. 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # $ docker build -t chriddyp/database-connector . 3 | # $ PLOTLY_DOMAIN_API="api-local.plot.ly" PLOTLY_STREAMBED_SSL_ENABLED="true" docker run -p 9494:9494 -v ~/.plotly/connector:/home -e PLOTLY_CONNECTOR_PLOTLY_API_DOMAIN=$PLOTLY_DOMAIN_API -e PLOTLY_CONNECTOR_PLOTLY_API_SSL_ENABLED=$PLOTLY_STREAMBED_SSL_ENABLED -e PLOTLY_CONNECTOR_IS_RUNNING_INSIDE_ON_PREM="true" PLOTLY_CONNECTOR_STORAGE_PATH="/home" -d chriddyp/database-connector 4 | # 5 | # Depends on the following ENV variables: 6 | # - PLOTLY_DOMAIN_API (e.g. api-local.plot.ly) 7 | # - PLOTLY_STREAMBED_SSL_ENABLED (e.g. true) 8 | # 9 | # Note that the command line argument PLOTLY_CONNECTOR_IS_RUNNING_INSIDE_ON_PREM="true" 10 | # should only be set if the connector is running on an on prem server. 11 | # If it is running on a local machine but it is targeting a remote on prem server, 12 | # then it should not be set. 13 | # 14 | # Adapted from https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ 15 | 16 | FROM node:8 17 | 18 | # I'd like to install the latest version of npm with something like this: 19 | # RUN npm install --global npm@4.0.3 20 | # But unforunately that fails with 21 | # It doesn't seem like there is a good solution in the community yet 22 | # See: https://github.com/npm/npm/issues/9863 23 | 24 | # The App saves folders to the `os.homedir()` directory. 25 | ENV HOME=/home/ 26 | 27 | # Create app directory 28 | RUN mkdir -p /usr/src/app 29 | WORKDIR /usr/src/app 30 | 31 | # Create a directory that the app can save files to and that the host can access 32 | # To map to the files that are saved on your host, run with: 33 | # $ docker run -v ~/.plotly/connector:/plotly-connector 34 | # TODO - With that command ^^, it's redudant to call `VOLUME /plotly-connector` right? 35 | # Save that directory as an ENV variable for the app to use when saving files 36 | # By default, the app will save these files in the home directory 37 | ENV PLOTLY_CONNECTOR_DATA_FOLDER="/plotly-connector" 38 | 39 | # Log to stdout in addition to the file 40 | ENV PLOTLY_CONNECTOR_LOG_TO_STDOUT="true" 41 | 42 | # Install app dependencies 43 | COPY package.json /usr/src/app 44 | COPY yarn.lock /usr/src/app 45 | RUN yarn install 46 | 47 | COPY . /usr/src/app 48 | RUN yarn run heroku-postbuild 49 | 50 | ENV PLOTLY_CONNECTOR_PORT 9494 51 | EXPOSE 9494 52 | ENTRYPOINT yarn run start-headless 53 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | ### PostGIS 2 | - [dc counties](https://github.com/alexandresobolevski/plotly_examples/tree/master/postgis/dc) 3 | - [montreal places](https://github.com/alexandresobolevski/plotly_examples/tree/master/postgis/montreal) 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Plotly, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NEW_RELEASE.md: -------------------------------------------------------------------------------- 1 | This document lists conventions and steps to follow for creating a release. 2 | 3 | # Version Number 4 | 5 | - Patch version numbers are reserved for OnPrem releases. For example, version 6 | `2.4.3` corresponds to an OnPrem release based on Falcon `v2.4.0`. 7 | 8 | # Release Checklist 9 | 10 | - [ ] Create branch `vM.m.p` 11 | - [ ] Update `CHANGELOG.md` 12 | - [ ] Update version number in `package.json` 13 | - [ ] Confirm CircleCI build works 14 | - [ ] Confirm TravisCI build works 15 | - [ ] Confirm AppVeyor build works 16 | - [ ] Confirm Quay build works 17 | - [ ] Confirm Windows installer works and Falcon is able to connect to DB2 and sqlite 18 | - [ ] Confirm Mac installer works and Falcon is able to connect to DB2 and sqlite 19 | - [ ] Do not use Firefox to download the Linux installer from CircleCI ([issue #471](https://github.com/plotly/falcon-sql-client/issues/471)) 20 | - [ ] Confirm Linux installer works and Falcon is able to connect to DB2 and sqlite 21 | - [ ] Ensure `yarn build` has been run before manual build 22 | - [ ] Build deb installer manually 23 | - [ ] Confirm deb installer works and Falcon is able to connect to sqlite 24 | - [ ] Build AppImage installer manually 25 | - [ ] Build rpm installer manually 26 | - [ ] Rebase and merge into `master` 27 | - [ ] Tag current `master` as `vM.m.p` and annotate it with the changelog message (this should automatically update the latest release in github) 28 | - [ ] Edit the release to attach all the installers. 29 | - [ ] Check links in https://plot.ly/free-sql-client-download/ 30 | 31 | ## Additional Checklist for an OnPrem Release 32 | 33 | - [ ] Create branch `M.m-onprem` 34 | - [ ] Create Quay tag `M.m-onprem` (Visit https://quay.io/repository/plotly/falcon-sql-client?tab=tags , click the gear icon next to the thing you want to tag, then choose “Add new…") 35 | 36 | ## Additional Checklist for an OnPrem Update 37 | 38 | - [ ] Create Quay tag `M.m.p-onprem` (Visit https://quay.io/repository/plotly/falcon-sql-client?tab=tags , click the gear icon next to the thing you want to tag, then choose “Add new…") 39 | 40 | -------------------------------------------------------------------------------- /ORACLE.md: -------------------------------------------------------------------------------- 1 | # Installation of the free Oracle Install Client 2 | 3 | Unlike the other connectors and as of this writing, the Oracle bindings for 4 | Node.js, [oracledb](https://www.npmjs.com/package/oracledb), do not include the 5 | necessary Oracle Client libraries and users are required to create an account on 6 | [Oracle](https://login.oracle.com/mysso/signon.jsp) before downloading them. 7 | 8 | Although the installation procedure is very well documented 9 | [here](https://github.com/oracle/node-oracledb/blob/master/INSTALL.md#instructions), 10 | see below a quick guide for Windows, Mac and Ubuntu. 11 | 12 | 13 | ## Windows 14 | 15 | *(instructions taken from [oracledb](https://github.com/oracle/node-oracledb/blob/master/INSTALL.md#364-install-the-free-oracle-instant-client-zip))* 16 | 17 | 1. Download the free 64-bit Instant Client Basic ZIP file from [Oracle Technology Network](http://www.oracle.com/technetwork/topics/winx64soft-089540.html). 18 | 2. Extract `instantclient-basic-windows.x64-12.2.0.1.0.zip` to a folder, such as `C:\oracle\instantclient_12_2`. 19 | 3. Add this folder to `PATH`. For example on Windows 7, update `PATH` in `Control Panel -> System -> Advanced System Settings -> Advanced -> Environment Variables -> System variables -> PATH` and add your path, such as `C:\oracle\instantclient_12_2`. 20 | 4. Download and install the correct Visual Studio Redistributable from Microsoft. Instant Client 12.2 requires the [Visual Studio 2013 redistributable](https://support.microsoft.com/en-us/kb/2977003#bookmark-vs2013). 21 | 22 | 23 | ## Mac 24 | 25 | *(instructions taken from [oracledb](https://github.com/oracle/node-oracledb/blob/master/INSTALL.md#354-install-the-free-oracle-instant-client-basic-zip-file))* 26 | 27 | Download the free Basic 64-bit ZIP from [Oracle Technology Network](http://www.oracle.com/technetwork/topics/intel-macsoft-096467.html) and unzip it, for example: 28 | 29 | ``` 30 | mkdir -p /opt/oracle 31 | unzip instantclient-basic-macos.x64-12.2.0.1.0.zip 32 | ``` 33 | 34 | Create a symbolic link for the 'client shared library' in the user default library path such as in `~/lib` or `/usr/local/lib`. For example: 35 | 36 | ``` 37 | mkdir ~/lib 38 | ln -s instantclient_12_2/libclntsh.dylib.12.1 ~/lib/ 39 | ``` 40 | 41 | Alternatively, copy the required OCI libraries, for example: 42 | 43 | ``` 44 | mkdir ~/lib 45 | cp instantclient_12_2/{libclntsh.dylib.12.1,libclntshcore.dylib.12.1,libons.dylib,libnnz12.dylib,libociei.dylib} ~/lib/ 46 | ``` 47 | 48 | 49 | ## Ubuntu 50 | 51 | 1. Install requirements: `sudo apt-get -qq update && sudo apt-get --no-install-recommends -qq install alien bc libaio1` 52 | 2. Create an account on [Oracle](https://login.oracle.com/mysso/signon.jsp) 53 | 3. Download the Oracle Instant Client from [here](http://download.oracle.com/otn/linux/oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip) 54 | 4. Unzip `rpm` package: `unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip` 55 | 5. Convert `rpm` package into `deb`: `alien oracle-xe-11.2.0-1.0.x86_64.rpm` 56 | 6. Install `deb` package: `sudo dpkg -i oracle-instantclient12.2-basiclite_12.2.0.1.0-2_amd64.deb` 57 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: PLOTLY_CONNECTOR_PORT=$PORT PLOTLY_CONNECTOR_STORAGE_PATH="/app/mock-storage" PLOTLY_CONNECTOR_AUTH_ENABLED=false node ./dist/headless-bundle.js 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plotly-database-connector", 3 | "scripts": { 4 | }, 5 | "env": { 6 | "PLOTLY_CONNECTOR_STORAGE_PATH": { 7 | "required": true 8 | } 9 | }, 10 | "formation": { 11 | "web": { 12 | "quantity": 1 13 | } 14 | }, 15 | "addons": [ 16 | "papertrail" 17 | ], 18 | "buildpacks": [ 19 | { 20 | "url": "heroku/nodejs" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /app/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/background.tiff -------------------------------------------------------------------------------- /app/components/Configuration.react.js: -------------------------------------------------------------------------------- 1 | import cookie from 'react-cookies'; 2 | import React, { Component, PropTypes } from 'react'; 3 | import {bindActionCreators} from 'redux'; 4 | import {connect} from 'react-redux'; 5 | 6 | import Login from './Login.react.js'; 7 | import * as SessionsActions from '../actions/sessions.js'; 8 | import Settings from './Settings/Settings.react.js'; 9 | import {isElectron, setUsernameListener} from '../utils/utils.js'; 10 | 11 | 12 | class Configuration extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | authEnabled: (cookie.load('db-connector-auth-enabled') === 'true'), 17 | clientId: cookie.load('db-connector-oauth2-client-id'), 18 | isMenuOpen: false, 19 | username: cookie.load('db-connector-user') 20 | }; 21 | this.toggle = this.toggle.bind(this); 22 | this.close = this.close.bind(this); 23 | this.logOut = this.logOut.bind(this); 24 | 25 | /* 26 | * If this is an electron app, then we don't have access to cookies and 27 | * we don't require authentication in the API. 28 | * In the browser, the username is set with a cookie but in electron 29 | * this is set using electron's ipcRenderer. 30 | */ 31 | setUsernameListener((event, message) => { 32 | this.setState({username: message});} 33 | ); 34 | } 35 | 36 | toggle() { 37 | this.setState({ isMenuOpen: !this.state.isMenuOpen }); 38 | } 39 | 40 | close() { 41 | this.setState({ isMenuOpen: false }); 42 | } 43 | 44 | logOut() { 45 | /* 46 | * Delete all the cookies and reset user state. This does not kill 47 | * any running connections, but user will not be able to access them 48 | * without logging in again. 49 | */ 50 | cookie.remove('db-connector-user'); 51 | cookie.remove('plotly-auth-token'); 52 | cookie.remove('db-connector-auth-token'); 53 | this.setState({ username: ''}); 54 | 55 | // reload page when running in browser: 56 | if (!isElectron()) { 57 | window.location.reload(); 58 | } 59 | } 60 | 61 | render() { 62 | return (isElectron() || !this.state.authEnabled || this.state.username) ? ( 63 |
64 | 65 |
66 | ) : ( 67 |
68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | Configuration.propTypes = { 75 | sessionsActions: PropTypes.object 76 | }; 77 | 78 | function mapStateToProps(state) { 79 | return {sessions: state.sessions}; 80 | } 81 | 82 | function mapDispatchToProps(dispatch) { 83 | const sessionsActions = bindActionCreators(SessionsActions, dispatch); 84 | return {sessionsActions}; 85 | } 86 | 87 | export default connect(mapStateToProps, mapDispatchToProps)(Configuration); 88 | -------------------------------------------------------------------------------- /app/components/Link.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function Link(props) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/Settings/Logger/Logger.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ImmutablePropTypes from 'react-immutable-proptypes'; 3 | import classnames from 'classnames'; 4 | 5 | const Logger = props => { 6 | if (!props.logs || props.logs.length === 0) return null; 7 | 8 | const renderLogs = props.logs.map(log => ( 9 |
{log.timestamp} - {log.logEntry.toString()}
10 | )); 11 | 12 | const testClass = () => { 13 | /* 14 | Return the number of logs to easily track updates when testing 15 | */ 16 | return `test-${props.logs.length}-entries`; 17 | }; 18 | 19 | return ( 20 |
24 |
Logs
25 |
26 |                 {renderLogs}
27 |             
28 |
29 | ); 30 | }; 31 | 32 | Logger.propTypes = { 33 | logs: ImmutablePropTypes.map.isRequired 34 | }; 35 | 36 | export default Logger; 37 | -------------------------------------------------------------------------------- /app/components/Settings/Logger/LoggerController.react.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ImmutablePropTypes from 'react-immutable-proptypes'; 3 | import Logger from './Logger.react'; 4 | 5 | export default class LoggerController extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {logs: []}; 9 | } 10 | 11 | componentWillReceiveProps(nextProps) { 12 | 13 | if (nextProps.ipc.has('log') && 14 | nextProps.ipc.get('log') !== this.props.ipc.get('log')) { 15 | this.state.logs.unshift(nextProps.ipc.get('log').toJS()); 16 | this.setState({logs: this.state.logs}); 17 | } 18 | 19 | const errorPath = ['error', 'message']; 20 | if (nextProps.ipc.hasIn(errorPath) && 21 | nextProps.ipc.getIn(errorPath) !== 22 | this.props.ipc.getIn(errorPath)) { 23 | 24 | this.state.logs.unshift({ 25 | logEntry: nextProps.ipc.getIn(errorPath), 26 | timestamp: nextProps.ipc.getIn(['error', 'timestamp']) 27 | }); 28 | 29 | this.setState({logs: this.state.logs}); 30 | } 31 | } 32 | 33 | render() { 34 | return ; 35 | } 36 | } 37 | 38 | LoggerController.propTypes = { 39 | ipc: ImmutablePropTypes.map.isRequired 40 | }; 41 | -------------------------------------------------------------------------------- /app/components/Settings/OptionsDropdown/OptionsDropdown.react.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SQLOptions from './sql-options'; 4 | import ESIndicesOptions from './es-indices-options'; 5 | import ESDocsOptions from './es-docs-options'; 6 | 7 | export default class OptionsDropdown extends Component { 8 | 9 | static propTypes = { 10 | // See type of react-select's Select `value` property 11 | selectedTable: PropTypes.string, 12 | selectedIndex: PropTypes.string, 13 | 14 | tablesRequest: PropTypes.object, 15 | setTable: PropTypes.func, 16 | elasticsearchMappingsRequest: PropTypes.object, 17 | setIndex: PropTypes.func 18 | } 19 | 20 | /** 21 | * Options Dropdown is an options drop down 22 | * @param {object} props - Component Properties 23 | * @param {object} props.selectedTable - The selected table 24 | * @param {object} props.tablesRequest - The Requested Table 25 | * @param {function} props.setTable - The set table function 26 | * @param {object} props.elasticsearchMappingsRequest - The ES Mapping Request 27 | * @param {object} props.selectedTable - The selected table 28 | * @param {object} props.selectedIndex - The Selected Index 29 | * @param {function} props.setIndex - The Set the index 30 | */ 31 | constructor(props) { 32 | super(props); 33 | } 34 | 35 | render() { 36 | const { 37 | selectedTable, 38 | tablesRequest, 39 | setTable, 40 | elasticsearchMappingsRequest: EMR, 41 | setIndex, 42 | selectedIndex 43 | } = this.props; 44 | return ( 45 |
46 | 50 | 54 | 59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/components/Settings/OptionsDropdown/es-docs-options.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {keys} from 'ramda'; 4 | import Select from 'react-select'; 5 | 6 | export default class ESDocsOptions extends Component { 7 | static propTypes = { 8 | selectedTable: PropTypes.string, 9 | selectedIndex: PropTypes.string, 10 | setTable: PropTypes.func, 11 | elasticsearchMappingsRequest: PropTypes.object 12 | } 13 | 14 | /** 15 | * ES Docs Options is an options drop down 16 | * @param {object} props - Component Properties 17 | * @param {object} props.elasticsearchMappingsRequest - The ES Mapping Request 18 | * @param {object} props.selectedTable - The selected table 19 | * @param {object} props.selectedIndex - The Selected Index 20 | * @param {object} props.setTable - The table 21 | */ 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | render() { 27 | const { 28 | selectedTable, 29 | selectedIndex, 30 | elasticsearchMappingsRequest: EMR, 31 | setTable 32 | } = this.props; 33 | 34 | if (!selectedIndex) { 35 | return null; 36 | } 37 | 38 | const tablesList = keys(EMR.content[selectedIndex].mappings); 39 | if (tablesList.length === 0) { 40 | return
{'No docs found'}
; 41 | } 42 | 43 | return ( 44 |
47 | ({label: t, value: t}))} 54 | value={selectedIndex} 55 | searchable={false} 56 | onChange={option => { 57 | setIndex(option.value); 58 | }} 59 | /> 60 |
61 | ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/components/Settings/OptionsDropdown/sql-options.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {flatten} from 'ramda'; 4 | import Select from 'react-select'; 5 | import {COLORS} from '../../../constants/constants'; 6 | 7 | export default class SQLOptions extends Component { 8 | 9 | static propTypes = { 10 | selectedTable: PropTypes.string, 11 | tablesRequest: PropTypes.object, 12 | setTable: PropTypes.func 13 | }; 14 | 15 | /** 16 | * SQLOptions is an options drop down down 17 | * @param {object} props - Component Properties 18 | * @param {object} props.selectedTable - The selected table 19 | * @param {object} props.tablesRequest - The Requested Table 20 | * @param {function} props.setTable - The set table function 21 | */ 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | render() { 27 | const {selectedTable, tablesRequest, setTable} = this.props; 28 | if (!tablesRequest.status) { 29 | return null; 30 | } else if (tablesRequest.status === 'loading') { 31 | return
{'Loading tables'}
; 32 | } else if (tablesRequest.status > 300) { 33 | // TODO - Make this prettier. 34 | return ( 35 |
36 |
{'Hm.. there was an error loading up your tables'}
37 |
{JSON.stringify(tablesRequest)}
38 |
39 | ); 40 | } else if (tablesRequest.status === 200) { 41 | const tablesList = flatten(tablesRequest.content); 42 | if (tablesList.length === 0) { 43 | return
{'No tables found'}
; 44 | } 45 | return ( 46 |
49 | ; 36 | HourInput.propTypes = { 37 | value: PropTypes.number.isRequired, 38 | onChange: PropTypes.func.isRequired 39 | }; 40 | 41 | export const MinuteInput = props => ( 42 | ; 50 | DayInput.propTypes = { 51 | value: PropTypes.number.isRequired, 52 | onChange: PropTypes.func.isRequired 53 | }; 54 | 55 | export const AmPmInput = props => ( 56 | ; 64 | DateInput.propTypes = { 65 | value: PropTypes.number.isRequired, 66 | onChange: PropTypes.func.isRequired 67 | }; 68 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/modals/create-modal/create-modal.css: -------------------------------------------------------------------------------- 1 | .create-modal .container { 2 | background: #F5F7FB; 3 | } 4 | 5 | .create-modal .innerColumn { 6 | background: #fff; 7 | position: relative; 8 | } 9 | 10 | .create-modal .header { 11 | margin-bottom: 16px; 12 | padding: 0 32px; 13 | } 14 | 15 | .create-modal .button { 16 | position: absolute; 17 | top: 16px; 18 | right: 16px; 19 | padding: 2px 4px; 20 | } 21 | 22 | .create-modal .close-btn { 23 | line-height: 14px; 24 | min-height: 16px; 25 | } 26 | 27 | .create-modal .detailsColumn { 28 | padding: 0 32px; 29 | } 30 | 31 | .create-modal p { 32 | margin: 16px 0; 33 | padding: 16px; 34 | } 35 | 36 | .create-modal .submit { 37 | width: auto; 38 | margin: 24px 32px 16px; 39 | min-height: 28px; 40 | } 41 | 42 | .create-modal .save-warning { 43 | font-size: 12px; 44 | margin: 0 32px 16px; 45 | opacity: 0.7; 46 | } 47 | 48 | .create-modal .row { 49 | justify-content: flex-start; 50 | } 51 | 52 | .create-modal .Select-input > input { 53 | margin: 0 auto; 54 | width: 100%; 55 | height: auto; 56 | } 57 | 58 | .create-modal .Select-create-option-placeholder, .create-modal .Select-create-option-placeholder > span { 59 | color: #506784 !important; 60 | } 61 | 62 | .create-modal .dropdown { 63 | padding: 0; 64 | margin-bottom: 16px; 65 | width: 100%; 66 | max-width: 432px; 67 | } 68 | 69 | .create-modal .row-header { 70 | width: 20%; 71 | } 72 | 73 | .create-modal .row-body { 74 | width: 80%; 75 | } 76 | 77 | .create-modal .row-body > pre { 78 | margin-top: 5px; 79 | overflow-y: auto; 80 | max-height: 300px; 81 | } 82 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/modals/login-modal/login-modal.css: -------------------------------------------------------------------------------- 1 | .login-modal .container { 2 | background: #F5F7FB; 3 | position: relative; 4 | } 5 | 6 | .login-modal .header { 7 | margin-bottom: 16px; 8 | padding: 0 32px; 9 | } 10 | 11 | .login-modal .button { 12 | position: absolute; 13 | top: 16px; 14 | right: 16px; 15 | padding: 2px 4px; 16 | } 17 | 18 | .login-modal .close-btn { 19 | line-height: 14px; 20 | min-height: 16px; 21 | } 22 | 23 | .login-modal p { 24 | margin: 56px 0 8px; 25 | padding: 16px; 26 | } 27 | 28 | .login-modal .actions button { 29 | width: 50%; 30 | margin: 24px 32px 32px; 31 | } 32 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/modals/login-modal/login-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {CopyToClipboard} from 'react-copy-to-clipboard'; 4 | 5 | import {Row, Column} from '../../../../layout'; 6 | import Modal from '../../../../modal'; 7 | 8 | import './login-modal.css'; 9 | 10 | const containerOverrideStyle = {width: '400px'}; 11 | const successTextStyle = {color: '#00cc96'}; 12 | 13 | class PromptLoginModal extends React.Component { 14 | static propTypes = { 15 | open: PropTypes.bool.isRequired, 16 | preview: PropTypes.object, 17 | onClickAway: PropTypes.func.isRequired, 18 | onSubmit: PropTypes.func.isRequired 19 | }; 20 | 21 | constructor() { 22 | super(); 23 | this.state = { 24 | copyCoolingDown: false 25 | }; 26 | 27 | this.onCopy = this.onCopy.bind(this); 28 | } 29 | 30 | onCopy() { 31 | if (!this.state.copyCoolingDown) { 32 | this.setState({copyCoolingDown: true}); 33 | setTimeout(() => this.setState({copyCoolingDown: false}), 2000); 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 | 40 | 41 | 44 | 45 |

46 | To create a scheduled query, you'll need to be logged into Chart Studio. 47 |
48 |
49 | Note: logging in will reset your query, click the button below to copy the query to your 50 | clipboard. 51 |

52 |
53 | 54 | 57 | 58 | 65 | 66 | 67 |
68 |
69 | ); 70 | } 71 | } 72 | 73 | export default PromptLoginModal; 74 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/modals/tags-modal/tags-modal.css: -------------------------------------------------------------------------------- 1 | .tag-modal .count { 2 | padding: 4px 16px 4px 0px; 3 | margin-right: 16px; 4 | border-right: 1px solid rgba(0, 0, 0, 0.12); 5 | } 6 | 7 | .tag-modal .note { 8 | font-size: 14px; 9 | color: #797979; 10 | } 11 | 12 | .createTag { 13 | margin: 24px 0 16px; 14 | padding: 0 24px; 15 | } 16 | 17 | .create-modal .createTag .tagName { 18 | margin: 0 24px; 19 | } 20 | 21 | .tagsContainer { 22 | border: 1px solid #c8d4e3; 23 | 24 | max-height: 328px; 25 | overflow-y: auto; 26 | } 27 | 28 | .scrollContainer { 29 | justify-content: flex-start !important; 30 | } 31 | 32 | .description.row-body { 33 | color: #797979; 34 | } 35 | 36 | .tagRow { 37 | flex-shrink: 0; 38 | background: white; 39 | padding: 16px 24px; 40 | border-bottom: 1px solid #c8d4e3; 41 | } 42 | 43 | .tagRow:last-of-type { 44 | border-bottom: none; 45 | } 46 | 47 | .color-box { 48 | cursor: pointer; 49 | margin: 0; 50 | width: 30px; 51 | height: 30px; 52 | margin-right: 24px; 53 | border: 1px solid #E0E7EF; 54 | box-sizing: border-box; 55 | border-radius: 3px; 56 | } 57 | 58 | .delete { 59 | cursor: pointer; 60 | color: #FF5E5E; 61 | margin-left: auto; 62 | font-size: 24px; 63 | line-height: 1; 64 | } 65 | 66 | .tagRow .delete-button { 67 | background: #FF5E5E; 68 | border: none; 69 | margin-right: 0; 70 | } 71 | 72 | .chrome-picker input { 73 | /* reset styles */ 74 | margin: initial !important; 75 | padding: initial; 76 | width: initial; 77 | float: none; 78 | } 79 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/pickers/color-picker.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import enhanceWithClickOutside from 'react-click-outside'; 4 | import {ChromePicker} from 'react-color'; 5 | 6 | class ColorPicker extends React.Component { 7 | static propTypes = { 8 | color: PropTypes.string, 9 | onClickAway: PropTypes.func 10 | }; 11 | 12 | static defaultProps = { 13 | onClickAway: () => {} 14 | }; 15 | 16 | handleClickOutside(e) { 17 | if (this.state.open) { 18 | this.setState({open: false}); 19 | this.props.onClickAway(e); 20 | } 21 | } 22 | 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | open: false 27 | }; 28 | this.openColorPicker = this.openColorPicker.bind(this); 29 | } 30 | 31 | openColorPicker() { 32 | this.setState({open: true}); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | {this.state.open && ( 39 |
40 | 41 |
42 | )} 43 |
44 | ); 45 | } 46 | } 47 | 48 | export default enhanceWithClickOutside(ColorPicker); 49 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/api-call-counts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const formatCalls = count => count.toLocaleString('en'); 5 | const pluralize = (word, num) => (num > 1 ? `${word}s` : word); 6 | 7 | export const AdditionalCallsPreview = props => { 8 | const {additionalCalls, currTotal} = props; 9 | const newTotal = additionalCalls + currTotal; 10 | 11 | return ( 12 |
13 | API Usage: {formatCalls(additionalCalls)} {pluralize('call', additionalCalls)} 14 | /day (new total: {formatCalls(newTotal)}) 15 |
16 | ); 17 | }; 18 | AdditionalCallsPreview.propTypes = { 19 | additionalCalls: PropTypes.number.isRequired, 20 | currTotal: PropTypes.number.isRequired 21 | }; 22 | AdditionalCallsPreview.defaultProps = { 23 | additionalCalls: 0, 24 | currTotal: 0 25 | }; 26 | 27 | export const toIndividualCallCountString = count => `${formatCalls(count)} API ${pluralize('call', count)} per day`; 28 | 29 | export const IndividualCallCount = ({count}) => { 30 | return {toIndividualCallCountString(count)}; 31 | }; 32 | IndividualCallCount.propTypes = { 33 | count: PropTypes.number.isRequired 34 | }; 35 | 36 | export const CallCountWidget = ({count}) => { 37 | return ( 38 | 39 |

40 | 46 | API USAGE 47 | 48 | 49 | {formatCalls(count)} {pluralize('call', count)} 50 | /day 51 | 52 | (for this connection) 53 |

54 |
55 | ); 56 | }; 57 | CallCountWidget.propTypes = { 58 | count: PropTypes.number.isRequired 59 | }; 60 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/execution-details.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactToolTip from 'react-tooltip'; 4 | import ms from 'ms'; 5 | import moment from 'moment'; 6 | import pluralize from 'pluralize'; 7 | 8 | const ExecutionDetails = props => { 9 | return ( 10 | 11 | {pluralize('row', props.rowCount || 0, true)} 12 | {' in '} 13 | 5 * 60 // 5 minutes 16 | ? `completed execution at ${moment(props.completedAt).format('h:mm a')}` 17 | : '' 18 | } 19 | > 20 | {ms(props.duration * 1000, {long: true})} 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | ExecutionDetails.propTypes = { 28 | duration: PropTypes.number, 29 | rowCount: PropTypes.number, 30 | completedAt: PropTypes.number 31 | }; 32 | 33 | export default ExecutionDetails; 34 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/request-error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ErrorMessage from '../../../error'; 4 | 5 | const propTypes = { 6 | children: PropTypes.string, 7 | onClick: PropTypes.func 8 | }; 9 | 10 | const outlineStyle = {outline: 'none'}; 11 | 12 | function capitalize(s = '') { 13 | return s[0].toUpperCase() + s.slice(1); 14 | } 15 | 16 | function FormattedMessage(props) { 17 | const content = capitalize(props.children); 18 | 19 | if (content.toLowerCase().includes('syntax')) { 20 | return ( 21 | 22 | {capitalize(content.replace('QueryExecutionError: ', ''))}.
23 |
24 | Syntax errors are usually easy to fix. We recommend making sure the query is correct in the{' '} 25 |
preview editor before scheduling it. 26 | 27 | ); 28 | } 29 | 30 | if (content.startsWith('QueryExecutionError')) { 31 | return ( 32 |
33 | 34 | An error occurred while executing the query. This is may be an issue with the query itself. 35 | 36 |
{content}
37 |
38 | ); 39 | } 40 | 41 | if (content.startsWith('PlotlyApiError')) { 42 | return ( 43 |
44 | 45 | An unexpected error occurred syncing the query to the Chart Studio. Please try again later. 46 | 47 |
{content}
48 |
49 | ); 50 | } 51 | 52 | if (content.startsWith('MetadataError')) { 53 | return ( 54 |
55 | 56 | An unexpected error occurred while uploading query metadata. Please try again now. 57 | 58 |
{capitalize(content.replace('MetadataError: ', ''))}
59 |
60 | ); 61 | } 62 | 63 | return content.slice(0, 100); 64 | } 65 | 66 | FormattedMessage.propTypes = propTypes; 67 | 68 | function RequestError(props) { 69 | return ( 70 | 71 | 72 | 73 | ); 74 | } 75 | 76 | RequestError.propTypes = propTypes; 77 | 78 | export default RequestError; 79 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/sql.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Highlight from 'react-highlight'; 4 | 5 | export const SQL = props => {props.children}; 6 | 7 | SQL.propTypes = { 8 | children: PropTypes.string, 9 | className: PropTypes.string 10 | }; 11 | 12 | export default SQL; 13 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/status.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import {EXE_STATUS} from '../../../../../shared/constants.js'; 6 | 7 | const Status = ({status, size}) => { 8 | if (status === EXE_STATUS.ok) { 9 | return ; 10 | } 11 | 12 | if (status === EXE_STATUS.running) { 13 | return ; 14 | } 15 | 16 | return ; 17 | }; 18 | 19 | Status.propTypes = { 20 | status: PropTypes.string, 21 | size: PropTypes.number 22 | }; 23 | 24 | Status.defaultProps = { 25 | status: EXE_STATUS.ok 26 | }; 27 | 28 | export default Status; 29 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/tag.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /* eslint-disable */ 5 | const Tag = ({name, color, style, className}) => ( 6 | 21 | {name} 22 | 23 | ); 24 | 25 | Tag.propTypes = { 26 | name: PropTypes.string.isRequired, 27 | color: PropTypes.string 28 | }; 29 | 30 | export default Tag; 31 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/timed-message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class TimedMessage extends React.Component { 5 | static propTypes = { 6 | children: PropTypes.node, 7 | timeout: PropTypes.number.isRequired 8 | }; 9 | static defaultProps = { 10 | timeout: 5000 11 | }; 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | show: false 18 | }; 19 | 20 | const component = this; 21 | this.timer = setTimeout(function() { 22 | component.setState({show: true}); 23 | }, this.props.timeout); 24 | } 25 | 26 | componentWillUnmount() { 27 | clearTimeout(this.timer); 28 | } 29 | 30 | render() { 31 | if (this.state.show) { 32 | return this.props.children; 33 | } 34 | 35 | return null; 36 | } 37 | } 38 | 39 | export default TimedMessage; 40 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/presentational/timestamp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactToolTip from 'react-tooltip'; 4 | import moment from 'moment'; 5 | 6 | const toIso = timestamp => { 7 | const date = new Date(timestamp); 8 | 9 | return date.getTime() > 0 ? date.toISOString() : 'Invalid ISO'; 10 | }; 11 | 12 | const formatAbsolute = (timestamp, inline) => { 13 | const start = new Date(); 14 | const end = new Date(); 15 | start.setHours(0, 0, 0, 0); 16 | end.setHours(23, 59, 59, 999); 17 | 18 | const startOfToday = start.getTime(); 19 | const endOfToday = end.getTime(); 20 | 21 | const isToday = timestamp >= startOfToday && timestamp <= endOfToday; 22 | 23 | return isToday 24 | ? `${inline ? 't' : 'T'}oday at ${moment(timestamp).format('h:mm a')}` 25 | : `${moment(timestamp).format('h:mm a')} on ${moment(timestamp).format('MMM Do')}`; 26 | }; 27 | 28 | const formatRelative = ({value, checkIfRunning}) => { 29 | if (checkIfRunning && value < Date.now()) { 30 | return 'Currently running'; 31 | } 32 | 33 | return moment(value).fromNow(); 34 | }; 35 | 36 | const Timestamp = props => { 37 | const {value, checkIfRunning, inline} = props; 38 | return ( 39 | 40 | {formatRelative({value, checkIfRunning})} 41 | ({toIso(props.value)}) 42 | 43 | 44 | ); 45 | }; 46 | 47 | Timestamp.propTypes = { 48 | value: PropTypes.number, 49 | checkIfRunning: PropTypes.bool, 50 | inline: PropTypes.bool 51 | }; 52 | 53 | export default Timestamp; 54 | -------------------------------------------------------------------------------- /app/components/Settings/scheduler/scheduler.css: -------------------------------------------------------------------------------- 1 | /* SQL token highlighting */ 2 | .default .hljs-keyword, 3 | .default .hljs-selector-tag { 4 | color: #ab63fa; 5 | } 6 | 7 | .default .hljs-number, 8 | .default .hljs-meta, 9 | .default .hljs-built_in, 10 | .default .hljs-builtin-name, 11 | .default .hljs-literal, 12 | .default .hljs-type, 13 | .default .hljs-params { 14 | color: #00cc96; 15 | } 16 | 17 | .default .hljs-string, 18 | .default .hljs-symbol, 19 | .default .hljs-bullet { 20 | color: #119DFF; 21 | } 22 | 23 | .default .hljs { 24 | color: #585260; 25 | } 26 | 27 | .bold .hljs-keyword, 28 | .bold .hljs-selector-tag { 29 | font-weight: bold; 30 | } 31 | 32 | .scheduler pre { 33 | white-space: pre-wrap; 34 | } 35 | 36 | /* custom ReactDataGrid styles for scheduled queries table */ 37 | .scheduler-table .react-grid-Row--odd .react-grid-Cell { 38 | background: #fff; 39 | } 40 | 41 | .scheduler-table .react-grid-Row--odd:hover .react-grid-Cell { 42 | background-color: #f9f9f9; 43 | } 44 | 45 | /* fix horizontal overflow issues */ 46 | .scheduler-table .react-grid-Cell { 47 | box-sizing: border-box; 48 | border-right: none; 49 | } 50 | 51 | .scheduler-table .react-grid-Row { 52 | cursor: pointer; 53 | } 54 | 55 | .scheduler-table .react-grid-Grid { 56 | min-height: calc(100vh - 380px) !important; 57 | } 58 | 59 | .scheduler-table .react-grid-Canvas { 60 | height: calc(100vh - 352px) !important; 61 | } 62 | 63 | .scheduler-table pre { 64 | margin: 0; 65 | } 66 | 67 | .sql pre { 68 | margin: 0 auto; 69 | max-height: 100%; 70 | } 71 | 72 | .scheduler .CodeMirror { 73 | width: 100%; 74 | } 75 | 76 | .ellipsis { 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | overflow: hidden; 80 | } 81 | 82 | .ellipsis pre { 83 | text-overflow: ellipsis; 84 | white-space: nowrap; 85 | overflow: hidden; 86 | } 87 | 88 | .meta-preview button { 89 | min-height: 28px; 90 | } 91 | 92 | .meta-preview pre { 93 | margin: 0; 94 | } 95 | 96 | .meta-preview .Select-input > input { 97 | margin: 0 auto; 98 | width: 100%; 99 | height: auto; 100 | } 101 | 102 | .meta-preview .Select-create-option-placeholder, .meta-preview .Select-create-option-placeholder > span { 103 | color: #506784 !important; 104 | } 105 | 106 | .tag-manager-text { 107 | cursor: pointer; 108 | font-size: 12px; 109 | color: #9D9D9D; 110 | } 111 | -------------------------------------------------------------------------------- /app/components/error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const style = {height: 'auto'}; 5 | const ErrorMessage = props =>
{props.children}
; 6 | ErrorMessage.propTypes = { 7 | children: PropTypes.node.isRequired 8 | }; 9 | 10 | export default ErrorMessage; 11 | -------------------------------------------------------------------------------- /app/components/layout.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This module contains a collection of helpful primitive components 3 | * for layout purposes. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | 8 | export const Row = props => ( 9 |
20 | {props.children} 21 |
22 | ); 23 | 24 | Row.propTypes = { 25 | children: PropTypes.node, 26 | style: PropTypes.object 27 | }; 28 | 29 | export const Column = props => ( 30 | 35 | ); 36 | 37 | Column.propTypes = { 38 | style: PropTypes.object 39 | }; 40 | -------------------------------------------------------------------------------- /app/components/modal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import enhanceWithClickOutside from 'react-click-outside'; 5 | 6 | const ClickAway = enhanceWithClickOutside( 7 | class extends Component { 8 | static propTypes = { 9 | onClickAway: PropTypes.func, 10 | children: PropTypes.node 11 | } 12 | handleClickOutside(e) { 13 | this.props.onClickAway(e); 14 | } 15 | render() { 16 | return this.props.children; 17 | } 18 | } 19 | ); 20 | 21 | const Modal = props => 22 | props.open ? ( 23 |
39 | 40 | {props.children} 41 | 42 |
43 | ) : null; 44 | 45 | Modal.propTypes = { 46 | children: PropTypes.node, 47 | onClickAway: PropTypes.func, 48 | open: PropTypes.bool, 49 | className: PropTypes.string 50 | }; 51 | 52 | export default Modal; 53 | -------------------------------------------------------------------------------- /app/components/success.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const style = {fontSize: 16}; 5 | const SuccessMessage = (props) => ( 6 |
7 |
{props.message}
8 | {props.children} 9 |
10 | ); 11 | 12 | SuccessMessage.propTypes = { 13 | children: PropTypes.node, 14 | message: PropTypes.string 15 | }; 16 | 17 | export default SuccessMessage; 18 | -------------------------------------------------------------------------------- /app/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /app/icons/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app.icns -------------------------------------------------------------------------------- /app/icons/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app.ico -------------------------------------------------------------------------------- /app/icons/app_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app_1024x1024.png -------------------------------------------------------------------------------- /app/icons/app_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app_16x16.png -------------------------------------------------------------------------------- /app/icons/app_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app_256x256.png -------------------------------------------------------------------------------- /app/icons/app_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app_32x32.png -------------------------------------------------------------------------------- /app/icons/app_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/icons/app_512x512.png -------------------------------------------------------------------------------- /app/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/add.png -------------------------------------------------------------------------------- /app/images/apache_drill-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/apache_drill-logo.png -------------------------------------------------------------------------------- /app/images/athena-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/athena-logo.png -------------------------------------------------------------------------------- /app/images/bigquery-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/bigquery-logo.png -------------------------------------------------------------------------------- /app/images/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/checkmark.png -------------------------------------------------------------------------------- /app/images/clickhouse-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/clickhouse-logo.png -------------------------------------------------------------------------------- /app/images/csv-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/csv-logo.png -------------------------------------------------------------------------------- /app/images/dataworld-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/dataworld-logo.png -------------------------------------------------------------------------------- /app/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/delete.png -------------------------------------------------------------------------------- /app/images/elastic-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/elastic-logo.png -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/favicon.ico -------------------------------------------------------------------------------- /app/images/green-dot.svg: -------------------------------------------------------------------------------- 1 | icon-status_online_01 -------------------------------------------------------------------------------- /app/images/ibmdb2-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/ibmdb2-logo.png -------------------------------------------------------------------------------- /app/images/impala-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/impala-logo.png -------------------------------------------------------------------------------- /app/images/import-modal-http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/import-modal-http.png -------------------------------------------------------------------------------- /app/images/import-modal-https.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/import-modal-https.png -------------------------------------------------------------------------------- /app/images/livy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/livy-logo.png -------------------------------------------------------------------------------- /app/images/mariadb-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mariadb-logo-small.png -------------------------------------------------------------------------------- /app/images/mariadb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mariadb-logo.png -------------------------------------------------------------------------------- /app/images/mssql-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mssql-logo-small.png -------------------------------------------------------------------------------- /app/images/mssql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mssql-logo.png -------------------------------------------------------------------------------- /app/images/mysql-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mysql-logo-small.png -------------------------------------------------------------------------------- /app/images/mysql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/mysql-logo.png -------------------------------------------------------------------------------- /app/images/oracle-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/oracle-logo.png -------------------------------------------------------------------------------- /app/images/plotly-connector-logo.svg: -------------------------------------------------------------------------------- 1 | dbconnector -------------------------------------------------------------------------------- /app/images/plotly-logo-no-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/plotly-logo-no-name.png -------------------------------------------------------------------------------- /app/images/plotly-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/plotly-logo.png -------------------------------------------------------------------------------- /app/images/postgres-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/postgres-logo-small.png -------------------------------------------------------------------------------- /app/images/postgres-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/postgres-logo.png -------------------------------------------------------------------------------- /app/images/red-dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | reddot 5 | 6 | Calque 1 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/images/redshift-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/redshift-logo.png -------------------------------------------------------------------------------- /app/images/running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/running.png -------------------------------------------------------------------------------- /app/images/s3-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/s3-logo.png -------------------------------------------------------------------------------- /app/images/spark-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/spark-logo.png -------------------------------------------------------------------------------- /app/images/sqlite-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/sqlite-logo-small.png -------------------------------------------------------------------------------- /app/images/sqlite-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/sqlite-logo.png -------------------------------------------------------------------------------- /app/images/unfold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/unfold.png -------------------------------------------------------------------------------- /app/images/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/app/images/x.png -------------------------------------------------------------------------------- /app/images/yellow-dot.svg: -------------------------------------------------------------------------------- 1 | 2 | yellow dot 3 | 4 | Layer 1 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Workaround to use `react-router@3` in React 16 4 | import PropTypes from 'prop-types'; // eslint-disable-line no-unused-vars 5 | 6 | import {render} from 'react-dom'; 7 | import {Provider} from 'react-redux'; 8 | import {Route, Router, browserHistory} from 'react-router'; 9 | 10 | import configureStore from './store/configureStore'; 11 | import {build, version} from '../package.json'; 12 | 13 | import Login from './components/Login.react'; 14 | import Configuration from './components/Configuration.react'; 15 | import Status from './components/Oauth.react'; 16 | import {homeUrl, isOnPrem} from './utils/utils'; 17 | 18 | const store = configureStore(); 19 | 20 | window.document.title = isOnPrem() ? 21 | `${build.productName}` : 22 | `${build.productName} v${version}`; 23 | 24 | render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('root') 33 | ); 34 | -------------------------------------------------------------------------------- /app/store/configureStore.development.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { persistState } from 'redux-devtools'; 3 | import thunk from 'redux-thunk'; 4 | import createLogger from 'redux-logger'; 5 | import rootReducer from '../reducers'; 6 | import DevTools from '../containers/DevTools'; 7 | 8 | const logger = createLogger({ 9 | level: 'info', 10 | collapsed: true 11 | }); 12 | 13 | const enhancer = compose( 14 | applyMiddleware(thunk, logger), 15 | // electronEnhancer(true), // ({test: true}), 16 | DevTools.instrument(), 17 | persistState( 18 | window.location.href.match( 19 | /[?&]debug_session=([^&]+)\b/ 20 | ) 21 | ), 22 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 23 | ); 24 | 25 | export default function configureStore(initialState) { 26 | const store = createStore(rootReducer, initialState, enhancer); 27 | 28 | if (module.hot) { 29 | module.hot.accept('../reducers', () => 30 | store.replaceReducer(require('../reducers')) 31 | ); 32 | } 33 | 34 | window.store = store; 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.production'); 3 | } else { 4 | module.exports = require('./configureStore.development'); 5 | } 6 | -------------------------------------------------------------------------------- /app/store/configureStore.production.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | const enhancer = applyMiddleware(thunk); 6 | 7 | export default function configureStore(initialState) { 8 | const store = createStore(rootReducer, initialState, enhancer); 9 | 10 | window.store = store; 11 | 12 | return store; 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/queryUtils.js: -------------------------------------------------------------------------------- 1 | export function mapQueryToDailyCallCount (query) { 2 | const { cronInterval, refreshInterval } = query || {}; 3 | if (!cronInterval && !refreshInterval) { 4 | return 0; 5 | } 6 | 7 | if (cronInterval) { 8 | if (cronInterval === '* * * * *') { 9 | return 1440; // number of minutes in a day 10 | } else if (cronInterval === '*/5 * * * *') { 11 | return 288; // number of 5 minute blocks per day 12 | } else if (cronInterval.match(/\S+? \* \* \* \*/)) { 13 | return 24; // number of hours in a day 14 | } 15 | 16 | // could be daily or weekly. Deliberately return same amount either way. 17 | // Provides worst-case estimate for user. 18 | return 1; 19 | } 20 | 21 | // otherwise, fallback to refresh interval 22 | return Math.floor(86400 / refreshInterval); // seconds per day / scheduling interval in seconds 23 | } 24 | -------------------------------------------------------------------------------- /app/utils/utils.js: -------------------------------------------------------------------------------- 1 | import {contains} from 'ramda'; 2 | 3 | export function isElectron() { 4 | // the electron's main process preloads a script that sets up `window.$falcon` 5 | return Boolean(window.$falcon); 6 | } 7 | 8 | export function setUsernameListener(callback) { 9 | if (isElectron()) { 10 | window.$falcon.setUsernameListener(callback); 11 | } 12 | } 13 | 14 | export function showOpenDialog(options, callback) { 15 | if (isElectron()) { 16 | return window.$falcon.showOpenDialog(options, callback); 17 | } 18 | } 19 | 20 | export function baseUrl() { 21 | /* 22 | * Return the base URL of the app. 23 | * If the user is running this app locally and is configuring their database, 24 | * then they will be at https://their-subdomain.plot.ly/database-connector 25 | * and this function will return https://their-subdomain.plot.ly 26 | * If the user is on on-prem, then the connector is prefixed behind 27 | * /external-data-connector, so they will be configuring their connections at 28 | * https://plotly.acme.com/external-data-connector/database-connector and this function 29 | * will return https://plotly.acme.com/external-data-connector/ 30 | */ 31 | let url = window.location.href; 32 | if (url.endsWith('/')) { 33 | url = url.slice(0, url.length - 1); 34 | } 35 | if (url.endsWith('database-connector')) { 36 | url = url.slice(0, url.length - 'database-connector'.length); 37 | } 38 | return url; 39 | } 40 | 41 | export function usesHttpsProtocol() { 42 | return contains('https://', baseUrl()); 43 | } 44 | 45 | export function isOnPrem() { 46 | return (contains('external-data-connector', baseUrl())); 47 | } 48 | 49 | export function plotlyUrl() { 50 | if (isOnPrem()) { 51 | return window.location.origin; 52 | } 53 | return 'https://plot.ly'; 54 | } 55 | 56 | export function homeUrl() { 57 | return (isOnPrem()) ? 58 | '/external-data-connector' : 59 | ''; 60 | } 61 | 62 | export function datasetUrl(fid) { 63 | const [account, gridId] = fid.split(':'); 64 | return `${plotlyUrl()}/~${account}/${gridId}`; 65 | } 66 | 67 | export function getPathNames(url) { 68 | const parser = document.createElement('a'); 69 | parser.href = url; 70 | const pathNames = parser.pathname.split('/'); 71 | 72 | return pathNames; 73 | } 74 | 75 | export function decapitalize(startString) { 76 | if (startString.length === 0) { 77 | return ''; 78 | } 79 | return startString.slice(0, 1).toLowerCase() + startString.slice(1); 80 | } 81 | -------------------------------------------------------------------------------- /app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.4.{build} 2 | 3 | environment: 4 | matrix: 5 | - nodejs_version: 8 6 | POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 7 | 8 | platform: 9 | - x64 10 | 11 | services: 12 | - postgresql96 13 | 14 | cache: 15 | - '%USERPROFILE%\.electron' 16 | 17 | init: 18 | - git config --global core.autocrlf input 19 | - SET PATH=%POSTGRES_PATH%\bin;%PATH% 20 | 21 | 22 | install: 23 | - ps: Install-Product node $env:nodejs_version $env:platform 24 | - yarn install --force 25 | 26 | build_script: 27 | - yarn run build 28 | - yarn run pack 29 | 30 | after_build: 31 | - ps: | 32 | $FALCON_VERSION = node -p "require('./package.json').version" 33 | cd release 34 | 7z a win-falcon-v$FALCON_VERSION.zip *.exe 35 | 36 | artifacts: 37 | - path: release\*.zip 38 | 39 | test: off 40 | -------------------------------------------------------------------------------- /backend/constants.js: -------------------------------------------------------------------------------- 1 | import {getSetting} from './settings'; 2 | 3 | export function getUnsecuredCookieOptions() { 4 | return { 5 | path: getSetting('WEB_BASE_PATHNAME') 6 | }; 7 | } 8 | 9 | export function getCookieOptions() { 10 | return { 11 | secure: getSetting('SSL_ENABLED'), 12 | path: getSetting('WEB_BASE_PATHNAME') 13 | }; 14 | } 15 | 16 | export function getAccessTokenCookieOptions() { 17 | return { 18 | secure: getSetting('SSL_ENABLED'), 19 | path: getSetting('WEB_BASE_PATHNAME'), 20 | maxAge: getSetting('ACCESS_TOKEN_AGE') 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /backend/headless.development.js: -------------------------------------------------------------------------------- 1 | // Entry point for running the app without electron 2 | import Logger from './logger'; 3 | import Servers from './routes.js'; 4 | import {getSetting} from './settings.js'; 5 | 6 | /* 7 | * If running on the same machine as plotly on-premise, 8 | * then we do not need to verify SSL certificates as 9 | * requests to plotly will never leave the server. 10 | * 11 | * This is to allow users to use Plotly On-Prem with 12 | * a self-signed certificate generated by replicated 13 | * or by themselves without actually specifying the 14 | * path of the actual certificate. 15 | * See https://github.com/plotly/streambed/issues/8948. 16 | * 17 | * Note: if any requests are made to other domains 18 | * or if any other services end up running inside 19 | * this docker container, then this setting should 20 | * be applied on a request-by-request basis as an 21 | * argument of the `fetch` `agent` instead of 22 | * set as a process variable. 23 | */ 24 | if (getSetting('IS_RUNNING_INSIDE_ON_PREM')) { 25 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 26 | } 27 | 28 | const servers = new Servers(); 29 | Logger.log('Starting server', 2); 30 | servers.httpServer.start(); 31 | Logger.log('Loading persistent queries', 2); 32 | servers.queryScheduler.loadQueries(); 33 | -------------------------------------------------------------------------------- /backend/init.js: -------------------------------------------------------------------------------- 1 | import Logger from './logger'; 2 | 3 | import { 4 | deleteAllConnections, 5 | deleteBadConnections 6 | } from './persistent/Connections.js'; 7 | import {getSetting} from './settings.js'; 8 | 9 | const setCSVStorageSize = require('./persistent/datastores/csv.js').setStorageSize; 10 | 11 | export default function init() { 12 | try { 13 | deleteBadConnections(); 14 | } catch (error) { 15 | Logger.log(`Failed to delete bad connections: ${error.message}`); 16 | deleteAllConnections(); 17 | } 18 | 19 | try { 20 | setCSVStorageSize(getSetting('CSV_STORAGE_SIZE')); 21 | } catch (error) { 22 | Logger.log(`Failed to get setting CSV_STORAGE_SIZE: ${error.message}`); 23 | setCSVStorageSize(0); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/logger.js: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan'; 2 | import {createStoragePath} from './utils/homeFiles'; 3 | import {getSetting} from './settings'; 4 | 5 | // TODO - Set max size of the log file 6 | // Note: class is named "Loggr" because name "Logger" would cause 7 | // `import/no-named-as-default` ESLint rule to complain when 8 | // the default logger (i.e. this module's default export) would 9 | // be imported as `import Logger from 'logger'`. 10 | export class Loggr { 11 | constructor() { 12 | createStoragePath(); 13 | let streams; 14 | if (getSetting('LOG_TO_STDOUT')) { 15 | streams = [{ 16 | level: 'info', 17 | stream: process.stdout 18 | }]; 19 | } else { 20 | streams = [{ 21 | level: 'info', 22 | path: getSetting('LOG_PATH') 23 | }]; 24 | } 25 | 26 | this.logToFile = bunyan.createLogger({ 27 | name: 'plotly-database-connector-logger', 28 | streams 29 | }); 30 | 31 | this.log = (logEntry, code = 2) => { 32 | switch (code) { 33 | case 0: 34 | this.logToFile.error(logEntry); 35 | break; 36 | case 1: 37 | this.logToFile.warn(logEntry); 38 | break; 39 | case 2: 40 | this.logToFile.info(logEntry); 41 | break; 42 | default: 43 | this.logToFile.info(logEntry); 44 | } 45 | }; 46 | 47 | } 48 | } 49 | 50 | export default new Loggr(); 51 | -------------------------------------------------------------------------------- /backend/persistent/Queries.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import {findIndex, propEq} from 'ramda'; 3 | import YAML from 'yamljs'; 4 | 5 | import {getSetting} from '../settings.js'; 6 | import {createStoragePath} from '../utils/homeFiles'; 7 | import {stripUndefinedKeys} from '../utils/persistenceUtils.js'; 8 | 9 | export function getQuery(fid) { 10 | const queries = getQueries(); 11 | return queries.find(query => query.fid === fid); 12 | } 13 | 14 | export function getQueries() { 15 | if (fs.existsSync(getSetting('QUERIES_PATH'))) { 16 | return YAML.load(getSetting('QUERIES_PATH').toString()); 17 | } 18 | return []; 19 | } 20 | 21 | export function saveQuery(queryObject) { 22 | const queries = getQueries(); 23 | queries.push(stripUndefinedKeys(queryObject)); 24 | if (!fs.existsSync(getSetting('STORAGE_PATH'))) { 25 | createStoragePath(); 26 | } 27 | fs.writeFileSync(getSetting('QUERIES_PATH'), YAML.stringify(queries, 4)); 28 | } 29 | 30 | export function updateQuery(fid, updatedQueryData) { 31 | const existingQuery = getQuery(fid); 32 | if (!existingQuery) { 33 | // don't allow appending data to query that doesn't exist 34 | return; 35 | } 36 | 37 | const updatedQuery = stripUndefinedKeys({ 38 | ...existingQuery, 39 | ...updatedQueryData 40 | }); 41 | 42 | deleteQuery(fid); 43 | saveQuery(updatedQuery); 44 | } 45 | 46 | export function deleteQuery(fid) { 47 | const queries = getQueries(); 48 | const index = findIndex(propEq('fid', fid), queries); 49 | 50 | if (index > -1) { 51 | queries.splice(index, 1); 52 | fs.writeFileSync(getSetting('QUERIES_PATH'), YAML.stringify(queries, 4)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/persistent/Tags.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import shortid from 'shortid'; 3 | import {findIndex, propEq} from 'ramda'; 4 | import YAML from 'yamljs'; 5 | 6 | import {getSetting} from '../settings.js'; 7 | import {createStoragePath} from '../utils/homeFiles'; 8 | import {stripUndefinedKeys} from '../utils/persistenceUtils.js'; 9 | 10 | export const HEX_CODE_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; 11 | export const MAX_TAG_LENGTH = 30; 12 | 13 | export function getTag(id) { 14 | const tags = getTags(); 15 | return tags.find(tag => tag.id === id); 16 | } 17 | 18 | export function getTags() { 19 | if (fs.existsSync(getSetting('TAGS_PATH'))) { 20 | return YAML.load(getSetting('TAGS_PATH').toString()); 21 | } 22 | return []; 23 | } 24 | 25 | export function saveTag(newTag) { 26 | if (!newTag.id) { 27 | newTag.id = shortid.generate(); 28 | } 29 | 30 | const tags = getTags(); 31 | tags.push(newTag); 32 | 33 | if (!fs.existsSync(getSetting('STORAGE_PATH'))) { 34 | createStoragePath(); 35 | } 36 | fs.writeFileSync(getSetting('TAGS_PATH'), YAML.stringify(tags, 4)); 37 | 38 | return newTag; 39 | } 40 | 41 | export function updateTag(id, updatedTagData) { 42 | const existingTag = getTag(id); 43 | if (!existingTag) { 44 | // don't allow appending data to query that doesn't exist 45 | return; 46 | } 47 | 48 | const updatedTag = stripUndefinedKeys({ 49 | ...existingTag, 50 | ...updatedTagData 51 | }); 52 | 53 | deleteTag(id); 54 | return saveTag(updatedTag); 55 | } 56 | 57 | export function deleteTag(id) { 58 | const tags = getTags(); 59 | const index = findIndex(propEq('id', id), tags); 60 | 61 | if (index > -1) { 62 | tags.splice(index, 1); 63 | fs.writeFileSync(getSetting('TAGS_PATH'), YAML.stringify(tags, 4)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/persistent/datastores/ApacheDrill.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import * as S3 from './S3.js'; 3 | 4 | 5 | export function connect(connection) { 6 | const {host, port} = connection; 7 | const url = `${host}:${port}/query.json`; 8 | return fetch(url); 9 | } 10 | 11 | export function query(queryStatement, connection) { 12 | const {host, port} = connection; 13 | const url = `${host}:${port}/query.json`; 14 | 15 | return new Promise((resolve, reject) => { 16 | fetch(url, { 17 | headers: { 18 | 'Accept': 'application/json', 19 | 'Content-Type': 'application/json' 20 | }, 21 | method: 'POST', 22 | body: JSON.stringify({ 23 | queryType: 'SQL', 24 | query: queryStatement 25 | }) 26 | }) 27 | .then(response => response.json()) 28 | .then(json => { 29 | const {errorMessage, rows, columns} = json; 30 | if (errorMessage) { 31 | reject({message: errorMessage}); 32 | } else { 33 | resolve({ 34 | rows: rows.map(row => columns.map(c => row[c])), 35 | columnnames: columns 36 | }); 37 | } 38 | }) 39 | .catch(reject); 40 | }); 41 | 42 | } 43 | 44 | export function storage(connection) { 45 | const {host, port} = connection; 46 | const url = `${host}:${port}/storage.json`; 47 | return fetch(url).then(res => res.json()); 48 | } 49 | 50 | // TODO - Make this more flexible? 51 | export function listS3Files(connection) { 52 | return S3.files({ 53 | bucket: connection.bucket, 54 | accessKeyId: connection.accessKeyId, 55 | secretAccessKey: connection.secretAccessKey 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /backend/persistent/datastores/Elasticsearch.js: -------------------------------------------------------------------------------- 1 | import {parseElasticsearch} from '../../parse'; 2 | import fetch from 'node-fetch'; 3 | 4 | const KEEP_ALIVE_FOR = '1m'; // minutes 5 | const MAX_RESULTS_SIZE = 10 * 1000; 6 | 7 | function request(relativeUrl, connection, {body, method, queryStringParams = ''}) { 8 | const {host, port, username, password} = connection; 9 | let url; 10 | if (typeof port !== 'undefined' && port !== '') { 11 | url = `${host}:${port}/${relativeUrl}?format=json${queryStringParams}`; 12 | } else { 13 | url = `${host}/${relativeUrl}?format=json${queryStringParams}`; 14 | } 15 | const headers = { 16 | 'Accept': 'application/json', 17 | 'Content-Type': 'application/json' 18 | }; 19 | if (username && password) { 20 | headers.Authorization = 'Basic ' + new Buffer( 21 | username + ':' + password 22 | ).toString('base64'); 23 | } 24 | return fetch(url, { 25 | headers, 26 | method, 27 | body: body ? JSON.stringify(body) : null 28 | }); 29 | } 30 | 31 | export function connect(connection) { 32 | return request('_cat/indices/', connection, {method: 'GET'}); 33 | } 34 | 35 | export function query(queryStmt, connection) { 36 | const queryObject = JSON.parse(queryStmt); 37 | const {body, index, type} = queryObject; 38 | /* 39 | * If size is not defined or smaller than 10K, keep scroll disabled and 40 | * let elasticsearch handle it, which it defaults right now to 10. 41 | * If it is, and it is higher than 10K then enable scroll. 42 | */ 43 | const scrollEnabled = parseInt(body.size, 10) > MAX_RESULTS_SIZE; 44 | /* 45 | * In order to use scrolling, the initial search request should specify 46 | * the scroll parameter in the query string, which tells Elasticsearch 47 | * how long it should keep the “search context” alive. 48 | */ 49 | const scrollParam = scrollEnabled ? `&scroll=${KEEP_ALIVE_FOR}` : ''; 50 | 51 | return elasticsearchMappings(connection) 52 | .then(mappings => { 53 | const mapping = mappings[index].mappings[type].properties; 54 | return request(`${index}/${type}/_search`, connection, { 55 | body, 56 | method: 'POST', 57 | queryStringParams: scrollParam 58 | }) 59 | .then(res => res.json().then(results => { 60 | if (res.status === 200) { 61 | return parseElasticsearch(body, results, mapping); 62 | } 63 | throw new Error(JSON.stringify(results)); 64 | })); 65 | }); 66 | } 67 | 68 | export function elasticsearchMappings(connection) { 69 | return request('_all/_mappings', connection, {method: 'GET'}) 70 | .then(res => res.json()); 71 | } 72 | -------------------------------------------------------------------------------- /backend/persistent/datastores/S3.js: -------------------------------------------------------------------------------- 1 | import AWS from 'aws-sdk'; 2 | import {parseCSV} from '../../parse.js'; 3 | 4 | function createClient(connection) { 5 | AWS.config.update({ 6 | accessKeyId: connection.accessKeyId, 7 | secretAccessKey: connection.secretAccessKey 8 | }); 9 | return new AWS.S3(); 10 | } 11 | 12 | 13 | export function connect(connection) { 14 | const {bucket} = connection; 15 | const client = createClient(connection); 16 | return new Promise((resolve, reject) => { 17 | client.listObjects({Bucket: bucket}, err => { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | /* 28 | * Download a (csv) file from S3, parse it 29 | * and return the results. 30 | */ 31 | export function query(key, connection) { 32 | const {bucket} = connection; 33 | const client = createClient(connection); 34 | return new Promise((resolve, reject) => { 35 | client.getObject({Bucket: bucket, Key: key}, (err, response) => { 36 | if (err) { 37 | reject(err); 38 | return; 39 | } 40 | const textData = response.Body.toString(); 41 | // TODO ^ handle binary files too 42 | parseCSV(textData).then(resolve); 43 | }); 44 | }); 45 | } 46 | 47 | /* 48 | * List all of the files in an S3 bucket 49 | */ 50 | export function files(connection) { 51 | const {bucket} = connection; 52 | const client = createClient(connection); 53 | return new Promise((resolve, reject) => { 54 | client.listObjects({Bucket: bucket}, (err, data) => { 55 | if (err) { 56 | reject(err); 57 | return; 58 | } 59 | resolve(data.Contents); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /backend/persistent/datastores/ibmdb2.js: -------------------------------------------------------------------------------- 1 | const ibmdb = require('ibm_db'); 2 | 3 | import {dissoc} from 'ramda'; 4 | 5 | import Logger from '../../logger'; 6 | import {parseSQL} from '../../parse'; 7 | 8 | 9 | const clients = {}; 10 | 11 | function getClient(connection) { 12 | const connectionString = getConnectionString(connection); 13 | 14 | let client = clients[connectionString]; 15 | if (!client) { 16 | client = new Promise(function(resolve, reject) { 17 | ibmdb.open(connectionString, function(err, conn) { 18 | if (err) reject(err); 19 | else resolve(conn); 20 | }); 21 | }); 22 | 23 | clients[connectionString] = client; 24 | } 25 | 26 | return client; 27 | } 28 | 29 | function getConnectionString(connection) { 30 | return `DATABASE=${connection.database};UID=${connection.username};PWD=${connection.password};` + 31 | `HOSTNAME=${connection.host};PORT=${connection.port};PROTOCOL=TCPIP`; 32 | } 33 | 34 | export function connect(connection) { 35 | Logger.log('' + 36 | 'Attempting to authenticate with connection ' + 37 | `${JSON.stringify(dissoc('password', connection), null, 2)} ` + 38 | '(password omitted)' 39 | ); 40 | 41 | return getClient(connection); 42 | } 43 | 44 | export function query(queryStmt, connection) { 45 | return getClient(connection) 46 | .then(function(client) { 47 | return new Promise(function(resolve, reject) { 48 | client.query(queryStmt, function(err, rows) { 49 | if (err) reject(err); 50 | else resolve(parseSQL(rows)); 51 | }); 52 | }); 53 | }); 54 | } 55 | 56 | const SYSTEM_SCHEMAS = ['SYSCAT', 'SYSIBM', 'SYSIBMADM', 'SYSPUBLIC', 'SYSSTAT', 'SYSTOOLS']; 57 | const WHERE = SYSTEM_SCHEMAS.map(t => `CREATOR <> '${t}'`).join(' AND '); 58 | const QUERY = `SELECT NAME, CREATOR FROM SYSIBM.SYSTABLES WHERE ${WHERE}`; 59 | 60 | export function tables(connection) { 61 | return getClient(connection) 62 | .then(function(client) { 63 | return new Promise(function(resolve, reject) { 64 | client.query(QUERY, function(err, rows) { 65 | if (err) reject(err); 66 | else resolve(rows.map(row => `${row.CREATOR}.${row.NAME}`)); 67 | }); 68 | }); 69 | }); 70 | } 71 | 72 | export function schemas(connection) { 73 | return query( 74 | "SELECT TABNAME, COLNAME, TYPENAME FROM syscat.columns WHERE SUBSTR(TABSCHEMA,1,3) != 'SYS'", 75 | connection 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /backend/persistent/datastores/pool.js: -------------------------------------------------------------------------------- 1 | export default class Pool { 2 | /** 3 | * Pool keeps a list of clients indexed by connection objects 4 | * 5 | * @param {function} newClient Function that takes a connection object and creates a new client 6 | * @param {function} sameConnection Function that returns whether two connection objects are the same 7 | */ 8 | constructor(newClient, sameConnection) { 9 | this.newClient = newClient; 10 | this.sameConnection = sameConnection; 11 | this._pool = []; 12 | } 13 | 14 | /** 15 | * Get client indexed by connection (if no client found, a new client is created using newClient) 16 | * @param {object} connection Connection object 17 | * @returns {*} client for connection 18 | */ 19 | getClient(connection) { 20 | for (let i = this._pool.length - 1; i >= 0; i--) { 21 | if (this.sameConnection(connection, this._pool[i][0])) { 22 | return this._pool[i][1]; 23 | } 24 | } 25 | 26 | const client = this.newClient(connection); 27 | this._pool.push([connection, client]); 28 | return client; 29 | } 30 | 31 | /** 32 | * Remove connection from pool 33 | * @param {object} connection Connection object 34 | * @returns {*} removed client (or undefined, if no client was found) 35 | */ 36 | remove(connection) { 37 | for (let i = this._pool.length - 1; i >= 0; i--) { 38 | if (this.sameConnection(connection, this._pool[i][0])) { 39 | return this._pool.splice(i, 1)[0][1]; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/preload.js: -------------------------------------------------------------------------------- 1 | // this file is preloaded by electron in the renderer process 2 | // (it has access to electron's API, even when nodeIntegration has been disabled) 3 | 4 | const {ipcRenderer, remote} = require('electron'); 5 | 6 | const propertyOptions = { 7 | configurable: false, 8 | enumerable: false, 9 | writable: false 10 | }; 11 | 12 | process.once('loaded', () => { 13 | const $falcon = {}; 14 | 15 | Object.defineProperty(window, '$falcon', { 16 | value: $falcon, 17 | ...propertyOptions 18 | }); 19 | 20 | Object.defineProperty($falcon, 'showOpenDialog', { 21 | value: remote.dialog.showOpenDialog, 22 | ...propertyOptions 23 | }); 24 | 25 | let onUsername; 26 | let receivedUsernameBeforeCallback; 27 | let username; 28 | function sendUsername(event, user) { 29 | try { 30 | if (onUsername) { 31 | receivedUsernameBeforeCallback = false; 32 | onUsername(event, user); 33 | } else { 34 | receivedUsernameBeforeCallback = true; 35 | username = user; 36 | } 37 | } catch (error) { 38 | console.error(error); // eslint-disable-line 39 | } 40 | } 41 | 42 | ipcRenderer.on('username', sendUsername); 43 | 44 | Object.defineProperty($falcon, 'setUsernameListener', { 45 | value: (value) => { 46 | onUsername = value; 47 | 48 | if (receivedUsernameBeforeCallback) { 49 | sendUsername(null, username); 50 | } 51 | }, 52 | ...propertyOptions 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /backend/utils/authUtils.js: -------------------------------------------------------------------------------- 1 | import uuidv4 from 'uuid/v4'; 2 | import {getSetting, saveSetting} from '../settings'; 3 | 4 | export function generateAndSaveAccessToken() { 5 | const currentTime = Date.now(); 6 | 7 | if (getSetting('ACCESS_TOKEN_EXPIRY') < currentTime) { 8 | const accessToken = uuidv4(); 9 | saveSetting('ACCESS_TOKEN', accessToken); 10 | 11 | // ACCESS_TOKEN_AGE is expressed in seconds, so we convert it to milliseconds: 12 | saveSetting('ACCESS_TOKEN_EXPIRY', currentTime + (getSetting('ACCESS_TOKEN_AGE') * 1000)); 13 | return accessToken; 14 | } 15 | 16 | return getSetting('ACCESS_TOKEN'); 17 | } 18 | -------------------------------------------------------------------------------- /backend/utils/cronUtils.js: -------------------------------------------------------------------------------- 1 | const MAX_CRON_MINUTE = 59; 2 | 3 | export function mapRefreshToCron (refreshInterval) { 4 | const now = new Date(); 5 | 6 | // try to intelligently select interval 7 | if (refreshInterval <= (60 * (1 + 5)) / 2) { 8 | // case: refreshInterval is closer to 1 minute than to 5 minutes 9 | return '* * * * *'; 10 | } else if (refreshInterval <= (60 * (5 + 60)) / 2) { 11 | // case: refreshInterval is closer to 5 minutes than to 1 hour 12 | return `${now.getSeconds()} ${computeMinutes(now)} * * * *`; 13 | } else if (refreshInterval <= (60 * 60 * (1 + 24)) / 2) { 14 | // case: refreshInterval is closer to 1 hour than to 1 day 15 | return `${now.getMinutes()} * * * *`; 16 | } else if (refreshInterval <= (24 * 60 * 60 * (1 + 7)) / 2) { 17 | // case: refreshInterval is closer to 1 day than to 1 week 18 | return `${now.getMinutes()} ${now.getHours()} * * *`; 19 | } 20 | 21 | // otherwise, default to once a week 22 | return `${now.getMinutes()} ${now.getHours()} * * ${now.getDay()}`; 23 | } 24 | 25 | export function mapCronToRefresh (cronInterval) { 26 | const DEFAULT_INTERVAL = 60 * 60 * 24 * 7; // default to weekly 27 | 28 | if (!cronInterval) { 29 | return DEFAULT_INTERVAL; 30 | } 31 | 32 | if (cronInterval === '* * * * *') { 33 | return 60; 34 | } else if (cronInterval === '*/5 * * * *') { 35 | return 60 * 5; 36 | } else if (cronInterval.match(/\S+? \* \* \* \*/)) { 37 | return 60 * 60; 38 | } else if (cronInterval.match(/\S+? \S+? \* \* \*/)) { 39 | return 60 * 60 * 24; 40 | } 41 | 42 | return DEFAULT_INTERVAL; 43 | } 44 | 45 | function computeMinutes (now) { 46 | let currMinute = now.getMinutes() % 5; // start at 5 min offset 47 | const minutes = []; 48 | 49 | while (currMinute < MAX_CRON_MINUTE) { 50 | minutes.push(currMinute); 51 | currMinute += 5; 52 | } 53 | 54 | return minutes.join(','); 55 | } -------------------------------------------------------------------------------- /backend/utils/gridUtils.js: -------------------------------------------------------------------------------- 1 | export function extractOrderedUids (grid) { 2 | try { 3 | return Object.keys(grid.cols) 4 | .map(key => grid.cols[key]) 5 | .sort((a, b) => a.order - b.order) 6 | .map(col => col.uid); 7 | } catch (e) { 8 | return null; 9 | } 10 | } -------------------------------------------------------------------------------- /backend/utils/homeFiles.js: -------------------------------------------------------------------------------- 1 | import {getSetting} from '../settings'; 2 | import mkdirp from 'mkdirp'; 3 | 4 | export function createStoragePath () { 5 | mkdirp.sync(getSetting('STORAGE_PATH')); 6 | } 7 | -------------------------------------------------------------------------------- /backend/utils/persistenceUtils.js: -------------------------------------------------------------------------------- 1 | import {reject} from 'ramda'; 2 | 3 | // prevent 'yamljs' from coerecing undefined keys into null 4 | export function stripUndefinedKeys (obj) { 5 | return reject(isUndefined, obj); 6 | } 7 | 8 | function isUndefined (val) { 9 | return typeof val === 'undefined'; 10 | } -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8-browsers 6 | - image: quay.io/plotly/falcon-test-spark 7 | - image: quay.io/plotly/falcon-test-db2 8 | - image: quay.io/plotly/falcon-test-clickhouse 9 | environment: 10 | PATH: "/usr/local/opt/curl/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/home/circleci/project/node_modules/.bin" 11 | working_directory: /home/circleci/falcon 12 | steps: 13 | - restore_cache: 14 | key: v1-source-{{ .Branch }}-{{ .Revision }} 15 | keys: 16 | - v1-source-{{ .Branch }}- 17 | - v1-source- 18 | - checkout 19 | - save_cache: 20 | key: v1-source-{{ .Branch }}-{{ .Revision }} 21 | paths: 22 | - ".git" 23 | - restore_cache: 24 | key: v2-yarn-{{ checksum "yarn.lock" }} 25 | keys: 26 | - v2-yarn- 27 | - run: node --version 28 | - run: yarn --version 29 | - run: yarn install 30 | - run: yarn run rebuild:modules:electron 31 | - save_cache: 32 | key: v2-yarn-{{ checksum "yarn.lock" }} 33 | paths: 34 | - node_modules 35 | - run: yarn run build 36 | - run: yarn run test 37 | - run: yarn run pack 38 | 39 | # create artifacts/ and move installer from release/falcon-sql-client-vM.m.p.tar.gz to artifacts/linux-falcon-vM.m.p.tar.gz 40 | - run: mkdir artifacts 41 | - run: for f in release/falcon-sql-client-*.tar.gz ; do mv $f artifacts/linux-falcon-v${f#release/falcon-sql-client-}; done 42 | 43 | - store_artifacts: 44 | path: artifacts 45 | -------------------------------------------------------------------------------- /scripts/fix-module-ibmdb.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | function execSync(command) { 4 | console.log('>>', command); // eslint-disable-line no-console 5 | return require('child_process').execSync(command, {stdio: 'inherit'}); 6 | } 7 | 8 | // Workaround for https://github.com/ibmdb/node-ibm_db/issues/329 9 | if (os.platform() === 'darwin') { 10 | execSync([ 11 | 'install_name_tool -change', 12 | '`pwd`/node_modules/ibm_db/installer/clidriver/lib/libdb2.dylib', 13 | '@loader_path/../../installer/clidriver/lib/libdb2.dylib', 14 | 'node_modules/ibm_db/build/Release/odbc_bindings.node' 15 | ].join(' ')); 16 | } 17 | -------------------------------------------------------------------------------- /scripts/rebuild-modules.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const electronEnv = (process.argv[2] === '--electron'); 4 | 5 | function execSync(command) { 6 | console.log('>>', command); // eslint-disable-line no-console 7 | return require('child_process').execSync(command, {stdio: 'inherit'}); 8 | } 9 | 10 | if (electronEnv) { 11 | const localBin = require('child_process').execSync('yarn bin').toString().trim(); 12 | const installAppDeps = path.join(localBin, 'electron-builder install-app-deps'); 13 | execSync(installAppDeps); 14 | } else { 15 | execSync('yarn install --force'); 16 | } 17 | -------------------------------------------------------------------------------- /shared/constants.js: -------------------------------------------------------------------------------- 1 | export const EXE_STATUS = { 2 | running: 'running', 3 | ok: 'ok', 4 | failed: 'failed' 5 | }; -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /static/css-vendor/Resizer.css: -------------------------------------------------------------------------------- 1 | .Resizer { 2 | background: #000; 3 | opacity: .2; 4 | z-index: 1; 5 | -moz-box-sizing: border-box; 6 | -webkit-box-sizing: border-box; 7 | box-sizing: border-box; 8 | -moz-background-clip: padding; 9 | -webkit-background-clip: padding; 10 | background-clip: padding-box; 11 | } 12 | 13 | .Resizer:hover { 14 | -webkit-transition: all 2s ease; 15 | transition: all 2s ease; 16 | } 17 | 18 | .Resizer.horizontal { 19 | height: 11px; 20 | margin: -5px 0; 21 | border-top: 5px solid rgba(255, 255, 255, 0); 22 | border-bottom: 5px solid rgba(255, 255, 255, 0); 23 | cursor: row-resize; 24 | width: 100%; 25 | } 26 | 27 | .Resizer.horizontal:hover { 28 | border-top: 5px solid rgba(0, 0, 0, 0.5); 29 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 30 | } 31 | 32 | .Resizer.vertical { 33 | width: 11px; 34 | margin: 0 -5px; 35 | border-left: 5px solid rgba(255, 255, 255, 0); 36 | border-right: 5px solid rgba(255, 255, 255, 0); 37 | cursor: col-resize; 38 | } 39 | 40 | .Resizer.vertical:hover { 41 | border-left: 5px solid rgba(0, 0, 0, 0.5); 42 | border-right: 5px solid rgba(0, 0, 0, 0.5); 43 | } 44 | .Resizer.disabled { 45 | cursor: not-allowed; 46 | } 47 | .Resizer.disabled:hover { 48 | border-color: transparent; 49 | } -------------------------------------------------------------------------------- /static/css-vendor/Treeview.css: -------------------------------------------------------------------------------- 1 | /* the tree node's style */ 2 | .tree-view { 3 | overflow-y: hidden; 4 | overflow-x: hidden; 5 | font-size: 13px; 6 | padding: 2px 0; 7 | font-family: 'Ubuntu', courier, monospace; 8 | } 9 | 10 | .tree-view_item { 11 | /* immediate child of .tree-view, for styling convenience */ 12 | } 13 | 14 | /* style for the children nodes container */ 15 | .tree-view_children { 16 | margin-left: 16px; 17 | } 18 | 19 | .tree-view_children-collapsed { 20 | height: 0px; 21 | } 22 | 23 | .tree-view_arrow { 24 | cursor: pointer; 25 | margin-right: 6px; 26 | display: inline-block; 27 | -webkit-user-select: none; 28 | -moz-user-select: none; 29 | -ms-user-select: none; 30 | user-select: none; 31 | display: inline; 32 | color: #a2b1c6; 33 | } 34 | 35 | .tree-view_arrow:hover { 36 | color: #119DFF; 37 | } 38 | 39 | .tree-view_arrow:after { 40 | content: '▾'; 41 | font-size: 14px; 42 | } 43 | 44 | .tree-view_arrow-collapsed:after { 45 | content: '►'; 46 | font-size: 8px; 47 | } 48 | 49 | /* rotate the triangle to close it */ 50 | .tree-view_arrow-collapsed { 51 | -webkit-transform: rotate(-90deg); 52 | -moz-transform: rotate(-90deg); 53 | -ms-transform: rotate(-90deg); 54 | transform: rotate(-90deg); 55 | } 56 | 57 | .tree-view .info { 58 | padding: 0; 59 | font-size: 13px; 60 | overflow: hidden; 61 | white-space: nowrap; 62 | padding: 2px 0; 63 | } 64 | 65 | .tree-view .info code { 66 | font-size: 11px; 67 | padding: 1px 6px; 68 | background: #EBF0F8; 69 | color: black; 70 | border-radius: 2px; 71 | border: 1px solid #C8D4E3; 72 | } 73 | 74 | .tree-view-container { 75 | height: 100%; 76 | background: rgb(251, 252, 253); 77 | border-radius: 0 0 0 6px; 78 | overflow: hidden; 79 | } 80 | 81 | .loading { 82 | padding: 10px; 83 | } 84 | 85 | .loading:after { 86 | overflow: hidden; 87 | display: inline-block; 88 | vertical-align: bottom; 89 | -webkit-animation: ellipsis steps(4,end) 900ms infinite; 90 | animation: ellipsis steps(4,end) 900ms infinite; 91 | content: "\2026"; /* ascii code for the ellipsis character */ 92 | width: 0px; 93 | } 94 | 95 | @keyframes ellipsis { 96 | to { 97 | width: 1.25em; 98 | } 99 | } 100 | 101 | @-webkit-keyframes ellipsis { 102 | to { 103 | width: 1.25em; 104 | } 105 | } -------------------------------------------------------------------------------- /static/css-vendor/react-select-custom.css: -------------------------------------------------------------------------------- 1 | .Select-menu-outer { 2 | z-index: 999 !important; 3 | } 4 | -------------------------------------------------------------------------------- /static/css-vendor/react-tabs.css: -------------------------------------------------------------------------------- 1 | .react-tabs__tab-list { 2 | padding: 5px 0; 3 | margin: 0; 4 | text-transform: uppercase; 5 | font-family: 'Dosis'; 6 | color: #506784; 7 | font-size: 18px; 8 | border-bottom: 1px solid #c8dfe3; 9 | background: #fff; 10 | } 11 | 12 | .react-tabs__tab { 13 | display: inline-block; 14 | border-bottom: none; 15 | position: relative; 16 | list-style: none; 17 | padding: 6px 20px; 18 | cursor: pointer; 19 | } 20 | 21 | .react-tabs__tab--selected { 22 | background: #fff; 23 | border-radius: 0; 24 | color: #119dff; 25 | } 26 | 27 | .react-tabs__tab-panel { 28 | display: none; 29 | } 30 | 31 | .react-tabs__tab-panel--selected { 32 | display: block; 33 | } 34 | 35 | .react-tabs__tab--disabled { 36 | color: GrayText; 37 | cursor: default; 38 | } 39 | 40 | .react-tabs__tab:focus { 41 | outline: none; 42 | } 43 | 44 | .react-tabs__tab:focus:after { 45 | content: ""; 46 | position: absolute; 47 | height: 5px; 48 | left: -4px; 49 | right: -4px; 50 | bottom: -5px; 51 | background: #fff; 52 | } 53 | 54 | .big-whitespace-tab { 55 | padding: 50px 200px; 56 | } 57 | 58 | .react-tabs__tab-list { 59 | -webkit-animation: fadein 2s; /* Safari, Chrome and Opera > 12.1 */ 60 | -moz-animation: fadein 2s; /* Firefox < 16 */ 61 | -ms-animation: fadein 2s; /* Internet Explorer */ 62 | -o-animation: fadein 2s; /* Opera < 12.1 */ 63 | animation: fadein 2s; 64 | } 65 | 66 | @keyframes fadein { 67 | from { opacity: 0; } 68 | to { opacity: 1; } 69 | } 70 | 71 | /* Firefox < 16 */ 72 | @-moz-keyframes fadein { 73 | from { opacity: 0; } 74 | to { opacity: 1; } 75 | } 76 | 77 | /* Safari, Chrome and Opera > 12.1 */ 78 | @-webkit-keyframes fadein { 79 | from { opacity: 0; } 80 | to { opacity: 1; } 81 | } 82 | 83 | /* Internet Explorer */ 84 | @-ms-keyframes fadein { 85 | from { opacity: 0; } 86 | to { opacity: 1; } 87 | } 88 | 89 | /* Opera < 12.1 */ 90 | @-o-keyframes fadein { 91 | from { opacity: 0; } 92 | to { opacity: 1; } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /static/css-vendor/show-hint.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-hints { 2 | position: absolute; 3 | z-index: 10; 4 | overflow: hidden; 5 | list-style: none; 6 | 7 | margin: 0; 8 | padding: 2px; 9 | 10 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 11 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 12 | border-radius: 3px; 13 | border: 1px solid silver; 14 | 15 | background: white; 16 | font-size: 90%; 17 | font-family: monospace; 18 | 19 | max-height: 20em; 20 | overflow-y: auto; 21 | } 22 | 23 | .CodeMirror-hint { 24 | margin: 0; 25 | padding: 0 4px; 26 | border-radius: 2px; 27 | white-space: pre; 28 | color: black; 29 | cursor: pointer; 30 | } 31 | 32 | li.CodeMirror-hint-active { 33 | background: #08f; 34 | color: white; 35 | } -------------------------------------------------------------------------------- /static/css/Configuration.css: -------------------------------------------------------------------------------- 1 | .fullApp { 2 | } 3 | 4 | .header { 5 | vertical-align: top; 6 | } 7 | 8 | .logoAndTitle { 9 | display: inline-block; 10 | } 11 | 12 | .plotlyLogo { 13 | width: 100px; 14 | display: inline-block; 15 | } 16 | 17 | .applicationTitle { 18 | margin-top: 25px; 19 | display: inline-block; 20 | vertical-align: top; 21 | color: #096CB3; 22 | } 23 | 24 | .userName { 25 | color: #096CB3; 26 | } 27 | 28 | .supportLinksContainer { 29 | float: right; 30 | } 31 | 32 | .externalLinkContainer { 33 | display: inline-block; 34 | } 35 | 36 | .externalLink { 37 | color: #119DFF; 38 | cursor: pointer; 39 | text-decoration: underline; 40 | cursor: pointer; 41 | padding: 10px; 42 | } 43 | 44 | .settings { 45 | background-color: white; 46 | padding-top: 20px; 47 | margin: 0px 20px 0px 20px; 48 | } 49 | 50 | .onPremForm { 51 | text-align: center; 52 | } 53 | 54 | .onPremDomainInput { 55 | text-align: center; 56 | } 57 | 58 | .addOnPremLink { 59 | cursor: pointer; 60 | text-align: center; 61 | } 62 | -------------------------------------------------------------------------------- /static/css/ConnectButton.css: -------------------------------------------------------------------------------- 1 | .connectButtonContainer { 2 | margin-top: 30px; 3 | border-top: thin #c8dfe3 dashed; 4 | padding-top: 30px; 5 | } 6 | 7 | .errorMessage { 8 | color: red; 9 | height: 30px; 10 | padding-top: 10px; 11 | } 12 | 13 | .footer { 14 | padding-top: 10px; 15 | } 16 | -------------------------------------------------------------------------------- /static/css/DialectSelector.css: -------------------------------------------------------------------------------- 1 | .logoImage { 2 | height: auto; 3 | max-height: 75%; 4 | max-width: 100%; 5 | width: auto; 6 | } 7 | -------------------------------------------------------------------------------- /static/css/Link.css: -------------------------------------------------------------------------------- 1 | .a { 2 | color: blue; 3 | cursor: pointer; 4 | text-decoration: underline; 5 | } 6 | 7 | .a:hover { 8 | color: #0D76BF; 9 | } 10 | -------------------------------------------------------------------------------- /static/css/Logger.css: -------------------------------------------------------------------------------- 1 | .renderLogs { 2 | max-height: 300px; 3 | overflow-y: scroll; 4 | } 5 | -------------------------------------------------------------------------------- /static/css/Preview.css: -------------------------------------------------------------------------------- 1 | table { 2 | border-collapse: collapse; 3 | border-spacing: 0; 4 | } 5 | 6 | th, td { 7 | border: 1px solid #828282; 8 | color: #828282; 9 | font-size: 0.875em; 10 | padding: 0.3125rem 0.625rem; 11 | text-align: left; 12 | } 13 | 14 | .previewContainer { 15 | border-radius: 3px; 16 | margin: 0 25px 25px 25px; 17 | overflow: auto; 18 | } 19 | 20 | .runButton { 21 | bottom: 18px; 22 | position: absolute; 23 | right: 18px; 24 | width: 70px; 25 | z-index: 10; 26 | } 27 | 28 | .errorStatus pre { 29 | background: none; 30 | border: none; 31 | border-bottom: 1px solid #ebf0f8; 32 | color: rgb(211, 96, 70); 33 | font-family: 'Ubuntu Mono'; 34 | font-size: 12px; 35 | margin: 0 0 20px 0; 36 | max-height: 150px; 37 | overflow-x: auto; 38 | overflow-y: auto; 39 | padding: 20px; 40 | } 41 | 42 | .successMsg p { 43 | background: none; 44 | border: none; 45 | color: #00cc96; 46 | font-family: 'Ubuntu Mono'; 47 | font-size: 14px; 48 | margin: 0; 49 | padding: 20px; 50 | } 51 | 52 | .container-title { 53 | font-size: 12px; 54 | margin-bottom: 8px; 55 | } 56 | 57 | .export-options-group { 58 | margin: 8px 0 20px; 59 | padding: 20px; 60 | background: #f3f6fa; 61 | border: 1px solid #ebf0f8; 62 | } 63 | -------------------------------------------------------------------------------- /static/css/Settings.css: -------------------------------------------------------------------------------- 1 | .openTab { 2 | background: rgb(251, 252, 253); 3 | border-bottom: 1px solid #c8dfe3; 4 | border-left: 1px solid #c8dfe3; 5 | border-radius: 0 0 6px 6px; 6 | border-right: 1px solid #c8dfe3; 7 | } 8 | 9 | .configurationContainer { 10 | font-family: 'Ubuntu'; 11 | text-align: center; 12 | width: 100%; 13 | } 14 | 15 | .disabledSection { 16 | opacity: 0.4; 17 | pointer-events: none; 18 | } 19 | 20 | .info { 21 | font-size: 14px; 22 | padding: 25px; 23 | } 24 | 25 | .dialectSelector { 26 | margin-top: 32px; 27 | } 28 | 29 | .dialectSelector .Select { 30 | width: 56%; 31 | height: 40px; 32 | margin-left: 20px; 33 | font-size: 14px; 34 | font-weight: 300; 35 | text-align: left; 36 | } 37 | 38 | .editButtonContainer { 39 | margin: 20px 0; 40 | } 41 | 42 | .url { 43 | text-decoration: underline; 44 | } 45 | -------------------------------------------------------------------------------- /static/css/TableDropdown.css: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | text-transform: capitalize; 3 | cursor: pointer; 4 | width: 80%; 5 | padding-bottom: 10px; 6 | padding-left: 25px; 7 | } 8 | -------------------------------------------------------------------------------- /static/css/Tabs.css: -------------------------------------------------------------------------------- 1 | 2 | .tabManagerWrapper { 3 | text-align: left; 4 | width: 100%; 5 | white-space: nowrap; 6 | overflow-x: auto; 7 | overflow-y: hidden; 8 | } 9 | 10 | .tabManagerContainer { 11 | margin-left: auto; 12 | margin-right: auto; 13 | text-align: left; 14 | } 15 | 16 | 17 | .tabAddWrapper { 18 | background-color: white; 19 | display: inline-block; 20 | border-width: 1px; 21 | border-color: #c8dfe3; 22 | border-style: solid; 23 | vertical-align: middle; 24 | height: 36px; 25 | width: 36px; 26 | border-top-width: 0; 27 | border-bottom-width: 3px; 28 | } 29 | 30 | .tabWrapper { 31 | display: inline-block; 32 | cursor: pointer; 33 | border-left: 1px solid #c8dfe3; 34 | border-bottom: 1px solid #c8dfe3; 35 | border-top: 1px solid #c8dfe3; 36 | background-color: white; 37 | color: #687ba6; 38 | padding: 5px 10px 5px 10px; 39 | vertical-align: middle; 40 | max-width: 300px; 41 | white-space: nowrap; 42 | overflow-x: hidden; 43 | text-overflow: ellipsis; 44 | border-top: 3px solid #c8dfe3; 45 | } 46 | 47 | .tabLogo { 48 | height: 20px; 49 | display: inline; 50 | vertical-align: middle; 51 | padding-right: 5px; 52 | } 53 | 54 | .tabIdentifier { 55 | margin: 0px 20px 0px 0px; 56 | display: inline; 57 | vertical-align: middle; 58 | } 59 | 60 | .tabDelete { 61 | width: 12px; 62 | padding-right: 5px; 63 | vertical-align: middle; 64 | } 65 | 66 | .tabAdd { 67 | width: 20px; 68 | padding: 8px; 69 | } 70 | 71 | 72 | .tabWrapper:hover, tabWrapper:focus{ 73 | cursor: pointer; 74 | } 75 | 76 | .tabAdd:hover, tab:focus{ 77 | cursor: pointer; 78 | } 79 | 80 | .tabDelete:hover, tab:focus{ 81 | cursor: pointer; 82 | } 83 | 84 | .tabWrapperSelected { 85 | border-bottom: 1px solid #fff; 86 | border-top: 3px solid #119dff; 87 | } 88 | 89 | .tabAddText { 90 | display: inline-block;; 91 | padding: 10px 20px 10px 20px; 92 | color: #8BB5FE; 93 | } 94 | 95 | /* this flips the scroll y bar from bottom to up of the tab tab */ 96 | .Flipped, .Flipped .tabWrapper, .Flipped .tabAddText 97 | { 98 | transform:rotateX(180deg); 99 | -ms-transform:rotateX(180deg); /* IE 9 */ 100 | -webkit-transform:rotateX(180deg); /* Safari and Chrome */ 101 | } 102 | -------------------------------------------------------------------------------- /static/css/UserConnections.css: -------------------------------------------------------------------------------- 1 | label { 2 | text-align: right; 3 | float: left; 4 | width: 35%; 5 | line-height: 40px; 6 | font-size: 16px; 7 | } 8 | 9 | .wrapInput { 10 | width: 65%; 11 | float: right; 12 | } 13 | 14 | input { 15 | border: none; 16 | box-sizing: border-box; 17 | border-bottom-width: 3px; 18 | border-bottom-color: #dfe8f3; 19 | border-bottom-style: solid; 20 | color: #6D6D6D; 21 | font-weight: 300; 22 | width: 56%; 23 | margin-left: 20px; 24 | float: left; 25 | font-size: 14px; 26 | height: 40px; 27 | padding: 10px; 28 | } 29 | 30 | input:hover,input:focus { 31 | border-bottom-color: #119dff; 32 | outline: none; 33 | } 34 | 35 | .inputContainer { 36 | clear: both; 37 | overflow: hidden; 38 | padding: 5px 0; 39 | } 40 | 41 | .description { 42 | font-size: 12px; 43 | text-align: left; 44 | margin-left: calc(35% + 20px); 45 | max-width: 35%; 46 | } 47 | 48 | .documentationLink { 49 | display: inline-block !important; 50 | padding: 5px; 51 | text-transform: capitalize; 52 | font-size: 18px; 53 | font-weight: 100; 54 | margin: 10px 40px 0px 40px; 55 | text-align: left; 56 | height: 30px; 57 | width: 80%; 58 | } 59 | 60 | .sampleCredentials { 61 | float: right; 62 | margin-right: 10px; 63 | } -------------------------------------------------------------------------------- /static/css/fixed-data-table-custom.css: -------------------------------------------------------------------------------- 1 | .public_fixedDataTable_main, .fixedDataTableLayout_main public_fixedDataTable_main { 2 | font-size: 12px !important; 3 | border-color: #c8d4e3; 4 | } 5 | 6 | .public_fixedDataTable_header, .public_fixedDataTable_header .public_fixedDataTableCell_main { 7 | background-image: none; 8 | background: #f3f6fa; 9 | } -------------------------------------------------------------------------------- /static/fonts/dosis-v7-latin-ext_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/fonts/dosis-v7-latin-ext_latin-regular.woff2 -------------------------------------------------------------------------------- /static/fonts/open-sans-v15-vietnamese_greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/fonts/open-sans-v15-vietnamese_greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /static/fonts/ubuntu-mono-v7-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/fonts/ubuntu-mono-v7-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /static/fonts/ubuntu-v11-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/fonts/ubuntu-v11-greek_latin-ext_greek-ext_cyrillic-ext_cyrillic_latin-regular.woff2 -------------------------------------------------------------------------------- /static/images/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-144x144.png -------------------------------------------------------------------------------- /static/images/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-192x192.png -------------------------------------------------------------------------------- /static/images/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-36x36.png -------------------------------------------------------------------------------- /static/images/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-48x48.png -------------------------------------------------------------------------------- /static/images/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-72x72.png -------------------------------------------------------------------------------- /static/images/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/android-icon-96x96.png -------------------------------------------------------------------------------- /static/images/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-114x114.png -------------------------------------------------------------------------------- /static/images/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-120x120.png -------------------------------------------------------------------------------- /static/images/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-144x144.png -------------------------------------------------------------------------------- /static/images/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-152x152.png -------------------------------------------------------------------------------- /static/images/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-180x180.png -------------------------------------------------------------------------------- /static/images/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-57x57.png -------------------------------------------------------------------------------- /static/images/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-60x60.png -------------------------------------------------------------------------------- /static/images/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-72x72.png -------------------------------------------------------------------------------- /static/images/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-76x76.png -------------------------------------------------------------------------------- /static/images/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon-precomposed.png -------------------------------------------------------------------------------- /static/images/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/apple-icon.png -------------------------------------------------------------------------------- /static/images/falcon-logo-by-plotly-stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/falcon-logo-by-plotly-stripe.png -------------------------------------------------------------------------------- /static/images/falcon_hero.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/falcon_hero.gif -------------------------------------------------------------------------------- /static/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/favicon-16x16.png -------------------------------------------------------------------------------- /static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /static/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/favicon-96x96.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/ms-icon-144x144.png -------------------------------------------------------------------------------- /static/images/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/ms-icon-150x150.png -------------------------------------------------------------------------------- /static/images/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/ms-icon-310x310.png -------------------------------------------------------------------------------- /static/images/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/static/images/ms-icon-70x70.png -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/app/components/Settings/DialectSelector/DialectSelector.test.js: -------------------------------------------------------------------------------- 1 | jest.unmock('../../../../../app/components/Settings/DialectSelector/DialectSelector.react.js'); 2 | jest.unmock('../../../../../app/constants/constants.js'); 3 | 4 | import DialectSelector from '../../../../../app/components/Settings/DialectSelector/DialectSelector.react.js'; 5 | import {DIALECTS} from '../../../../../app/constants/constants.js'; 6 | import React from 'react'; 7 | import Select from 'react-select'; 8 | import {mount, configure} from 'enzyme'; 9 | import Adapter from 'enzyme-adapter-react-16'; 10 | 11 | describe('Dialog Selector Test', () => { 12 | beforeAll(() => { 13 | configure({adapter: new Adapter()}); 14 | }); 15 | 16 | it('should verify Dialect Selector created without dialect', () => { 17 | const updateConnection = function() {}; 18 | const connectionObject = {}; 19 | 20 | const selector = mount( 21 | 22 | ); 23 | 24 | // Number of Logos should match number of images 25 | expect(selector.find(Select).prop('options').length).toEqual(Object.keys(DIALECTS).length); 26 | 27 | // Dialect not selected so should not be found 28 | expect(selector.find('.Select-option .is-selected').length).toBe(0); 29 | }); 30 | 31 | it('should verify Dialect Selector created with dialect', () => { 32 | const updateConnection = function() {}; 33 | const connectionObject = { 34 | dialect: 'mysql' 35 | }; 36 | 37 | const selector = mount( 38 | 39 | ); 40 | 41 | // open selector 42 | selector.find('.Select-control').simulate('keyDown', {keyCode: 40}); 43 | 44 | expect( 45 | selector 46 | .find('.Select-option .is-selected') 47 | .first() 48 | .text() 49 | ).toBe(connectionObject.dialect); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/app/components/Settings/Preview/sql-table.test.jsx: -------------------------------------------------------------------------------- 1 | jest.unmock('../../../../../app/components/Settings/Preview/sql-table.jsx'); 2 | import SQLTable from '../../../../../app/components/Settings/Preview/sql-table.jsx'; 3 | import React from 'react'; 4 | import {configure, mount} from 'enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | describe('SQLTable', () => { 8 | let sqlTable, columnNames, rows; 9 | 10 | beforeAll(() => { 11 | configure({adapter: new Adapter()}); 12 | 13 | columnNames = ['col 1', 'col 2', 'col 3', 'col 4']; 14 | 15 | rows = [1, 2, 3, 4, 5].map(row => 16 | [1, 2, 3, 4].map(col => 17 | `(${row},${col})` 18 | ) 19 | ); 20 | 21 | sqlTable = mount( 22 | 26 | ); 27 | }); 28 | 29 | it('honors props', () => { 30 | const tableHeaderCells = sqlTable.find('.react-grid-HeaderCell'); 31 | 32 | expect(tableHeaderCells.length).toBe(columnNames.length); 33 | 34 | tableHeaderCells.forEach((headerCell, index) => { 35 | const value = headerCell.find('.widget-HeaderCell__value').first(); 36 | expect(value.text()).toBe(columnNames[index]); 37 | }); 38 | 39 | const tableRows = sqlTable.find('.react-grid-Row'); 40 | 41 | expect(tableRows.length).toBe(rows.length); 42 | tableRows.forEach((tableRow, i) => { 43 | const tableRowCells = tableRow.find('.react-grid-Cell__value'); 44 | tableRowCells.forEach((cell, j) => { 45 | const value = cell.childAt(0).childAt(0).childAt(0); 46 | expect(value.text()).toBe(rows[i][j]); 47 | }); 48 | }); 49 | }); 50 | 51 | it('can toggle row filters', () => { 52 | const toggle = sqlTable.find('a').first(); 53 | 54 | // before clicking 55 | expect(sqlTable.find('.react-grid-HeaderRow').length).toBe(1); 56 | 57 | toggle.simulate('click'); 58 | 59 | // after clicking 60 | expect(sqlTable.find('.react-grid-HeaderRow').length).toBe(2); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/app/components/Settings/scheduler/color-picker.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount, configure} from 'enzyme'; 3 | import {ChromePicker} from 'react-color'; 4 | 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | const ColorPicker = require('../../../../../app/components/Settings/scheduler/pickers/color-picker.jsx'); 8 | 9 | jest.mock('react-click-outside', () => i => i); 10 | describe('Color Picker Tests', () => { 11 | beforeAll(() => { 12 | configure({adapter: new Adapter()}); 13 | // workaround `Error: Not implemented: HTMLCanvasElement.prototype.getContext` 14 | HTMLCanvasElement.prototype.getContext = function() { 15 | return null; 16 | }; 17 | }); 18 | 19 | it('should only call onClickAway if open', () => { 20 | const onClickAway = jest.fn(); 21 | const component = mount(); 22 | 23 | component.instance().handleClickOutside(); 24 | expect(component.find(ChromePicker).length).toBe(0); 25 | expect(onClickAway).not.toHaveBeenCalled(); 26 | 27 | component.find('.color-box').simulate('click'); 28 | component.update(); 29 | 30 | expect(component.find(ChromePicker).length).toBe(1); 31 | component.instance().handleClickOutside(); 32 | expect(onClickAway).toHaveBeenCalled(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/app/components/Settings/scheduler/login-modal.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount, configure} from 'enzyme'; 3 | 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | const LoginModal = require('../../../../../app/components/Settings/scheduler/modals/login-modal/login-modal.jsx'); 7 | 8 | describe('Login Modal Tests', () => { 9 | beforeAll(() => { 10 | configure({adapter: new Adapter()}); 11 | }); 12 | 13 | it('should only set text if preview is truthy', () => { 14 | const component = mount( {}} onClickAway={() => {}} />); 15 | 16 | expect(component.find('CopyToClipboard').props().text).toBe(''); 17 | }); 18 | 19 | it('should set state on copy', async () => { 20 | jest.useFakeTimers(); 21 | const component = mount( 22 | {}} onClickAway={() => {}} /> 23 | ); 24 | 25 | expect( 26 | component 27 | .find('button') 28 | .at(2) 29 | .text() 30 | ).toBe('Copy Query'); 31 | 32 | component.instance().onCopy(); 33 | expect(component.state('copyCoolingDown')).toBe(true); 34 | component.instance().onCopy(); 35 | expect( 36 | component 37 | .find('button') 38 | .at(2) 39 | .text() 40 | ).toBe('Copied!'); 41 | 42 | jest.runAllTimers(); 43 | 44 | expect(component.state('copyCoolingDown')).toBe(false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/app/utils.js: -------------------------------------------------------------------------------- 1 | export const wait = (time = 0) => new Promise(resolve => setTimeout(resolve, time)); 2 | -------------------------------------------------------------------------------- /test/athena/SETUP_ATHENA.md: -------------------------------------------------------------------------------- 1 | # Additional Resources 2 | The following is a link to AWS Athena documentation on setting up an athena table and database 3 | 4 | [AWS Athean](https://docs.aws.amazon.com/athena/latest/ug/getting-started.html) 5 | 6 | In addition, there is a live tutorial built into the AWS Athena Console that provides 7 | step by step instructions with pictures on how to create a database and table (including 8 | sample data) 9 | 10 | # Setup 11 | The following is the high level instructions for setting up an Athena Database and Table. 12 | 13 | ## Requirements: 14 | 1. A valid AWS Account and access to the AWS Console 15 | 2. Experience working within the AWS Console 16 | 3. Experience creating S3 Buckets and pushing content to it 17 | 4. Experience with AWS IAMs for creating users and roles 18 | 19 | 20 | ## Instructions: 21 | 1. If you don't already have an S3 bucket create an S3 bucket and copy the athena-sample.csv to it 22 | 2. In the AWS Console Select Athena Service 23 | 3. If this is your first time create a database with Athena click on Getting Started 24 | 4. Click on Create Table 25 | 5. It will give you the option for Automatically (with Glue) or Manually. Choose Manually 26 | 6. For Database choose Create a new database 27 | 7. Enter in Database Name, Tablet Name, and the S3 Location where you uploaded the CSV file and then Next 28 | 8. Choose CSV for the option and Click on Next 29 | 9. Add the following 4 columns with the following information and then click on Next: 30 | i. location - string 31 | ii. alcohol - double, 32 | 10. Skip adding Partitions and click on Create Table 33 | 11. It will generate Create External Skip (see sample below). Click on Run Query (this will create table) 34 | 35 | ```sql 36 | CREATE EXTERNAL TABLE IF NOT EXISTS alcohol_consumption_by_country_2010 ( 37 | `location` string, 38 | `alcohol` double, 39 | ) 40 | ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe' 41 | WITH SERDEPROPERTIES ( 42 | 'serialization.format' = ',', 43 | 'field.delim' = ',' 44 | ) LOCATION 's3://plot.ly-athena/test-csv/' 45 | TBLPROPERTIES ('has_encrypted_data'='false'); 46 | ``` 47 | 12. Run sample SQL to see the results ( select * from alcohol_consumption_by_country_2010 ) and it should return sample data -------------------------------------------------------------------------------- /test/backend/Connections.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import {assoc} from 'ramda'; 3 | import fs from 'fs'; 4 | 5 | import * as Connections from '../../backend/persistent/Connections.js'; 6 | import {getSetting} from '../../backend/settings.js'; 7 | 8 | describe('Connections', function () { 9 | 10 | beforeEach(function() { 11 | const connectionsPath = getSetting('CONNECTIONS_PATH'); 12 | try { 13 | fs.unlinkSync(connectionsPath); 14 | } catch (e) { 15 | // empty intentionally 16 | } 17 | }); 18 | 19 | it('saveConnection saves a connection', function() { 20 | assert.deepEqual( 21 | Connections.getConnections(), 22 | [], 23 | 'connections are empty' 24 | ); 25 | const connectionObject = { 26 | dialect: 'postgres' 27 | }; 28 | const connectionId = Connections.saveConnection(connectionObject); 29 | assert.deepEqual( 30 | Connections.getConnections(), 31 | [assoc('id', connectionId, connectionObject)], 32 | 'connection was saved' 33 | ); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/backend/Queries.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import { getQuery, saveQuery, updateQuery } from '../../backend/persistent/Queries'; 4 | import { 5 | clearSettings 6 | } from './utils.js'; 7 | 8 | const QUERY = { 9 | fid: 'fid' 10 | }; 11 | 12 | // Suppressing ESLint cause Mocha ensures `this` is bound in test functions 13 | /* eslint-disable no-invalid-this*/ 14 | describe('Queries', function() { 15 | beforeEach(function () { 16 | clearSettings('QUERIES_PATH'); 17 | }); 18 | 19 | it('updating subset of query data does not remove existing data', function () { 20 | const QUERY_WITH_SOME_DATA = { 21 | ...QUERY, 22 | initialData: '_' 23 | }; 24 | 25 | saveQuery(QUERY_WITH_SOME_DATA); 26 | 27 | const newQueryData = { newData: '__' }; 28 | updateQuery(QUERY_WITH_SOME_DATA.fid, newQueryData); 29 | 30 | const updatedQuery = getQuery(QUERY_WITH_SOME_DATA.fid); 31 | assert.equal(updatedQuery.initialData, '_'); 32 | assert.equal(updatedQuery.newData, '__'); 33 | }); 34 | 35 | it('does not store undefined keys', function () { 36 | const QUERY_WITH_UNDEFINED_KEYS = { 37 | ...QUERY, 38 | /* eslint-disable no-undefined */ 39 | undefinedKey: undefined, 40 | secondUndefinedKey: undefined 41 | }; 42 | 43 | const initialQueryKeys = Object.keys(QUERY_WITH_UNDEFINED_KEYS); 44 | assert.equal(initialQueryKeys.includes('undefinedKey'), true); 45 | assert.equal(initialQueryKeys.includes('secondUndefinedKey'), true); 46 | 47 | saveQuery(QUERY_WITH_UNDEFINED_KEYS); 48 | 49 | const retrievedQueryKeys = Object.keys(getQuery(QUERY_WITH_UNDEFINED_KEYS.fid)); 50 | assert.equal(retrievedQueryKeys.includes('undefinedKey'), false); 51 | assert.equal(retrievedQueryKeys.includes('secondUndefinedKey'), false); 52 | }); 53 | }); -------------------------------------------------------------------------------- /test/backend/Tags.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import {getTags, getTag, saveTag, deleteTag, updateTag} from '../../backend/persistent/Tags'; 4 | import {clearSettings} from './utils.js'; 5 | 6 | const TAG = {name: 'name', color: 'color'}; 7 | const TAG_2 = {name: 'name2', color: 'color2'}; 8 | 9 | // Suppressing ESLint cause Mocha ensures `this` is bound in test functions 10 | /* eslint-disable no-invalid-this */ 11 | describe('Tags', function() { 12 | beforeEach(function() { 13 | clearSettings('TAGS_PATH'); 14 | }); 15 | 16 | it('saves and retrieves individual tags', function() { 17 | const savedTag = saveTag(TAG); 18 | assert.deepEqual({...TAG, id: savedTag.id}, savedTag); 19 | 20 | const retrievedTag = getTag(savedTag.id); 21 | assert.deepEqual(savedTag, retrievedTag); 22 | 23 | const savedTag2 = saveTag(TAG_2); 24 | assert.deepEqual({...TAG_2, id: savedTag2.id}, savedTag2); 25 | 26 | const retrievedTag2 = getTag(savedTag2.id); 27 | assert.deepEqual(savedTag2, retrievedTag2); 28 | }); 29 | 30 | it('updates tags', function() { 31 | const savedTag = saveTag(TAG); 32 | assert.deepEqual({...TAG, id: savedTag.id}, savedTag); 33 | 34 | const UPDATED_COLOR = 'updatedColor'; 35 | updateTag(savedTag.id, {color: UPDATED_COLOR}); 36 | 37 | const updatedTag = getTag(savedTag.id); 38 | assert.deepEqual(updatedTag, {...TAG, id: savedTag.id, color: UPDATED_COLOR}); 39 | }); 40 | 41 | it('gets all tags', function() { 42 | const savedTag = saveTag(TAG); 43 | const savedTag2 = saveTag(TAG_2); 44 | const savedTags = [savedTag, savedTag2]; 45 | 46 | const retrievedTags = getTags(); 47 | assert.deepEqual(savedTags, retrievedTags); 48 | }); 49 | 50 | it('deletes tags', function() { 51 | const savedTag = saveTag(TAG); 52 | const savedTag2 = saveTag(TAG_2); 53 | 54 | assert.equal(getTags().length, 2); 55 | 56 | deleteTag(savedTag.id); 57 | const remainingTags = getTags(); 58 | assert.equal(remainingTags.length, 1); 59 | assert.deepEqual(remainingTags[0], savedTag2); 60 | 61 | deleteTag(savedTag2.id); 62 | assert.equal(getTags().length, 0); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/backend/datastores.clickhouse.spec.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import { DIALECTS } from '../../app/constants/constants.js'; 4 | import { 5 | connect, 6 | query, 7 | schemas, 8 | tables 9 | } from '../../backend/persistent/datastores/Datastores.js'; 10 | 11 | const connection = { 12 | dialect: DIALECTS.CLICKHOUSE, 13 | host: 'localhost', 14 | port: 8123, 15 | username: 'default', 16 | password: '', 17 | database: 'plotly' 18 | }; 19 | 20 | describe('ClickHouse', function() { 21 | it('connect succeeds', function() { 22 | return connect(connection); 23 | }); 24 | 25 | it('tables returns list of tables', function() { 26 | return tables(connection).then(result => assert.include(result, 'consumption')); 27 | }); 28 | 29 | it('schemas returns schemas for all tables', function() { 30 | return schemas(connection).then(({ rows, columnnames }) => { 31 | assert.deepInclude(rows, ['consumption', 'alcohol', 'Float32']); 32 | assert.deepInclude(rows, ['consumption', 'location', 'String']); 33 | assert.deepEqual(columnnames, ['tablename', 'column_name', 'data_type']); 34 | }); 35 | }); 36 | 37 | it('query returns rows and column names', function() { 38 | return query( 39 | 'SELECT * FROM consumption LIMIT 5', 40 | connection 41 | ).then(({ rows, columnnames }) => { 42 | assert.deepEqual(rows, [ 43 | ['Belarus', 17.5], 44 | ['Moldova', 16.8], 45 | ['Lithuania', 15.4], 46 | ['Russia', 15.1], 47 | ['Romania', 14.4] 48 | ]); 49 | assert.deepEqual(columnnames, ['location', 'alcohol']); 50 | }); 51 | }); 52 | 53 | it('query rejects with an error when the query is invalid', function() { 54 | return query('invalid query', connection).catch(err => assert.exists(err)); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/backend/datastores.ibmdb.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import {DIALECTS} from '../../app/constants/constants.js'; 4 | 5 | import { 6 | query, connect, tables 7 | } from '../../backend/persistent/datastores/Datastores.js'; 8 | 9 | const connection = { 10 | dialect: DIALECTS.IBM_DB2, 11 | username: 'db2user1', 12 | password: 'w8wfy99DvEmgkBsE', 13 | host: 'db2.test.plotly.host', 14 | port: 50000, 15 | database: 'plotly' 16 | }; 17 | 18 | // Circle CI uses test databases running locally on machine: 19 | if (process.env.CIRCLECI) { 20 | connection.host = 'localhost'; 21 | } 22 | 23 | xdescribe('IBM DB2:', function () { 24 | it('connect succeeds', function(done) { 25 | connect(connection).then(database => { 26 | assert(database.connected); 27 | }).then(done); 28 | }); 29 | 30 | it('query returns rows and column names', function(done) { 31 | query( 32 | 'SELECT * FROM DB2INST1.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 FETCH FIRST 5 ROWS ONLY', 33 | connection 34 | ).then(results => { 35 | assert.deepEqual(results.rows, [ 36 | ['Belarus', 17.5], 37 | ['Moldova', 16.8], 38 | ['Lithuania', 15.4], 39 | ['Russia', 15.1], 40 | ['Romania', 14.4] 41 | ]); 42 | assert.deepEqual(results.columnnames, ['LOCATION', 'ALCOHOL']); 43 | }).then(done); 44 | }); 45 | 46 | it('tables returns list of tables', function(done) { 47 | tables(connection).then(result => { 48 | assert.deepEqual(result, ['DB2INST1.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010']); 49 | }).then(done); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/backend/datastores.impala.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import {apacheImpalaConnection as connection} from './utils.js'; 4 | import { 5 | query, connect, tables 6 | } from '../../backend/persistent/datastores/Datastores.js'; 7 | 8 | // Suppressing ESLint cause Mocha ensures `this` is bound in test functions 9 | /* eslint-disable no-invalid-this */ 10 | describe.skip('Apache Impala:', function () { 11 | 12 | it('connect succeeds', function() { 13 | this.timeout(180 * 1000); 14 | return connect(connection); 15 | }); 16 | 17 | it('tables returns list of tables', function() { 18 | return tables(connection).then(result => { 19 | const tableName = (connection.database) ? 20 | `${connection.database}.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010`.toUpperCase() : 21 | 'ALCOHOL_CONSUMPTION_BY_COUNTRY_2010'; 22 | 23 | assert.deepEqual(result, [tableName]); 24 | }); 25 | }); 26 | 27 | it('query returns rows and column names', function() { 28 | const tableName = (connection.database) ? 29 | `${connection.database}.ALCOHOL_CONSUMPTION_BY_COUNTRY_2010`.toUpperCase() : 30 | 'ALCOHOL_CONSUMPTION_BY_COUNTRY_2010'; 31 | 32 | return query(`SELECT * FROM ${tableName}\nLIMIT 5`, connection).then(results => { 33 | assert.deepEqual(results.rows, [ 34 | ['Belarus', '17.5'], 35 | ['Moldova', '16.8'], 36 | ['Lithuania', '15.4'], 37 | ['Russia', '15.1'], 38 | ['Romania', '14.4'] 39 | ]); 40 | assert.deepEqual(results.columnnames, ['loc', 'alcohol']); 41 | }); 42 | }); 43 | 44 | it('connect for invalid credentials fails', function() { 45 | connection.host = 'http://lah-lah.lemons.com'; 46 | 47 | return connect(connection).catch(err => { 48 | // reset hostname 49 | connection.host = '35.184.155.127'; 50 | 51 | assert.equal(err, ('Error: Error: getaddrinfo ENOTFOUND ' + 52 | 'http://lah-lah.lemons.com ' + 53 | 'http://lah-lah.lemons.com:21000')); 54 | }); 55 | }); 56 | }); 57 | /* eslint-enable no-invalid-this */ 58 | -------------------------------------------------------------------------------- /test/backend/datastores.oracle.spec.js: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | 3 | import {DIALECTS} from '../../app/constants/constants.js'; 4 | 5 | import { 6 | connect, 7 | disconnect, 8 | query, 9 | schemas, 10 | tables 11 | } from '../../backend/persistent/datastores/Datastores.js'; 12 | 13 | const connection = { 14 | dialect: DIALECTS.ORACLE, 15 | username: 'XDB', 16 | password: 'xdb', 17 | connectionString: 'localhost/XE' 18 | }; 19 | 20 | // Skip tests if there is no working installation of oracledb 21 | let oracledb; 22 | try { 23 | oracledb = require('oracledb'); 24 | } catch (err) { 25 | if (!process.env.CIRCLECI) { 26 | console.log('Skipping `datastores.oracle.spec.js`:', err); // eslint-disable-line 27 | } 28 | } 29 | 30 | ((oracledb) ? describe : xdescribe)('Oracle:', function () { 31 | it('connect succeeds', function() { 32 | return connect(connection); 33 | }); 34 | 35 | it('tables returns list of tables', function() { 36 | return tables(connection).then(result => { 37 | assert.include(result, 'CONSUMPTION2010', result); 38 | }); 39 | }); 40 | 41 | it('schemas returns schemas for all tables', function() { 42 | return schemas(connection).then(results => { 43 | assert.deepInclude(results.rows, ['CONSUMPTION2010', 'ALCOHOL', 'NUMBER']); 44 | assert.deepInclude(results.rows, ['CONSUMPTION2010', 'LOCATION', 'VARCHAR2']); 45 | assert.deepEqual(results.columnnames, ['TABLE_NAME', 'COLUMN_NAME', 'DATA_TYPE']); 46 | }); 47 | }); 48 | 49 | it('query returns rows and column names', function() { 50 | return query( 51 | 'SELECT * FROM CONSUMPTION2010 WHERE ROWNUM <= 5', 52 | connection 53 | ).then(results => { 54 | assert.deepEqual(results.rows, [ 55 | ['Belarus', 17.5], 56 | ['Moldova', 16.8], 57 | ['Lithuania', 15.4], 58 | ['Russia', 15.1], 59 | ['Romania', 14.4] 60 | ]); 61 | assert.deepEqual(results.columnnames, ['LOCATION', 'ALCOHOL']); 62 | }); 63 | }); 64 | 65 | it('disconnect succeeds', function() { 66 | return disconnect(connection); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/backend/parse.spec.js: -------------------------------------------------------------------------------- 1 | import {parseCSV} from '../../backend/parse.js'; 2 | import {assert} from 'chai'; 3 | 4 | const TEST_CASES = [ 5 | { 6 | input: 'Aardvark,Banana Camel\n3.14,4', 7 | output: [['Aardvark', 'Banana Camel'], ['3.14', '4']], 8 | name: 'simple' 9 | }, 10 | { 11 | input: 'Aardvark,"Banana, Camel"\n3.14,4', 12 | output: [['Aardvark', 'Banana, Camel'], ['3.14', '4']], 13 | name: 'Quote protected commas' 14 | }, 15 | { 16 | input: 'Aardvark, " Banana, Camel"\n3.14, 2015-01-01 12:10:14', 17 | output: [['Aardvark', ' Banana, Camel'], ['3.14', '2015-01-01 12:10:14']], 18 | name: 'Extra spaces and dates' 19 | }, 20 | { 21 | input: 'Single', 22 | output: [['Single']], 23 | name: 'single entry' 24 | }, 25 | { 26 | input: '', 27 | output: [[], []], 28 | name: 'Empty' 29 | } 30 | ]; 31 | 32 | describe('parseCSV', function() { 33 | TEST_CASES.forEach(function(testCase) { 34 | const {name, input, output} = testCase; 35 | it(`Parses a simple CSV string for test case ${name}`, function(done) { 36 | parseCSV(input).then(function(rowsAndColumns) { 37 | assert.deepEqual( 38 | output[0], rowsAndColumns.columnnames 39 | ); 40 | assert.deepEqual( 41 | output.slice(1), rowsAndColumns.rows 42 | ); 43 | done(); 44 | }).catch(done); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/backend/plotly_datasets.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/falcon/843628318423364073d8488b0fbc9cec0b1ecc92/test/backend/plotly_datasets.db -------------------------------------------------------------------------------- /test/docker/README.md: -------------------------------------------------------------------------------- 1 | Test databases 2 | -------------- 3 | 4 | This directory contains DockerFiles and deployment configurations for creating the test instances of following databases: 5 | 6 | * Apache Impala 7 | * Apache Spark 8 | * IBM DB2 9 | 10 | 11 | 12 | Commands 13 | ======== 14 | 15 | To Create GKE Cluster: 16 | 17 | ```sh 18 | gcloud container clusters create falcon-test-dbs --zone us-central1-b --machine-type n1-standard-2 --num-nodes 3 --enable-autoupgrade 19 | ``` 20 | 21 | To create deployments for test databases: 22 | 23 | ```sh 24 | kubectl apply -f deployment/ 25 | ``` 26 | -------------------------------------------------------------------------------- /test/docker/clickhouse/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM yandex/clickhouse-server:18.14 2 | 3 | ADD https://raw.githubusercontent.com/plotly/datasets/master/2010_alcohol_consumption_by_country.csv /2010_alcohol_consumption_by_country.csv 4 | 5 | RUN sed -i -e "1d" /2010_alcohol_consumption_by_country.csv 6 | 7 | RUN chmod 777 /2010_alcohol_consumption_by_country.csv 8 | 9 | RUN apt-get -y update && apt-get -y install curl clickhouse-client 10 | 11 | COPY setup.sh / 12 | 13 | RUN chmod +x /setup.sh 14 | 15 | EXPOSE 9000 8123 16 | 17 | CMD /setup.sh -------------------------------------------------------------------------------- /test/docker/clickhouse/README.md: -------------------------------------------------------------------------------- 1 | The Dockerfile in this folder builds a Docker image that starts a ClickHouse server daemon and sets up a test database. 2 | 3 | # License 4 | 5 | This Dockerfile uses [yandex/clickhouse-server](https://hub.docker.com/r/yandex/clickhouse-server/) as a base image. 6 | 7 | Please note that Yandex ClickHouse is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) and you may not use this except in compliance with the License. 8 | 9 | # Usage 10 | 11 | ## Build 12 | 13 | Run the command below in the folder where the Dockerfile is located: 14 | 15 | ```sh 16 | docker build . -t falcon-clickhouse 17 | ``` 18 | 19 | ## Run 20 | 21 | Run the command below inside a terminal, to start an instance listening on ports 8123 and 9000: 22 | 23 | ```sh 24 | docker run -ti -p 8123:8123 -p 9000:9000 falcon-clickhouse 25 | ``` 26 | 27 | To stop the container, just press `CTRL-C`. -------------------------------------------------------------------------------- /test/docker/clickhouse/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | clickhouse-server --config-file=/etc/clickhouse-server/config.xml & 5 | 6 | # Wait for ClickHouse server to be ready 7 | until $(curl --output /dev/null --silent --head --fail http://localhost:8123); do 8 | echo -e -n '.' 9 | sleep 2.5 10 | done 11 | 12 | clickhouse-client --query="CREATE DATABASE plotly"; 13 | 14 | clickhouse-client --database=plotly --query="CREATE TABLE consumption (location String, alcohol Float32) ENGINE = Memory"; 15 | 16 | cat /2010_alcohol_consumption_by_country.csv | clickhouse-client --database=plotly --query="INSERT INTO consumption FORMAT CSV"; 17 | 18 | # Keep ClickHouse server alive 19 | while true 20 | do 21 | sleep 30 22 | done 23 | -------------------------------------------------------------------------------- /test/docker/deployment/db2-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | name: db2-node 6 | name: db2-node 7 | spec: 8 | replicas: 3 9 | template: 10 | metadata: 11 | labels: 12 | name: db2-node 13 | spec: 14 | containers: 15 | - image: gcr.io/plotly-testing/falcon/test-db2 16 | name: db2-node 17 | ports: 18 | - containerPort: 50000 19 | -------------------------------------------------------------------------------- /test/docker/deployment/db2-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: db2-service 6 | name: db2-service 7 | spec: 8 | ports: 9 | - protocol: TCP 10 | port: 50000 11 | targetPort: 50000 12 | selector: 13 | name: db2-node 14 | type: LoadBalancer 15 | -------------------------------------------------------------------------------- /test/docker/deployment/impala-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | name: impala-node 6 | name: impala-node 7 | spec: 8 | replicas: 3 9 | template: 10 | metadata: 11 | labels: 12 | name: impala-node 13 | spec: 14 | containers: 15 | - image: gcr.io/plotly-testing/falcon/test-impala 16 | name: impala-node 17 | ports: 18 | - containerPort: 21000 19 | -------------------------------------------------------------------------------- /test/docker/deployment/impala-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: impala-service 6 | name: impala-service 7 | spec: 8 | ports: 9 | - protocol: TCP 10 | port: 21000 11 | targetPort: 21000 12 | selector: 13 | name: impala-node 14 | type: LoadBalancer 15 | -------------------------------------------------------------------------------- /test/docker/deployment/spark-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | name: spark-node 6 | name: spark-node 7 | spec: 8 | replicas: 3 9 | template: 10 | metadata: 11 | labels: 12 | name: spark-node 13 | spec: 14 | containers: 15 | - image: gcr.io/plotly-testing/falcon/test-spark 16 | name: spark-node 17 | ports: 18 | - containerPort: 8998 19 | -------------------------------------------------------------------------------- /test/docker/deployment/spark-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: spark-service 6 | name: spark-service 7 | spec: 8 | ports: 9 | - protocol: TCP 10 | port: 8998 11 | targetPort: 8998 12 | selector: 13 | name: spark-node 14 | type: LoadBalancer 15 | -------------------------------------------------------------------------------- /test/docker/elasticsearch/Dockerfile-v5: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:5.6.7 2 | 3 | USER elasticsearch 4 | 5 | COPY setup-v5.bash . 6 | COPY test-types.elastic-2.4-types.ndjson . 7 | COPY sample-data.test-type.ndjson . 8 | 9 | RUN elasticsearch -d -Ediscovery.type=single-node -Expack.security.enabled=false && bash setup-v5.bash 10 | -------------------------------------------------------------------------------- /test/docker/elasticsearch/test-types.elastic-2.4-types.ndjson: -------------------------------------------------------------------------------- 1 | {"index":{"_index":"test-types","_type":"elastic-2.4-types","_id":"1"}} 2 | {"ip":"205.7.19.54","geo_point-2":"-45.0649063041,-11.9183865969","double":0.6310475331632162,"token":"Hector Jai Brenna Mary Gabrielle","boolean":true,"string-1":"connection harmonies camp loss customs","string-2":"Cyprus","geo_point-3":[-11.918386596878577,-45.06490630412356],"date":"2016-12-04T16:05:13.816943-05:00","integer":0,"geo_point-1":{"lat":-45.06490630412356,"lon":-11.918386596878577}} 3 | {"index":{"_index":"test-types","_type":"elastic-2.4-types","_id":"2"}} 4 | {"ip":"208.49.20.91","geo_point-2":"73.4598645169,94.969768064","double":0.04016600628707845,"token":"Winston Aubrey Perla","boolean":true,"string-1":"meters recruiter chases villages platter","string-2":"South Africa","geo_point-3":[94.96976806404194,73.45986451687563],"date":"2016-12-04T16:04:43.816943-05:00","integer":1,"geo_point-1":{"lat":73.45986451687563,"lon":94.96976806404194}} 5 | {"index":{"_index":"test-types","_type":"elastic-2.4-types","_id":"3"}} 6 | {"ip":"134.119.173.12","geo_point-2":"57.491330009,-28.0041090462","double":0.4387754352694577,"token":"Jorge Arthur Scott Kaleigh Chad Roanne","boolean":true,"string-1":"kites clump circles reduction parcel","string-2":"Angola","geo_point-3":[-28.004109046235016,57.4913300090476],"date":"2016-12-04T16:04:13.816943-05:00","integer":2,"geo_point-1":{"lat":57.4913300090476,"lon":-28.004109046235016}} 7 | -------------------------------------------------------------------------------- /test/docker/ibmdb2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ibmcom/db2express-c:latest 2 | 3 | ARG DB2USER1_PASSWORD 4 | ARG DB2INST1_PASSWORD 5 | ARG LICENSE 6 | 7 | COPY "build.sh" "/build.sh" 8 | 9 | RUN curl https://raw.githubusercontent.com/plotly/datasets/master/2010_alcohol_consumption_by_country.csv -o /2010_alcohol_consumption_by_country.csv 10 | 11 | # Install DB2 Express-C 12 | RUN /entrypoint.sh "true" 13 | 14 | # Setup sample database 15 | RUN /build.sh 16 | 17 | # Start database instance 18 | ENTRYPOINT su - db2inst1 -c db2start && (while true; do sleep 1000; done) 19 | 20 | EXPOSE 50000 21 | -------------------------------------------------------------------------------- /test/docker/ibmdb2/README.md: -------------------------------------------------------------------------------- 1 | The Dockerfile in this folder builds a Docker image that starts an instance of 2 | IBM DB2 Express-C with an admin user (db2inst1), a SELECT-only user (db2user1), 3 | and a database (sample). 4 | 5 | 6 | # License 7 | 8 | This Dockerfile uses 9 | [ibmcom/db2express-c](https://hub.docker.com/r/ibmcom/db2express-c/) as a base 10 | image. 11 | 12 | Please, note that IBM DB2 Express-C is [licensed under the IBM International 13 | License Agreement for Non-Warranted 14 | Programs](http://www-03.ibm.com/software/sla/sladb.nsf/displaylis/5DF1EE126832D3F185257DAB0064BEFA?OpenDocument), 15 | which does not permit further distribution. And therefore, it does not permit 16 | the distribution of images built using this Dockerfile. 17 | 18 | 19 | # Usage 20 | 21 | 22 | ## Build 23 | 24 | Run the command below in the folder where the Dockerfile is located (This 25 | command may take several minutes to complete): 26 | 27 | ```sh 28 | docker build . -t pdc-db2 --build-arg LICENSE=accept --build-arg DB2INST1_PASSWORD= --build-arg DB2USER1_PASSWORD= --no-cache 29 | 30 | ``` 31 | 32 | Note that to run the command above is necessary to: 33 | 34 | - accept [the IBM International License Agreement for Non-Warranted 35 | Programs](http://www-03.ibm.com/software/sla/sladb.nsf/displaylis/5DF1EE126832D3F185257DAB0064BEFA?OpenDocument) 36 | by setting `LICENSE` to `accept`. 37 | 38 | - set the admin's password by setting `DB2INST1_PASSWORD` 39 | 40 | - set the SELECT-only user's password by setting `DB2USER_PASSWORD` 41 | 42 | 43 | 44 | ## Run 45 | 46 | Run the command below inside a terminal, to start an instance listening on port 47 | 50000: 48 | 49 | ```sh 50 | docker run -ti -p 50000:50000 pdc-db2 51 | ``` 52 | 53 | To stop the container, just press `CTRL-C`. 54 | -------------------------------------------------------------------------------- /test/docker/ibmdb2/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | DB_ADMIN=db2inst1 5 | DB_USER=db2user1 6 | 7 | case $(whoami) in 8 | root) 9 | # Setup user ${DB_USER} 10 | adduser ${DB_USER} 11 | (echo ${DB2USER1_PASSWORD}; echo ${DB2USER1_PASSWORD}) | passwd ${DB_USER} > /dev/null 2>&1 12 | 13 | # Grant SELECT privilege on tables in schema ${DB_ADMIN} to user ${DB_USER} 14 | su - $DB_ADMIN -c $0 15 | 16 | ;; 17 | 18 | ${DB_ADMIN}) 19 | # Start database instance 20 | db2start 21 | 22 | db2 create database plotly 23 | 24 | # Grant CONNECT privilege on database user ${DB_USER} 25 | db2 connect to plotly 26 | 27 | db2 "CREATE TABLE alcohol_consumption_by_country_2010 (Location nvarchar(64), Alcohol double)" 28 | db2 import from /2010_alcohol_consumption_by_country.csv of del skipcount 1 insert into alcohol_consumption_by_country_2010 || true 29 | 30 | echo db2 grant connect on database to user ${DB_USER} 31 | db2 grant connect on database to user ${DB_USER} 32 | 33 | db2 connect to plotly 34 | echo db2 grant select on ${DB_ADMIN}.alcohol_consumption_by_country_2010 to user ${DB_USER} 35 | db2 grant select on ${DB_ADMIN}.alcohol_consumption_by_country_2010 to user ${DB_USER} 36 | 37 | # Stop database instance 38 | db2 force applications all 39 | db2 terminate 40 | db2stop 41 | 42 | ;; 43 | 44 | *) 45 | echo "error: '$0' must be invoked by root" 46 | 47 | esac 48 | -------------------------------------------------------------------------------- /test/docker/impala/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM codingtony/impala 2 | 3 | # Copy test-db scripts: 4 | COPY bin/* /bin/ 5 | RUN chmod +x /bin/setup_database.sh; 6 | RUN chmod +x /bin/setup_test_db.sql; 7 | 8 | EXPOSE 9000 50010 50020 50070 50075 21000 21050 25000 25010 25020 9 | 10 | CMD /start.sh && /bin/setup_database.sh; 11 | -------------------------------------------------------------------------------- /test/docker/impala/README.md: -------------------------------------------------------------------------------- 1 | The Dockerfile in this folder builds a Docker image that starts the hadoop components, impala daemons 2 | and sets up a test database. 3 | 4 | This Dockerfile uses 5 | [codingtony/docker-impala](https://hub.docker.com/r/codingtony/impala/) as a base 6 | image. 7 | 8 | # Usage 9 | 10 | 11 | ## Build 12 | 13 | Run the command below in the folder where the Dockerfile is located (This 14 | command may take several minutes to complete): 15 | 16 | ```sh 17 | docker build . -t falcon-impala 18 | 19 | ``` 20 | 21 | 22 | ## Run 23 | 24 | Run the command below inside a terminal, to start an instance listening on ports 25 | 25000 and 21000: 26 | 27 | ```sh 28 | docker run -ti -p 25000:25000 -p 21000:21000 falcon-impala 29 | ``` 30 | 31 | Visit http://localhost:25000 to check the status of running impala daemons. 32 | To stop the container, just press `CTRL-C`. 33 | -------------------------------------------------------------------------------- /test/docker/impala/bin/setup_database.sh: -------------------------------------------------------------------------------- 1 | apt-get -y update; 2 | apt-get -y install unzip curl; 3 | cd / && curl https://codeload.github.com/plotly/datasets/zip/master >| plotly_datasets.zip 4 | 5 | # unzip datasets: 6 | unzip plotly_datasets.zip 7 | mv datasets-master /plotly_datasets 8 | 9 | # Strip header line from test csv: 10 | sed -i -e "1d" /plotly_datasets/2010_alcohol_consumption_by_country.csv 11 | 12 | 13 | # Upload to hdfs as 'impala' user 14 | su - impala -c "hdfs dfs -put /plotly_datasets/2010_alcohol_consumption_by_country.csv /user/impala/2010_alcohol_consumption_by_country.csv" 15 | 16 | impala-shell -f /bin/setup_test_db.sql 17 | 18 | # To keep the container running: 19 | while true 20 | do 21 | sleep 30 22 | echo "Impala-test db is up and running." 23 | done 24 | -------------------------------------------------------------------------------- /test/docker/impala/bin/setup_test_db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE PLOTLY; 2 | USE PLOTLY; 3 | CREATE TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 (LOC STRING, ALCOHOL DOUBLE) ROW FORMAT DELIMITED FIELDS TERMINATED BY ','; 4 | LOAD DATA INPATH '/user/impala/2010_alcohol_consumption_by_country.csv' OVERWRITE INTO TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010; 5 | -------------------------------------------------------------------------------- /test/docker/oracle/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wnameless/oracle-xe-11g:16.04 2 | 3 | EXPOSE 1521 4 | 5 | ADD https://raw.githubusercontent.com/plotly/datasets/master/2010_alcohol_consumption_by_country.csv /2010_alcohol_consumption_by_country.csv 6 | COPY setup.sql / 7 | COPY setup.ctl / 8 | COPY setup.sh /docker-entrypoint-initdb.d/ 9 | 10 | ENV ORACLE_ENABLE_XDB true 11 | -------------------------------------------------------------------------------- /test/docker/oracle/README.md: -------------------------------------------------------------------------------- 1 | The Dockerfile in this folder builds a Docker image that starts an instance of 2 | Oracle Database 11g Express Edition with the logins `SYSTEM/oracle` and 3 | `XDB/xdb`, and the sample database `consumption2010`. 4 | 5 | 6 | # License 7 | 8 | This Dockerfile uses 9 | [wnameless/oracle-xe-11g](https://hub.docker.com/r/wnameless/oracle-xe-11g/) as 10 | a base image. 11 | 12 | Please, note that Oracle Database Express Edition is [licensed under the Oracle 13 | Technology Network Developer License 14 | Terms](http://www.oracle.com/technetwork/licenses/database-11g-express-license-459621.html). 15 | 16 | 17 | # Usage 18 | 19 | 20 | ## Build 21 | 22 | Run the command below in the folder where the Dockerfile is located: 23 | 24 | ```sh 25 | docker build . -t falcon-test-oracle 26 | 27 | ``` 28 | 29 | 30 | ## Run 31 | 32 | Run the command below inside a terminal, to start an instance listening on port 33 | 1521: 34 | 35 | ```sh 36 | docker run --rm -ti -p 1521:1521 falcon-test-oracle 37 | ``` 38 | 39 | To stop the container, just press `CTRL-C`. 40 | -------------------------------------------------------------------------------- /test/docker/oracle/setup.ctl: -------------------------------------------------------------------------------- 1 | OPTIONS(SKIP=1) 2 | 3 | LOAD DATA 4 | INFILE "/2010_alcohol_consumption_by_country.csv" 5 | 6 | REPLACE 7 | INTO TABLE consumption2010 8 | 9 | FIELDS TERMINATED BY "," 10 | OPTIONALLY ENCLOSED BY '"' 11 | (location, alcohol) 12 | -------------------------------------------------------------------------------- /test/docker/oracle/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sqlplus XDB/xdb @/setup.sql 5 | sqlldr userid=XDB/xdb control=/setup.ctl 6 | 7 | echo Ready 8 | -------------------------------------------------------------------------------- /test/docker/oracle/setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE consumption2010 ( 2 | location varchar2(50), 3 | alcohol number 4 | ); 5 | QUIT 6 | -------------------------------------------------------------------------------- /test/docker/spark/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sequenceiq/hadoop-ubuntu:2.6.0 2 | 3 | # Get latest java: 4 | COPY bin/setup_java.sh /setup_java.sh 5 | RUN chmod +x /setup_java.sh; 6 | RUN /setup_java.sh; 7 | ENV JAVA_HOME=/usr/lib/jvm/java-8-oracle/jre 8 | 9 | # Get hive: 10 | ARG HIVE_VERSION=2.2.0 11 | RUN wget http://www-eu.apache.org/dist/hive/hive-$HIVE_VERSION/apache-hive-$HIVE_VERSION-bin.tar.gz && \ 12 | tar -xzvf apache-hive-$HIVE_VERSION-bin.tar.gz && \ 13 | mv apache-hive-$HIVE_VERSION-bin hive; 14 | 15 | ENV HIVE_HOME=/hive \ 16 | PATH=/hive/bin:$PATH \ 17 | HIVE_CONF_DIR=/hive/conf/ 18 | 19 | COPY conf/* $HIVE_CONF_DIR 20 | COPY conf/my.cnf /etc/ 21 | 22 | # Install mysql as hive metastore 23 | COPY setup_mysql.sh /setup_mysql.sh 24 | RUN chmod +x /setup_mysql.sh; 25 | RUN /setup_mysql.sh; 26 | # RUN /hive/bin/schematool -dbType mysql -initSchema -verbose; 27 | 28 | # Install spark 29 | RUN curl -s http://apache.stu.edu.tw/spark/spark-2.2.0/spark-2.2.0-bin-hadoop2.6.tgz >| spark-2.2.0.tgz 30 | RUN tar -xvf spark-2.2.0.tgz -C /usr/local/ && cd /usr/local && ln -s spark-2.2.0-bin-hadoop2.6 spark 31 | 32 | ENV SPARK_JAR=hdfs:///spark/spark-assembly-2.2.0-hadoop2.6.0.jar \ 33 | SPARK_HOME=/usr/local/spark \ 34 | SPARK_HIVE=true \ 35 | PATH=$PATH:/usr/local/spark/bin \ 36 | PYTHONPATH=/usr/local/spark/python/:$PYTHONPATH \ 37 | PYTHONPATH=/usr/local/spark/python/lib/py4j-0.8.2.1-src.zip:$PYTHONPATH 38 | 39 | RUN cp -r $HIVE_HOME/lib/mysql.jar $SPARK_HOME/jars/ 40 | COPY conf/* $SPARK_HOME/conf/ 41 | COPY bin/* /bin/ 42 | 43 | # Setup Livy 44 | RUN chmod +x /bin/setup_livy.sh; 45 | RUN /bin/setup_livy.sh; 46 | COPY conf/livy.conf /livy/conf/ 47 | 48 | # Workaround for mysql 49 | RUN echo "skip-grant-tables" | tee -a /etc/my.cnf && service mysql restart; exit 0; 50 | 51 | EXPOSE 8998 52 | 53 | ENTRYPOINT /etc/bootstrap.sh && service mysql start && chmod +x /bin/setup_database.sh && sync && /bin/setup_database.sh && /livy/bin/livy-server; 54 | -------------------------------------------------------------------------------- /test/docker/spark/README.md: -------------------------------------------------------------------------------- 1 | 2 | The Dockerfile in this folder builds a Docker image that starts the hadoop components, spark daemons, sets up a test database and runs apache-livy server. 3 | 4 | This Dockerfile uses 5 | [sequenceiq/hadoop-ubuntu](https://hub.docker.com/r/sequenceiq/hadoop-ubuntu/) as a base 6 | image. 7 | 8 | ## Build 9 | 10 | Run the command below in the folder where the Dockerfile is located (This 11 | command may take several minutes to complete): 12 | 13 | ```sh 14 | docker build . -t falcon-spark 15 | 16 | ``` 17 | 18 | 19 | ## Run 20 | 21 | Run the command below inside a terminal, to start an instance listening on port 8998: 22 | 23 | ```sh 24 | docker run -ti -p 8998:8998 falcon-spark 25 | ``` 26 | 27 | Visit http://localhost:8998 to check the status of running livy server and information about running sessions. 28 | To stop the container, just press `CTRL-C`. 29 | -------------------------------------------------------------------------------- /test/docker/spark/bin/create_hive_tables.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pyspark import SparkContext, SparkConf, HiveContext, SQLContext 3 | 4 | if __name__ == '__main__': 5 | conf = SparkConf().setAppName("Plotly Exports") 6 | sc = SparkContext(conf=conf) 7 | hive_context = HiveContext(sc) 8 | print '=== Creating Database ===' 9 | hive_context.sql('CREATE DATABASE PLOTLY') 10 | hive_context.sql('USE PLOTLY') 11 | 12 | print '=== Creating Table ===' 13 | hive_context.sql("CREATE TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 " 14 | "(LOCATION STRING, ALCOHOL DOUBLE) ROW FORMAT " 15 | "DELIMITED FIELDS TERMINATED BY ',' " 16 | "TBLPROPERTIES (\"skip.header.line.count\"=\"1\")") 17 | print "=== loading data into table ===" 18 | hive_context.sql("LOAD DATA LOCAL INPATH " 19 | "'/plotly_datasets/2010_alcohol_consumption_by_country.csv' " 20 | "OVERWRITE INTO TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010") 21 | sys.exit() 22 | -------------------------------------------------------------------------------- /test/docker/spark/bin/livy_python_requirements.txt: -------------------------------------------------------------------------------- 1 | cloudpickle 2 | requests 3 | requests-kerberos 4 | flake8 5 | flaky 6 | pytest 7 | -------------------------------------------------------------------------------- /test/docker/spark/bin/setup_database.sh: -------------------------------------------------------------------------------- 1 | # Fetch Plotly master datasets to $HOME. 2 | apt-get install unzip 3 | cd && curl https://codeload.github.com/plotly/datasets/zip/master >| plotly_datasets.zip 4 | 5 | # unzip datasets: 6 | unzip plotly_datasets.zip 7 | mv datasets-master /plotly_datasets 8 | 9 | # Strip header line from test csv: 10 | sed -i -e "1d" /plotly_datasets/2010_alcohol_consumption_by_country.csv 11 | 12 | # create table 13 | spark-submit /bin/create_hive_tables.py 14 | -------------------------------------------------------------------------------- /test/docker/spark/bin/setup_java.sh: -------------------------------------------------------------------------------- 1 | apt-get update 2 | apt-get install --assume-yes software-properties-common python-software-properties git 3 | add-apt-repository ppa:webupd8team/java -y 4 | apt-get update 5 | 6 | echo debconf shared/accepted-oracle-license-v1-1 select true | sudo debconf-set-selections 7 | echo debconf shared/accepted-oracle-license-v1-1 seen true | sudo debconf-set-selections 8 | sudo apt-get install --assume-yes oracle-java8-installer 9 | export JAVA_HOME=/usr/lib/jvm/java-8-oracle 10 | -------------------------------------------------------------------------------- /test/docker/spark/bin/setup_livy.sh: -------------------------------------------------------------------------------- 1 | # Install maven: 2 | wget http://mirror.olnevhost.net/pub/apache/maven/binaries/apache-maven-3.2.1-bin.tar.gz 3 | tar xvf apache-maven-3.2.1-bin.tar.gz 4 | export M2=/apache-maven-3.2.1/bin 5 | export PATH=$M2:$PATH 6 | 7 | 8 | apt-get install --assume-yes python-dev python-setuptools python-pip python-wheel libkrb5-dev 9 | 10 | # Upgrade pip: 11 | pip install -U pip wheel setuptools 12 | pip install -r /bin/livy_python_requirements.txt 13 | 14 | # Get livy: 15 | git clone https://github.com/cloudera/livy.git 16 | cd livy 17 | JAVA_HOME=/usr/lib/jvm/java-8-oracle/jre MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=256m" /apache-maven-3.2.1/bin/mvn -DskipTests clean package 18 | 19 | # Setup livy logging directory 20 | mkdir -p /livy/logs; 21 | -------------------------------------------------------------------------------- /test/docker/spark/conf/hive-env.sh: -------------------------------------------------------------------------------- 1 | # Thanks to https://community.hortonworks.com/questions/9079/remote-debug-hiveserver2.html 2 | # Enable remote debugging of hiveserver2. 3 | if [ "$SERVICE" = "hiveserver2" ]; then 4 | export HADOOP_OPTS="$HADOOP_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001" 5 | fi 6 | 7 | -------------------------------------------------------------------------------- /test/docker/spark/conf/hive-log4j2.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | status = INFO 18 | name = HiveLog4j2 19 | packages = org.apache.hadoop.hive.ql.log 20 | 21 | # list of properties 22 | property.hive.log.level = DEBUG 23 | property.hive.root.logger = DRFA 24 | property.hive.log.dir = ${sys:java.io.tmpdir}/${sys:user.name} 25 | property.hive.log.file = hive.log 26 | 27 | # list of all appenders 28 | appenders = console, DRFA 29 | 30 | # console appender 31 | appender.console.type = Console 32 | appender.console.name = console 33 | appender.console.target = SYSTEM_ERR 34 | appender.console.layout.type = PatternLayout 35 | appender.console.layout.pattern = %d{yy/MM/dd HH:mm:ss} [%t]: %p %c{2}: %m%n 36 | 37 | # daily rolling file appender 38 | appender.DRFA.type = RollingFile 39 | appender.DRFA.name = DRFA 40 | appender.DRFA.fileName = ${sys:hive.log.dir}/${sys:hive.log.file} 41 | # Use %pid in the filePattern to append @ to the filename if you want separate log files for different CLI session 42 | appender.DRFA.filePattern = ${sys:hive.log.dir}/${sys:hive.log.file}.%d{yyyy-MM-dd} 43 | appender.DRFA.layout.type = PatternLayout 44 | appender.DRFA.layout.pattern = %d{ISO8601} %-5p [%t]: %c{2} (%F:%M(%L)) - %m%n 45 | appender.DRFA.policies.type = Policies 46 | appender.DRFA.policies.time.type = TimeBasedTriggeringPolicy 47 | appender.DRFA.policies.time.interval = 1 48 | appender.DRFA.policies.time.modulate = true 49 | appender.DRFA.strategy.type = DefaultRolloverStrategy 50 | appender.DRFA.strategy.max = 30 51 | 52 | # list of all loggers 53 | loggers = NIOServerCnxn, ClientCnxnSocketNIO, DataNucleus, Datastore, JPOX 54 | 55 | logger.NIOServerCnxn.name = org.apache.zookeeper.server.NIOServerCnxn 56 | logger.NIOServerCnxn.level = WARN 57 | 58 | logger.ClientCnxnSocketNIO.name = org.apache.zookeeper.ClientCnxnSocketNIO 59 | logger.ClientCnxnSocketNIO.level = WARN 60 | 61 | logger.DataNucleus.name = DataNucleus 62 | logger.DataNucleus.level = ERROR 63 | 64 | logger.Datastore.name = Datastore 65 | logger.Datastore.level = ERROR 66 | 67 | logger.JPOX.name = JPOX 68 | logger.JPOX.level = ERROR 69 | 70 | # root logger 71 | rootLogger.level = ${sys:hive.log.level} 72 | rootLogger.appenderRefs = root 73 | rootLogger.appenderRef.root.ref = ${sys:hive.root.logger} 74 | -------------------------------------------------------------------------------- /test/docker/spark/conf/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | bind-address=0.0.0.0 3 | -------------------------------------------------------------------------------- /test/docker/spark/create_hive_tables.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pyspark import SparkContext, SparkConf, HiveContext, SQLContext 3 | 4 | if __name__ == '__main__': 5 | conf = SparkConf().setAppName("Plotly Exports") 6 | sc = SparkContext(conf=conf) 7 | hive_context = HiveContext(sc) 8 | print '=== Creating Database ===' 9 | hive_context.sql('CREATE DATABASE PLOTLY') 10 | hive_context.sql('USE PLOTLY') 11 | 12 | print '=== Creating Table ===' 13 | hive_context.sql("CREATE TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010 " 14 | "(LOCATION STRING, ALCOHOL FLOAT) ROW FORMAT " 15 | "DELIMITED FIELDS TERMINATED BY ',' " 16 | "TBLPROPERTIES (\"skip.header.line.count\"=\"1\")") 17 | print "=== loading data into table ===" 18 | hive_context.sql("LOAD DATA LOCAL INPATH " 19 | "'/plotly_datasets/2010_alcohol_consumption_by_country.csv' " 20 | "OVERWRITE INTO TABLE ALCOHOL_CONSUMPTION_BY_COUNTRY_2010") 21 | sys.exit() 22 | -------------------------------------------------------------------------------- /test/docker/spark/setup_database.sh: -------------------------------------------------------------------------------- 1 | # Fetch Plotly master datasets to $HOME. 2 | cd && curl https://codeload.github.com/plotly/datasets/zip/master >| plotly_datasets.zip 3 | 4 | # unzip datasets: 5 | unzip plotly_datasets.zip 6 | mv datasets-master plotly_datasets 7 | 8 | # create table 9 | spark-submit /create_hive_tables.py 10 | -------------------------------------------------------------------------------- /test/docker/spark/setup_livy.sh: -------------------------------------------------------------------------------- 1 | 2 | # Install pip and python dependencies to build livy: 3 | curl https://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo >| /etc/yum.repos.d/epel-apache-maven.repo 4 | sed -i s/\$releasever/6/g /etc/yum.repos.d/epel-apache-maven.repo 5 | yum -y install ant asciidoc apache-maven cyrus-sasl-devel cyrus-sasl-gssapi cyrus-sasl-plain gcc gcc-c++ git krb5-devel libffi-devel libxml2-devel libxslt-devel make mysql mysql-devel openldap-devel python-devel python-setuptools python-pip python-wheel sqlite-devel gmp-devel 6 | 7 | # Upgrade pip: 8 | pip install -U pip wheel setuptools 9 | pip install -r /requirements.txt 10 | 11 | # Get livy: 12 | git clone https://github.com/cloudera/livy.git 13 | cd livy 14 | export MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=128m" 15 | mvn -DskipTests clean package 16 | 17 | # Setup livy logging directory 18 | mkdir -p /livy/logs 19 | -------------------------------------------------------------------------------- /test/docker/spark/setup_mysql.sh: -------------------------------------------------------------------------------- 1 | sudo apt-get update 2 | export DEBIAN_FRONTEND=noninteractive 3 | apt-get -y install mysql-server mysql-client libmysql-java 4 | 5 | ln -s /usr/share/java/mysql.jar $HIVE_HOME/lib; 6 | service mysql restart 7 | mysql -e'CREATE DATABASE hive;' 8 | mysql -e'use hive;' 9 | # mysql -e'SOURCE $HIVE_HOME/scripts/metastore/upgrade/mysql/hive-schema-2.2.0.mysql.sql;' 10 | mysql -e'GRANT ALL PRIVILEGES ON *.* TO hive@"%" IDENTIFIED BY "hive"; flush privileges;'; 11 | mysql -e'GRANT ALL PRIVILEGES ON *.* TO hive@"localhost" IDENTIFIED BY "hive"; flush privileges;'; 12 | # Fix any conflicting host tables: 13 | mysql -e'use mysql; repair table host use_frm;'; 14 | service mysql restart 15 | /hive/bin/schematool -dbType mysql -initSchema -verbose; 16 | -------------------------------------------------------------------------------- /test/integration_test.js: -------------------------------------------------------------------------------- 1 | import chromedriver from 'chromedriver'; 2 | import webdriver from 'selenium-webdriver'; 3 | import electronPath from 'electron'; 4 | 5 | import fs from 'fs'; 6 | import fsExtra from 'fs-extra'; 7 | import path from 'path'; 8 | 9 | import * as settings from '../backend/settings.js'; 10 | import {createStoragePath} from '../backend/utils/homeFiles'; 11 | 12 | const delay = time => new Promise(resolve => setTimeout(resolve, time)); 13 | 14 | // initialize credentials 15 | clearConnectorFolder(); 16 | saveCredentials(); 17 | 18 | // start chromedriver 19 | chromedriver.start(); 20 | process.on('exit', chromedriver.stop); 21 | 22 | // Suppressing ESLint cause Mocha ensures `this` is bound in test functions 23 | /* eslint-disable no-invalid-this */ 24 | describe('plotly database connector', function() { 25 | this.timeout(10 * 60 * 1000); 26 | 27 | before(() => 28 | // wait for chromedriver to start up 29 | delay(1 * 1000) 30 | .then(() => { 31 | // start electron app 32 | this.driver = new webdriver.Builder() 33 | .usingServer('http://localhost:9515') 34 | .withCapabilities({ 35 | chromeOptions: { 36 | binary: electronPath, 37 | args: [`app=${path.resolve()}`] 38 | } 39 | }) 40 | .forBrowser('electron') 41 | .build(); 42 | 43 | // wait for electron app to start up 44 | return delay(3 * 1000); 45 | }) 46 | // ensure the electron app has loaded 47 | .then(() => this.driver.wait(webdriver.until.titleContains('Falcon'))) 48 | ); 49 | 50 | after(() => 51 | Promise.resolve() 52 | .then(() => this.driver && this.driver.quit()) 53 | .then(() => chromedriver.stop()) 54 | ); 55 | }); 56 | 57 | function saveCredentials() { 58 | const usersValue = JSON.stringify([{ 59 | 'username': 'plotly-database-connector', 60 | 'accessToken': '2MiYq1Oive6RRjC6y9D4u7DjlXSs7k' 61 | }]); 62 | if (!fs.existsSync(settings.getSetting('STORAGE_PATH'))) { 63 | createStoragePath(); 64 | } 65 | fs.writeFileSync( 66 | settings.getSetting('SETTINGS_PATH'), 67 | `USERS: ${usersValue}` 68 | ); 69 | } 70 | 71 | function clearConnectorFolder() { 72 | try { 73 | fsExtra.removeSync(settings.getSetting('STORAGE_PATH')); 74 | } catch (e) { 75 | console.warn(e); // eslint-disable-line no-console 76 | } 77 | } 78 | /* eslint-enable no-invalid-this */ 79 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | module: { 5 | noParse: [/alasql/], 6 | rules: [{ 7 | test: /\.jsx?$/, 8 | use: [{ 9 | loader: 'babel-loader' 10 | }], 11 | exclude: /node_modules/ 12 | }, { 13 | test: /\.css$/, 14 | use: [{ 15 | loader: 'style-loader' 16 | }, { 17 | loader: 'css-loader' 18 | }] 19 | }] 20 | }, 21 | output: { 22 | path: path.join(__dirname, 'dist'), 23 | filename: 'bundle.js', 24 | libraryTarget: 'commonjs2' 25 | }, 26 | resolve: { 27 | extensions: ['.js', '.jsx', '.json'] 28 | }, 29 | plugins: [ 30 | ], 31 | externals: [ 32 | { 33 | 'csv-parse': 'commonjs csv-parse', 34 | 'data-urls': 'commonjs data-urls', 35 | 'dtrace-provider': 'commonjs dtrace-provider', 36 | 'font-awesome': 'font-awesome', 37 | 'ibm_db': 'commonjs ibm_db', 38 | 'mysql': 'mysql', 39 | 'oracledb': 'commonjs oracledb', 40 | 'pg': 'pg', 41 | 'pg-hstore': 'pg-hstore', 42 | 'restify': 'commonjs restify', 43 | 'sequelize': 'commonjs sequelize', 44 | 'source-map-support': 'source-map-support', 45 | 'sqlite3': 'sqlite3', 46 | 'tedious': 'tedious', 47 | 'whatwg-encoding': 'commonjs whatwg-encoding' 48 | } 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin'; 4 | 5 | export default { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: './backend/main.development', 11 | 12 | output: { 13 | path: __dirname, 14 | filename: './backend/main.js' 15 | }, 16 | 17 | plugins: [ 18 | ...baseConfig.plugins, 19 | 20 | new UglifyJsPlugin({ 21 | sourceMap: false 22 | }), 23 | new webpack.BannerPlugin( 24 | {banner: 'require("source-map-support").install();', 25 | raw: true, entryOnly: false } 26 | ), 27 | new webpack.DefinePlugin({ 28 | __DEV__: false, 29 | 'process.env.NODE_ENV': JSON.stringify('production') 30 | }), 31 | // https://github.com/felixge/node-formidable/issues/337 32 | new webpack.DefinePlugin({ 'global.GENTLY': false }), 33 | 34 | new webpack.LoaderOptionsPlugin({ 35 | minimize: true 36 | }) 37 | ], 38 | 39 | target: 'electron-main', 40 | 41 | node: { 42 | __dirname: false, 43 | __filename: false 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /webpack.config.headless.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin'; 4 | 5 | export default { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: './backend/headless.development.js', 11 | 12 | output: { 13 | path: __dirname, 14 | filename: './dist/headless-bundle.js' 15 | }, 16 | 17 | plugins: [ 18 | ...baseConfig.plugins, 19 | 20 | new UglifyJsPlugin({ 21 | sourceMap: false 22 | }), 23 | new webpack.BannerPlugin( 24 | {banner: 'require("source-map-support").install();', 25 | raw: true, entryOnly: false } 26 | ), 27 | new webpack.DefinePlugin({ 28 | 'process.env.NODE_ENV': JSON.stringify('production') 29 | }), 30 | // https://github.com/felixge/node-formidable/issues/337 31 | new webpack.DefinePlugin({ 'global.GENTLY': false }) 32 | ], 33 | 34 | target: 'electron', 35 | 36 | node: { 37 | __dirname: false, 38 | __filename: false 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | 4 | const config = { 5 | ...baseConfig, 6 | 7 | devtool: 'source-map', 8 | 9 | entry: './app/index', 10 | 11 | output: { 12 | ...baseConfig.output, 13 | 14 | publicPath: '../dist/' 15 | }, 16 | 17 | plugins: [ 18 | ...baseConfig.plugins, 19 | 20 | new webpack.DefinePlugin({ 21 | __DEV__: false, 22 | 'process.env.NODE_ENV': JSON.stringify('production') 23 | }) 24 | ], 25 | 26 | target: 'electron-renderer' 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /webpack.config.web.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | import path from 'path'; 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: { 11 | 'web': './app/index' 12 | }, 13 | 14 | output: { 15 | ...baseConfig.output, 16 | path: path.join(__dirname, 'static'), 17 | filename: '[name]-bundle.min.js', 18 | libraryTarget: 'var' 19 | }, 20 | 21 | plugins: [ 22 | ...baseConfig.plugins, 23 | new webpack.DefinePlugin({ 24 | __DEV__: false, 25 | 'process.env.NODE_ENV': JSON.stringify('production') 26 | }) 27 | ], 28 | 29 | target: 'web' 30 | }; 31 | 32 | export default config; 33 | --------------------------------------------------------------------------------