Compare commits

..

49 Commits
0.8.5 ... 0.8.6

Author SHA1 Message Date
Maxime Beauchemin
c5dead4791 v0.8.6 2016-04-07 14:26:28 -07:00
Maxime Beauchemin
d933a21216 Prettyfying an image in the docs 2016-04-07 14:26:28 -07:00
michellethomas
59169bfc96 Merge pull request #212 from airbnb/big_number_total
Adding a big number total viz type that is not a timeseries metric
2016-04-07 14:22:12 -07:00
Maxime Beauchemin
bcca840f01 Adding from __future__ imports (#288)
* Adding from __future__ imports

* fixes

* Fixing doctests

* Removing unused ColorFactory (in js now)

* linting
2016-04-07 08:39:08 -07:00
mherr
90a3b9f2c4 Update INTHEWILD.md
added #GfKDataLab as a Caravel-User
2016-04-06 23:56:19 -07:00
Maxime Beauchemin
a37e431150 Adding _images to .gitignore 2016-04-06 23:32:35 -07:00
Maxime Beauchemin
bf38c714a5 Adding missing images 2016-04-06 21:10:41 -07:00
Julian Nadeau
6b0b03e009 Fix localhost link in installation docs 2016-04-06 20:13:04 -07:00
Maxime Beauchemin
8556b098f9 Enable Time Grain Option for Redshift 2016-04-06 20:12:24 -07:00
Julian Nadeau
d122b37f5d Add python-pip to the install docs 2016-04-06 18:11:24 -07:00
Maxime Beauchemin
1756c27930 Adding upgrade instructions to docs 2016-04-06 18:02:40 -07:00
Aaron Critchley
7867267608 SqlAlchemy -> SQLAlchemy in README.md
* SqlAlchemy -> SQLAlchemy in README.md

Source: http://www.sqlalchemy.org/

* SqlAlchemy -> SQLAlchemy in tutorial.rst
2016-04-06 17:54:41 -07:00
Maxime Beauchemin
92d588694b Improving the Installation docs 2016-04-06 08:46:32 -07:00
Maxime Beauchemin
d10eaeccc9 Adding a Gallery to the docs 2016-04-06 08:46:21 -07:00
greens231
c2bb49fec5 Fix 4e6a06bad7a8_init.py migration script to work with Postgres
* Update 4e6a06bad7a8_init.py

* removed comments
2016-04-06 08:24:52 -07:00
Maxime Beauchemin
062f2b81cf Datasource dropdown in Explore view 2016-04-06 08:23:27 -07:00
andrewhn
65e72d0d07 Csv download improvements
* name + extension for generated csv and json files

* write csv index where data is meaningful
2016-04-06 08:22:49 -07:00
Maxime Beauchemin
345727635e Adding y_axis_format to DistributionBarViz 2016-04-06 08:20:24 -07:00
Maxime Beauchemin
c2baa53b06 bugfix datatables move to new package 2016-04-05 21:40:24 -07:00
Maxime Beauchemin
31758827ae + button on Slice list view redirects to Table view with alert 2016-04-05 13:33:02 -07:00
Maxime Beauchemin
81de51bf6f Minor tweaks 2016-04-04 21:39:33 -07:00
andrewhn
0d1f27dbc1 add postgres grains 2016-04-04 20:56:10 -07:00
Maxime Beauchemin
c7282882d5 Fixing the pagination display on welcome 2016-04-04 20:47:12 -07:00
Maxime Beauchemin
f9d04e8a72 Fixed refresh_datasource redirect 2016-04-04 20:43:06 -07:00
Maxime Beauchemin
bf2e804331 Removed trailing coma in Database.extra default 2016-04-04 17:42:31 -07:00
Maxime Beauchemin
c349b0a1c1 Fixed link url in docs 2016-04-04 17:14:08 -07:00
Maxime Beauchemin
4d640b5a3d [fix] panel overflowing on welcome page 2016-04-04 16:56:10 -07:00
Maxime Beauchemin
380c3f0c75 Using boostrap panels for form fieldsets in explore view 2016-04-04 16:14:55 -07:00
andrewhn
e3e8202c98 clear element before redrawing sankey 2016-04-04 16:13:54 -07:00
Maxime Beauchemin
889844407f Adding extra options to deeper configure sqlalchemy 2016-04-04 16:13:08 -07:00
Maxime Beauchemin
f1830c36cf A better welcome page 2016-04-04 16:12:28 -07:00
Chris Williams
92f73b67ca Move window.alert() calls to bootstrap modals. Also log errors to console.
* Move window.alert() calls to bootstrap modals. Also log errors that occur to console.

* move misc modal to basic template so it's available on all pages.
2016-04-04 16:11:23 -07:00
skje
9c1af66ba4 Fix ignored SQL where clauses 2016-04-04 16:03:21 -07:00
Maxime Beauchemin
2b31ab498b [hotfix] fixing json endpoint 2016-04-04 15:20:10 -07:00
Maxime Beauchemin
034fd077e1 Doc formating fix 2016-04-04 10:36:51 -07:00
Maxime Beauchemin
ca4443247e Prettyfying the Caravel on README 2016-04-03 20:08:36 -07:00
Maxime Beauchemin
6f96252e45 A logo on the navbar 2016-04-03 20:03:27 -07:00
Maxime Beauchemin
0b93fd373d [hotfix] hashing unicode in py3 2016-04-03 14:04:53 -07:00
Jiayu Liu
c3789d53b4 Removing duplicate get_table in fetch_metadata 2016-04-03 07:40:58 -07:00
Maxime Beauchemin
aec3c0b358 Fixing bug when datasource has been deleted 2016-04-03 07:37:18 -07:00
Karel Vervaeke
ef45c20558 Hash cache keys to avoid too keys being too long. Resolves #240 2016-04-03 07:33:43 -07:00
Maxime Beauchemin
10ab678fc6 Finishing up the tutorial 2016-04-02 23:34:47 -07:00
Maxime Beauchemin
87fb40ae26 Bumping version of npm up 2016-04-02 23:24:36 -07:00
Maxime Beauchemin
d245fb91b5 travis tweaks 2016-04-02 23:20:05 -07:00
Maxime Beauchemin
c60032acce Pillow with a capital P 2016-04-02 23:17:27 -07:00
Maxime Beauchemin
d2f51900f1 Adding a tutorial 2016-04-02 23:11:52 -07:00
Maxime Beauchemin
93405dc23a Clarify SQLALCHEMY_DATABASE_URI in the docs 2016-04-01 16:41:02 -07:00
Maxime Beauchemin
dafdb51f35 Adding Caravel Docker link to README 2016-04-01 16:16:21 -07:00
Maxime Beauchemin
2d0fdf7e59 Adding a CHANGELOG.md 2016-04-01 13:55:04 -07:00
78 changed files with 979 additions and 306 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ babel
.coverage
_build
_static
_images
caravel/bin/caravelc
env_py3
.eggs

View File

@@ -6,13 +6,13 @@ cache:
directories:
- $HOME/.wheelhouse/
before_install:
- rm -rf ~/.npm
- mkdir ~/.npm
- npm install -g npm@'>=2.7.1'
install:
- pip wheel -w $HOME/.wheelhouse -f $HOME/.wheelhouse .
- pip install --find-links=$HOME/.wheelhouse --no-index .
- pip install --find-links=$HOME/.wheelhouse --no-index -r dev-reqs.txt
- cd caravel/assets
- npm --version
- npm install
- npm run lint
- npm run prod

185
CHANGELOG.md Normal file
View File

@@ -0,0 +1,185 @@
## Change Log
### 0.8.5 (2016/04/01 20:30 +00:00)
- [#234](https://github.com/airbnb/caravel/pull/234) Pin pandas, remove numpy (@mistercrunch)
- [#229](https://github.com/airbnb/caravel/pull/229) Remove "requirements.txt" mention from README (@jmcomets)
- [#225](https://github.com/airbnb/caravel/pull/225) remove power units from sankey diagram (@williaster)
- [#219](https://github.com/airbnb/caravel/pull/219) Add 'Percent of previous' to sunburst vis. Appease npm warnings for data tables and d3.layout.cloud (@williaster)
- [#224](https://github.com/airbnb/caravel/pull/224) Fixing minor typos in the readme (@cyrusstoller)
- [#214](https://github.com/airbnb/caravel/pull/214) Fix an installation bug. (@kim-pham-airbnb)
- [#218](https://github.com/airbnb/caravel/pull/218) Redirecting URL from previous names to caravel (@mistercrunch)
- [#223](https://github.com/airbnb/caravel/pull/223) Fixed typo in README (@thebucknerlife)
- [#222](https://github.com/airbnb/caravel/pull/222) remove duplicate Druid.io section in README.md (@brchristian)
- [#213](https://github.com/airbnb/caravel/pull/213) Fix a bug when loading DruidDatasource. (@kim-pham-airbnb)
- [#204](https://github.com/airbnb/caravel/pull/204) Fixing the order and coverage report for the unit tests (@mistercrunch)
- [#209](https://github.com/airbnb/caravel/pull/209) Fresh screenshots (@mistercrunch)
- [#206](https://github.com/airbnb/caravel/pull/206) Caravel (@mistercrunch)
- [#205](https://github.com/airbnb/caravel/pull/205) fix sunburst error. add `less` to package.json (@williaster)
- [#203](https://github.com/airbnb/caravel/pull/203) Fixing mysql install (@mistercrunch)
- [#202](https://github.com/airbnb/caravel/pull/202) Using setup.py nosetests to run tests (@mistercrunch)
- [#199](https://github.com/airbnb/caravel/pull/199) Fix a few minor bugs (@mistercrunch)
- [#200](https://github.com/airbnb/caravel/pull/200) Add a sankey example (@mistercrunch)
- [#192](https://github.com/airbnb/caravel/pull/192) Fix Druid metadata refresh. (@kim-pham-airbnb)
- [#198](https://github.com/airbnb/caravel/pull/198) A welcome page (@mistercrunch)
- [#197](https://github.com/airbnb/caravel/pull/197) Adding a DRUID_IS_ACTIVE flag and changing nav bar (@NiharikaRay)
- [#196](https://github.com/airbnb/caravel/pull/196) Fixing issues around fk nullable=False on audit fields (@mistercrunch)
### 0.8.4 (2016/03/24 05:26 +00:00)
- [#193](https://github.com/airbnb/caravel/pull/193) Adding favorites for Slices and Dashboards (@mistercrunch)
### 0.8.2 (2016/03/23 20:43 +00:00)
- [#188](https://github.com/airbnb/caravel/pull/188) Introducing a caching layer! (@mistercrunch)
### 0.8.1 (2016/03/21 23:41 +00:00)
- [#191](https://github.com/airbnb/caravel/pull/191) Add week ending and week start to grain (@airbnb)
- [#190](https://github.com/airbnb/caravel/pull/190) Cranking up version numbers (@mistercrunch)
- [#184](https://github.com/airbnb/caravel/pull/184) sunburst improvements (@williaster)
- [#186](https://github.com/airbnb/caravel/pull/186) Adding docstrings ! (@mistercrunch)
- [#181](https://github.com/airbnb/caravel/pull/181) Dynamic time granularity on any datetime column (@mistercrunch)
- [#182](https://github.com/airbnb/caravel/pull/182) more css fixes (@williaster)
- [#178](https://github.com/airbnb/caravel/pull/178) Allowing all extra fields in AuditMixin to be nullable (@mistercrunch)
- [#175](https://github.com/airbnb/caravel/pull/175) refactor dashboard chart html, make several css improvements. (@williaster)
### 0.8.0 (2016/03/11 03:33 +00:00)
- [#172](https://github.com/airbnb/caravel/pull/172) Fixing the python and js packaging (@mistercrunch)
- [#171](https://github.com/airbnb/caravel/pull/171) Fixing multiple refresh bug in filter_box (@mistercrunch)
- [#169](https://github.com/airbnb/caravel/pull/169) Fixing the look of select2 components (@mistercrunch)
- [#168](https://github.com/airbnb/caravel/pull/168) Getting travis to build the npm related stuff (@mistercrunch)
- [#166](https://github.com/airbnb/caravel/pull/166) make css theme customization easier by using less for bootstrap themes (@williaster)
- [#163](https://github.com/airbnb/caravel/pull/163) Shipping with CSS templates out of the box (@mistercrunch)
- [#164](https://github.com/airbnb/caravel/pull/164) Improving the docs (@mistercrunch)
- [#165](https://github.com/airbnb/caravel/pull/165) Fixing window resize for explore and standalone (@mistercrunch)
- [#161](https://github.com/airbnb/caravel/pull/161) Add linting to package.json, do all of the linting. (@williaster)
- [#160](https://github.com/airbnb/caravel/pull/160) Fixing the dashed line when using time compare (@mistercrunch)
- [#159](https://github.com/airbnb/caravel/pull/159) Fixing the standalone mode (@mistercrunch)
- [#158](https://github.com/airbnb/caravel/pull/158) Refactor (@mistercrunch)
- [#154](https://github.com/airbnb/caravel/pull/154) Digging into leap year bug and improvming tests (@mistercrunch)
- [#157](https://github.com/airbnb/caravel/pull/157) add button to auto-copy short URLs in /explore page (@williaster)
- [#149](https://github.com/airbnb/caravel/pull/149) Allowing to make certain widgets immune to filter (@mistercrunch)
- [#151](https://github.com/airbnb/caravel/pull/151) Linting (@mistercrunch)
- [#153](https://github.com/airbnb/caravel/pull/153) Improve README (@tay)
- [#139](https://github.com/airbnb/caravel/pull/139) NPMification & Reactification (@williaster, @mistercrunch)
- [#147](https://github.com/airbnb/caravel/pull/147) Tackling Featured Datasets (@mistercrunch)
- [#148](https://github.com/airbnb/caravel/pull/148) Fix typo (@tay)
- [#145](https://github.com/airbnb/caravel/pull/145) Moving files around ... yay! (@mistercrunch)
- [#142](https://github.com/airbnb/caravel/pull/142) A few cosmetic fixes (nvd3 tooltips, buttons, tables) (@mistercrunch)
- [#141](https://github.com/airbnb/caravel/pull/141) A simple base template for npm (@mistercrunch)
- [#140](https://github.com/airbnb/caravel/pull/140) use the latest segment to extract metadata (@dayzzz)
- [#136](https://github.com/airbnb/caravel/pull/136) Improved the bar char to allow for dimensional breakdowns (@mistercrunch)
- [#134](https://github.com/airbnb/caravel/pull/134) Fixing the roles auto maintenance (@mistercrunch)
- [#132](https://github.com/airbnb/caravel/pull/132) [nvd3] fixing the legend toggle bug (@mistercrunch)
- [#131](https://github.com/airbnb/caravel/pull/131) More tests using doctests! (@mistercrunch)
- [#130](https://github.com/airbnb/caravel/pull/130) Logging more (@mistercrunch)
- [#129](https://github.com/airbnb/caravel/pull/129) Renaming Classes related to Druid (@mistercrunch)
- [#127](https://github.com/airbnb/caravel/pull/127) SQL editor, eventually will be tied to a flow to create views (@mistercrunch)
- [#128](https://github.com/airbnb/caravel/pull/128) Allowing definition of css templates (@mistercrunch)
- [#126](https://github.com/airbnb/caravel/pull/126) New viz: Heatmap! (@mistercrunch)
- [#125](https://github.com/airbnb/caravel/pull/125) Consistent colors rendered client side (@mistercrunch)
- [#124](https://github.com/airbnb/caravel/pull/124) A more cohesive color strategy (@mistercrunch)
### 0.7.0 (2016/01/23 15:16 +00:00)
- [#123](https://github.com/airbnb/caravel/pull/123) Adding a color factory (@mistercrunch)
- [#122](https://github.com/airbnb/caravel/pull/122) Adding Parallel coordinates viz (@mistercrunch)
- [#121](https://github.com/airbnb/caravel/pull/121) Iframe (@mistercrunch)
- [#120](https://github.com/airbnb/caravel/pull/120) Slice information can be displayed in dashboard (@mistercrunch)
- [#117](https://github.com/airbnb/caravel/pull/117) Doing some refactoring (@mistercrunch)
- [#115](https://github.com/airbnb/caravel/pull/115) Providing options for Y axis number formating (@mistercrunch)
- [#112](https://github.com/airbnb/caravel/pull/112) Adding an URL shortner (@mistercrunch)
- [#113](https://github.com/airbnb/caravel/pull/113) Prettier checkboxes (@mistercrunch)
- [#111](https://github.com/airbnb/caravel/pull/111) Loading another example amazing dash (@mistercrunch)
- [#109](https://github.com/airbnb/caravel/pull/109) Getting browser history to work on the explore view (@mistercrunch)
- [#108](https://github.com/airbnb/caravel/pull/108) pulling to the front on hover (@BradBaker)
- [#104](https://github.com/airbnb/caravel/pull/104) simplifying tooltip code (@BradBaker)
- [#105](https://github.com/airbnb/caravel/pull/105) adding stagger for all charts that have a date axis (@BradBaker)
- [#102](https://github.com/airbnb/caravel/pull/102) Fix for 2-axis charts where it shrinks them a little bit (@bradmbaker, @BradBaker)
- [#101](https://github.com/airbnb/caravel/pull/101) Add a Gitter chat badge to README.md (@gitter-badger)
- [#100](https://github.com/airbnb/caravel/pull/100) Update tooltips with new classes (@bradmbaker)
- [#99](https://github.com/airbnb/caravel/pull/99) Time resampling as in Pandas (@mistercrunch)
- [#98](https://github.com/airbnb/caravel/pull/98) Change Scaling to Operate on SVG instead of Div (@bradmbaker)
- [#96](https://github.com/airbnb/caravel/pull/96) Adding a filter box widget (@mistercrunch)
- [#95](https://github.com/airbnb/caravel/pull/95) Working on docs (@mistercrunch)
- [#94](https://github.com/airbnb/caravel/pull/94) Massive js refactor + Dashboard filters (@mistercrunch)
- [#93](https://github.com/airbnb/caravel/pull/93) Controller (@mistercrunch)
- [#92](https://github.com/airbnb/caravel/pull/92) Allowing not to group by on table view (@mistercrunch)
- [#91](https://github.com/airbnb/caravel/pull/91) Exports (@mistercrunch)
- [#90](https://github.com/airbnb/caravel/pull/90) A basic squeleton for the docs (@mistercrunch)
- [#89](https://github.com/airbnb/caravel/pull/89) Featured datasets (@michellethomas)
- [#87](https://github.com/airbnb/caravel/pull/87) fixing a few bugs with tool tip overflow (@BradBaker)
- [#88](https://github.com/airbnb/caravel/pull/88) World Map viz with bubbles (@mistercrunch)
- [#86](https://github.com/airbnb/caravel/pull/86) adjusting date formats (@BradBaker)
- [#85](https://github.com/airbnb/caravel/pull/85) Adding sankey diagrams (@mistercrunch)
- [#84](https://github.com/airbnb/caravel/pull/84) Adding directed force layout viz (@mistercrunch)
- [#83](https://github.com/airbnb/caravel/pull/83) Big JS refactor (@mistercrunch)
- [#82](https://github.com/airbnb/caravel/pull/82) letting tooltips in the dashboard overflow (@BradBaker)
- [#81](https://github.com/airbnb/caravel/pull/81) Slightly better layout for explore page (@mistercrunch)
- [#80](https://github.com/airbnb/caravel/pull/80) Checkboxes everywhere (@mistercrunch)
- [#79](https://github.com/airbnb/caravel/pull/79) Cleanup around multiple select fields (@mistercrunch)
### 0.6.0 (2015/12/11 01:17 +00:00)
- [#77](https://github.com/airbnb/caravel/pull/77) Better tooltips and more ways to integrate them easily (@mistercrunch)
- [#76](https://github.com/airbnb/caravel/pull/76) Introducing form overrides for label and tooltips (@mistercrunch)
- [#75](https://github.com/airbnb/caravel/pull/75) New viz: sunbursts (@mistercrunch)
- [#74](https://github.com/airbnb/caravel/pull/74) Introducing fieldsets (@mistercrunch)
- [#73](https://github.com/airbnb/caravel/pull/73) Airflowlike theme (@mistercrunch)
- [#72](https://github.com/airbnb/caravel/pull/72) Logging slice and dash views (@mistercrunch)
- [#70](https://github.com/airbnb/caravel/pull/70) Adding url slug support for dashboard model (@mistercrunch)
- [#71](https://github.com/airbnb/caravel/pull/71) Add option to show minmax on x axis (@mistercrunch)
- [#69](https://github.com/airbnb/caravel/pull/69) Allowing for [Save AS] and [Overwrite] (@mistercrunch)
- [#68](https://github.com/airbnb/caravel/pull/68) Adding cumsum to rolling functions (@mistercrunch)
- [#67](https://github.com/airbnb/caravel/pull/67) Fix debug mode calls get_json twice (@mistercrunch)
- [#66](https://github.com/airbnb/caravel/pull/66) Adding a PivotTableViz (@mistercrunch)
- [#65](https://github.com/airbnb/caravel/pull/65) Adding custom HAVING clause (@mistercrunch)
- [#64](https://github.com/airbnb/caravel/pull/64) Preserving the ordering in selectmultiple (@mistercrunch)
- [#63](https://github.com/airbnb/caravel/pull/63) Encrypting the passwords out of connection strings (@mistercrunch)
- [#61](https://github.com/airbnb/caravel/pull/61) BetterBooleanField to fix html omitting non-checked <input> (@patrickleotardif)
- [#60](https://github.com/airbnb/caravel/pull/60) Fix Markup Widget bug (@NiharikaRay)
- [#59](https://github.com/airbnb/caravel/pull/59) Adding y-axis format option (@patrickleotardif)
- [#58](https://github.com/airbnb/caravel/pull/58) Setting min_periods to 1 for rolling windows (@mistercrunch)
- [#56](https://github.com/airbnb/caravel/pull/56) adding sort order of the slices on changed_on field (@mistercrunch)
### 0.5.2 (2015/10/24 01:06 +00:00)
- [#53](https://github.com/airbnb/caravel/pull/53) Py3 (@mistercrunch)
- [#51](https://github.com/airbnb/caravel/pull/51) Adding timezone offset as a datasource param (@mistercrunch)
- [#52](https://github.com/airbnb/caravel/pull/52) Speed up travis builds with wheels (@mistercrunch)
- [#48](https://github.com/airbnb/caravel/pull/48) Allowing to specify the gunicorn timeout in CLI and config (@mistercrunch)
### 0.5.0 (2015/10/13 01:09 +00:00)
- [#46](https://github.com/airbnb/caravel/pull/46) Allowing to change the "Time Column" on SqlA (@mistercrunch)
- [#45](https://github.com/airbnb/caravel/pull/45) Bootstrapping widgets from javascript initializer. (@akuhn)
- [#43](https://github.com/airbnb/caravel/pull/43) Supporting arbitrary expressions (@mistercrunch)
- [#42](https://github.com/airbnb/caravel/pull/42) Adding ability to style a dashboard with CSS (@mistercrunch)
- [#41](https://github.com/airbnb/caravel/pull/41) Cleaning up the static folder (@mistercrunch)
- [#35](https://github.com/airbnb/caravel/pull/35) A first draft on default security roles (@mistercrunch)
- [#40](https://github.com/airbnb/caravel/pull/40) Introducing time comparison (@mistercrunch)
- [#39](https://github.com/airbnb/caravel/pull/39) Adding interpolation choice for line charts (@mistercrunch)
- [#38](https://github.com/airbnb/caravel/pull/38) Extract css rules and scripts into separate files. (@akuhn)
- [#37](https://github.com/airbnb/caravel/pull/37) Viz type (@mistercrunch)
- [#36](https://github.com/airbnb/caravel/pull/36) Extract widget javascript to separate files. (@akuhn)
- [#34](https://github.com/airbnb/caravel/pull/34) Ripping out Highcharts. (@mistercrunch)
### 0.4.0 (2015/09/27 04:39 +00:00)
- [#33](https://github.com/airbnb/caravel/pull/33) Adding nvd3 support (@mistercrunch)
- [#32](https://github.com/airbnb/caravel/pull/32) Adding a foundation for unit tests (@mistercrunch)
- [#31](https://github.com/airbnb/caravel/pull/31) Adding a button to test connections (@mistercrunch)
- [#30](https://github.com/airbnb/caravel/pull/30) Word cloud widget! (@mistercrunch)
- [#29](https://github.com/airbnb/caravel/pull/29) Adding support for markup (html/markdown) widgets (@mistercrunch)
- [#28](https://github.com/airbnb/caravel/pull/28) Fix default Sqlite path. (@noddi)
- [#27](https://github.com/airbnb/caravel/pull/27) More refactor and bugfixes (@mistercrunch)
- [#26](https://github.com/airbnb/caravel/pull/26) Bugfix (@mistercrunch)
- [#25](https://github.com/airbnb/caravel/pull/25) Adding basic dashboarding support! (@mistercrunch)
- [#23](https://github.com/airbnb/caravel/pull/23) Custom WHERE clause for tables (not druid) + error handling refactor (@mistercrunch)
- [#22](https://github.com/airbnb/caravel/pull/22) Form factory refactor (@mistercrunch)
- [#20](https://github.com/airbnb/caravel/pull/20) add tzinfo config, useful when start druid without utc timezone (@wbchn)
### 0.2.1 (2015/09/05 22:08 +00:00)
- [#19](https://github.com/airbnb/caravel/pull/19) Preparing pypi package (@mistercrunch)
### 0.2.0 (2015/09/05 20:43 +00:00)
- [#16](https://github.com/airbnb/caravel/pull/16) Adding Bubble charts (@mistercrunch)
- [#13](https://github.com/airbnb/caravel/pull/13) Now supporting SQL Multiple database (@mistercrunch)
- [#12](https://github.com/airbnb/caravel/pull/12) Cosmetricks (@mistercrunch)
- [#11](https://github.com/airbnb/caravel/pull/11) Fixing the ways metrics are autogenerated (@mistercrunch)
- [#10](https://github.com/airbnb/caravel/pull/10) Now enabling multi-cluster, connection info managed in UI (@mistercrunch)
- [#9](https://github.com/airbnb/caravel/pull/9) Multi delete action on datasources (@mistercrunch)
- [#8](https://github.com/airbnb/caravel/pull/8) Preventing bad json from creating problems (@mistercrunch)
- [#3](https://github.com/airbnb/caravel/pull/3) Implementing my own highcharts wrapper (@mistercrunch)

View File

@@ -4,7 +4,7 @@ to add your organization and/or project to this document!
Organizations
----------
- [Airbnb](https://github.com/airbnb)
- your company name here!
- [GfK Data Lab] (http://datalab.gfk.com)
Projects
----------

View File

@@ -1,6 +1,6 @@
Caravel
=========
<img src="http://i.imgur.com/H0Kyvyi.jpg" alt="Caravel" width="500"/>
<img src="http://i.imgur.com/H0Kyvyi.jpg" style="border-radius: 20px; box-shadow:5px 5px 5px gray;" alt="Caravel" width="500"/>
[![Build Status](https://travis-ci.org/airbnb/caravel.svg?branch=master)](https://travis-ci.org/airbnb/caravel)
[![PyPI version](https://badge.fury.io/py/caravel.svg)](https://badge.fury.io/py/caravel)
@@ -31,7 +31,8 @@ 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 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
@@ -39,9 +40,9 @@ Caravel provides:
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
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
@@ -50,7 +51,7 @@ 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
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).
@@ -84,10 +85,11 @@ More screenshots
![img](http://i.imgur.com/ahHoCuS.png)
Related Links
Resources
-------------
* [Caravel Google Group](https://groups.google.com/forum/#!forum/airbnb_caravel)
* [Gitter (live chat) Channel](https://gitter.im/airbnb/caravel)
* [Docker image](https://hub.docker.com/r/kochalex/caravel/) (community contributed)
Tip of the Hat

View File

@@ -1,4 +1,8 @@
"""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

View File

@@ -1,3 +1,8 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
error = (
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+

View File

@@ -63,7 +63,7 @@
"newline-after-var": [0],
"no-bitwise": [0],
"no-cond-assign": [2],
"no-console": [2],
"no-console": [1, { allow: ["warn", "error"] }],
"no-const-assign": [2],
"no-constant-condition": [2],
"no-control-regex": [2],

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -2,6 +2,7 @@ var $ = window.$ = require('jquery');
var jQuery = window.jQuery = $;
var px = require('./modules/caravel.js');
var d3 = require('d3');
var showModal = require('./modules/utils.js').showModal;
require('bootstrap');
var ace = require('brace');
@@ -21,12 +22,17 @@ var Dashboard = function (dashboardData) {
var sliceObjects = [],
dash = this;
dashboard.slices.forEach(function (data) {
var slice = px.Slice(data, dash);
$("#slice_" + data.slice_id).find('a.refresh').click(function () {
slice.render(true);
});
sliceObjects.push(slice);
slice.render();
if (data.error) {
var html = '<div class="alert alert-danger">' + data.error + '</div>';
$("#slice_" + data.slice_id).find('.token').html(html);
} else {
var slice = px.Slice(data, dash);
$("#slice_" + data.slice_id).find('a.refresh').click(function () {
slice.render(true);
});
sliceObjects.push(slice);
slice.render();
}
});
this.slices = sliceObjects;
},
@@ -142,10 +148,17 @@ var Dashboard = function (dashboardData) {
data: JSON.stringify(data)
},
success: function () {
alert("Saved!");
showModal({
title: "Success",
body: "This dashboard was saved successfully."
});
},
error: function () {
alert("Error :(");
error: function (error) {
showModal({
title: "Error",
body: "Sorry, there was an error saving this dashboard:<br />" + error
});
console.warn("Save dashboard error", error);
}
});
});
@@ -173,7 +186,10 @@ var Dashboard = function (dashboardData) {
});
$('#filters').click(function () {
alert(dashboard.readFilters());
showModal({
title: "<span class='fa fa-info-circle'></span> Current Global Filters",
body: "The following global filters are currently applied:<br/>" + dashboard.readFilters()
});
});
$('#refresh_dash').click(function () {
dashboard.slices.forEach(function (slice) {

View File

@@ -6,6 +6,7 @@
var $ = window.$ = require('jquery');
var jQuery = window.jQuery = $;
var px = require('./modules/caravel.js');
var showModal = require('./modules/utils.js').showModal;
require('jquery-ui');
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
@@ -53,7 +54,7 @@ function prepForm() {
});
}
function druidify(force, pushState) {
function query(force, pushState) {
if (force === undefined) {
force = false;
}
@@ -89,9 +90,9 @@ function initExploreView() {
if (parent.hasClass("collapsed")) {
if (animation) {
parent.find(".fieldset_content").slideDown();
parent.find(".panel-body").slideDown();
} else {
parent.find(".fieldset_content").show();
parent.find(".panel-body").show();
}
parent.removeClass("collapsed");
parent.find("span.collapser").text("[-]");
@@ -103,9 +104,9 @@ function initExploreView() {
}
} else { // not collapsed
if (animation) {
parent.find(".fieldset_content").slideUp();
parent.find(".panel-body").slideUp();
} else {
parent.find(".fieldset_content").hide();
parent.find(".panel-body").hide();
}
parent.addClass("collapsed");
@@ -121,8 +122,9 @@ function initExploreView() {
px.initFavStars();
$('legend').click(function () {
$('form .panel-heading').click(function () {
toggle_fieldset($(this), true);
$(this).css('cursor', 'pointer');
});
function copyURLToClipboard(url) {
@@ -181,15 +183,25 @@ function initExploreView() {
$shortner.popover('destroy');
}
},
error: function () {
alert("Error :(");
error: function (error) {
showModal({
title: "Error",
body: "Sorry, an error occurred during this operation:<br/>" + error
});
console.warn("Short URL error", error);
}
});
});
$("#viz_type").change(function () {
$("#query").submit();
});
$("#datasource_id").change(function () {
var url = $(this).find('option:selected').attr('url');
window.location = url;
});
var collapsed_fieldsets = get_collapsed_fieldsets();
for (var i = 0; i < collapsed_fieldsets.length; i++) {
toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false);
@@ -260,8 +272,8 @@ function initExploreView() {
}
});
$(".druidify").click(function () {
druidify(true);
$(".query").click(function () {
query(true);
});
function create_choices(term, data) {
@@ -325,7 +337,7 @@ $(document).ready(function () {
$('.slice').data('slice', slice);
// call vis render method, which issues ajax
druidify(false, false);
query(false, false);
// make checkbox inputs display as toggles
$(':checkbox')

View File

@@ -2,14 +2,13 @@ var $ = require('jquery');
var jQuery = $;
var d3 = require('d3');
require('../../stylesheets/caravel.css');
// vis sources
var sourceMap = {
area: 'nvd3_vis.js',
bar: 'nvd3_vis.js',
bubble: 'nvd3_vis.js',
big_number: 'big_number.js',
big_number_total: 'big_number.js',
compare: 'nvd3_vis.js',
dist_bar: 'nvd3_vis.js',
directed_force: 'directed_force.js',

View File

@@ -1,3 +1,4 @@
var $ = require('jquery');
var d3 = require('d3');
/*
@@ -50,6 +51,30 @@ function wrapSvgText(text, width, adjustedY) {
});
}
/**
* Sets the body and title content of a modal, and shows it. Assumes HTML for modal exists and that
* it handles closing (i.e., works with bootstrap)
*
* @param {object} options object of the form
* {
* title: {string},
* body: {string},
* modalSelector: {string, default: '.misc-modal' },
* titleSelector: {string, default: '.misc-modal .modal-title' },
* bodySelector: {string, default: '.misc-modal .modal-body' },
* }
*/
function showModal(options) {
options.modalSelector = options.modalSelector || ".misc-modal";
options.titleSelector = options.titleSelector || ".misc-modal .modal-title";
options.bodySelector = options.bodySelector || ".misc-modal .modal-body";
$(options.titleSelector).html(options.title || "");
$(options.bodySelector).html(options.body || "");
$(options.modalSelector).modal("show");
}
module.exports = {
wrapSvgText: wrapSvgText
wrapSvgText: wrapSvgText,
showModal: showModal
};

View File

@@ -1,7 +1,10 @@
var $ = window.$ = require('jquery');
var jQuery = window.jQuery = $;
var showModal = require('./modules/utils.js').showModal;
require('select2');
require('datatables');
require('datatables.net-bs');
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
require('bootstrap');
var ace = require('brace');
@@ -41,7 +44,10 @@ $(document).ready(function () {
$("#dbtable").on("change", showTableMetadata);
showTableMetadata();
$("#create_view").click(function () {
alert("Not implemented");
showModal({
title: "Error",
body: "Sorry, this feature is not yet implemented"
});
});
$(".sqlcontent").show();

View File

@@ -1,15 +1,15 @@
var $ = window.$ = require('jquery');
var jQuery = window.jQuery = $;
require('../stylesheets/caravel.css');
require('../stylesheets/welcome.css');
require('bootstrap');
require('datatables');
require('datatables.net-bs');
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
require('../node_modules/cal-heatmap/cal-heatmap.css');
var CalHeatMap = require('cal-heatmap');
function modelViewTable(selector, modelEndpoint, ordering) {
function modelViewTable(selector, modelEndpoint) {
// Builds a dataTable from a flask appbuilder api endpoint
$.getJSON(modelEndpoint + '/api/read', function (data) {
var tableData = jQuery.map(data.result, function (el, i) {
@@ -21,13 +21,32 @@ function modelViewTable(selector, modelEndpoint, ordering) {
var cols = jQuery.map(data.list_columns, function (col, i) {
return { sTitle: data.label_columns[col] };
});
var panel = $(selector).parents('.panel');
panel.find("img.loading").remove();
$(selector).DataTable({
aaData: tableData,
aoColumns: cols,
bPaginate: false,
order: ordering,
searching: false
bPaginate: true,
pageLength: 10,
bLengthChange: false,
aaSorting: [],
searching: true,
bInfo: false
});
// Hack to move the searchbox in the right spot
var search = panel.find(".dataTables_filter input");
search.addClass('form-control').detach();
search.appendTo(panel.find(".search"));
panel.find('.dataTables_filter').remove();
// Hack to display the page navigator properly
panel.find('.col-sm-5').remove();
var nav = panel.find('.col-sm-7');
nav.removeClass('col-sm-7');
nav.addClass('col-sm-12');
$(selector).slideDown();
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
});
}

View File

@@ -51,7 +51,7 @@
"d3-tip": "^0.6.7",
"datamaps": "^0.4.4",
"datatables-bootstrap3-plugin": "^0.4.0",
"datatables.net": "^1.10.11",
"datatables.net-bs": "^1.10.11",
"exports-loader": "^0.6.3",
"font-awesome": "^4.5.0",
"gridster": "^0.5.6",

View File

@@ -116,25 +116,6 @@ span.title-block {
font-size: 20px;
}
fieldset.fs-style {
font-family: Verdana, Arial, sans-serif;
font-size: small;
font-weight: normal;
border: 1px solid #CCC;
background-color: #F4F4F4;
border-radius: 6px;
padding: 10px;
margin: 0px 0px 10px 0px;
}
legend.legend-style {
font-size: 14px;
padding: 0px 6px;
cursor: pointer;
margin: 0px;
color: #444;
background-color: transparent;
font-weight: bold;
}
.nvtooltip {
//position: relative !important;
z-index: 888;
@@ -142,10 +123,6 @@ legend.legend-style {
.nvtooltip table td{
font-size: 11px !important;
}
legend {
width: auto;
border-bottom: 0px;
}
.navbar {
-webkit-box-shadow: 0px 3px 3px #AAA;
-moz-box-shadow: 0px 3px 3px #AAA;

View File

@@ -1,21 +1,21 @@
.welcome .widget{
border-radius: 0;
border: 1px solid #ccc;
box-shadow: 2px 1px 5px -2px #aaa;
background-color: #fff;
}
.welcome .widget .header {
background-color: #f1f1f1;
text-align: center;
}
.welcome .widget>div {
padding: 3px;
overflow: auto;
max-height: 500px;
}
.table i {
padding-top: 6px;
}
img.loading {
width: 25px;
}
.welcome table {
display: none;
width: 100%;
}
input {
margin-left: 5px;
margin-top: 8px;
}
.panel-body {
overflow: auto;
}

View File

@@ -1,4 +1,5 @@
.big_number g.axis text {
.big_number g.axis text,
.big_number_total g.axis text {
font-size: 10px;
font-weight: normal;
color: gray;
@@ -8,18 +9,21 @@
font-weight: none;
}
.big_number text.big {
.big_number text.big,
.big_number_total text.big{
stroke: black;
text-anchor: middle;
fill: black;
}
.big_number g.tick line {
.big_number g.tick line,
.big_number_total g.tick line{
stroke-width: 1px;
stroke: grey;
}
.big_number .domain {
.big_number .domain,
.big_number_total .domain{
fill: none;
stroke: black;
stroke-width: 1;

View File

@@ -31,7 +31,12 @@ function bigNumberVis(slice) {
var data = json.data;
var compare_suffix = ' ' + json.compare_suffix;
var v_compare = null;
var v = data[data.length - 1][1];
var v = null;
if (data.length > 1) {
v = data[data.length - 1][1];
} else {
v = data[data.length - 1][0];
}
if (json.compare_lag > 0) {
var pos = data.length - (json.compare_lag + 1);
if (pos >= 0) {
@@ -97,6 +102,19 @@ function bigNumberVis(slice) {
.style('font-size', d3.min([height, width]) / 3.5)
.attr('fill', 'white');
//Printing big number subheader text
if (json.subheader !== null) {
g.append('text')
.attr('x', width / 2)
.attr('y', y + d3.min([height, width]) / 4.5)
.text(json.subheader)
.attr('id', 'subheader_text')
.style('font-size', d3.min([height, width]) / 16)
.style('text-anchor', 'middle')
.attr('fill', c)
.attr('stroke', c);
}
var c = scale_color(v_compare);
//Printing compare %

View File

@@ -1,7 +1,7 @@
var $ = window.$ = require('jquery');
var jQuery = window.jQuery = $;
require('datatables');
require('datatables.net-bs');
require('./pivot_table.css');
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');

View File

@@ -20,6 +20,7 @@ function sankeyVis(slice) {
var formatNumber = d3.format(",.0f");
div.selectAll("*").remove();
var svg = div.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)

View File

@@ -188,7 +188,7 @@ function sunburstVis(slice) {
arcs.selectAll("path")
.style("stroke-width", null)
.style("stroke", null)
.style("opacity", 0.3);
.style("opacity", 0.7);
// Then highlight only those that are an ancestor of the current segment.
arcs.selectAll("path")

View File

@@ -3,7 +3,7 @@ var jQuery = window.jQuery = $;
var d3 = require('d3');
require('./table.css');
require('datatables');
require('datatables.net-bs');
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
function tableVis(slice) {

View File

@@ -1,4 +1,8 @@
#!/usr/bin/env python
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from datetime import datetime
import logging

View File

@@ -4,6 +4,11 @@ All configuration in this file can be overridden by providing a local_config
in your PYTHONPATH as there is a ``from local_config import *``
at the end of this file.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
from flask_appbuilder.security.manager import AUTH_DB
from dateutil import tz

View File

@@ -1,4 +1,8 @@
"""Loads datasets, dashboards and slices in a new caravel instance"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import gzip
import json
@@ -529,6 +533,16 @@ def load_birth_names():
defaults,
viz_type="big_number", granularity="ds",
compare_lag="5", compare_suffix="over 5Y")),
Slice(
slice_name="Number of Girls",
viz_type='big_number_total',
datasource_type='table',
table=tbl,
params=get_slice_json(
defaults,
viz_type="big_number_total", granularity="ds",
flt_col_1='gender', flt_eq_1='girl',
subheader='total female participants')),
Slice(
slice_name="Genders",
viz_type='pie',

View File

@@ -1,4 +1,8 @@
"""This module contains data related to countries and is used for geo mapping"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
countries = [
{

View File

@@ -1,4 +1,8 @@
"""Contains the logic to create cohesive forms on the explore view"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from wtforms import (
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
@@ -500,6 +504,11 @@ class FormFactory(object):
"relative time period. Expects relative time delta "
"in natural language (example: 24 hours, 7 days, "
"56 weeks, 365 days")),
'subheader': TextField(
'Subheader',
description=(
"Description text that shows up below your Big "
"Number")),
}
@staticmethod

View File

@@ -28,10 +28,8 @@ def upgrade():
sa.Column('broker_port', sa.Integer(), nullable=True),
sa.Column('broker_endpoint', sa.String(length=256), nullable=True),
sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('cluster_name')
)
@@ -41,10 +39,8 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('dashboard_title', sa.String(length=500), nullable=True),
sa.Column('position_json', sa.Text(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('dbs',
@@ -53,10 +49,8 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('database_name', sa.String(length=250), nullable=True),
sa.Column('sqlalchemy_uri', sa.String(length=1024), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('database_name')
)
@@ -69,14 +63,10 @@ def upgrade():
sa.Column('is_hidden', sa.Boolean(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('default_endpoint', sa.Text(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('cluster_name', sa.String(length=250), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=False),
sa.Column('created_by_fk', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['cluster_name'], ['clusters.cluster_name'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
sa.Column('user_id', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('cluster_name', sa.String(length=250), sa.ForeignKey("clusters.cluster_name"), nullable=True),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('datasource_name')
)
@@ -87,12 +77,9 @@ def upgrade():
sa.Column('table_name', sa.String(length=250), nullable=True),
sa.Column('main_dttm_col', sa.String(length=250), nullable=True),
sa.Column('default_endpoint', sa.Text(), nullable=True),
sa.Column('database_id', sa.Integer(), nullable=False),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['database_id'], ['dbs.id'], ),
sa.Column('database_id', sa.Integer(), sa.ForeignKey("dbs.id"), nullable=False),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('table_name')
)
@@ -101,7 +88,7 @@ def upgrade():
sa.Column('changed_on', sa.DateTime(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('datasource_name', sa.String(length=250), nullable=True),
sa.Column('column_name', sa.String(length=256), nullable=True),
sa.Column('column_name', sa.String(length=256), sa.ForeignKey("datasources.datasource_name"), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('type', sa.String(length=32), nullable=True),
sa.Column('groupby', sa.Boolean(), nullable=True),
@@ -111,11 +98,8 @@ def upgrade():
sa.Column('min', sa.Boolean(), nullable=True),
sa.Column('filterable', sa.Boolean(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('metrics',
@@ -123,7 +107,7 @@ def upgrade():
sa.Column('metric_name', sa.String(length=512), nullable=True),
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
sa.Column('metric_type', sa.String(length=32), nullable=True),
sa.Column('datasource_name', sa.String(length=250), nullable=True),
sa.Column('datasource_name', sa.String(length=250), sa.ForeignKey("datasources.datasource_name"), nullable=True),
sa.Column('json', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
@@ -134,18 +118,14 @@ def upgrade():
sa.Column('changed_on', sa.DateTime(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('slice_name', sa.String(length=250), nullable=True),
sa.Column('druid_datasource_id', sa.Integer(), nullable=True),
sa.Column('table_id', sa.Integer(), nullable=True),
sa.Column('druid_datasource_id', sa.Integer(), sa.ForeignKey("datasources.id"), nullable=True),
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
sa.Column('datasource_type', sa.String(length=200), nullable=True),
sa.Column('datasource_name', sa.String(length=2000), nullable=True),
sa.Column('viz_type', sa.String(length=250), nullable=True),
sa.Column('params', sa.Text(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['druid_datasource_id'], ['datasources.id'], ),
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sql_metrics',
@@ -155,21 +135,18 @@ def upgrade():
sa.Column('metric_name', sa.String(length=512), nullable=True),
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
sa.Column('metric_type', sa.String(length=32), nullable=True),
sa.Column('table_id', sa.Integer(), nullable=True),
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
sa.Column('expression', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('table_columns',
sa.Column('created_on', sa.DateTime(), nullable=False),
sa.Column('changed_on', sa.DateTime(), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('table_id', sa.Integer(), nullable=True),
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
sa.Column('column_name', sa.String(length=256), nullable=True),
sa.Column('is_dttm', sa.Boolean(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
@@ -181,19 +158,14 @@ def upgrade():
sa.Column('min', sa.Boolean(), nullable=True),
sa.Column('filterable', sa.Boolean(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_by_fk', sa.Integer(), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('dashboard_slices',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('dashboard_id', sa.Integer(), nullable=True),
sa.Column('slice_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ),
sa.ForeignKeyConstraint(['slice_id'], ['slices.id'], ),
sa.Column('dashboard_id', sa.Integer(), sa.ForeignKey("dashboards.id"), nullable=True),
sa.Column('slice_id', sa.Integer(), sa.ForeignKey("slices.id"), nullable=True),
sa.PrimaryKeyConstraint('id')
)
### end Alembic commands ###

View File

@@ -0,0 +1,22 @@
"""Adding extra field to Database model
Revision ID: 867bf4f117f9
Revises: fee7b758c130
Create Date: 2016-04-03 15:23:20.280841
"""
# revision identifiers, used by Alembic.
revision = '867bf4f117f9'
down_revision = 'fee7b758c130'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('dbs', sa.Column('extra', sa.Text(), nullable=True))
def downgrade():
op.drop_column('dbs', 'extra')

View File

@@ -1,4 +1,8 @@
"""A collection of ORM sqlalchemy models for Caravel"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from copy import deepcopy, copy
from collections import namedtuple
@@ -9,6 +13,7 @@ import logging
from six import string_types
import sqlparse
import requests
import textwrap
from dateutil.parser import parse
from flask import flash, request, g
@@ -151,10 +156,8 @@ class Slice(Model, AuditMixinNullable):
@utils.memoized
def viz(self):
d = json.loads(self.params)
viz = viz_types[self.viz_type](
self.datasource,
form_data=d)
return viz
viz_class = viz_types[self.viz_type]
return viz_class(self.datasource, form_data=d)
@property
def description_markeddown(self):
@@ -166,7 +169,13 @@ class Slice(Model, AuditMixinNullable):
@property
def data(self):
d = self.viz.data
d = {}
self.token = ''
try:
d = self.viz.data
self.token = d.get('token')
except Exception as e:
d['error'] = str(e)
d['slice_id'] = self.id
return d
@@ -186,8 +195,8 @@ class Slice(Model, AuditMixinNullable):
slice_params['slice_name'] = self.slice_name
from werkzeug.urls import Href
href = Href(
"/caravel/explore/{self.datasource_type}/"
"{self.datasource_id}/".format(self=self))
"/caravel/explore/{obj.datasource_type}/"
"{obj.datasource_id}/".format(obj=self))
return href(slice_params)
@property
@@ -197,8 +206,8 @@ class Slice(Model, AuditMixinNullable):
@property
def slice_link(self):
url = self.slice_url
return '<a href="{url}">{self.slice_name}</a>'.format(
url=url, self=self)
return '<a href="{url}">{obj.slice_name}</a>'.format(
url=url, obj=self)
dashboard_slices = Table(
@@ -239,7 +248,7 @@ class Dashboard(Model, AuditMixinNullable):
return {}
def dashboard_link(self):
return '<a href="{self.url}">{self.dashboard_title}</a>'.format(self=self)
return '<a href="{obj.url}">{obj.dashboard_title}</a>'.format(obj=self)
@property
def json_data(self):
@@ -288,12 +297,20 @@ class Database(Model, AuditMixinNullable):
sqlalchemy_uri = Column(String(1024))
password = Column(EncryptedType(String(1024), config.get('SECRET_KEY')))
cache_timeout = Column(Integer)
extra = Column(Text, default=textwrap.dedent("""\
{
"metadata_params": {},
"engine_params": {}
}
"""))
def __repr__(self):
return self.database_name
def get_sqla_engine(self):
return create_engine(self.sqlalchemy_uri_decrypted)
extra = self.get_extra()
params = extra.get('engine_params', {})
return create_engine(self.sqlalchemy_uri_decrypted, **params)
def safe_sqlalchemy_uri(self):
return self.sqlalchemy_uri
@@ -324,7 +341,16 @@ class Database(Model, AuditMixinNullable):
Grain('week', 'DATE_SUB({col}, INTERVAL DAYOFWEEK({col}) - 1 DAY)'),
Grain('month', 'DATE_SUB({col}, INTERVAL DAYOFMONTH({col}) - 1 DAY)'),
),
'postgresql': (
Grain("Time Column", "{col}"),
Grain("hour", "DATE_TRUNC('hour', {col})"),
Grain("day", "DATE_TRUNC('day', {col})"),
Grain("week", "DATE_TRUNC('week', {col})"),
Grain("month", "DATE_TRUNC('month', {col})"),
Grain("year", "DATE_TRUNC('year', {col})"),
),
}
db_time_grains['redshift'] = db_time_grains['postgresql']
for db_type, grains in db_time_grains.items():
if self.sqlalchemy_uri.startswith(db_type):
return grains
@@ -332,8 +358,18 @@ class Database(Model, AuditMixinNullable):
def grains_dict(self):
return {grain.name: grain for grain in self.grains()}
def get_extra(self):
extra = {}
if self.extra:
try:
extra = json.loads(self.extra)
except Exception as e:
logging.error(e)
return extra
def get_table(self, table_name):
meta = MetaData()
extra = self.get_extra()
meta = MetaData(**extra.get('metadata_params', {}))
return Table(
table_name, meta,
autoload=True,
@@ -400,12 +436,12 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
@property
def perm(self):
return (
"[{self.database}].[{self.table_name}]"
"(id:{self.id})").format(self=self)
"[{obj.database}].[{obj.table_name}]"
"(id:{obj.id})").format(obj=self)
@property
def full_name(self):
return "[{self.database}].[{self.table_name}]".format(self=self)
return "[{obj.database}].[{obj.table_name}]".format(obj=self)
@property
def dttm_cols(self):
@@ -435,11 +471,16 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
def name(self):
return self.table_name
@property
def explore_url(self):
if self.default_endpoint:
return self.default_endpoint
else:
return "/caravel/explore/{obj.type}/{obj.id}/".format(obj=self)
@property
def table_link(self):
url = "/caravel/explore/{self.type}/{self.id}/".format(self=self)
return '<a href="{url}">{self.table_name}</a>'.format(
url=url, self=self)
return '<a href="{obj.explore_url}">{obj.table_name}</a>'.format(obj=self)
@property
def metrics_combo(self):
@@ -571,6 +612,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
having_clause_and += [text(extras['having'])]
if granularity:
qry = qry.where(and_(*(time_filter + where_clause_and)))
else:
qry = qry.where(and_(*where_clause_and))
qry = qry.having(and_(*having_clause_and))
if groupby:
qry = qry.order_by(desc(main_metric_expr))
@@ -605,7 +648,6 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
def fetch_metadata(self):
"""Fetches the metadata for the table and merges it in"""
table = self.database.get_table(self.table_name)
try:
table = self.database.get_table(self.table_name)
except Exception as e:
@@ -778,9 +820,9 @@ class DruidCluster(Model, AuditMixinNullable):
def refresh_datasources(self):
endpoint = (
"http://{self.coordinator_host}:{self.coordinator_port}/"
"{self.coordinator_endpoint}/datasources"
).format(self=self)
"http://{obj.coordinator_host}:{obj.coordinator_port}/"
"{obj.coordinator_endpoint}/datasources"
).format(obj=self)
datasources = json.loads(requests.get(endpoint).text)
for datasource in datasources:
@@ -824,8 +866,8 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
@property
def perm(self):
return (
"[{self.cluster_name}].[{self.datasource_name}]"
"(id:{self.id})").format(self=self)
"[{obj.cluster_name}].[{obj.datasource_name}]"
"(id:{obj.id})").format(obj=self)
@property
def url(self):
@@ -840,17 +882,17 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
@property
def full_name(self):
return (
"[{self.cluster_name}]."
"[{self.datasource_name}]").format(self=self)
"[{obj.cluster_name}]."
"[{obj.datasource_name}]").format(obj=self)
def __repr__(self):
return self.datasource_name
@property
def datasource_link(self):
url = "/caravel/explore/{self.type}/{self.id}/".format(self=self)
return '<a href="{url}">{self.datasource_name}</a>'.format(
url=url, self=self)
url = "/caravel/explore/{obj.type}/{obj.id}/".format(obj=self)
return '<a href="{url}">{obj.datasource_name}</a>'.format(
url=url, obj=self)
def get_metric_obj(self, metric_name):
return [

View File

@@ -12,15 +12,13 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% if appbuilder.app_icon %}
<a class="navbar-brand" style="padding:10px;" href="{{appbuilder.get_url_for_index}}">
<img width="30" src="{{appbuilder.app_icon}}">
<a class="navbar-brand" style="padding:7px;opacity:0.9;" href="{{appbuilder.get_url_for_index}}">
<img width="50" src="/static/assets/images/caravel_logo.png">
</a>
{% endif %}
<span class="navbar-brand">
<a href="{{appbuilder.get_url_for_index}}">
{{ appbuilder.app_name }}
</a>
<a href="{{appbuilder.get_url_for_index}}">
{{ appbuilder.app_name }}
</a>
</span>
</div>
<div class="navbar-collapse collapse">

View File

@@ -0,0 +1,4 @@
<script>
var msg = "Click on a table link to create a Slice";
window.location = "/r/msg/?url={{ '/tablemodelview/list/' }}&msg=" + msg;
</script>

View File

@@ -1,7 +1,7 @@
{% extends "appbuilder/baselayout.html" %}
{% block head_css %}
<link rel="icon" type="image/png" href="/static/img/favicon.png">
<link rel="icon" type="image/png" href="/static/assets/images/favicon.png">
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/caravel.css" />
{{super()}}
{% endblock %}

View File

@@ -8,7 +8,8 @@
{% block head_meta %}{% endblock %}
{% block head_css %}
<link rel="stylesheet" type="text/css" href="/static/assets/node_modules/font-awesome/css/font-awesome.min.css" />
<link rel="icon" type="image/png" href="/static/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/caravel.css" />
<link rel="icon" type="image/png" href="/static/assets/images/favicon.png">
{% endblock %}
{% block head_js %}
<script src="/static/assets/javascripts/dist/css-theme.entry.js"></script>
@@ -30,6 +31,24 @@
</div>
{% endblock %}
<!-- Modal for misc messages / alerts -->
<div class="misc-modal modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% block tail_js %}
{% endblock %}
</body>

View File

@@ -68,21 +68,22 @@
</div>
</div>
</div>
<div class="gridster content_fluid" style="visibility: hidden;">
<ul>
{% for slice in dashboard.slices %}
{% set pos = pos_dict.get(slice.id, {}) %}
{% set viz = slice.viz %}
<li
id="slice_{{ slice.id }}"
slice_id="{{ slice.id }}"
class="widget {{ slice.viz.viz_type }}"
class="widget {{ slice.viz_type }}"
data-row="{{ pos.row or 1 }}"
data-col="{{ pos.col or loop.index }}"
data-sizex="{{ pos.size_x or 4 }}"
data-sizey="{{ pos.size_y or 4 }}">
<div class="chart-header">
<div class="row">
<div class="col-md-12 text-center header">
@@ -124,9 +125,9 @@
</div>
<div class="row chart-container">
<input type="hidden" slice_id="{{ slice.id }}" value="false">
<div id="{{ viz.token }}" class="token col-md-12">
<div id="{{ slice.token }}" class="token col-md-12">
<img src="{{ url_for("static", filename="assets/images/loading.gif") }}" class="loading" alt="loading">
<div class="slice_container" id="{{ viz.token }}_con"></div>
<div class="slice_container" id="{{ slice.token }}_con"></div>
</div>
</div>
</li>

View File

@@ -29,18 +29,16 @@
<div class="datasource container-fluid">
<form id="query" method="GET" style="display: none;">
<div class="header">
<span class="btn btn-default notbtn" title="datasource" data-toggle="tooltip">
{{ datasource.full_name }}
{% if datasource.description %}
<a data-toggle="modal" data-target="#sourceinfo_modal">
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
</a>
{% endif %}
<a class="" href="/{{ datasource.baselink }}/edit/{{ datasource.id }}" data-toggle="tooltip" title="Edit">
<i class="fa fa-edit"></i>
</a>
<span title="Data Source" data-toggle="tooltip">
<select id="datasource_id" class="select2">
{% for ds in datasources %}
<option url="{{ ds.explore_url }}" {{ "selected" if ds.id == datasource.id }} value="{{ ds.id }}">{{ ds.full_name }}<i class="fa fa-info"></i></option>
{% endfor %}
</select>
</span>
<span title="Visualization Type" data-toggle="tooltip">
{{ form.get_field("viz_type")(class_="select2") }}
</span>
<span>{{ form.get_field("viz_type")(class_="select2") }}</span>
{% if slice %}
<span class="btn btn-default notbtn" title="Slice" data-toggle="tooltip" data-placement="bottom">
<span class="favstar" class_name="Slice" obj_id="{{ slice.id }}"></span>
@@ -81,7 +79,7 @@
<div id="form_container" class="col-left-fixed">
<div class="row center-block">
<div class="btn-group query-and-save">
<button type="button" class="btn btn-primary druidify">
<button type="button" class="btn btn-primary query">
<i class="fa fa-bolt"></i>Query
</button>
{% if viz.form_data.slice_id %}
@@ -96,9 +94,9 @@
</div>
<br/>
{% for fieldset in form.fieldsets %}
<fieldset class="fs-style">
<div class="panel panel-default">
{% if fieldset.label %}
<legend class="legend-style">
<div class="panel-heading">
<span class="legend_label">{{ fieldset.label }}</span>
{% if fieldset.description %}
<i class="fa fa-info-circle" data-toggle="tooltip"
@@ -106,9 +104,9 @@
title="{{ fieldset.description }}"></i>
{% endif %}
<span class="collapser"> [-]</span>
</legend>
</div>
{% endif %}
<div class="fieldset_content">
<div class="panel-body">
{% for fieldname in fieldset.fields %}
{% if not fieldname %}
<hr/>
@@ -129,17 +127,17 @@
{% endif %}
{% endfor %}
</div>
</fieldset>
</div>
{% endfor %}
<fieldset class="fs-style">
<legend class="legend-style">
<div class="panel panel-default">
<div class="panel-heading">
<span class="legend_label">Filters</span>
<i class="fa fa-info-circle" data-toggle="tooltip"
data-placement="bottom"
title="Filters are defined using comma delimited strings as in 'US,FR,Other'"></i>
<span class="collapser"> [-]</span>
</legend>
<div class="fieldset_content">
</div>
<div class="panel-body">
<div id="flt0" style="display: none;">
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
<div class="row">
@@ -157,7 +155,7 @@
<span>Add filter</span>
</button>
</div>
</fieldset>
</div>
{{ form.slice_id() }}
{{ form.slice_name() }}
{{ form.collapsed_fieldsets() }}

View File

@@ -10,25 +10,51 @@
{% block body %}
<div class="container welcome">
<div class="header">
<h3><i class='fa fa-star'></i> Welcome!</h3>
<h3>Welcome!</h3>
</div>
<hr/>
<div id="cal-heatmap"></div>
<hr/>
<div class="row">
<div class="col-md-6">
<div class="widget">
<div class="header"><h4><i class="fa fa-dashboard"></i> Dashboards</h4></div>
<div>
<table id="dash_table" class="table"></table>
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<span class="pull-left">
<h5><i class="fa fa-dashboard"></i> Dashboards</h5>
</span>
<span class="search pull-right"></span>
<span class="pull-right">
<h5><i class="fa fa-search"></i></h5>
</span>
</div>
</div>
</div>
<div class="panel-body">
<img class="loading" src="/static/assets/images/loading.gif"/>
<table id="dash_table" class="table table-condensed" width="100%"></table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="widget">
<div class="header"><h4><i class="fa fa-bar-chart"></i> Slices</h4></div>
<div>
<table id="slice_table" class="table"></table>
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<span class="pull-left">
<h5><i class="fa fa-bar-chart"></i> Slices</h5>
</span>
<span class="search pull-right"></span>
<span class="pull-right">
<h5><i class="fa fa-search"></i></h5>
</span>
</div>
</div>
</div>
<div class="panel-body">
<img class="loading" src="/static/assets/images/loading.gif"/>
<table id="slice_table" class="table table-condensed" width="100%"></table>
</div>
</div>
</div>

View File

@@ -1,7 +1,10 @@
"""Utility functions used across Caravel"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from datetime import datetime
import hashlib
import functools
import json
import logging
@@ -10,6 +13,7 @@ from dateutil.parser import parse
from sqlalchemy.types import TypeDecorator, TEXT
from markdown import markdown as md
import parsedatetime
from flask import Markup
from flask_appbuilder.security.sqla import models as ab_models
@@ -132,43 +136,6 @@ class JSONEncodedDict(TypeDecorator):
return value
class ColorFactory(object):
"""Used to generated arrays of colors server side"""
BNB_COLORS = [
# rausch hackb kazan babu lima beach barol
'#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
'#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
'#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e',
]
def __init__(self, hash_based=False):
self.d = {}
self.hash_based = hash_based
def get(self, s):
"""Gets a color from a string and memoize the association
>>> cf = ColorFactory()
>>> cf.get('item_1')
'#ff5a5f'
>>> cf.get('item_2')
'#7b0051'
>>> cf.get('item_1')
'#ff5a5f'
"""
if self.hash_based:
s = s.encode('utf-8')
h = hashlib.md5(s)
i = int(h.hexdigest(), 16)
else:
if s not in self.d:
self.d[s] = len(self.d)
i = self.d[s]
return self.BNB_COLORS[i % len(self.BNB_COLORS)]
def init(caravel):
"""Inits the Caravel application with security roles and such"""
db = caravel.db
@@ -241,13 +208,16 @@ def json_iso_dttm_ser(obj):
return obj
def markdown(s):
def markdown(s, markup_wrap=False):
s = s or ''
return md(s, [
s = md(s, [
'markdown.extensions.tables',
'markdown.extensions.fenced_code',
'markdown.extensions.codehilite',
])
if markup_wrap:
s = Markup(s)
return s
def readfile(filepath):

View File

@@ -1,4 +1,8 @@
"""Flask web views for Caravel"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from datetime import datetime
import json
@@ -36,6 +40,15 @@ def validate_json(form, field): # noqa
raise ValidationError("json isn't valid")
def generate_download_headers(extension):
filename = datetime.now().strftime("%Y%m%d_%H%M%S")
content_disp = "attachment; filename={}.{}".format(filename, extension)
headers = {
"Content-Disposition": content_disp,
}
return headers
class DeleteMixin(object):
@action(
"muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
@@ -117,7 +130,8 @@ class DatabaseView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.Database)
list_columns = ['database_name', 'sql_link', 'created_by_', 'changed_on']
order_columns = utils.list_minus(list_columns, ['created_by_'])
add_columns = ['database_name', 'sqlalchemy_uri', 'cache_timeout']
add_columns = [
'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra']
search_exclude_columns = ('password',)
edit_columns = add_columns
add_template = "caravel/models/database/add.html"
@@ -127,7 +141,16 @@ class DatabaseView(CaravelModelView, DeleteMixin): # noqa
'sqlalchemy_uri': (
"Refer to the SqlAlchemy docs for more information on how "
"to structure your URI here: "
"http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html")
"http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html"),
'extra': utils.markdown(
"JSON string containing extra configuration elements. "
"The ``engine_params`` object gets unpacked into the "
"[sqlalchemy.create_engine]"
"(http://docs.sqlalchemy.org/en/latest/core/engines.html#"
"sqlalchemy.create_engine) call, while the ``metadata_params`` "
"gets unpacked into the [sqlalchemy.MetaData]"
"(http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html"
"#sqlalchemy.schema.MetaData) call. ", True),
}
def pre_add(self, db):
@@ -213,6 +236,7 @@ if config['DRUID_IS_ACTIVE']:
class SliceModelView(CaravelModelView, DeleteMixin): # noqa
datamodel = SQLAInterface(models.Slice)
add_template = "caravel/add_slice.html"
can_add = False
label_columns = {
'created_by_': 'Creator',
@@ -220,8 +244,8 @@ class SliceModelView(CaravelModelView, DeleteMixin): # noqa
}
list_columns = [
'slice_link', 'viz_type',
'datasource_link', 'created_by_', 'changed_on']
order_columns = utils.list_minus(list_columns, ['created_by_'])
'datasource_link', 'created_by_', 'modified']
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
edit_columns = [
'slice_name', 'description', 'viz_type', 'druid_datasource',
'table', 'dashboards', 'params', 'cache_timeout']
@@ -248,6 +272,9 @@ class SliceAsync(SliceModelView): # noqa
'created_by_', 'modified', 'icons']
label_columns = {
'icons': ' ',
'created_by_': 'Creator',
'viz_type': 'Type',
'slice_link': 'Slice',
}
appbuilder.add_view_no_menu(SliceAsync)
@@ -258,8 +285,8 @@ class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
label_columns = {
'created_by_': 'Creator',
}
list_columns = ['dashboard_link', 'created_by_', 'changed_on']
order_columns = utils.list_minus(list_columns, ['created_by_'])
list_columns = ['dashboard_link', 'created_by_', 'modified']
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
edit_columns = [
'dashboard_title', 'slug', 'slices', 'position_json', 'css',
'json_metadata']
@@ -298,6 +325,10 @@ appbuilder.add_view(
class DashboardModelViewAsync(DashboardModelView): # noqa
list_columns = ['dashboard_link', 'created_by_', 'modified']
label_columns = {
'created_by_': 'Creator',
'dashboard_link': 'Dashboard',
}
appbuilder.add_view_no_menu(DashboardModelViewAsync)
@@ -388,6 +419,12 @@ class R(BaseView):
return("{request.headers[Host]}/r/{obj.id}".format(
request=request, obj=obj))
@expose("/msg/")
def msg(self):
"""Redirects to specified url while flash a message"""
flash(request.args.get("msg"), "info")
return redirect(request.args.get("url"))
appbuilder.add_view_no_menu(R)
@@ -402,12 +439,14 @@ class Caravel(BaseView):
def explore(self, datasource_type, datasource_id):
datasource_class = models.SqlaTable \
if datasource_type == "table" else models.DruidDatasource
datasource = (
datasources = (
db.session
.query(datasource_class)
.filter_by(id=datasource_id)
.first()
.all()
)
datasources = sorted(datasources, key=lambda ds: ds.full_name)
datasource = [ds for ds in datasources if int(datasource_id) == ds.id]
datasource = datasource[0] if datasource else None
slice_id = request.args.get("slice_id")
slc = None
if slice_id:
@@ -417,7 +456,7 @@ class Caravel(BaseView):
.first()
)
if not datasource:
flash("The datasource seem to have been deleted", "alert")
flash("The datasource seems to have been deleted", "alert")
all_datasource_access = self.appbuilder.sm.has_access(
'all_datasource_access', 'all_datasource_access')
@@ -453,6 +492,7 @@ class Caravel(BaseView):
resp = Response(
payload,
status=status,
headers=generate_download_headers("json"),
mimetype="application/json")
return resp
elif request.args.get("csv") == "true":
@@ -461,6 +501,7 @@ class Caravel(BaseView):
return Response(
payload,
status=status,
headers=generate_download_headers("csv"),
mimetype="application/csv")
else:
if request.args.get("standalone") == "true":
@@ -468,7 +509,8 @@ class Caravel(BaseView):
else:
template = "caravel/explore.html"
resp = self.render_template(template, viz=obj, slice=slc)
resp = self.render_template(
template, viz=obj, slice=slc, datasources=datasources)
try:
pass
except Exception as e:
@@ -633,7 +675,6 @@ class Caravel(BaseView):
qry = qry.filter_by(slug=dashboard_id)
templates = session.query(models.CssTemplate).all()
dash = qry.first()
# Hack to log the dashboard_id properly, even when getting a slug
@@ -760,7 +801,7 @@ class Caravel(BaseView):
"[" + cluster.cluster_name + "]",
'info')
session.commit()
return redirect("/datasourcemodelview/list/")
return redirect("/druiddatasourcemodelview/list/")
@expose("/autocomplete/<datasource>/<column>/")
def autocomplete(self, datasource, column):

View File

@@ -3,12 +3,17 @@
These objects represent the backend of all the visualizations that
Caravel can render.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from collections import OrderedDict, defaultdict
from datetime import datetime, timedelta
import json
import logging
import uuid
import hashlib
from flask import flash, request, Markup
from markdown import markdown
@@ -42,6 +47,8 @@ class BaseViz(object):
def __init__(self, datasource, form_data, slice=None):
self.orig_form_data = form_data
if not datasource:
raise Exception("Viz is missing a datasource")
self.datasource = datasource
self.request = request
self.viz_type = form_data.get("viz_type")
@@ -240,7 +247,7 @@ class BaseViz(object):
'data': self.get_data(),
'query': self.query,
'form_data': self.form_data,
'json_endpoint': cache_key,
'json_endpoint': self.json_endpoint,
'csv_endpoint': self.csv_endpoint,
'standalone_endpoint': self.standalone_endpoint,
'cache_timeout': cache_timeout,
@@ -254,7 +261,8 @@ class BaseViz(object):
def get_csv(self):
df = self.get_df()
return df.to_csv(index=False)
include_index = not isinstance(df.index, pd.RangeIndex)
return df.to_csv(index=include_index)
def get_data(self):
return []
@@ -265,7 +273,8 @@ class BaseViz(object):
@property
def cache_key(self):
return self.get_url(json="true", force="false")
url = self.get_url(json="true", force="false")
return hashlib.md5(url.encode('utf-8')).hexdigest()
@property
def csv_endpoint(self):
@@ -404,7 +413,7 @@ class PivotTableViz(BaseViz):
na_rep='',
classes=(
"dataframe table table-striped table-bordered "
"table-condensed table-hover"))
"table-condensed table-hover").split(" "))
class MarkupViz(BaseViz):
@@ -549,7 +558,7 @@ class BigNumberViz(BaseViz):
"""Put emphasis on a single metric with this big number viz"""
viz_type = "big_number"
verbose_name = "Big Number"
verbose_name = "Big Number with Trendline"
is_timeseries = True
fieldsets = ({
'label': None,
@@ -593,6 +602,51 @@ class BigNumberViz(BaseViz):
}
class BigNumberTotalViz(BaseViz):
"""Put emphasis on a single metric with this big number viz"""
viz_type = "big_number_total"
verbose_name = "Big Number"
is_timeseries = False
fieldsets = ({
'label': None,
'fields': (
'metric',
'subheader',
'y_axis_format',
)
},)
form_overrides = {
'y_axis_format': {
'label': 'Number format',
}
}
def reassignments(self):
metric = self.form_data.get('metric')
if not metric:
self.form_data['metric'] = self.orig_form_data.get('metrics')
def query_obj(self):
d = super(BigNumberTotalViz, self).query_obj()
metric = self.form_data.get('metric')
if not metric:
raise Exception("Pick a metric!")
d['metrics'] = [self.form_data.get('metric')]
self.form_data['metric'] = metric
return d
def get_data(self):
form_data = self.form_data
df = self.get_df()
df = df.sort(columns=df.columns[0])
return {
'data': df.values.tolist(),
'subheader': form_data.get('subheader', ''),
}
class NVD3TimeSeriesViz(NVD3Viz):
"""A rich line chart component with tons of options"""
@@ -829,6 +883,7 @@ class DistributionBarViz(DistributionPieViz):
'metrics',
'row_limit',
('show_legend', 'bar_stacked'),
('y_axis_format', None),
)
},)
form_overrides = {
@@ -1280,6 +1335,7 @@ viz_types_list = [
MarkupViz,
WordCloudViz,
BigNumberViz,
BigNumberTotalViz,
SunburstViz,
DirectedForceViz,
SankeyViz,

17
docs/_static/docs.css vendored
View File

@@ -6,6 +6,23 @@ div.navbar {
margin-bottom: 0px;
}
p {
margin-top: 5px;
margin-bottom: 15px;
}
#tutorial img {
border: 1px solid gray;
box-shadow: 5px 5px 5px #888888;
margin-bottom: 10px;
}
#gallery img {
border: 1px solid gray;
box-shadow: 5px 5px 5px #888888;
margin: 10px;
}
.carousel img {
max-height: 500px;
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
rm -rf _build
make html
cp -r ../caravel/assets/images/ _build/html/_static/img/
#cp -r ../caravel/assets/images/ _build/html/_static/img/
cp -r ../caravel/assets/images/ _static/img/
rm -rf /tmp/caravel-docs
cp -r _build/html /tmp/caravel-docs

View File

@@ -188,7 +188,7 @@ html_show_sourcelink = False
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
html_show_copyright = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the

45
docs/gallery.rst Normal file
View File

@@ -0,0 +1,45 @@
Gallery
=======
.. image:: _static/img/viz_thumbnails/line.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/bubble.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/table.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/pie.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/bar.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/sankey.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/word_cloud.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/filter.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/pivot_table.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/force_directed.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/percent_change.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/sunburst.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/stacked.png
:scale: 25 %
.. image:: _static/img/viz_thumbnails/big_number.png
:scale: 25 %

View File

@@ -1,3 +1,5 @@
.. image:: _static/img/caravel.jpg
.. warning:: This project used to be name Panoramix and has been renamed
to Caravel in March 2016
@@ -8,17 +10,17 @@ Features
---------
- A rich set of data visualizations, integrated from some of the best
visualization libraries
visualization libraries
- Create and share simple dashboards
- An extensible, high-granularity security/permission model allowing
intricate rules on who can access individual features and the dataset
intricate rules on who can access individual features and the dataset
- Enterprise-ready authentication with integration with major authentication
providers (database, OpenID, LDAP, OAuth & REMOTE_USER through
Flask AppBuilder)
providers (database, OpenID, LDAP, OAuth & REMOTE_USER through
Flask AppBuilder)
- A simple semantic layer, allowing users to control how data sources are
displayed in the UI by defining which fields should show up in which
drop-down and which aggregation and function metrics are made available
to the user
displayed in the UI by defining which fields should show up in which
drop-down and which aggregation and function metrics are made available
to the user
- Integration with most RDBMS through SqlAlchemy
- Deep integration with Druid.io
@@ -29,7 +31,9 @@ Contents
:maxdepth: 2
installation
user_guide
tutorial
videos
gallery
Indices and tables

View File

@@ -24,26 +24,30 @@ Here's how to install them:
For **Debian** and **Ubuntu**, the following command will ensure that
the required dependencies are installed: ::
sudo apt-get install build-essential libssl-dev libffi-dev python-dev
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip
For **Fedora** and **RHEL-derivatives**, the following command will ensure
that the required dependencies are installed: ::
sudo yum upgrade python-setuptools
sudo yum install gcc libffi-devel python-devel python-pip python-wheel openssl-devel
sudo yum install gcc libffi-devel python-devel openssl-devel
**OSX**, system python is not recommended. brew's python also ships with pip ::
**OSX** ::
brew install pkg-config libffi openssl
brew install pkg-config libffi openssl python
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography
**Windows** isn't officially supported at this point, but if you want to
attempt it: ::
attempt it, download `get-pip.py <https://bootstrap.pypa.io/get-pip.py>`_, and run ``python get-pip.py`` which may need admin access. Then run the following: ::
C:\> \path\to\vcvarsall.bat x86_amd64
C:\> set LIB=C:\OpenSSL-1.0.1f-64bit\lib;%LIB%
C:\> set INCLUDE=C:\OpenSSL-1.0.1f-64bit\include;%INCLUDE%
C:\> pip install cryptography
# You may also have to create C:\Temp
C:\> md C:\Temp
Caravel installation and initialization
---------------------------------------
@@ -69,7 +73,7 @@ Follow these few simple steps to install Caravel.::
After installation, you should be able to point your browser to the right
hostname:port [http://localhost:8088](http://localhost:8088), login using
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 Caravel to be aware of, and they should show up in
@@ -98,7 +102,11 @@ of the parameters you can copy / paste in that configuration module: ::
# Your App secret key
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
# The SQLAlchemy connection string.
# The SQLAlchemy connection string to your database backend
# This connection defines the path to the database that stores your
# caravel metadata (slices, connections, tables, dashboards, ...).
# Note that the connection information to connect to the datasources
# you want to explore are managed directly in the web UI
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/caravel.db'
# Flask-WTF flag for CSRF
@@ -131,13 +139,52 @@ data source's configuration, to your database's and ultimately falls back
into your global default defined in ``CACHE_CONFIG``.
Deeper SQLAlchemy integration
-----------------------------
It is possible to tweak the database connection information using the
parameters exposed by SQLAlchemy. In the ``Database`` edit view, you will
find an ``extra`` field as a ``JSON`` blob.
.. image:: _static/img/tutorial/add_db.png
:scale: 50 %
This JSON string contains extra configuration elements. The ``engine_params``
object gets unpacked into the
`sqlalchemy.create_engine <http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine>`_ call,
while the ``metadata_params`` get unpacked into the
`sqlalchemy.MetaData <http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html#sqlalchemy.schema.MetaData>`_ call. Refer to the SQLAlchemy docs for more information.
Postgres & Redshift
-------------------
Postgres and Redshift use the concept of **schema** as a logical entity
on top of the **database**. For Caravel to connect to a specific schema,
you can either specify it in the ``metadata_params`` key of the ``extra``
JSON blob described above, or you can use a database user name to connect to
the database that matches the schema name you are interested it.
Druid
-----
* From the UI, enter the information about your clusters in the
``Admin->Clusters`` menu by hitting the + sign.
``Admin->Clusters`` menu by hitting the + sign.
* Once the Druid cluster connection information is entered, hit the
``Admin->Refresh Metadata`` menu item to populate
``Admin->Refresh Metadata`` menu item to populate
* Navigate to your datasources
Note that you can run the ``caravel refresh_druid`` command to refresh the
metadata from your Druid cluster(s)
Upgrading
---------
Upgrading should be as straightforward as running::
pip install caravel --upgrade
caravel db upgrade

100
docs/tutorial.rst Normal file
View File

@@ -0,0 +1,100 @@
Tutorial
========
This basic linear tutorial will take you through connecting to a database,
adding a table, creating a slice and a dashboard. First you'll need to tell
Caravel where to find the database you want to
query. First go to the database menu
.. image:: _static/img/tutorial/db_menu.png
:scale: 30 %
Now click on the ``+`` button to add a new entry
.. image:: _static/img/tutorial/db_plus.png
:scale: 30 %
Fill in an arbitrary reference name for the database, and you SQLAlchemy
URI. To figure out how to construct your URI, check out the
`SQLAlchemy documentation <http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html>`_.
Then you can test your connection. If it works, you'll see a positive popup
and list of the tables that SQLAlchemy has found for that URI.
.. image:: _static/img/tutorial/db_added.png
:scale: 30 %
Once your database has been added, it's time to add your table. Navigate
using the navigation bar at the top to ``Sources -> Tables`` and click the
plus (``+``) sign there (similar to the one ).
Now enter the name of the table in the ``Table Name`` textbox, and select
the database you just created in the ``Database`` dropdown, hit save. At this
moment, Caravel fetched the column names, their data types and tries to guess
which fields are metrics in dimensions. From the list view, edit the table
that you just created by clicking the tiny pen icon.
.. image:: _static/img/tutorial/pen.png
:scale: 30 %
Now you're in the table editor, click on the "List Table Column" tab,
showing you the list of columns in your table as well as their data types.
.. image:: _static/img/tutorial/matrix.png
:scale: 30 %
Click the checkboxes here that inform Caravel how your columns should be
shown in the explore view, and which metrics should be created. Make sure
to inform Caravel about your date columns. You could also create
"SQL expression" columns here, or metrics in that tab as aggregate expressions,
but let's not do that just yet. Hit ``save``.
You should now be back in the ``Table List`` view. Click on the name of the
table you just created. You enter the "Explore" view for your table.
.. image:: _static/img/tutorial/explore.png
:scale: 30 %
The next step is to create a Slice. First, make sure to use a time filter
that is relevant.
.. note::
You can use some "natural language time expressions"
either as relative (as in ``now``, ``4 weeks ago``, or ``1 year ago``) as well
as hard date or time expressions (as in ``3015``, ``3016-01-01`` or
``May``).
Alter the form's option and click ``Query`` until you get to an interesting
cut of data, and click ``SAVE AS``, enter a name, and you just created your first
slice.
.. image:: _static/img/tutorial/created.png
:scale: 30 %
This slice is now accessible in the slice list from the
``Menu -> Slices`` at any time. Note that this view is easily filterable and
searchable.
.. image:: _static/img/tutorial/search.png
:scale: 30 %
Now let's create a dashboard. A dashboard is simply a collection of slices
with metadata around their sizes, positions, CSS style and a few other things.
Navigate to the dashboard list view ``Menu -> Dashboard`` and click the plus
(``+``) sign. In the form, enter a name and pick the slice you just created.
.. image:: _static/img/tutorial/new_dash.png
:scale: 30 %
Hit ``Save``, you should be back in ``Menu -> Dashboard``. Now enter your
new dashboard.
.. image:: _static/img/tutorial/in_new_dash.png
:scale: 30 %
Here you are. You can now resize and move the different slice(s), style them
in the CSS modal window, and save right from here. For now, renaming the
dashboard or adding on a new slice is done through the dashboard edit view,
which is the same form as you used when you originally created the dashboard,
and is accessible by clicking the ``edit`` pen icon from the dashboard list
view (``Menu -> Dashboards``)

View File

@@ -1,7 +1,7 @@
User Guide
==========
Videos
======
The user guide is a collection of short videos showing different aspect
Here is a collection of short videos showing different aspect
of Caravel.
Quick Intro

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages
version = '0.8.5'
version = '0.8.6'
setup(
name='caravel',