Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c667f3cc |
@@ -1,34 +0,0 @@
|
||||
engines:
|
||||
csslint:
|
||||
enabled: false
|
||||
duplication:
|
||||
enabled: false
|
||||
eslint:
|
||||
enabled: true
|
||||
config:
|
||||
config: caravel/assets/.eslintrc
|
||||
pep8:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: false
|
||||
radon:
|
||||
enabled: true
|
||||
checks:
|
||||
Complexity:
|
||||
enabled: false
|
||||
ratings:
|
||||
paths:
|
||||
- "**.py"
|
||||
- "caravel/assets/**.js"
|
||||
- "caravel/assets/**.jsx"
|
||||
exclude_paths:
|
||||
- ".*"
|
||||
- "**.pyc"
|
||||
- "**.gz"
|
||||
- "env/"
|
||||
- "tests/"
|
||||
- "caravel/assets/images/"
|
||||
- "caravel/assets/vendor/"
|
||||
- "caravel/assets/node_modules/"
|
||||
- "caravel/assets/javascripts/dist/"
|
||||
- "caravel/migrations"
|
||||
@@ -1 +0,0 @@
|
||||
repo_token: eESbYiv4An6KEvjpmguDs4L7YkubXbqn1
|
||||
23
.gitignore
vendored
@@ -1,28 +1,7 @@
|
||||
*.pyc
|
||||
changelog.sh
|
||||
.DS_Store
|
||||
.coverage
|
||||
_build
|
||||
_static
|
||||
_images
|
||||
caravel/bin/caravelc
|
||||
env_py3
|
||||
.eggs
|
||||
build
|
||||
*.db
|
||||
tmp
|
||||
caravel_config.py
|
||||
panoramix_config.py
|
||||
local_config.py
|
||||
env
|
||||
dist
|
||||
caravel.egg-info/
|
||||
app.db
|
||||
*.bak
|
||||
.idea
|
||||
*.sqllite
|
||||
|
||||
# Node.js, webpack artifacts
|
||||
*.entry.js
|
||||
*.js.map
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
doc-warnings: yes
|
||||
test-warnings: no
|
||||
strictness: medium
|
||||
max-line-length: 90
|
||||
uses:
|
||||
- flask
|
||||
autodetect: yes
|
||||
pylint:
|
||||
disable:
|
||||
- cyclic-import
|
||||
- invalid-name
|
||||
- logging-format-interpolation
|
||||
options:
|
||||
docstring-min-length: 10
|
||||
pep8:
|
||||
full: true
|
||||
ignore-paths:
|
||||
- docs
|
||||
- caravel/migrations/env.py
|
||||
- caravel/ascii_art.py
|
||||
ignore-patterns:
|
||||
- ^example/doc_.*\.py$
|
||||
- (^|/)docs(/|$)
|
||||
38
.travis.yml
@@ -1,38 +0,0 @@
|
||||
|
||||
language: python
|
||||
addons:
|
||||
code_climate:
|
||||
repo_token: 5f3a06c425eef7be4b43627d7d07a3e46c45bdc07155217825ff7c49cb6a470c
|
||||
apt:
|
||||
sources:
|
||||
- deadsnakes
|
||||
packages:
|
||||
- python3.5
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.wheelhouse/
|
||||
env:
|
||||
global:
|
||||
- TRAVIS_CACHE=$HOME/.travis_cache/
|
||||
- TRAVIS_NODE_VERSION="5.11"
|
||||
matrix:
|
||||
- TOX_ENV=javascript
|
||||
- TOX_ENV=py34-postgres
|
||||
- TOX_ENV=py34-sqlite
|
||||
- TOX_ENV=py27-mysql
|
||||
- TOX_ENV=py27-sqlite
|
||||
before_install:
|
||||
- npm install -g npm@'>=3.9.5'
|
||||
before_script:
|
||||
- mysql -e 'drop database if exists caravel; create database caravel DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
|
||||
- mysql -u root -e "CREATE USER 'mysqluser'@'localhost' IDENTIFIED BY 'mysqluserpassword';"
|
||||
- mysql -u root -e "GRANT ALL ON caravel.* TO 'mysqluser'@'localhost';"
|
||||
- psql -c 'create database caravel;' -U postgres
|
||||
- psql -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';" -U postgres
|
||||
- export PATH=${PATH}:/tmp/hive/bin
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install tox tox-travis
|
||||
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install
|
||||
script: tox -e $TOX_ENV
|
||||
1349
CHANGELOG.md
213
CONTRIBUTING.md
@@ -9,7 +9,7 @@ You can contribute in many ways:
|
||||
|
||||
### Report Bugs
|
||||
|
||||
Report bugs through GitHub
|
||||
Report bugs through Gihub
|
||||
|
||||
If you are reporting a bug, please include:
|
||||
|
||||
@@ -28,16 +28,20 @@ open to whoever wants to implement it.
|
||||
Look through the GitHub issues for features. Anything tagged with
|
||||
"feature" is open to whoever wants to implement it.
|
||||
|
||||
We've created the operators, hooks, macros and executors we needed, but we
|
||||
made sure that this part of Airflow is extensible. New operators,
|
||||
hooks and operators are very welcomed!
|
||||
|
||||
### Documentation
|
||||
|
||||
Caravel could always use better documentation,
|
||||
whether as part of the official Caravel docs,
|
||||
Airflow could always use better documentation,
|
||||
whether as part of the official Airflow docs,
|
||||
in docstrings, `docs/*.rst` or even on the web as blog posts or
|
||||
articles.
|
||||
|
||||
### Submit Feedback
|
||||
|
||||
The best way to send feedback is to file an issue on GitHub.
|
||||
The best way to send feedback is to file an issue on Github.
|
||||
|
||||
If you are proposing a feature:
|
||||
|
||||
@@ -47,144 +51,23 @@ If you are proposing a feature:
|
||||
- Remember that this is a volunteer-driven project, and that
|
||||
contributions are welcome :)
|
||||
|
||||
## Latest Documentation
|
||||
## Latests Documentation
|
||||
|
||||
Latest documentation and tutorial are available [here](http://airbnb.io/caravel)
|
||||
|
||||
## Setting up a Python development environment
|
||||
|
||||
Check the [OS dependencies](http://airbnb.io/caravel/installation.html#os-dependencies) before follows these steps.
|
||||
|
||||
# fork the repo on GitHub and then clone it
|
||||
# alternatively you may want to clone the main repo but that won't work
|
||||
# so well if you are planning on sending PRs
|
||||
# git clone git@github.com:airbnb/caravel.git
|
||||
|
||||
# [optional] setup a virtual env and activate it
|
||||
virtualenv env
|
||||
source env/bin/activate
|
||||
|
||||
# install for development
|
||||
python setup.py develop
|
||||
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app caravel
|
||||
|
||||
# Initialize the database
|
||||
caravel db upgrade
|
||||
|
||||
# Create default roles and permissions
|
||||
caravel init
|
||||
|
||||
# Load some data to play with
|
||||
caravel load_examples
|
||||
|
||||
# start a dev web server
|
||||
caravel runserver -d
|
||||
|
||||
|
||||
## Setting up the node / npm javascript environment
|
||||
|
||||
`caravel/assets` contains all npm-managed, front end assets.
|
||||
Flask-Appbuilder itself comes bundled with jQuery and bootstrap.
|
||||
While these may be phased out over time, these packages are currently not
|
||||
managed with npm.
|
||||
|
||||
### Node/npm versions
|
||||
Make sure you are using recent versions of node and npm. No problems have been found with node>=5.10 and npm>=3.9.
|
||||
|
||||
### Using npm to generate bundled files
|
||||
|
||||
#### npm
|
||||
First, npm must be available in your environment. If it is not you can run the following commands
|
||||
(taken from [this source](https://gist.github.com/DanHerbert/9520689))
|
||||
```
|
||||
brew install node --without-npm
|
||||
echo prefix=~/.npm-packages >> ~/.npmrc
|
||||
curl -L https://www.npmjs.com/install.sh | sh
|
||||
```
|
||||
|
||||
The final step is to add `~/.npm-packages/bin` to your `PATH` so commands you install globally are usable.
|
||||
Add something like this to your `.bashrc` file, then `source ~/.bashrc` to reflect the change.
|
||||
```
|
||||
export PATH="$HOME/.npm-packages/bin:$PATH"
|
||||
```
|
||||
|
||||
#### npm packages
|
||||
To install third party libraries defined in `package.json`, run the
|
||||
following within the `caravel/assets/` directory which will install them in a
|
||||
new `node_modules/` folder within `assets/`.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To parse and generate bundled files for caravel, run either of the
|
||||
following commands. The `dev` flag will keep the npm script running and
|
||||
re-run it upon any changes within the assets directory.
|
||||
|
||||
```
|
||||
# Compiles the production / optimized js & css
|
||||
npm run prod
|
||||
|
||||
# Start a web server that manages and updates your assets as you modify them
|
||||
npm run dev
|
||||
```
|
||||
|
||||
For every development session you will have to start a flask dev server
|
||||
as well as an npm watcher
|
||||
|
||||
```
|
||||
caravel runserver -d -p 8081
|
||||
npm run dev
|
||||
```
|
||||
[API Documentation](http://pythonhosted.com/airflow)
|
||||
|
||||
## Testing
|
||||
|
||||
Python tests can be run with:
|
||||
Install development requirements:
|
||||
|
||||
./run_tests.sh
|
||||
pip install -r requirements.txt
|
||||
|
||||
We use [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Enzyme](http://airbnb.io/enzyme/) to test Javascript. Tests can be run with:
|
||||
Tests can then be run with:
|
||||
|
||||
cd /caravel/caravel/assets/javascripts
|
||||
npm i
|
||||
npm run test
|
||||
|
||||
## Linting
|
||||
./run_unit_tests.sh
|
||||
|
||||
Lint the project with:
|
||||
|
||||
# for python changes
|
||||
flake8 changes tests
|
||||
flake8 changes caravel
|
||||
|
||||
# for javascript
|
||||
npm run lint
|
||||
|
||||
## Linting with codeclimate
|
||||
Codeclimate is a service we use to measure code quality and test coverage. To get codeclimate's report on your branch, ideally before sending your PR, you can setup codeclimate against your Caravel fork. After you push to your fork, you should be able to get the report at http://codeclimate.com . Alternatively, if you prefer to work locally, you can install the codeclimate cli tool.
|
||||
|
||||
*Install the codeclimate cli tool*
|
||||
```
|
||||
curl -L https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine && chmod +x /usr/local/bin/docker-machine
|
||||
brew install docker
|
||||
docker-machine create --driver virtual box default
|
||||
docker-machine env default
|
||||
eval "$(docker-machine env default)"
|
||||
docker pull codeclimate/codeclimate
|
||||
brew tap codeclimate/formulae
|
||||
brew install codeclimate
|
||||
```
|
||||
|
||||
*Run the lint command:*
|
||||
```
|
||||
docker-machine start
|
||||
eval "$(docker-machine env default)”
|
||||
codeclimate analyze
|
||||
```
|
||||
More info can be found here: https://docs.codeclimate.com/docs/open-source-free
|
||||
|
||||
|
||||
## API documentation
|
||||
|
||||
@@ -192,81 +75,19 @@ Generate the documentation with:
|
||||
|
||||
cd docs && ./build.sh
|
||||
|
||||
## CSS Themes
|
||||
As part of the npm build process, CSS for Caravel is compiled from `Less`, a dynamic stylesheet language.
|
||||
|
||||
It's possible to customize or add your own theme to Caravel, either by overriding CSS rules or preferably
|
||||
by modifying the Less variables or files in `assets/stylesheets/less/`.
|
||||
|
||||
The `variables.less` and `bootswatch.less` files that ship with Caravel are derived from
|
||||
[Bootswatch](https://bootswatch.com) and thus extend Bootstrap. Modify variables in these files directly, or
|
||||
swap them out entirely with the equivalent files from other Bootswatch (themes)[https://github.com/thomaspark/bootswatch.git]
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
Before you submit a pull request from your forked repo, check that it
|
||||
Before you submit a pull request from your forked repo, check that it
|
||||
meets these guidelines:
|
||||
|
||||
1. The pull request should include tests, either as doctests,
|
||||
unit tests, or both.
|
||||
2. If the pull request adds functionality, the docs should be updated
|
||||
as part of the same PR. Doc string are often sufficient, make
|
||||
as part of the same PR. Doc string are often sufficient, make
|
||||
sure to follow the sphinx compatible standards.
|
||||
3. The pull request should work for Python 2.6, 2.7, and ideally python 3.3.
|
||||
``from __future__ import`` will be required in every `.py` file soon.
|
||||
`from __future__ import ` will be required in every `.py` file soon.
|
||||
4. Code will be reviewed by re running the unittests, flake8 and syntax
|
||||
should be as rigorous as the core Python project.
|
||||
5. Please rebase and resolve all conflicts before submitting.
|
||||
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Babel](http://babel.pocoo.org/en/latest/) to translate Caravel. The
|
||||
key is to instrument the strings that need translation using
|
||||
`from flask_babel import lazy_gettext as _`. Once this is imported in
|
||||
a module, all you have to do is to `_("Wrap your strings")` using the
|
||||
underscore `_` "function".
|
||||
|
||||
To enable changing language in your environment, you can simply add the
|
||||
`LANGUAGES` parameter to your `caravel_config.py`. Having more than one
|
||||
options here will add a language selection dropdown on the right side of the
|
||||
navigation bar.
|
||||
|
||||
LANGUAGES = {
|
||||
'en': {'flag': 'us', 'name': 'English'},
|
||||
'fr': {'flag': 'fr', 'name': 'French'},
|
||||
'zh': {'flag': 'cn', 'name': 'Chinese'},
|
||||
}
|
||||
|
||||
As per the [Flask AppBuilder documentation] about translation, to create a
|
||||
new language dictionary, run the following command:
|
||||
|
||||
pybabel init -i ./babel/messages.pot -d caravel/translations -l es
|
||||
|
||||
Then it's a matter of running the statement below to gather all stings that
|
||||
need translation
|
||||
|
||||
fabmanager babel-extract --target caravel/translations/
|
||||
|
||||
You can then translate the strings gathered in files located under
|
||||
`caravel/translation`, where there's one per language. For the translations
|
||||
to take effect, they need to be compiled using this command:
|
||||
|
||||
fabmanager babel-compile --target caravel/translations/
|
||||
|
||||
|
||||
## Adding new datasources
|
||||
|
||||
1. Create Models and Views for the datasource, add them under caravel folder, like a new my_models.py
|
||||
with models for cluster, datasources, columns and metrics and my_views.py with clustermodelview
|
||||
and datasourcemodelview.
|
||||
|
||||
2. Create db migration files for the new models
|
||||
|
||||
3. Specify this variable to add the datasource model and from which module it is from in config.py:
|
||||
|
||||
For example:
|
||||
|
||||
`ADDITIONAL_MODULE_DS_MAP = {'caravel.my_models': ['MyDatasource', 'MyOtherDatasource']}`
|
||||
|
||||
This means it'll register MyDatasource and MyOtherDatasource in caravel.my_models module in the source registry.
|
||||
|
||||
14
INTHEWILD.md
@@ -1,14 +0,0 @@
|
||||
Please use [pull requests](https://github.com/airbnb/caravel/pull/new/master)
|
||||
to add your organization and/or project to this document!
|
||||
|
||||
Organizations
|
||||
----------
|
||||
- [Airbnb](https://github.com/airbnb)
|
||||
- [GfK Data Lab] (http://datalab.gfk.com)
|
||||
- [Maieutical Labs] (https://cloudschooling.it)
|
||||
- [Shopkick] (https://www.shopkick.com)
|
||||
- [Amino] (https://amino.com)
|
||||
|
||||
Projects
|
||||
----------
|
||||
- None we know of yet
|
||||
@@ -1,9 +0,0 @@
|
||||
recursive-include caravel/templates *
|
||||
recursive-include caravel/static *
|
||||
recursive-exclude caravel/static/assets/node_modules *
|
||||
recursive-include caravel/static/assets/node_modules/font-awesome *
|
||||
recursive-exclude caravel/static/docs *
|
||||
recursive-exclude caravel/static/spec *
|
||||
recursive-exclude tests *
|
||||
recursive-include caravel/data *
|
||||
recursive-include caravel/migrations *
|
||||
220
README.md
@@ -1,137 +1,125 @@
|
||||
Caravel
|
||||
Panoramix
|
||||
=========
|
||||
<img src="http://i.imgur.com/H0Kyvyi.jpg" style="border-radius: 20px; box-shadow:5px 5px 5px gray;" alt="Caravel" width="500"/>
|
||||
|
||||
[](https://travis-ci.org/airbnb/caravel)
|
||||
[](https://badge.fury.io/py/caravel)
|
||||
[](https://coveralls.io/github/airbnb/caravel?branch=master)
|
||||
[](https://codeclimate.com/github/airbnb/caravel/coverage)
|
||||
[](https://landscape.io/github/airbnb/caravel/master)
|
||||
[](https://codeclimate.com/github/airbnb/caravel)
|
||||
[](https://pypi.python.org/pypi/caravel)
|
||||
[](https://requires.io/github/airbnb/caravel/requirements/?branch=master)
|
||||
[](https://gitter.im/airbnb/caravel?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://airbnb.io/caravel/)
|
||||
[](https://david-dm.org/airbnb/caravel?path=caravel/assets)
|
||||
|
||||
Caravel is a data exploration platform designed to be visual, intuitive
|
||||
Panoramix is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||
[this project used to be named **Panoramix**]
|
||||
Buzz Phrases
|
||||
------------
|
||||
|
||||
* Analytics at the speed of thought!
|
||||
* Instantaneous learning curve
|
||||
* Realtime analytics when querying [Druid.io](http://druid.io)
|
||||
* Extentsible to infinity
|
||||
|
||||
Screenshots & Gifs
|
||||
------------------
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
Caravel
|
||||
---------
|
||||
Caravel's main goal is to make it easy to slice, dice and visualize data.
|
||||
It empowers users to perform **analytics at the speed of thought**.
|
||||
|
||||
Caravel provides:
|
||||
* A quick way to intuitively visualize datasets by allowing users to create
|
||||
and share interactive dashboards
|
||||
* A rich set of visualizations to analyze your data, as well as a flexible
|
||||
way to extend the capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER
|
||||
through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how data sources are
|
||||
displayed in the UI, by defining which fields should show up in
|
||||
which dropdown and which aggregation and function (metrics) are
|
||||
made available to the user
|
||||
* Deep integration with Druid allows for Caravel to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
* Fast loading dashboards with configurable caching
|
||||
|
||||

|
||||
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Caravel was originally designed on top of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SQLAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
Panoramix was originally designed on to of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SqlAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[many external databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
|
||||
|
||||
What is Druid?
|
||||
What's Druid?
|
||||
-------------
|
||||
From their website at http://druid.io
|
||||
|
||||
*Druid is an open-source analytics data store designed for
|
||||
business intelligence (OLAP) queries on event data. Druid provides low
|
||||
latency (real-time) data ingestion, flexible data exploration,
|
||||
and fast data aggregation. Existing Druid deployments have scaled to
|
||||
trillions of events and petabytes of data. Druid is best used to
|
||||
*Druid is an open-source analytics data store designed for
|
||||
business intelligence (OLAP) queries on event data. Druid provides low
|
||||
latency (real-time) data ingestion, flexible data exploration,
|
||||
and fast data aggregation. Existing Druid deployments have scaled to
|
||||
trillions of events and petabytes of data. Druid is best used to
|
||||
power analytic dashboards and applications.*
|
||||
|
||||
Panoramix
|
||||
---------
|
||||
Panoramix's main goal is to make it easy to slice, dice and visualize data
|
||||
out of Druid. It empowers its user to perform **analytics
|
||||
at the speed of thought**.
|
||||
|
||||
Installation & Configuration
|
||||
----------------------------
|
||||
Panoramix started as a hackathon project at Airbnb in while running a POC
|
||||
(proof of concept) on using Druid.
|
||||
|
||||
[See in the documentation](http://airbnb.io/caravel/installation.html)
|
||||
Panoramix provides:
|
||||
* A way to query intuitively a Druid dataset, allowing for grouping, filtering
|
||||
limiting and defining a time granularity
|
||||
* Many charts and visualization to analyze your data, as well as a flexible
|
||||
way to extend the visualization capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
on who can access which features, and integration with major
|
||||
authentication providers (through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how Druid datasources are
|
||||
displayed in the UI,
|
||||
by defining which fields should show up in which dropdown and which
|
||||
aggregation and function (metrics) are made available to the user
|
||||
|
||||
|
||||
More screenshots
|
||||
----------------
|
||||
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
---
|
||||

|
||||
|
||||
|
||||
Resources
|
||||
-------------
|
||||
* [Caravel Google Group](https://groups.google.com/forum/#!forum/airbnb_caravel)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/caravel)
|
||||
* [Docker image 1](https://hub.docker.com/r/kochalex/caravel/)
|
||||
[Docker image 2](https://hub.docker.com/r/amancevice/caravel/) (community contributed)
|
||||
* [Slides from Strata (March 2016)](https://drive.google.com/open?id=0B5PVE0gzO81oOVJkdF9aNkJMSmM)
|
||||
|
||||
|
||||
Tip of the Hat
|
||||
--------------
|
||||
|
||||
Caravel would not be possible without these great frameworks / libs
|
||||
|
||||
* Flask App Builder - Allowing us to focus on building the app quickly while
|
||||
getting the foundation for free
|
||||
* The Flask ecosystem - Simply amazing. So much Plug, easy play.
|
||||
* NVD3 - One of the best charting libraries out there
|
||||
* Much more, check out the `install_requires` section in the [setup.py](https://github.com/airbnb/caravel/blob/master/setup.py) file!
|
||||
|
||||
|
||||
Contributing
|
||||
Installation
|
||||
------------
|
||||
|
||||
Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/airbnb/caravel/blob/master/CONTRIBUTING.md)
|
||||
Follow these few simple steps to install Panoramix
|
||||
|
||||
```
|
||||
# Install panoramix
|
||||
pip install panoramix
|
||||
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app panoramix
|
||||
|
||||
# Start the web server
|
||||
panoramix
|
||||
```
|
||||
|
||||
After installation, you should be able to point your browser to the right
|
||||
hostname:port [http://localhost:8088](http://localhost:8088), login using
|
||||
the credential you entered while creating the admin account, and navigate to
|
||||
`Menu -> Admin -> Refresh Metadata`. This action should bring in all of
|
||||
your datasources for Panoramix to be aware of, and they should show up in
|
||||
`Menu -> Datasources`, from where you can start playing with your data!
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
To configure your application, you need to create a file (module)
|
||||
`panoramix_config.py` and make sure it is in your PYTHONPATH. Here are some
|
||||
of the parameters you can copy / paste in that configuration module:
|
||||
|
||||
```
|
||||
#---------------------------------------------------------
|
||||
# Panoramix specifix config
|
||||
#---------------------------------------------------------
|
||||
ROW_LIMIT = 5000
|
||||
WEBSERVER_THREADS = 8
|
||||
|
||||
PANORAMIX_WEBSERVER_PORT = 8088
|
||||
#---------------------------------------------------------
|
||||
|
||||
#---------------------------------------------------------
|
||||
# Flask App Builder configuration
|
||||
#---------------------------------------------------------
|
||||
# Your App secret key
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
|
||||
|
||||
# The SQLAlchemy connection string.
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///tmp/panoramix.db'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
CSRF_ENABLED = True
|
||||
|
||||
# Whether to run the web server in debug mode or not
|
||||
DEBUG = True
|
||||
```
|
||||
|
||||
This file also allows you to define configuration parameters used by
|
||||
Flask App Builder, the web framework used by Panoramix. Please consult
|
||||
the [Flask App Builder Documentation](http://flask-appbuilder.readthedocs.org/en/latest/config.html) for more information on how to configure Panoramix.
|
||||
|
||||
|
||||
* From the UI, enter the information about your clusters in the
|
||||
``Admin->Clusters`` menu by hitting the + sign.
|
||||
|
||||
* Once the Druid cluster connection information is entered, hit the
|
||||
``Admin->Refresh Metadata`` menu item to populate
|
||||
|
||||
* Navigate to your datasources
|
||||
|
||||
53
TODO.md
@@ -1,48 +1,7 @@
|
||||
# TODO
|
||||
List of TODO items for Caravel
|
||||
|
||||
## Important
|
||||
* **Getting proper JS testing:** unit tests on the Python side are pretty
|
||||
solid, but now we need a test suite for the JS part of the site,
|
||||
testing all the ajax-type calls
|
||||
* **Viz Plugins:** Allow people to define and share visualization plugins.
|
||||
ideally one would only need to drop in a set of files in a folder and
|
||||
Caravel would discover and expose the plugins
|
||||
|
||||
## Features
|
||||
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
|
||||
* **Default slice:** choose a default slice for the dataset instead of
|
||||
default endpoint
|
||||
* **Widget sets / chart grids:** a way to have all charts support making
|
||||
a series of charts and putting them in a grid. The same way that you
|
||||
can groupby for series, you could chart by. The form field set would be
|
||||
common and use a single field to "grid by", a limit number of chart as
|
||||
an N * N grid size.
|
||||
* **Advanced dashboard configuration:** currently you can define which
|
||||
slices in a dashboard are immune to filtering.
|
||||
* **Annotations layers:** allow for people to maintain data annotations,
|
||||
attached to a layer and time range. These layers can be added on top of
|
||||
some visualizations as annotations. An example of a layer might be
|
||||
"holidays" or "site outages", ...
|
||||
* **Slack integration** - TBD
|
||||
* **Comments:** allow for people to comment on slices and dashes
|
||||
|
||||
|
||||
## Easy-ish fix
|
||||
* Build matrix to include mysql using tox
|
||||
* CREATE VIEW button from SQL editor
|
||||
* Test button for when editing SQL expression
|
||||
* Slider form element
|
||||
* [druid] Allow for post aggregations (ratios!)
|
||||
* in/notin filters autocomplete (druid)
|
||||
|
||||
## New viz
|
||||
* Maps that use geocodes
|
||||
* Time animated scatter plots
|
||||
* Horizon charts
|
||||
* Calendar heatmap
|
||||
* Chord diagram
|
||||
* ...
|
||||
|
||||
## Community
|
||||
* Turorial vids
|
||||
* in/notin filters autocomplete
|
||||
* DRUID: Allow for post aggregations (ratios!)
|
||||
* compare time ranges
|
||||
* csv export out of table view
|
||||
* Save / bookmark / url shortener
|
||||
* SQL: Find a way to manage granularity
|
||||
|
||||
@@ -29,7 +29,7 @@ script_location = migrations
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = scheme://localhost/caravel
|
||||
sqlalchemy.url = scheme://localhost/panoramix
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[ignore: caravel/assets/node_modules/**]
|
||||
[python: caravel/**.py]
|
||||
[jinja2: caravel/**/templates/**.html]
|
||||
[python: **.py]
|
||||
[jinja2: **/templates/**.html]
|
||||
encoding = utf-8
|
||||
|
||||
1808
babel/messages.pot
Executable file → Normal file
@@ -1,112 +0,0 @@
|
||||
"""Package's main module!"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from flask import Flask, redirect
|
||||
from flask_appbuilder import SQLA, AppBuilder, IndexView
|
||||
from sqlalchemy import event, exc
|
||||
from flask_appbuilder.baseviews import expose
|
||||
from flask_cache import Cache
|
||||
from flask_migrate import Migrate
|
||||
from caravel.source_registry import SourceRegistry
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
|
||||
|
||||
APP_DIR = os.path.dirname(__file__)
|
||||
CONFIG_MODULE = os.environ.get('CARAVEL_CONFIG', 'caravel.config')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(CONFIG_MODULE)
|
||||
if not app.debug:
|
||||
# In production mode, add log handler to sys.stderr.
|
||||
app.logger.addHandler(logging.StreamHandler())
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
db = SQLA(app)
|
||||
|
||||
|
||||
@event.listens_for(db.engine, 'checkout')
|
||||
def checkout(dbapi_con, con_record, con_proxy):
|
||||
"""
|
||||
Making sure the connection is live, and preventing against:
|
||||
'MySQL server has gone away'
|
||||
|
||||
Copied from:
|
||||
http://stackoverflow.com/questions/30630120/mysql-keeps-losing-connection-during-celery-tasks
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
if hasattr(dbapi_con, 'ping'):
|
||||
dbapi_con.ping(False)
|
||||
else:
|
||||
cursor = dbapi_con.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
except TypeError:
|
||||
app.logger.debug('MySQL connection died. Restoring...')
|
||||
dbapi_con.ping()
|
||||
except dbapi_con.OperationalError as e:
|
||||
app.logger.warning(e)
|
||||
if e.args[0] in (2006, 2013, 2014, 2045, 2055):
|
||||
raise exc.DisconnectionError()
|
||||
else:
|
||||
raise
|
||||
return db
|
||||
|
||||
|
||||
cache = Cache(app, config=app.config.get('CACHE_CONFIG'))
|
||||
|
||||
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
|
||||
|
||||
# Logging configuration
|
||||
logging.basicConfig(format=app.config.get('LOG_FORMAT'))
|
||||
logging.getLogger().setLevel(app.config.get('LOG_LEVEL'))
|
||||
|
||||
if app.config.get('ENABLE_TIME_ROTATE'):
|
||||
logging.getLogger().setLevel(app.config.get('TIME_ROTATE_LOG_LEVEL'))
|
||||
handler = TimedRotatingFileHandler(app.config.get('FILENAME'),
|
||||
when=app.config.get('ROLLOVER'),
|
||||
interval=app.config.get('INTERVAL'),
|
||||
backupCount=app.config.get('BACKUP_COUNT'))
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
if app.config.get('ENABLE_CORS'):
|
||||
from flask_cors import CORS
|
||||
CORS(app, **app.config.get('CORS_OPTIONS'))
|
||||
|
||||
if app.config.get('ENABLE_PROXY_FIX'):
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
if app.config.get('UPLOAD_FOLDER'):
|
||||
try:
|
||||
os.makedirs(app.config.get('UPLOAD_FOLDER'))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class MyIndexView(IndexView):
|
||||
@expose('/')
|
||||
def index(self):
|
||||
return redirect('/caravel/welcome')
|
||||
|
||||
appbuilder = AppBuilder(
|
||||
app, db.session,
|
||||
base_template='caravel/base.html',
|
||||
indexview=MyIndexView,
|
||||
security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
|
||||
|
||||
sm = appbuilder.sm
|
||||
|
||||
get_session = appbuilder.get_session
|
||||
|
||||
# Registering sources
|
||||
module_datasource_map = app.config.get("DEFAULT_MODULE_DS_MAP")
|
||||
module_datasource_map.update(app.config.get("ADDITIONAL_MODULE_DS_MAP"))
|
||||
SourceRegistry.register_sources(module_datasource_map)
|
||||
|
||||
from caravel import views, config # noqa
|
||||
@@ -1,82 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
error = (
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8OI++=~~~~~~=+?IODMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMD$~~~~~~~~~~~~~~~~~~~~~~~=$MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMN8?:~~~~~~~~~~~~~~~~~~~~~~~~~~=+8NMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMO=~~~~~~~~~~~~~~~~~+I??~~~~~~~~~~~~~+DMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMNI~~~~~~~~~~~~~~~~~~IIIII=~~~~~~~~~~~~~~=NMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMM+=~~~~~~~~~~~~~~~~~~~=III+~~~~~~~~~~~~~~~~~?8MMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+++=~~~~8MMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMI=~~~~~~~~~~~~~~~~~~~~~~~~~III?I~~~~~~~~,:++++++~~8MMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMN7~~~~~~~~~~~~~~~~==+=~~~~~~=IIIII~~~~~~:. ..:=++=~=MMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMO=~~~~~~~~~~~~~~~~+++=~~~~~~~~??I?I~~~~~~. ...,~~~~IMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMM~~~~~~~~~~~~~~~~~+++:,~~~~~~~~~~~?=~~~~~:. ..~~~~~OMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMM$=~~~~~~~~~~~~~~~=++:.. ..~~~~~~~~~~~~~~~~,. . . :~~~~~OMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMM~~~~~~~~~~~~~~~~+++,. .~~~~~~~~~~~~~~~.. .. . .~~~~~=OMMMMMMMMMM\n"+
|
||||
"MMMMMMMM?~~~~~~~~~~~~~~~=+~. .~~~~~~~~~~~~~~. ,MMMMM,=~~~~~~NMMMMMMMMM\n"+
|
||||
"MMMMMMMN~~~~~~~~~~~~~~~~~,. .,~~~~~~~~~~~~~.. ZMMM,+Z:~~~~~~$MMMMMMMMM\n"+
|
||||
"MMMMMM8?~~~~~~~~~~~~~~~~~.. ..~~~~~~~~~~~~~:. DMMM,+D~~~~~~~~IMMMMMMMM\n"+
|
||||
"MMMMMMI~~~~~~~~~~~~~~~~~~.. :MMMO~~~~~~~~~~~~~~~,.. ?MMMMMI~~~~~~~~~MMMMMMMM\n"+
|
||||
"MMMMMM=~~~~~~~~~~~~~~~~~~.. MMM+=M:~~~~~~~~~~~~~:. .:IM$~~~~~~~~~~~8MMMMMMM\n"+
|
||||
"MMMMMD~~~~~~~~~~~~~~~~~~~:. MMM:,M:~~~~~~~~~~~~~~~.......:~~~~~~~~~~$MMMMMMM\n"+
|
||||
"MMMMMI~~~~~~~~~~~~~~~~~~~~, MMMMMM~~~~~~~~~~~~~~~~~~,..:~~~~~~~~~~~~+MMMMMMM\n"+
|
||||
"MMMMD+~~~~~~~~~~~~~~~~~~~~~. $MMMM$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=MMMMMMM\n"+
|
||||
"MMMM8~~~~~~~~~~~~~~~~~~~~~~:. . .:~~~~~~,..:. .=~~~~~~~~~~~~~~~~~~~~MMMMMMM\n"+
|
||||
"MMMMO~~~~~~~~~~~~~~~~~~~~~~~:, .:~~~~~=8.. .+ . =8ZI~~~~~~~~~~~~~~~~=MMMMMMM\n"+
|
||||
"MMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~:,,,:~~~~~~IZ8:. .O....888?~~~~~~~~~~~~~~~+MMMMMMM\n"+
|
||||
"MMMMO=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?888=...I~I88888O?~~~~~~~~~~~~~~7MMMMMMM\n"+
|
||||
"MMMMO~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Z888OO88888888888O?~~~~~~~~~~~~~OMMMMMMM\n"+
|
||||
"MMMMD+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=8888888888888888888~~~~~~~~~~~~+MMMMMMMM\n"+
|
||||
"MMMMM7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~?8888888888888888888?~~~~~~~~~~=$MMMMMMMM\n"+
|
||||
"MMMMMD~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$8888888888888888888O~~~~~~~~~~8MMMMMMMMM\n"+
|
||||
"MMMMMN=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888888888888ZZ7=~~~~~~~~?MMMMMMMMMM\n"+
|
||||
"MMMMMMZ=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+Z88888888Z7I===~~~~~~~~~~~~~=OMMMMMMMMMMM\n"+
|
||||
"MMMMMMN$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$88888O7?=~~~~~~~~~~~~~~~~~~OMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~I8OZ+~~~~~~~~~~~~~~~~~~~~=DMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMM8=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+$+=~~~~~~~~~~~~~~~~~~~~+MMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMD7~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$DMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMM?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=$OMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMD7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ZMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMZ7=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~78MMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMM8OI=~~~~~~~~~~~~~~~~~~~=+?ZDNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMNDZ7?++~=~==~+?IONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM")
|
||||
|
||||
stacktrace="""
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
=======================================================================================================
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
___ ___ ___
|
||||
( ) ( ) ( )
|
||||
.--. | |_ .---. .--. | | ___ | |_ ___ .-. .---. .--. .--.
|
||||
/ _ \ ( __) / .-, \ / \ | | ( ) ( __) ( ) \ / .-, \ / \ / \\
|
||||
. .' `. ; | | (__) ; | | .-. ; | | ' / | | | ' .-. ; (__) ; | | .-. ; | .-. ;
|
||||
| ' | | | | ___ .'` | | |(___) | |,' / | | ___ | / (___) .'` | | |(___) | | | |
|
||||
_\_`.(___) | |( ) / .'| | | | | . '. | |( ) | | / .'| | | | | |/ |
|
||||
( ). '. | | | | | / | | | | ___ | | `. \ | | | | | | | / | | | | ___ | ' _.'
|
||||
| | `\ | | ' | | ; | ; | | '( ) | | \ \ | ' | | | | ; | ; | | '( ) | .'.-.
|
||||
; '._,' ' ' `-' ; ' `-' | ' `-' | | | \ . ' `-' ; | | ' `-' | ' `-' | ' `-' /
|
||||
'.___.' `.__. `.__.'_. `.__,' (___ ) (___) `.__. (___) `.__.'_. `.__,' `.__.'
|
||||
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
=======================================================================================================
|
||||
-------------------------------------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
boat = """\
|
||||
+ +
|
||||
)`.).
|
||||
)``)``) .~~
|
||||
).-'.-')|)
|
||||
|-).-).-'_'-/
|
||||
~~~\ `o-o-o' /~~~~
|
||||
~~~'---.____/~~~"""
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets" : ["airbnb", "es2015", "react"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
**/*{.,-}min.js
|
||||
**/*.sh
|
||||
coverage/**
|
||||
dist/*
|
||||
images/*
|
||||
node_modules/*
|
||||
node_modules*/*
|
||||
spec/*
|
||||
stylesheets/*
|
||||
vendor/*
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": "airbnb",
|
||||
"parserOptions":{
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prefer-template": 0,
|
||||
"new-cap": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"guard-for-in": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"func-names": 0,
|
||||
"react/jsx-no-bind": 0,
|
||||
"no-confusing-arrow": 0,
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
verbose: true
|
||||
instrumentation:
|
||||
root: './javascripts'
|
||||
extensions: ['.js', '.jsx']
|
||||
excludes: [
|
||||
'dist/**'
|
||||
]
|
||||
embed-source: false
|
||||
variable: __coverage__
|
||||
compact: true
|
||||
preserve-comments: false
|
||||
complete-copy: false
|
||||
save-baseline: true
|
||||
baseline-file: ./coverage/coverage-baseline.json
|
||||
include-all-sources: true
|
||||
include-pid: false
|
||||
es-modules: true
|
||||
reporting:
|
||||
print: summary
|
||||
reports:
|
||||
- lcov
|
||||
dir: ./coverage
|
||||
watermarks:
|
||||
statements: [50, 80]
|
||||
lines: [50, 80]
|
||||
functions: [50, 80]
|
||||
branches: [50, 80]
|
||||
report-config:
|
||||
clover: {file: clover.xml}
|
||||
cobertura: {file: cobertura-coverage.xml}
|
||||
json: {file: coverage-final.json}
|
||||
json-summary: {file: coverage-summary.json}
|
||||
lcovonly: {file: lcov.info}
|
||||
teamcity: {file: null, blockName: Code Coverage Summary}
|
||||
text: {file: null, maxCols: 0}
|
||||
text-lcov: {file: lcov.info}
|
||||
text-summary: {file: null}
|
||||
hooks:
|
||||
hook-run-in-context: false
|
||||
post-require-hook: null
|
||||
handle-sigint: false
|
||||
check:
|
||||
global:
|
||||
statements: 0
|
||||
lines: 0
|
||||
branches: 0
|
||||
functions: 0
|
||||
excludes: []
|
||||
each:
|
||||
statements: 0
|
||||
lines: 0
|
||||
branches: 0
|
||||
functions: 0
|
||||
excludes: []
|
||||
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 702 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 738 KiB |
|
Before Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 133 KiB |
@@ -1,19 +0,0 @@
|
||||
|
||||
# TODO
|
||||
* Figure out how to organize the left panel, integrate Search
|
||||
* collapse sql beyond 10 lines
|
||||
* Security per-database (dropdown)
|
||||
* Get a to work
|
||||
|
||||
## Cosmetic
|
||||
* Result set font is too big
|
||||
* lmiit/timer/buttons wrap
|
||||
* table label is transparent
|
||||
* SqlEditor buttons
|
||||
* use react-bootstrap-prompt for query title input
|
||||
* Make tabs look great
|
||||
|
||||
# PROJECT
|
||||
* Write Runbook
|
||||
* Confirm backups
|
||||
* merge chef branch
|
||||
@@ -1,130 +0,0 @@
|
||||
export const RESET_STATE = 'RESET_STATE';
|
||||
export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
|
||||
export const CLONE_QUERY_TO_NEW_TAB = 'CLONE_QUERY_TO_NEW_TAB';
|
||||
export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR';
|
||||
export const MERGE_TABLE = 'MERGE_TABLE';
|
||||
export const REMOVE_TABLE = 'REMOVE_TABLE';
|
||||
export const START_QUERY = 'START_QUERY';
|
||||
export const STOP_QUERY = 'STOP_QUERY';
|
||||
export const END_QUERY = 'END_QUERY';
|
||||
export const REMOVE_QUERY = 'REMOVE_QUERY';
|
||||
export const EXPAND_TABLE = 'EXPAND_TABLE';
|
||||
export const COLLAPSE_TABLE = 'COLLAPSE_TABLE';
|
||||
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
|
||||
export const QUERY_FAILED = 'QUERY_FAILED';
|
||||
export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB';
|
||||
export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
|
||||
export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
|
||||
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
|
||||
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
|
||||
export const SET_DATABASES = 'SET_DATABASES';
|
||||
export const ADD_WORKSPACE_QUERY = 'ADD_WORKSPACE_QUERY';
|
||||
export const REMOVE_WORKSPACE_QUERY = 'REMOVE_WORKSPACE_QUERY';
|
||||
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
|
||||
export const ADD_ALERT = 'ADD_ALERT';
|
||||
export const REMOVE_ALERT = 'REMOVE_ALERT';
|
||||
export const REFRESH_QUERIES = 'REFRESH_QUERIES';
|
||||
export const SET_NETWORK_STATUS = 'SET_NETWORK_STATUS';
|
||||
|
||||
export function resetState() {
|
||||
return { type: RESET_STATE };
|
||||
}
|
||||
|
||||
export function setDatabases(databases) {
|
||||
return { type: SET_DATABASES, databases };
|
||||
}
|
||||
|
||||
export function addQueryEditor(queryEditor) {
|
||||
return { type: ADD_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function cloneQueryToNewTab(query) {
|
||||
return { type: CLONE_QUERY_TO_NEW_TAB, query };
|
||||
}
|
||||
|
||||
export function setNetworkStatus(networkOn) {
|
||||
return { type: SET_NETWORK_STATUS, networkOn };
|
||||
}
|
||||
|
||||
export function addAlert(alert) {
|
||||
return { type: ADD_ALERT, alert };
|
||||
}
|
||||
|
||||
export function removeAlert(alert) {
|
||||
return { type: REMOVE_ALERT, alert };
|
||||
}
|
||||
|
||||
export function setActiveQueryEditor(queryEditor) {
|
||||
return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function removeQueryEditor(queryEditor) {
|
||||
return { type: REMOVE_QUERY_EDITOR, queryEditor };
|
||||
}
|
||||
|
||||
export function removeQuery(query) {
|
||||
return { type: REMOVE_QUERY, query };
|
||||
}
|
||||
|
||||
export function queryEditorSetDb(queryEditor, dbId) {
|
||||
return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
|
||||
}
|
||||
|
||||
export function queryEditorSetSchema(queryEditor, schema) {
|
||||
return { type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema };
|
||||
}
|
||||
|
||||
export function queryEditorSetAutorun(queryEditor, autorun) {
|
||||
return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
|
||||
}
|
||||
|
||||
export function queryEditorSetTitle(queryEditor, title) {
|
||||
return { type: QUERY_EDITOR_SET_TITLE, queryEditor, title };
|
||||
}
|
||||
|
||||
export function queryEditorSetSql(queryEditor, sql) {
|
||||
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
|
||||
}
|
||||
|
||||
export function mergeTable(table) {
|
||||
return { type: MERGE_TABLE, table };
|
||||
}
|
||||
|
||||
export function expandTable(table) {
|
||||
return { type: EXPAND_TABLE, table };
|
||||
}
|
||||
|
||||
export function collapseTable(table) {
|
||||
return { type: COLLAPSE_TABLE, table };
|
||||
}
|
||||
|
||||
export function removeTable(table) {
|
||||
return { type: REMOVE_TABLE, table };
|
||||
}
|
||||
|
||||
export function startQuery(query) {
|
||||
return { type: START_QUERY, query };
|
||||
}
|
||||
|
||||
export function stopQuery(query) {
|
||||
return { type: STOP_QUERY, query };
|
||||
}
|
||||
|
||||
export function querySuccess(query, results) {
|
||||
return { type: QUERY_SUCCESS, query, results };
|
||||
}
|
||||
|
||||
export function queryFailed(query, msg) {
|
||||
return { type: QUERY_FAILED, query, msg };
|
||||
}
|
||||
|
||||
export function addWorkspaceQuery(query) {
|
||||
return { type: ADD_WORKSPACE_QUERY, query };
|
||||
}
|
||||
|
||||
export function removeWorkspaceQuery(query) {
|
||||
return { type: REMOVE_WORKSPACE_QUERY, query };
|
||||
}
|
||||
export function refreshQueries(alteredQueries) {
|
||||
return { type: REFRESH_QUERIES, alteredQueries };
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export const STATE_BSSTYLE_MAP = {
|
||||
failed: 'danger',
|
||||
pending: 'info',
|
||||
running: 'warning',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
export const STATUS_OPTIONS = ['success', 'failed', 'running'];
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
class Alerts extends React.Component {
|
||||
removeAlert(alert) {
|
||||
this.props.actions.removeAlert(alert);
|
||||
}
|
||||
render() {
|
||||
const alerts = this.props.alerts.map((alert) =>
|
||||
<Alert
|
||||
bsStyle={alert.bsStyle}
|
||||
style={{ width: '500px', textAlign: 'midddle', margin: '10px auto' }}
|
||||
>
|
||||
{alert.msg}
|
||||
<i
|
||||
className="fa fa-close pull-right"
|
||||
onClick={this.removeAlert.bind(this, alert)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
return (
|
||||
<div>{alerts}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Alerts.propTypes = {
|
||||
alerts: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(Alerts);
|
||||
@@ -1,71 +0,0 @@
|
||||
import * as Actions from '../actions';
|
||||
import React from 'react';
|
||||
|
||||
import TabbedSqlEditors from './TabbedSqlEditors';
|
||||
import QueryAutoRefresh from './QueryAutoRefresh';
|
||||
import QuerySearch from './QuerySearch';
|
||||
import Alerts from './Alerts';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hash: window.location.hash,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
window.addEventListener('hashchange', this.onHashChanged.bind(this));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('hashchange', this.onHashChanged.bind(this));
|
||||
}
|
||||
onHashChanged() {
|
||||
this.setState({ hash: window.location.hash });
|
||||
}
|
||||
render() {
|
||||
if (this.state.hash) {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<QuerySearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="App SqlLab">
|
||||
<div className="container-fluid">
|
||||
<QueryAutoRefresh />
|
||||
<Alerts alerts={this.props.alerts} />
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<TabbedSqlEditors />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
alerts: React.PropTypes.array,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
alerts: state.alerts,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
const ButtonWithTooltip = (props) => {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
delayShow={300}
|
||||
placement={props.placement}
|
||||
delayHide={150}
|
||||
>
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
bsStyle={props.bsStyle}
|
||||
bsSize={props.bsSize}
|
||||
disabled={props.disabled}
|
||||
className={props.className}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonWithTooltip.defaultProps = {
|
||||
onClick: () => {},
|
||||
disabled: false,
|
||||
placement: 'top',
|
||||
bsStyle: 'default',
|
||||
};
|
||||
|
||||
ButtonWithTooltip.propTypes = {
|
||||
bsSize: React.PropTypes.string,
|
||||
bsStyle: React.PropTypes.string,
|
||||
children: React.PropTypes.element,
|
||||
className: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default ButtonWithTooltip;
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
|
||||
const propTypes = {
|
||||
qe: React.PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
qe: null,
|
||||
};
|
||||
|
||||
export default class CopyQueryTabUrl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const search = window.location.search;
|
||||
const cleanUri = search ? uri.substring(0, uri.indexOf('?')) : uri;
|
||||
const query = search.substring(1);
|
||||
this.state = {
|
||||
uri,
|
||||
cleanUri,
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
getQueryLink() {
|
||||
const params = [];
|
||||
const qe = this.props.qe;
|
||||
if (qe.dbId) params.push('dbid=' + qe.dbId);
|
||||
if (qe.title) params.push('title=' + encodeURIComponent(qe.title));
|
||||
if (qe.schema) params.push('schema=' + encodeURIComponent(qe.schema));
|
||||
if (qe.autorun) params.push('autorun=' + qe.autorun);
|
||||
if (qe.sql) params.push('sql=' + encodeURIComponent(qe.sql));
|
||||
|
||||
const queryString = params.join('&');
|
||||
const queryLink = this.state.cleanUri + '?' + queryString;
|
||||
|
||||
return queryLink;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
inMenu
|
||||
text={this.getQueryLink()}
|
||||
copyNode={<span>share query</span>}
|
||||
tooltipText="copy URL to clipboard"
|
||||
shouldShowText={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CopyQueryTabUrl.propTypes = propTypes;
|
||||
CopyQueryTabUrl.defaultProps = defaultProps;
|
||||
@@ -1,65 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Select from 'react-select';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
class DatabaseSelect extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
databaseLoading: false,
|
||||
databaseOptions: [],
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.fetchDatabaseOptions();
|
||||
}
|
||||
changeDb(db) {
|
||||
this.props.onChange(db);
|
||||
}
|
||||
fetchDatabaseOptions() {
|
||||
this.setState({ databaseLoading: true });
|
||||
const url = '/databaseasync/api/read?_flt_0_expose_in_sqllab=1';
|
||||
$.get(url, (data) => {
|
||||
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
|
||||
this.setState({ databaseOptions: options, databaseLoading: false });
|
||||
this.props.actions.setDatabases(data.result);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
name="select-db"
|
||||
placeholder={`Select a database (${this.state.databaseOptions.length})`}
|
||||
options={this.state.databaseOptions}
|
||||
value={this.props.databaseId}
|
||||
isLoading={this.state.databaseLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeDb.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseSelect.propTypes = {
|
||||
onChange: React.PropTypes.func,
|
||||
actions: React.PropTypes.object,
|
||||
databaseId: React.PropTypes.number,
|
||||
};
|
||||
|
||||
DatabaseSelect.defaultProps = {
|
||||
onChange: () => {},
|
||||
databaseId: null,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DatabaseSelect);
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
|
||||
class Link extends React.Component {
|
||||
render() {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{this.props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
const link = (
|
||||
<a
|
||||
href={this.props.href}
|
||||
onClick={this.props.onClick}
|
||||
style={this.props.style}
|
||||
className={'Link ' + this.props.className}
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
);
|
||||
if (this.props.tooltip) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
placement={this.props.placement}
|
||||
delayShow={300}
|
||||
delayHide={150}
|
||||
>
|
||||
{link}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
Link.propTypes = {
|
||||
children: React.PropTypes.object,
|
||||
className: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
Link.defaultProps = {
|
||||
disabled: false,
|
||||
href: '#',
|
||||
tooltip: null,
|
||||
placement: 'top',
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default Link;
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
const $ = require('jquery');
|
||||
const QUERY_UPDATE_FREQ = 1000;
|
||||
const QUERY_UPDATE_BUFFER_MS = 5000;
|
||||
|
||||
class QueryAutoRefresh extends React.Component {
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
const url = '/caravel/queries/' + (this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS);
|
||||
// No updates in case of failure.
|
||||
$.getJSON(url, (data) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
this.props.actions.refreshQueries(data);
|
||||
}
|
||||
if (!this.props.networkOn) {
|
||||
this.props.actions.setNetworkStatus(true);
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (this.props.networkOn) {
|
||||
this.props.actions.setNetworkStatus(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
QueryAutoRefresh.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
queriesLastUpdate: React.PropTypes.number,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
QueryAutoRefresh.defaultProps = {
|
||||
// queries: null,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queriesLastUpdate: state.queriesLastUpdate,
|
||||
networkOn: state.networkOn,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryAutoRefresh);
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import QueryTable from './QueryTable';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const QueryHistory = (props) => {
|
||||
const activeQeId = props.tabHistory[props.tabHistory.length - 1];
|
||||
const queriesArray = [];
|
||||
for (const id in props.queries) {
|
||||
if (props.queries[id].sqlEditorId === activeQeId) {
|
||||
queriesArray.push(props.queries[id]);
|
||||
}
|
||||
}
|
||||
if (queriesArray.length > 0) {
|
||||
return (
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'started', 'duration', 'progress',
|
||||
'rows', 'sql', 'output', 'actions',
|
||||
]}
|
||||
queries={queriesArray}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
QueryHistory.defaultProps = {
|
||||
queries: {},
|
||||
};
|
||||
|
||||
QueryHistory.propTypes = {
|
||||
queries: React.PropTypes.object,
|
||||
tabHistory: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryHistory);
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class QueryLink extends React.Component {
|
||||
popTab() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.query.title,
|
||||
dbId: this.props.query.dbId,
|
||||
autorun: false,
|
||||
sql: this.props.query.sql,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<a
|
||||
href="#"
|
||||
tooltip="Pop this query in a new tab"
|
||||
onClick={this.popTab.bind(this)}
|
||||
>
|
||||
{this.props.query.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<Link
|
||||
onClick={this.props.actions.removeWorkspaceQuery.bind(this, this.props.query)}
|
||||
tooltip="Remove query from workspace"
|
||||
href="#"
|
||||
>
|
||||
×
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueryLink.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
QueryLink.defaultProps = {
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(QueryLink);
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'react-bootstrap';
|
||||
import Select from 'react-select';
|
||||
import QueryTable from './QueryTable';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
import { STATUS_OPTIONS } from '../common';
|
||||
|
||||
class QuerySearch extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
userLoading: false,
|
||||
userOptions: [],
|
||||
databaseId: null,
|
||||
userId: null,
|
||||
searchText: null,
|
||||
status: 'success',
|
||||
queriesArray: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
this.refreshQueries();
|
||||
}
|
||||
onUserClicked(userId) {
|
||||
this.setState({ userId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onDbClicked(dbId) {
|
||||
this.setState({ databaseId: dbId }, () => { this.refreshQueries(); });
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ databaseId: val });
|
||||
}
|
||||
insertParams(baseUrl, params) {
|
||||
return baseUrl + '?' + params.join('&');
|
||||
}
|
||||
changeUser(user) {
|
||||
const val = (user) ? user.value : null;
|
||||
this.setState({ userId: val });
|
||||
}
|
||||
changeStatus(status) {
|
||||
const val = (status) ? status.value : null;
|
||||
this.setState({ status: val });
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
fetchUsers() {
|
||||
this.setState({ userLoading: true });
|
||||
const url = '/users/api/read';
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const options = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
options.push({ value: data.pks[i], label: data.result[i].username });
|
||||
}
|
||||
this.setState({ userOptions: options, userLoading: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
refreshQueries() {
|
||||
const params = [
|
||||
`userId=${this.state.userId}`,
|
||||
`databaseId=${this.state.databaseId}`,
|
||||
`searchText=${this.state.searchText}`,
|
||||
`status=${this.state.status}`,
|
||||
];
|
||||
|
||||
const url = this.insertParams('/caravel/search_queries', params);
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
const newQueriesArray = [];
|
||||
for (const id in data) {
|
||||
newQueriesArray.push(data[id]);
|
||||
}
|
||||
this.setState({ queriesArray: newQueriesArray });
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="row space-1">
|
||||
<div className="col-sm-2">
|
||||
<Select
|
||||
name="select-user"
|
||||
placeholder="[User]"
|
||||
options={this.state.userOptions}
|
||||
value={this.state.userId}
|
||||
isLoading={this.state.userLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeUser.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.state.databaseId}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<Select
|
||||
name="select-state"
|
||||
placeholder="[Query Status]"
|
||||
options={STATUS_OPTIONS.map((s) => ({ value: s, label: s }))}
|
||||
value={this.state.status}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={this.changeStatus.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<Button bsSize="small" bsStyle="success" onClick={this.refreshQueries.bind(this)}>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'db', 'user',
|
||||
'progress', 'rows', 'sql', 'querylink',
|
||||
]}
|
||||
onUserClicked={this.onUserClicked.bind(this)}
|
||||
onDbClicked={this.onDbClicked.bind(this)}
|
||||
queries={this.state.queriesArray}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default QuerySearch;
|
||||
@@ -1,177 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Table } from 'reactable';
|
||||
import { ProgressBar } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
import SqlShrink from './SqlShrink';
|
||||
import { STATE_BSSTYLE_MAP } from '../common';
|
||||
import { fDuration } from '../../modules/dates';
|
||||
import { getLink } from '../../../utils/common';
|
||||
|
||||
|
||||
class QueryTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const cleanUri = uri.substring(0, uri.indexOf('#'));
|
||||
this.state = {
|
||||
cleanUri,
|
||||
showVisualizeModal: false,
|
||||
activeQuery: null,
|
||||
};
|
||||
}
|
||||
getQueryLink(dbId, sql) {
|
||||
const params = ['dbid=' + dbId, 'sql=' + sql, 'title=Untitled Query'];
|
||||
const link = getLink(this.state.cleanUri, params);
|
||||
return encodeURI(link);
|
||||
}
|
||||
hideVisualizeModal() {
|
||||
this.setState({ showVisualizeModal: false });
|
||||
}
|
||||
showVisualizeModal(query) {
|
||||
this.setState({ showVisualizeModal: true });
|
||||
this.setState({ activeQuery: query });
|
||||
}
|
||||
restoreSql(query) {
|
||||
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
|
||||
}
|
||||
|
||||
openQueryInNewTab(query) {
|
||||
this.props.actions.cloneQueryToNewTab(query);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.queries.map((query) => {
|
||||
const q = Object.assign({}, query);
|
||||
if (q.endDttm) {
|
||||
q.duration = fDuration(q.startDttm, q.endDttm);
|
||||
}
|
||||
q.user = (
|
||||
<button
|
||||
className="btn btn-link btn-xs"
|
||||
onClick={this.props.onUserClicked.bind(this, q.userId)}
|
||||
>
|
||||
{q.user}
|
||||
</button>
|
||||
);
|
||||
q.db = (
|
||||
<button
|
||||
className="btn btn-link btn-xs"
|
||||
onClick={this.props.onDbClicked.bind(this, q.dbId)}
|
||||
>
|
||||
{q.db}
|
||||
</button>
|
||||
);
|
||||
q.started = moment(q.startDttm).format('HH:mm:ss');
|
||||
const source = (q.ctas) ? q.executedSql : q.sql;
|
||||
q.sql = (
|
||||
<SqlShrink sql={source} maxWidth={100} />
|
||||
);
|
||||
q.output = q.tempTable;
|
||||
q.progress = (
|
||||
<ProgressBar
|
||||
style={{ width: '75px' }}
|
||||
striped
|
||||
now={q.progress}
|
||||
label={`${q.progress}%`}
|
||||
/>
|
||||
);
|
||||
let errorTooltip;
|
||||
if (q.errorMessage) {
|
||||
errorTooltip = (
|
||||
<Link tooltip={q.errorMessage}>
|
||||
<i className="fa fa-exclamation-circle text-danger" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
q.state = (
|
||||
<div>
|
||||
<span className={'m-r-3 label label-' + STATE_BSSTYLE_MAP[q.state]}>
|
||||
{q.state}
|
||||
</span>
|
||||
{errorTooltip}
|
||||
</div>
|
||||
);
|
||||
q.actions = (
|
||||
<div style={{ width: '75px' }}>
|
||||
<Link
|
||||
className="fa fa-line-chart m-r-3"
|
||||
tooltip="Visualize the data out of this query"
|
||||
onClick={this.showVisualizeModal.bind(this, query)}
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-pencil m-r-3"
|
||||
onClick={this.restoreSql.bind(this, query)}
|
||||
tooltip="Overwrite text in editor with a query on this table"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle m-r-3"
|
||||
onClick={this.openQueryInNewTab.bind(this, query)}
|
||||
tooltip="Run query in a new tab"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash m-r-3"
|
||||
tooltip="Remove query from log"
|
||||
onClick={this.props.actions.removeQuery.bind(this, query)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
q.querylink = (
|
||||
<div style={{ width: '100px' }}>
|
||||
<a
|
||||
href={this.getQueryLink(q.dbId, source)}
|
||||
className="btn btn-primary btn-xs"
|
||||
>
|
||||
<i className="fa fa-external-link" />Open in SQL Editor
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
return q;
|
||||
}).reverse();
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showVisualizeModal}
|
||||
query={this.state.activeQuery}
|
||||
onHide={this.hideVisualizeModal.bind(this)}
|
||||
/>
|
||||
<Table
|
||||
columns={this.props.columns}
|
||||
className="table table-condensed"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryTable.propTypes = {
|
||||
columns: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
queries: React.PropTypes.array,
|
||||
onUserClicked: React.PropTypes.func,
|
||||
onDbClicked: React.PropTypes.func,
|
||||
};
|
||||
QueryTable.defaultProps = {
|
||||
columns: ['started', 'duration', 'rows'],
|
||||
queries: [],
|
||||
onUserClicked: () => {},
|
||||
onDbClicked: () => {},
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryTable);
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, ButtonGroup } from 'react-bootstrap';
|
||||
import { Table } from 'reactable';
|
||||
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
|
||||
|
||||
class ResultSet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchText: '',
|
||||
showModal: false,
|
||||
};
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
showModal() {
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
hideModal() {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
render() {
|
||||
const results = this.props.query.results;
|
||||
let controls = <div className="noControls" />;
|
||||
if (this.props.showControls) {
|
||||
controls = (
|
||||
<div className="ResultSetControls">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.showModal.bind(this)}
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
</Button>
|
||||
<Button bsSize="small" href={'/caravel/csv/' + this.props.query.id}>
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (results && results.data && results.data.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showModal}
|
||||
query={this.props.query}
|
||||
onHide={this.hideModal.bind(this)}
|
||||
/>
|
||||
{controls}
|
||||
<div className="ResultSet">
|
||||
<Table
|
||||
data={results.data}
|
||||
columns={results.columns.map((col) => col.name)}
|
||||
sortable
|
||||
className="table table-condensed table-bordered"
|
||||
filterBy={this.state.searchText}
|
||||
filterable={results.columns}
|
||||
hideFilterInput
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<Alert bsStyle="warning">The query returned no data</Alert>);
|
||||
}
|
||||
}
|
||||
ResultSet.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
showControls: React.PropTypes.bool,
|
||||
search: React.PropTypes.bool,
|
||||
searchText: React.PropTypes.string,
|
||||
};
|
||||
ResultSet.defaultProps = {
|
||||
showControls: true,
|
||||
search: true,
|
||||
searchText: '',
|
||||
};
|
||||
|
||||
export default ResultSet;
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Alert, Button, Tab, Tabs } from 'react-bootstrap';
|
||||
import QueryHistory from './QueryHistory';
|
||||
import ResultSet from './ResultSet';
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class SouthPane extends React.Component {
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.latestQuery.tempTable,
|
||||
autorun: false,
|
||||
dbId: this.props.latestQuery.dbId,
|
||||
sql: `SELECT * FROM ${this.props.latestQuery.tempTable}`,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
let results = <div />;
|
||||
const latestQuery = this.props.latestQuery;
|
||||
if (latestQuery) {
|
||||
if (['running', 'pending'].includes(latestQuery.state)) {
|
||||
results = (
|
||||
<img className="loading" alt="Loading.." src="/static/assets/images/loading.gif" />
|
||||
);
|
||||
} else if (latestQuery.state === 'failed') {
|
||||
results = <Alert bsStyle="danger">{latestQuery.errorMessage}</Alert>;
|
||||
} else if (latestQuery.state === 'success' && latestQuery.ctas) {
|
||||
results = (
|
||||
<div>
|
||||
<Alert bsStyle="info">
|
||||
Table [<strong>{latestQuery.tempTable}</strong>] was created
|
||||
</Alert>
|
||||
<p>
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="m-r-5"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
>
|
||||
Query in a new tab
|
||||
</Button>
|
||||
<Button bsSize="small">Visualize</Button>
|
||||
</p>
|
||||
</div>);
|
||||
} else if (latestQuery.state === 'success') {
|
||||
results = <ResultSet showControls search query={latestQuery} />;
|
||||
}
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
}
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs bsStyle="tabs" id={shortid.generate()}>
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Query History" eventKey={2}>
|
||||
<QueryHistory />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SouthPane.propTypes = {
|
||||
latestQuery: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SouthPane.defaultProps = {
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(SouthPane);
|
||||
@@ -1,301 +0,0 @@
|
||||
const $ = require('jquery');
|
||||
import { now } from '../../modules/dates';
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Col,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Form,
|
||||
FormControl,
|
||||
Label,
|
||||
OverlayTrigger,
|
||||
Row,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import shortid from 'shortid';
|
||||
import SouthPane from './SouthPane';
|
||||
import Timer from './Timer';
|
||||
|
||||
import SqlEditorLeftBar from './SqlEditorLeftBar';
|
||||
|
||||
class SqlEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autorun: props.queryEditor.autorun,
|
||||
sql: props.queryEditor.sql,
|
||||
ctas: '',
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.onMount();
|
||||
}
|
||||
onMount() {
|
||||
if (this.state.autorun) {
|
||||
this.setState({ autorun: false });
|
||||
this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false);
|
||||
this.startQuery();
|
||||
}
|
||||
}
|
||||
runQuery(runAsync = false) {
|
||||
this.startQuery(runAsync);
|
||||
}
|
||||
startQuery(runAsync = false, ctas = false) {
|
||||
const that = this;
|
||||
const query = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
id: shortid.generate(),
|
||||
progress: 0,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sqlEditorId: this.props.queryEditor.id,
|
||||
startDttm: now(),
|
||||
state: 'running',
|
||||
tab: this.props.queryEditor.title,
|
||||
};
|
||||
if (runAsync) {
|
||||
query.state = 'pending';
|
||||
}
|
||||
|
||||
// Execute the Query
|
||||
that.props.actions.startQuery(query);
|
||||
|
||||
const sqlJsonUrl = '/caravel/sql_json/';
|
||||
const sqlJsonRequest = {
|
||||
client_id: query.id,
|
||||
database_id: this.props.queryEditor.dbId,
|
||||
json: true,
|
||||
runAsync,
|
||||
schema: this.props.queryEditor.schema,
|
||||
select_as_cta: ctas,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sql_editor_id: this.props.queryEditor.id,
|
||||
tab: this.props.queryEditor.title,
|
||||
tmp_table_name: this.state.ctas,
|
||||
};
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url: sqlJsonUrl,
|
||||
data: sqlJsonRequest,
|
||||
success(results) {
|
||||
if (!runAsync) {
|
||||
that.props.actions.querySuccess(query, results);
|
||||
}
|
||||
},
|
||||
error(err, textStatus, errorThrown) {
|
||||
let msg;
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
if (err.responseText !== undefined) {
|
||||
msg = err.responseText;
|
||||
}
|
||||
}
|
||||
if (textStatus === 'error' && errorThrown === '') {
|
||||
msg = 'Could not connect to server';
|
||||
} else if (msg === null) {
|
||||
msg = `[${textStatus}] ${errorThrown}`;
|
||||
}
|
||||
that.props.actions.queryFailed(query, msg);
|
||||
},
|
||||
});
|
||||
}
|
||||
stopQuery() {
|
||||
this.props.actions.stopQuery(this.props.latestQuery);
|
||||
}
|
||||
createTableAs() {
|
||||
this.startQuery(true, true);
|
||||
}
|
||||
textChange(text) {
|
||||
this.setState({ sql: text });
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, text);
|
||||
}
|
||||
addWorkspaceQuery() {
|
||||
this.props.actions.addWorkspaceQuery({
|
||||
id: shortid.generate(),
|
||||
sql: this.state.sql,
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
schema: this.props.queryEditor.schema,
|
||||
title: this.props.queryEditor.title,
|
||||
});
|
||||
}
|
||||
ctasChange() {}
|
||||
visualize() {}
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
|
||||
sqlEditorHeight() {
|
||||
// quick hack to make the white bg of the tab stretch full height.
|
||||
const tabNavHeight = 40;
|
||||
const navBarHeight = 56;
|
||||
const mysteryVerticalHeight = 50;
|
||||
return window.innerHeight - tabNavHeight - navBarHeight - mysteryVerticalHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
let runButtons = [];
|
||||
if (this.props.database && this.props.database.allow_run_sync) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="primary"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, false)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key={shortid.generate()}
|
||||
>
|
||||
<i className="fa fa-table" /> Run Query
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (this.props.database && this.props.database.allow_run_async) {
|
||||
runButtons.push(
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="primary"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this, true)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
key={shortid.generate()}
|
||||
>
|
||||
<i className="fa fa-table" /> Run Async
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
{runButtons}
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (this.props.latestQuery && this.props.latestQuery.state === 'running') {
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
bsSize="small"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.stopQuery.bind(this)}
|
||||
>
|
||||
<a className="fa fa-stop" /> Stop
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
let limitWarning = null;
|
||||
if (this.props.latestQuery && this.props.latestQuery.limit_reached) {
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
It appears that the number of rows in the query results displayed
|
||||
was limited on the server side to
|
||||
the {this.props.latestQuery.rows} limit.
|
||||
</Tooltip>
|
||||
);
|
||||
limitWarning = (
|
||||
<OverlayTrigger placement="left" overlay={tooltip}>
|
||||
<Label bsStyle="warning" className="m-r-5">LIMIT</Label>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
let ctasControls;
|
||||
if (this.props.database && this.props.database.allow_ctas) {
|
||||
ctasControls = (
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
type="text"
|
||||
bsSize="small"
|
||||
className="input-sm"
|
||||
placeholder="new table name"
|
||||
onChange={this.ctasChanged.bind(this)}
|
||||
/>
|
||||
<InputGroup.Button>
|
||||
<Button
|
||||
bsSize="small"
|
||||
disabled={this.state.ctas.length === 0}
|
||||
onClick={this.createTableAs.bind(this)}
|
||||
>
|
||||
<i className="fa fa-table" /> CTAS
|
||||
</Button>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
const editorBottomBar = (
|
||||
<div className="sql-toolbar clearfix">
|
||||
<div className="pull-left">
|
||||
<Form inline>
|
||||
{runButtons}
|
||||
{ctasControls}
|
||||
</Form>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{limitWarning}
|
||||
<Timer query={this.props.latestQuery} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<SqlEditorLeftBar queryEditor={this.props.queryEditor} />
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
name={this.props.queryEditor.id}
|
||||
theme="github"
|
||||
minLines={7}
|
||||
maxLines={30}
|
||||
onChange={this.textChange.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableBasicAutocompletion
|
||||
value={this.props.queryEditor.sql}
|
||||
/>
|
||||
{editorBottomBar}
|
||||
<br />
|
||||
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditor.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
database: React.PropTypes.object,
|
||||
latestQuery: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SqlEditor.defaultProps = {
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditor);
|
||||
@@ -1,195 +0,0 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
import Select from 'react-select';
|
||||
import { Label, Button } from 'react-bootstrap';
|
||||
import TableElement from './TableElement';
|
||||
import DatabaseSelect from './DatabaseSelect';
|
||||
|
||||
|
||||
class SqlEditorLeftBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchSchemas();
|
||||
this.fetchTables();
|
||||
}
|
||||
onChange(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
|
||||
if (!(db)) {
|
||||
this.setState({ tableOptions: [] });
|
||||
} else {
|
||||
this.fetchTables(val, this.props.queryEditor.schema);
|
||||
this.fetchSchemas(val);
|
||||
}
|
||||
}
|
||||
resetState() {
|
||||
this.props.actions.resetState();
|
||||
}
|
||||
fetchTables(dbId, schema) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
const actualSchema = schema || this.props.queryEditor.schema;
|
||||
this.setState({ tableLoading: true });
|
||||
this.setState({ tableOptions: [] });
|
||||
const url = `/caravel/tables/${actualDbId}/${actualSchema}`;
|
||||
$.get(url, (data) => {
|
||||
let tableOptions = data.tables.map((s) => ({ value: s, label: s }));
|
||||
const views = data.views.map((s) => ({ value: s, label: '[view] ' + s }));
|
||||
tableOptions = [...tableOptions, ...views];
|
||||
this.setState({ tableOptions });
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
changeSchema(schemaOpt) {
|
||||
const schema = (schemaOpt) ? schemaOpt.value : null;
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schema);
|
||||
}
|
||||
fetchSchemas(dbId) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const url = `/databasetablesasync/api/read?_flt_0_id=${actualDbId}`;
|
||||
$.get(url, (data) => {
|
||||
const schemas = data.result[0].all_schema_names;
|
||||
const schemaOptions = schemas.map((s) => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions });
|
||||
this.setState({ schemaLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const qe = this.props.queryEditor;
|
||||
let url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
|
||||
this.setState({ tableLoading: true });
|
||||
$.get(url, (data) => {
|
||||
this.props.actions.mergeTable({
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
name: data.name,
|
||||
indexes: data.indexes,
|
||||
schema: qe.schema,
|
||||
columns: data.columns,
|
||||
expanded: true,
|
||||
});
|
||||
this.setState({ tableLoading: false });
|
||||
})
|
||||
.fail(() => {
|
||||
this.props.actions.addAlert({
|
||||
msg: 'Error occurred while fetching metadata',
|
||||
bsStyle: 'danger',
|
||||
});
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
|
||||
url = `/caravel/extra_table_metadata/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
const table = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
schema: qe.schema,
|
||||
name: tableName,
|
||||
};
|
||||
Object.assign(table, data);
|
||||
this.props.actions.mergeTable(table);
|
||||
});
|
||||
}
|
||||
render() {
|
||||
let networkAlert = null;
|
||||
if (!this.props.networkOn) {
|
||||
networkAlert = <p><Label bsStyle="danger">OFFLINE</Label></p>;
|
||||
}
|
||||
const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id));
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
return (
|
||||
<div className="clearfix sql-toolbar">
|
||||
{networkAlert}
|
||||
<div>
|
||||
<DatabaseSelect
|
||||
onChange={this.onChange.bind(this)}
|
||||
databaseId={this.props.queryEditor.dbId}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={`Select a schema (${this.state.schemaOptions.length})`}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="m-t-5">
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder={`Add a table (${this.state.tableOptions.length})`}
|
||||
autosize={false}
|
||||
value={this.state.tableName}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
options={this.state.tableOptions}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="m-t-5">
|
||||
{tables.map((table) => (
|
||||
<TableElement table={table} queryEditor={this.props.queryEditor} key={table.id} />
|
||||
))}
|
||||
</div>
|
||||
{shouldShowReset &&
|
||||
<Button bsSize="small" bsStyle="danger" onClick={this.resetState.bind(this)}>
|
||||
<i className="fa fa-bomb" /> Reset State
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditorLeftBar.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
SqlEditorLeftBar.defaultProps = {
|
||||
tables: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tables: state.tables,
|
||||
networkOn: state.networkOn,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorLeftBar);
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
const SqlShrink = (props) => {
|
||||
const sql = props.sql || '';
|
||||
let lines = sql.split('\n');
|
||||
if (lines.length >= props.maxLines) {
|
||||
lines = lines.slice(0, props.maxLines);
|
||||
lines.push('{...}');
|
||||
}
|
||||
const shrunk = lines.map((line) => {
|
||||
if (line.length > props.maxWidth) {
|
||||
return line.slice(0, props.maxWidth) + '{...}';
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
return (
|
||||
<div>
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{shrunk}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SqlShrink.defaultProps = {
|
||||
maxWidth: 60,
|
||||
maxLines: 6,
|
||||
};
|
||||
|
||||
SqlShrink.propTypes = {
|
||||
sql: React.PropTypes.string,
|
||||
maxWidth: React.PropTypes.number,
|
||||
maxLines: React.PropTypes.number,
|
||||
};
|
||||
|
||||
export default SqlShrink;
|
||||
@@ -1,170 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropdownButton, MenuItem, Tab, Tabs } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import SqlEditor from './SqlEditor';
|
||||
import shortid from 'shortid';
|
||||
import { getParamFromQuery, getLink } from '../../../utils/common';
|
||||
import CopyQueryTabUrl from './CopyQueryTabUrl';
|
||||
|
||||
let queryCount = 1;
|
||||
|
||||
class TabbedSqlEditors extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uri = window.location.toString();
|
||||
const search = window.location.search;
|
||||
const cleanUri = search ? uri.substring(0, uri.indexOf('?')) : uri;
|
||||
const query = search.substring(1);
|
||||
this.state = {
|
||||
uri,
|
||||
cleanUri,
|
||||
query,
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
if (this.state.query) {
|
||||
queryCount++;
|
||||
const queryEditorProps = {
|
||||
id: shortid.generate(),
|
||||
title: getParamFromQuery(this.state.query, 'title'),
|
||||
dbId: getParamFromQuery(this.state.query, 'dbid'),
|
||||
schema: getParamFromQuery(this.state.query, 'schema'),
|
||||
autorun: getParamFromQuery(this.state.query, 'autorun'),
|
||||
sql: getParamFromQuery(this.state.query, 'sql'),
|
||||
};
|
||||
this.props.actions.addQueryEditor(queryEditorProps);
|
||||
// Clean the url in browser history
|
||||
window.history.replaceState({}, document.title, this.state.cleanUri);
|
||||
}
|
||||
}
|
||||
getQueryLink(qe) {
|
||||
const params = [];
|
||||
if (qe.dbId) params.push('dbid=' + qe.dbId);
|
||||
if (qe.title) params.push('title=' + qe.title);
|
||||
if (qe.schema) params.push('schema=' + qe.schema);
|
||||
if (qe.autorun) params.push('autorun=' + qe.autorun);
|
||||
if (qe.sql) params.push('sql=' + qe.sql);
|
||||
|
||||
return getLink(this.state.cleanUri, params);
|
||||
}
|
||||
renameTab(qe) {
|
||||
/* eslint no-alert: 0 */
|
||||
const newTitle = prompt('Enter a new title for the tab');
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
activeQueryEditor() {
|
||||
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
for (let i = 0; i < this.props.queryEditors.length; i++) {
|
||||
const qe = this.props.queryEditors[i];
|
||||
if (qe.id === qeid) {
|
||||
return qe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
newQueryEditor() {
|
||||
queryCount++;
|
||||
const activeQueryEditor = this.activeQueryEditor();
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Untitled Query ${queryCount}`,
|
||||
dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
|
||||
schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
|
||||
autorun: false,
|
||||
sql: 'SELECT ...',
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
handleSelect(key) {
|
||||
if (key === 'add_tab') {
|
||||
this.newQueryEditor();
|
||||
} else {
|
||||
this.props.actions.setActiveQueryEditor({ id: key });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
let latestQuery = this.props.queries[qe.latestQueryId];
|
||||
const database = this.props.databases[qe.dbId];
|
||||
const state = (latestQuery) ? latestQuery.state : '';
|
||||
const tabTitle = (
|
||||
<div>
|
||||
<div className={'circle ' + state} /> {qe.title} {' '}
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
id={'ddbtn-tab-' + i}
|
||||
title=""
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={this.props.actions.removeQueryEditor.bind(this, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="3">
|
||||
<i className="fa fa-clipboard" /> <CopyQueryTabUrl qe={qe} />
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<SqlEditor
|
||||
queryEditor={qe}
|
||||
latestQuery={latestQuery}
|
||||
database={database}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>);
|
||||
});
|
||||
return (
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
|
||||
onSelect={this.handleSelect.bind(this)}
|
||||
id="a11y-query-editor-tabs"
|
||||
>
|
||||
{editors}
|
||||
<Tab title={<div><i className="fa fa-plus-circle" /> </div>} eventKey="add_tab" />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
TabbedSqlEditors.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
databases: React.PropTypes.object,
|
||||
queries: React.PropTypes.object,
|
||||
queryEditors: React.PropTypes.array,
|
||||
tabHistory: React.PropTypes.array,
|
||||
};
|
||||
TabbedSqlEditors.defaultProps = {
|
||||
tabHistory: [],
|
||||
queryEditors: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
databases: state.databases,
|
||||
queryEditors: state.queryEditors,
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors);
|
||||
@@ -1,214 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ButtonGroup, Well } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import ModalTrigger from '../../components/ModalTrigger';
|
||||
import CopyToClipboard from '../../components/CopyToClipboard';
|
||||
|
||||
const propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
table: null,
|
||||
actions: {},
|
||||
};
|
||||
|
||||
class TableElement extends React.Component {
|
||||
setSelectStar() {
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, this.selectStar());
|
||||
}
|
||||
|
||||
selectStar() {
|
||||
let cols = '';
|
||||
this.props.table.columns.forEach((col, i) => {
|
||||
cols += col.name;
|
||||
if (i < this.props.table.columns.length - 1) {
|
||||
cols += ', ';
|
||||
}
|
||||
});
|
||||
let tableName = this.props.table.name;
|
||||
if (this.props.table.schema) {
|
||||
tableName = this.props.table.schema + '.' + tableName;
|
||||
}
|
||||
return `SELECT ${cols}\nFROM ${tableName}`;
|
||||
}
|
||||
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.table.name,
|
||||
dbId: this.props.table.dbId,
|
||||
autorun: true,
|
||||
sql: this.selectStar(),
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
|
||||
collapseTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.collapseTable(this.props.table);
|
||||
}
|
||||
|
||||
expandTable(e) {
|
||||
e.preventDefault();
|
||||
this.props.actions.expandTable(this.props.table);
|
||||
}
|
||||
|
||||
removeTable() {
|
||||
this.props.actions.removeTable(this.props.table);
|
||||
}
|
||||
|
||||
render() {
|
||||
const table = this.props.table;
|
||||
let metadata = null;
|
||||
let buttonToggle;
|
||||
|
||||
let header;
|
||||
if (table.partitions) {
|
||||
let partitionQuery;
|
||||
let partitionClipBoard;
|
||||
if (table.partitions.partitionQuery) {
|
||||
partitionQuery = table.partitions.partitionQuery;
|
||||
const tt = 'Copy partition query to clipboard';
|
||||
partitionClipBoard = (
|
||||
<CopyToClipboard
|
||||
text={partitionQuery}
|
||||
shouldShowText={false}
|
||||
tooltipText={tt}
|
||||
copyNode={<i className="fa fa-clipboard" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let latest = [];
|
||||
for (const k in table.partitions.latest) {
|
||||
latest.push(`${k}=${table.partitions.latest[k]}`);
|
||||
}
|
||||
latest = latest.join('/');
|
||||
header = (
|
||||
<Well bsSize="small">
|
||||
<div>
|
||||
<small>
|
||||
latest partition: {latest}
|
||||
</small> {partitionClipBoard}
|
||||
</div>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
if (table.expanded) {
|
||||
buttonToggle = (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { this.collapseTable(e); }}
|
||||
>
|
||||
<strong>{table.name}</strong>
|
||||
<small className="m-l-5"><i className="fa fa-minus" /></small>
|
||||
</a>
|
||||
);
|
||||
metadata = (
|
||||
<div>
|
||||
{header}
|
||||
<div className="table-columns">
|
||||
{table.columns.map((col) => {
|
||||
let name = col.name;
|
||||
if (col.indexed) {
|
||||
name = <strong>{col.name}</strong>;
|
||||
}
|
||||
return (
|
||||
<div className="clearfix table-column" key={shortid.generate()}>
|
||||
<div className="pull-left m-l-10">
|
||||
{name}
|
||||
</div>
|
||||
<div className="pull-right text-muted">
|
||||
<small> {col.type}</small>
|
||||
</div>
|
||||
</div>);
|
||||
})}
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
buttonToggle = (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { this.expandTable(e); }}
|
||||
>
|
||||
{table.name}
|
||||
<small className="m-l-5"><i className="fa fa-plus" /></small>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
let keyLink;
|
||||
if (table.indexes && table.indexes.length > 0) {
|
||||
keyLink = (
|
||||
<ModalTrigger
|
||||
modalTitle={
|
||||
<div>
|
||||
Keys for table <strong>{table.name}</strong>
|
||||
</div>
|
||||
}
|
||||
modalBody={
|
||||
<pre>{JSON.stringify(table.indexes, null, 4)}</pre>
|
||||
}
|
||||
triggerNode={
|
||||
<Link
|
||||
className="fa fa-key pull-left m-l-2"
|
||||
tooltip={`View indexes (${table.indexes.length})`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="TableElement">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
{buttonToggle}
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<ButtonGroup className="ws-el-controls pull-right">
|
||||
{keyLink}
|
||||
<Link
|
||||
className="fa fa-pencil pull-left m-l-2"
|
||||
onClick={this.setSelectStar.bind(this)}
|
||||
tooltip="Run query in this tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle pull-left m-l-2"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
tooltip="Run query in a new tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash pull-left m-l-2"
|
||||
onClick={this.removeTable.bind(this)}
|
||||
tooltip="Remove from workspace"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{metadata}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableElement.propTypes = propTypes;
|
||||
TableElement.defaultProps = defaultProps;
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(TableElement);
|
||||
export { TableElement };
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
|
||||
|
||||
const TableMetadata = function (props) {
|
||||
return (
|
||||
<BootstrapTable
|
||||
condensed
|
||||
data={props.table.columns}
|
||||
>
|
||||
<TableHeaderColumn dataField="id" isKey hidden>
|
||||
id
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="name">Name</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="type">Type</TableHeaderColumn>
|
||||
</BootstrapTable>
|
||||
);
|
||||
};
|
||||
|
||||
TableMetadata.propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
};
|
||||
|
||||
export default TableMetadata;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { now, fDuration } from '../../modules/dates';
|
||||
|
||||
import { STATE_BSSTYLE_MAP } from '../common.js';
|
||||
|
||||
class Timer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clockStr: '',
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 30);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
if (this.props && this.props.query) {
|
||||
const endDttm = this.props.query.endDttm || now();
|
||||
const clockStr = fDuration(this.props.query.startDttm, endDttm);
|
||||
this.setState({ clockStr });
|
||||
if (this.props.query.state !== 'running') {
|
||||
this.stopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.query && this.props.query.state === 'running') {
|
||||
this.startTimer();
|
||||
}
|
||||
let timerSpan = null;
|
||||
if (this.props && this.props.query) {
|
||||
const bsStyle = STATE_BSSTYLE_MAP[this.props.query.state];
|
||||
timerSpan = (
|
||||
<span className={'inlineBlock m-r-5 label label-' + bsStyle}>
|
||||
{this.state.clockStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return timerSpan;
|
||||
}
|
||||
}
|
||||
Timer.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
};
|
||||
Timer.defaultProps = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
@@ -1,227 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Col, Modal } from 'react-bootstrap';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import Select from 'react-select';
|
||||
import { Table } from 'reactable';
|
||||
import shortid from 'shortid';
|
||||
|
||||
const CHART_TYPES = [
|
||||
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
|
||||
{ value: 'pie', label: 'Pie Chart', requiresTime: false },
|
||||
{ value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
|
||||
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
|
||||
];
|
||||
|
||||
class VisualizeModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uniqueId = shortid.generate();
|
||||
this.state = {
|
||||
chartType: CHART_TYPES[0],
|
||||
datasourceName: uniqueId,
|
||||
columns: {},
|
||||
hints: [],
|
||||
};
|
||||
// update columns if possible
|
||||
this.setStateFromProps();
|
||||
}
|
||||
componentWillMount() {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.validate();
|
||||
}
|
||||
setStateFromProps() {
|
||||
if (!this.props.query || !this.props.query.results.columns) {
|
||||
return;
|
||||
}
|
||||
const columns = {};
|
||||
this.props.query.results.columns.forEach((col) => {
|
||||
columns[col.name] = col;
|
||||
});
|
||||
this.setState({ columns });
|
||||
}
|
||||
validate() {
|
||||
const hints = [];
|
||||
const cols = this.mergedColumns();
|
||||
const re = /^\w+$/;
|
||||
Object.keys(cols).forEach((colName) => {
|
||||
if (!re.test(colName)) {
|
||||
hints.push(
|
||||
<div>
|
||||
"{colName}" is not right as a column name, please alias it
|
||||
(as in SELECT count(*) <strong>AS my_alias</strong>) using only
|
||||
alphanumeric characters and underscores
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
if (this.state.chartType === null) {
|
||||
hints.push('Pick a chart type!');
|
||||
} else if (this.state.chartType.requiresTime) {
|
||||
let hasTime = false;
|
||||
for (const colName in cols) {
|
||||
const col = cols[colName];
|
||||
if (col.hasOwnProperty('is_date') && col.is_date) {
|
||||
hasTime = true;
|
||||
}
|
||||
}
|
||||
if (!hasTime) {
|
||||
hints.push('To use this chart type you need at least one column flagged as a date');
|
||||
}
|
||||
}
|
||||
this.setState({ hints });
|
||||
}
|
||||
changeChartType(option) {
|
||||
this.setState({ chartType: option }, this.validate);
|
||||
}
|
||||
mergedColumns() {
|
||||
const columns = Object.assign({}, this.state.columns);
|
||||
if (this.props.query && this.props.query.results.columns) {
|
||||
this.props.query.results.columns.forEach((col) => {
|
||||
if (columns[col.name] === undefined) {
|
||||
columns[col.name] = col;
|
||||
}
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
visualize() {
|
||||
const vizOptions = {
|
||||
chartType: this.state.chartType.value,
|
||||
datasourceName: this.state.datasourceName,
|
||||
columns: this.state.columns,
|
||||
sql: this.props.query.sql,
|
||||
dbId: this.props.query.dbId,
|
||||
};
|
||||
window.open('/caravel/sqllab_viz/?data=' + JSON.stringify(vizOptions));
|
||||
}
|
||||
changeDatasourceName(event) {
|
||||
this.setState({ datasourceName: event.target.value });
|
||||
this.validate();
|
||||
}
|
||||
changeCheckbox(attr, columnName, event) {
|
||||
let columns = this.mergedColumns();
|
||||
const column = Object.assign({}, columns[columnName], { [attr]: event.target.checked });
|
||||
columns = Object.assign({}, columns, { [columnName]: column });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
changeAggFunction(columnName, option) {
|
||||
let columns = this.mergedColumns();
|
||||
const val = (option) ? option.value : null;
|
||||
const column = Object.assign({}, columns[columnName], { agg: val });
|
||||
columns = Object.assign({}, columns, { [columnName]: column });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
render() {
|
||||
if (!(this.props.query)) {
|
||||
return <div />;
|
||||
}
|
||||
const tableData = this.props.query.results.columns.map((col) => ({
|
||||
column: col.name,
|
||||
is_dimension: (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_dim', col.name)}
|
||||
checked={(this.state.columns[col.name]) ? this.state.columns[col.name].is_dim : false}
|
||||
className="form-control"
|
||||
/>
|
||||
),
|
||||
is_date: (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-control"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_date', col.name)}
|
||||
checked={(this.state.columns[col.name]) ? this.state.columns[col.name].is_date : false}
|
||||
/>
|
||||
),
|
||||
agg_func: (
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'sum', label: 'SUM(x)' },
|
||||
{ value: 'min', label: 'MIN(x)' },
|
||||
{ value: 'max', label: 'MAX(x)' },
|
||||
{ value: 'avg', label: 'AVG(x)' },
|
||||
{ value: 'count_distinct', label: 'COUNT(DISTINCT x)' },
|
||||
]}
|
||||
onChange={this.changeAggFunction.bind(this, col.name)}
|
||||
value={(this.state.columns[col.name]) ? this.state.columns[col.name].agg : null}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
const alerts = this.state.hints.map((hint, i) => (
|
||||
<Alert bsStyle="warning" key={i}>{hint}</Alert>
|
||||
));
|
||||
const modal = (
|
||||
<div className="VisualizeModal">
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Visualize</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{alerts}
|
||||
<div className="row">
|
||||
<Col md={6}>
|
||||
Chart Type
|
||||
<Select
|
||||
name="select-chart-type"
|
||||
placeholder="[Chart Type]"
|
||||
options={CHART_TYPES}
|
||||
value={(this.state.chartType) ? this.state.chartType.value : null}
|
||||
autosize={false}
|
||||
onChange={this.changeChartType.bind(this)}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
Datasource Name
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
placeholder="datasource name"
|
||||
onChange={this.changeDatasourceName.bind(this)}
|
||||
value={this.state.datasourceName}
|
||||
/>
|
||||
</Col>
|
||||
</div>
|
||||
<hr />
|
||||
<Table
|
||||
className="table table-condensed"
|
||||
columns={['column', 'is_dimension', 'is_date', 'agg_func']}
|
||||
data={tableData}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.visualize.bind(this)}
|
||||
bsStyle="primary"
|
||||
disabled={(this.state.hints.length > 0)}
|
||||
>
|
||||
Visualize
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
return modal;
|
||||
}
|
||||
}
|
||||
VisualizeModal.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
show: React.PropTypes.bool,
|
||||
onHide: React.PropTypes.func,
|
||||
};
|
||||
VisualizeModal.defaultProps = {
|
||||
show: false,
|
||||
onHide: () => {},
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VisualizeModal);
|
||||