diff --git a/.travis.yml b/.travis.yml
index 336659866c..bd7bb7eb8a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,5 +34,4 @@ 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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2776da8c9f..6c681f461e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -211,6 +211,9 @@ following commands. The `dev` flag will keep the npm script running and
re-run it upon any changes within the assets directory.
```
+# Copies a conf file from the frontend to the backend
+npm run sync-backend
+
# Compiles the production / optimized js & css
npm run prod
diff --git a/run_tests.sh b/run_tests.sh
index db7bda6b7d..55760f633b 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -7,7 +7,6 @@ rm -f .coverage
export SUPERSET_CONFIG=tests.superset_test_config
set -e
superset/bin/superset db upgrade
-superset/bin/superset db upgrade # running twice on purpose as a test
superset/bin/superset version -v
python setup.py nosetests
coveralls
diff --git a/superset/__init__.py b/superset/__init__.py
index b9cc6b0041..1838c49b23 100644
--- a/superset/__init__.py
+++ b/superset/__init__.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import logging
import os
+import json
from logging.handlers import TimedRotatingFileHandler
from flask import Flask, redirect
@@ -21,6 +22,10 @@ from superset import utils
APP_DIR = os.path.dirname(__file__)
CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config')
+with open(APP_DIR + '/static/assets/backendSync.json', 'r') as f:
+ frontend_config = json.load(f)
+
+
app = Flask(__name__)
app.config.from_object(CONFIG_MODULE)
conf = app.config
diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json
new file mode 100644
index 0000000000..5a03b67b52
--- /dev/null
+++ b/superset/assets/backendSync.json
@@ -0,0 +1,2278 @@
+{
+ "fields": {
+ "datasource": {
+ "type": "SelectField",
+ "label": "Datasource",
+ "isLoading": true,
+ "clearable": false,
+ "default": null,
+ "description": ""
+ },
+ "viz_type": {
+ "type": "SelectField",
+ "label": "Visualization Type",
+ "clearable": false,
+ "default": "table",
+ "choices": [
+ [
+ "dist_bar",
+ "Distribution - Bar Chart",
+ "/static/assets/images/viz_thumbnails/dist_bar.png"
+ ],
+ [
+ "pie",
+ "Pie Chart",
+ "/static/assets/images/viz_thumbnails/pie.png"
+ ],
+ [
+ "line",
+ "Time Series - Line Chart",
+ "/static/assets/images/viz_thumbnails/line.png"
+ ],
+ [
+ "dual_line",
+ "Time Series - Dual Axis Line Chart",
+ "/static/assets/images/viz_thumbnails/dual_line.png"
+ ],
+ [
+ "bar",
+ "Time Series - Bar Chart",
+ "/static/assets/images/viz_thumbnails/bar.png"
+ ],
+ [
+ "compare",
+ "Time Series - Percent Change",
+ "/static/assets/images/viz_thumbnails/compare.png"
+ ],
+ [
+ "area",
+ "Time Series - Stacked",
+ "/static/assets/images/viz_thumbnails/area.png"
+ ],
+ [
+ "table",
+ "Table View",
+ "/static/assets/images/viz_thumbnails/table.png"
+ ],
+ [
+ "markup",
+ "Markup",
+ "/static/assets/images/viz_thumbnails/markup.png"
+ ],
+ [
+ "pivot_table",
+ "Pivot Table",
+ "/static/assets/images/viz_thumbnails/pivot_table.png"
+ ],
+ [
+ "separator",
+ "Separator",
+ "/static/assets/images/viz_thumbnails/separator.png"
+ ],
+ [
+ "word_cloud",
+ "Word Cloud",
+ "/static/assets/images/viz_thumbnails/word_cloud.png"
+ ],
+ [
+ "treemap",
+ "Treemap",
+ "/static/assets/images/viz_thumbnails/treemap.png"
+ ],
+ [
+ "cal_heatmap",
+ "Calendar Heatmap",
+ "/static/assets/images/viz_thumbnails/cal_heatmap.png"
+ ],
+ [
+ "box_plot",
+ "Box Plot",
+ "/static/assets/images/viz_thumbnails/box_plot.png"
+ ],
+ [
+ "bubble",
+ "Bubble Chart",
+ "/static/assets/images/viz_thumbnails/bubble.png"
+ ],
+ [
+ "bullet",
+ "Bullet Chart",
+ "/static/assets/images/viz_thumbnails/bullet.png"
+ ],
+ [
+ "big_number",
+ "Big Number with Trendline",
+ "/static/assets/images/viz_thumbnails/big_number.png"
+ ],
+ [
+ "big_number_total",
+ "Big Number",
+ "/static/assets/images/viz_thumbnails/big_number_total.png"
+ ],
+ [
+ "histogram",
+ "Histogram",
+ "/static/assets/images/viz_thumbnails/histogram.png"
+ ],
+ [
+ "sunburst",
+ "Sunburst",
+ "/static/assets/images/viz_thumbnails/sunburst.png"
+ ],
+ [
+ "sankey",
+ "Sankey",
+ "/static/assets/images/viz_thumbnails/sankey.png"
+ ],
+ [
+ "directed_force",
+ "Directed Force Layout",
+ "/static/assets/images/viz_thumbnails/directed_force.png"
+ ],
+ [
+ "world_map",
+ "World Map",
+ "/static/assets/images/viz_thumbnails/world_map.png"
+ ],
+ [
+ "filter_box",
+ "Filter Box",
+ "/static/assets/images/viz_thumbnails/filter_box.png"
+ ],
+ [
+ "iframe",
+ "iFrame",
+ "/static/assets/images/viz_thumbnails/iframe.png"
+ ],
+ [
+ "para",
+ "Parallel Coordinates",
+ "/static/assets/images/viz_thumbnails/para.png"
+ ],
+ [
+ "heatmap",
+ "Heatmap",
+ "/static/assets/images/viz_thumbnails/heatmap.png"
+ ],
+ [
+ "horizon",
+ "Horizon",
+ "/static/assets/images/viz_thumbnails/horizon.png"
+ ],
+ [
+ "mapbox",
+ "Mapbox",
+ "/static/assets/images/viz_thumbnails/mapbox.png"
+ ]
+ ],
+ "description": "The type of visualization to display"
+ },
+ "metrics": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "Metrics",
+ "validators": [
+ null
+ ],
+ "description": "One or many metrics to display"
+ },
+ "order_by_cols": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "Ordering",
+ "default": [],
+ "description": "One or many metrics to display"
+ },
+ "metric": {
+ "type": "SelectField",
+ "label": "Metric",
+ "clearable": false,
+ "description": "Choose the metric"
+ },
+ "metric_2": {
+ "type": "SelectField",
+ "label": "Right Axis Metric",
+ "choices": [],
+ "default": [],
+ "description": "Choose a metric for right axis"
+ },
+ "stacked_style": {
+ "type": "SelectField",
+ "label": "Stacked Style",
+ "choices": [
+ [
+ "stack",
+ "stack"
+ ],
+ [
+ "stream",
+ "stream"
+ ],
+ [
+ "expand",
+ "expand"
+ ]
+ ],
+ "default": "stack",
+ "description": ""
+ },
+ "linear_color_scheme": {
+ "type": "SelectField",
+ "label": "Linear Color Scheme",
+ "choices": [
+ [
+ "fire",
+ "fire"
+ ],
+ [
+ "blue_white_yellow",
+ "blue/white/yellow"
+ ],
+ [
+ "white_black",
+ "white/black"
+ ],
+ [
+ "black_white",
+ "black/white"
+ ]
+ ],
+ "default": "blue_white_yellow",
+ "description": ""
+ },
+ "normalize_across": {
+ "type": "SelectField",
+ "label": "Normalize Across",
+ "choices": [
+ [
+ "heatmap",
+ "heatmap"
+ ],
+ [
+ "x",
+ "x"
+ ],
+ [
+ "y",
+ "y"
+ ]
+ ],
+ "default": "heatmap",
+ "description": "Color will be rendered based on a ratio of the cell against the sum of across this criteria"
+ },
+ "horizon_color_scale": {
+ "type": "SelectField",
+ "label": "Horizon Color Scale",
+ "choices": [
+ [
+ "series",
+ "series"
+ ],
+ [
+ "overall",
+ "overall"
+ ],
+ [
+ "change",
+ "change"
+ ]
+ ],
+ "default": "series",
+ "description": "Defines how the color are attributed."
+ },
+ "canvas_image_rendering": {
+ "type": "SelectField",
+ "label": "Rendering",
+ "choices": [
+ [
+ "pixelated",
+ "pixelated (Sharp)"
+ ],
+ [
+ "auto",
+ "auto (Smooth)"
+ ]
+ ],
+ "default": "pixelated",
+ "description": "image-rendering CSS attribute of the canvas object that defines how the browser scales up the image"
+ },
+ "xscale_interval": {
+ "type": "SelectField",
+ "label": "XScale Interval",
+ "choices": [
+ [
+ 1,
+ "1"
+ ],
+ [
+ 2,
+ "2"
+ ],
+ [
+ 3,
+ "3"
+ ],
+ [
+ 4,
+ "4"
+ ],
+ [
+ 5,
+ "5"
+ ],
+ [
+ 6,
+ "6"
+ ],
+ [
+ 7,
+ "7"
+ ],
+ [
+ 8,
+ "8"
+ ],
+ [
+ 9,
+ "9"
+ ],
+ [
+ 10,
+ "10"
+ ],
+ [
+ 11,
+ "11"
+ ],
+ [
+ 12,
+ "12"
+ ],
+ [
+ 13,
+ "13"
+ ],
+ [
+ 14,
+ "14"
+ ],
+ [
+ 15,
+ "15"
+ ],
+ [
+ 16,
+ "16"
+ ],
+ [
+ 17,
+ "17"
+ ],
+ [
+ 18,
+ "18"
+ ],
+ [
+ 19,
+ "19"
+ ],
+ [
+ 20,
+ "20"
+ ],
+ [
+ 21,
+ "21"
+ ],
+ [
+ 22,
+ "22"
+ ],
+ [
+ 23,
+ "23"
+ ],
+ [
+ 24,
+ "24"
+ ],
+ [
+ 25,
+ "25"
+ ],
+ [
+ 26,
+ "26"
+ ],
+ [
+ 27,
+ "27"
+ ],
+ [
+ 28,
+ "28"
+ ],
+ [
+ 29,
+ "29"
+ ],
+ [
+ 30,
+ "30"
+ ],
+ [
+ 31,
+ "31"
+ ],
+ [
+ 32,
+ "32"
+ ],
+ [
+ 33,
+ "33"
+ ],
+ [
+ 34,
+ "34"
+ ],
+ [
+ 35,
+ "35"
+ ],
+ [
+ 36,
+ "36"
+ ],
+ [
+ 37,
+ "37"
+ ],
+ [
+ 38,
+ "38"
+ ],
+ [
+ 39,
+ "39"
+ ],
+ [
+ 40,
+ "40"
+ ],
+ [
+ 41,
+ "41"
+ ],
+ [
+ 42,
+ "42"
+ ],
+ [
+ 43,
+ "43"
+ ],
+ [
+ 44,
+ "44"
+ ],
+ [
+ 45,
+ "45"
+ ],
+ [
+ 46,
+ "46"
+ ],
+ [
+ 47,
+ "47"
+ ],
+ [
+ 48,
+ "48"
+ ],
+ [
+ 49,
+ "49"
+ ],
+ [
+ 50,
+ "50"
+ ]
+ ],
+ "default": "1",
+ "description": "Number of steps to take between ticks when displaying the X scale"
+ },
+ "yscale_interval": {
+ "type": "SelectField",
+ "label": "YScale Interval",
+ "choices": [
+ [
+ 1,
+ "1"
+ ],
+ [
+ 2,
+ "2"
+ ],
+ [
+ 3,
+ "3"
+ ],
+ [
+ 4,
+ "4"
+ ],
+ [
+ 5,
+ "5"
+ ],
+ [
+ 6,
+ "6"
+ ],
+ [
+ 7,
+ "7"
+ ],
+ [
+ 8,
+ "8"
+ ],
+ [
+ 9,
+ "9"
+ ],
+ [
+ 10,
+ "10"
+ ],
+ [
+ 11,
+ "11"
+ ],
+ [
+ 12,
+ "12"
+ ],
+ [
+ 13,
+ "13"
+ ],
+ [
+ 14,
+ "14"
+ ],
+ [
+ 15,
+ "15"
+ ],
+ [
+ 16,
+ "16"
+ ],
+ [
+ 17,
+ "17"
+ ],
+ [
+ 18,
+ "18"
+ ],
+ [
+ 19,
+ "19"
+ ],
+ [
+ 20,
+ "20"
+ ],
+ [
+ 21,
+ "21"
+ ],
+ [
+ 22,
+ "22"
+ ],
+ [
+ 23,
+ "23"
+ ],
+ [
+ 24,
+ "24"
+ ],
+ [
+ 25,
+ "25"
+ ],
+ [
+ 26,
+ "26"
+ ],
+ [
+ 27,
+ "27"
+ ],
+ [
+ 28,
+ "28"
+ ],
+ [
+ 29,
+ "29"
+ ],
+ [
+ 30,
+ "30"
+ ],
+ [
+ 31,
+ "31"
+ ],
+ [
+ 32,
+ "32"
+ ],
+ [
+ 33,
+ "33"
+ ],
+ [
+ 34,
+ "34"
+ ],
+ [
+ 35,
+ "35"
+ ],
+ [
+ 36,
+ "36"
+ ],
+ [
+ 37,
+ "37"
+ ],
+ [
+ 38,
+ "38"
+ ],
+ [
+ 39,
+ "39"
+ ],
+ [
+ 40,
+ "40"
+ ],
+ [
+ 41,
+ "41"
+ ],
+ [
+ 42,
+ "42"
+ ],
+ [
+ 43,
+ "43"
+ ],
+ [
+ 44,
+ "44"
+ ],
+ [
+ 45,
+ "45"
+ ],
+ [
+ 46,
+ "46"
+ ],
+ [
+ 47,
+ "47"
+ ],
+ [
+ 48,
+ "48"
+ ],
+ [
+ 49,
+ "49"
+ ],
+ [
+ 50,
+ "50"
+ ]
+ ],
+ "default": null,
+ "description": "Number of steps to take between ticks when displaying the Y scale"
+ },
+ "bar_stacked": {
+ "type": "CheckboxField",
+ "label": "Stacked Bars",
+ "renderTrigger": true,
+ "default": false,
+ "description": null
+ },
+ "show_markers": {
+ "type": "CheckboxField",
+ "label": "Show Markers",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Show data points as circle markers on the lines"
+ },
+ "show_bar_value": {
+ "type": "CheckboxField",
+ "label": "Bar Values",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Show the value on top of the bar"
+ },
+ "order_bars": {
+ "type": "CheckboxField",
+ "label": "Sort Bars",
+ "default": false,
+ "description": "Sort bars by x labels."
+ },
+ "show_controls": {
+ "type": "CheckboxField",
+ "label": "Extra Controls",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Whether to show extra controls or not. Extra controls include things like making mulitBar charts stacked or side by side."
+ },
+ "reduce_x_ticks": {
+ "type": "CheckboxField",
+ "label": "Reduce X ticks",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Reduces the number of X axis ticks to be rendered. If true, the x axis wont overflow and labels may be missing. If false, a minimum width will be applied to columns and the width may overflow into an horizontal scroll."
+ },
+ "include_series": {
+ "type": "CheckboxField",
+ "label": "Include Series",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Include series name as an axis"
+ },
+ "secondary_metric": {
+ "type": "SelectField",
+ "label": "Color Metric",
+ "default": null,
+ "description": "A metric to use for color"
+ },
+ "country_fieldtype": {
+ "type": "SelectField",
+ "label": "Country Field Type",
+ "default": "cca2",
+ "choices": [
+ [
+ "name",
+ "Full name"
+ ],
+ [
+ "cioc",
+ "code International Olympic Committee (cioc)"
+ ],
+ [
+ "cca2",
+ "code ISO 3166-1 alpha-2 (cca2)"
+ ],
+ [
+ "cca3",
+ "code ISO 3166-1 alpha-3 (cca3)"
+ ]
+ ],
+ "description": "The country code standard that Superset should expect to find in the [country] column"
+ },
+ "groupby": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "Group by",
+ "default": [],
+ "description": "One or many fields to group by"
+ },
+ "columns": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "Columns",
+ "default": [],
+ "description": "One or many fields to pivot as columns"
+ },
+ "all_columns": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "Columns",
+ "default": [],
+ "description": "Columns to display"
+ },
+ "all_columns_x": {
+ "type": "SelectField",
+ "label": "X",
+ "default": null,
+ "description": "Columns to display"
+ },
+ "all_columns_y": {
+ "type": "SelectField",
+ "label": "Y",
+ "default": null,
+ "description": "Columns to display"
+ },
+ "druid_time_origin": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Origin",
+ "choices": [
+ [
+ "",
+ "default"
+ ],
+ [
+ "now",
+ "now"
+ ]
+ ],
+ "default": null,
+ "description": "Defines the origin where time buckets start, accepts natural dates as in `now`, `sunday` or `1970-01-01`"
+ },
+ "bottom_margin": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Bottom Margin",
+ "choices": [
+ [
+ "auto",
+ "auto"
+ ],
+ [
+ 50,
+ "50"
+ ],
+ [
+ 75,
+ "75"
+ ],
+ [
+ 100,
+ "100"
+ ],
+ [
+ 125,
+ "125"
+ ],
+ [
+ 150,
+ "150"
+ ],
+ [
+ 200,
+ "200"
+ ]
+ ],
+ "default": "auto",
+ "description": "Bottom marging, in pixels, allowing for more room for axis labels"
+ },
+ "granularity": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Time Granularity",
+ "default": "one day",
+ "choices": [
+ [
+ "all",
+ "all"
+ ],
+ [
+ "5 seconds",
+ "5 seconds"
+ ],
+ [
+ "30 seconds",
+ "30 seconds"
+ ],
+ [
+ "1 minute",
+ "1 minute"
+ ],
+ [
+ "5 minutes",
+ "5 minutes"
+ ],
+ [
+ "1 hour",
+ "1 hour"
+ ],
+ [
+ "6 hour",
+ "6 hour"
+ ],
+ [
+ "1 day",
+ "1 day"
+ ],
+ [
+ "7 days",
+ "7 days"
+ ],
+ [
+ "week",
+ "week"
+ ],
+ [
+ "week_starting_sunday",
+ "week_starting_sunday"
+ ],
+ [
+ "week_ending_saturday",
+ "week_ending_saturday"
+ ],
+ [
+ "month",
+ "month"
+ ]
+ ],
+ "description": "The time granularity for the visualization. Note that you can type and use simple natural language as in `10 seconds`, `1 day` or `56 weeks`"
+ },
+ "domain_granularity": {
+ "type": "SelectField",
+ "label": "Domain",
+ "default": "month",
+ "choices": [
+ [
+ "hour",
+ "hour"
+ ],
+ [
+ "day",
+ "day"
+ ],
+ [
+ "week",
+ "week"
+ ],
+ [
+ "month",
+ "month"
+ ],
+ [
+ "year",
+ "year"
+ ]
+ ],
+ "description": "The time unit used for the grouping of blocks"
+ },
+ "subdomain_granularity": {
+ "type": "SelectField",
+ "label": "Subdomain",
+ "default": "day",
+ "choices": [
+ [
+ "min",
+ "min"
+ ],
+ [
+ "hour",
+ "hour"
+ ],
+ [
+ "day",
+ "day"
+ ],
+ [
+ "week",
+ "week"
+ ],
+ [
+ "month",
+ "month"
+ ]
+ ],
+ "description": "The time unit for each block. Should be a smaller unit than domain_granularity. Should be larger or equal to Time Grain"
+ },
+ "link_length": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Link Length",
+ "default": "200",
+ "choices": [
+ [
+ "10",
+ "10"
+ ],
+ [
+ "25",
+ "25"
+ ],
+ [
+ "50",
+ "50"
+ ],
+ [
+ "75",
+ "75"
+ ],
+ [
+ "100",
+ "100"
+ ],
+ [
+ "150",
+ "150"
+ ],
+ [
+ "200",
+ "200"
+ ],
+ [
+ "250",
+ "250"
+ ]
+ ],
+ "description": "Link length in the force layout"
+ },
+ "charge": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Charge",
+ "default": "-500",
+ "choices": [
+ [
+ "-50",
+ "-50"
+ ],
+ [
+ "-75",
+ "-75"
+ ],
+ [
+ "-100",
+ "-100"
+ ],
+ [
+ "-150",
+ "-150"
+ ],
+ [
+ "-200",
+ "-200"
+ ],
+ [
+ "-250",
+ "-250"
+ ],
+ [
+ "-500",
+ "-500"
+ ],
+ [
+ "-1000",
+ "-1000"
+ ],
+ [
+ "-2500",
+ "-2500"
+ ],
+ [
+ "-5000",
+ "-5000"
+ ]
+ ],
+ "description": "Charge in the force layout"
+ },
+ "granularity_sqla": {
+ "type": "SelectField",
+ "label": "Time Column",
+ "description": "The time column for the visualization. Note that you can define arbitrary expression that return a DATETIME column in the table or. Also note that the filter below is applied against this column or expression"
+ },
+ "time_grain_sqla": {
+ "type": "SelectField",
+ "label": "Time Grain",
+ "description": "The time granularity for the visualization. This applies a date transformation to alter your time column and defines a new time granularity. The options here are defined on a per database engine basis in the Superset source code."
+ },
+ "resample_rule": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Resample Rule",
+ "default": null,
+ "choices": [
+ [
+ "",
+ ""
+ ],
+ [
+ "1T",
+ "1T"
+ ],
+ [
+ "1H",
+ "1H"
+ ],
+ [
+ "1D",
+ "1D"
+ ],
+ [
+ "7D",
+ "7D"
+ ],
+ [
+ "1M",
+ "1M"
+ ],
+ [
+ "1AS",
+ "1AS"
+ ]
+ ],
+ "description": "Pandas resample rule"
+ },
+ "resample_how": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Resample How",
+ "default": null,
+ "choices": [
+ [
+ "",
+ ""
+ ],
+ [
+ "mean",
+ "mean"
+ ],
+ [
+ "sum",
+ "sum"
+ ],
+ [
+ "median",
+ "median"
+ ]
+ ],
+ "description": "Pandas resample how"
+ },
+ "resample_fillmethod": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Resample Fill Method",
+ "default": null,
+ "choices": [
+ [
+ "",
+ ""
+ ],
+ [
+ "ffill",
+ "ffill"
+ ],
+ [
+ "bfill",
+ "bfill"
+ ]
+ ],
+ "description": "Pandas resample fill method"
+ },
+ "since": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Since",
+ "default": "7 days ago",
+ "choices": [
+ [
+ "1 hour ago",
+ "1 hour ago"
+ ],
+ [
+ "12 hours ago",
+ "12 hours ago"
+ ],
+ [
+ "1 day ago",
+ "1 day ago"
+ ],
+ [
+ "7 days ago",
+ "7 days ago"
+ ],
+ [
+ "28 days ago",
+ "28 days ago"
+ ],
+ [
+ "90 days ago",
+ "90 days ago"
+ ],
+ [
+ "1 year ago",
+ "1 year ago"
+ ],
+ [
+ "100 year ago",
+ "100 year ago"
+ ]
+ ],
+ "description": "Timestamp from filter. This supports free form typing and natural language as in `1 day ago`, `28 days` or `3 years`"
+ },
+ "until": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Until",
+ "default": "now",
+ "choices": [
+ [
+ "now",
+ "now"
+ ],
+ [
+ "1 day ago",
+ "1 day ago"
+ ],
+ [
+ "7 days ago",
+ "7 days ago"
+ ],
+ [
+ "28 days ago",
+ "28 days ago"
+ ],
+ [
+ "90 days ago",
+ "90 days ago"
+ ],
+ [
+ "1 year ago",
+ "1 year ago"
+ ]
+ ]
+ },
+ "max_bubble_size": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Max Bubble Size",
+ "default": "25",
+ "choices": [
+ [
+ "5",
+ "5"
+ ],
+ [
+ "10",
+ "10"
+ ],
+ [
+ "15",
+ "15"
+ ],
+ [
+ "25",
+ "25"
+ ],
+ [
+ "50",
+ "50"
+ ],
+ [
+ "75",
+ "75"
+ ],
+ [
+ "100",
+ "100"
+ ]
+ ]
+ },
+ "whisker_options": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Whisker/outlier options",
+ "default": "Tukey",
+ "description": "Determines how whiskers and outliers are calculated.",
+ "choices": [
+ [
+ "Tukey",
+ "Tukey"
+ ],
+ [
+ "Min/max (no outliers)",
+ "Min/max (no outliers)"
+ ],
+ [
+ "2/98 percentiles",
+ "2/98 percentiles"
+ ],
+ [
+ "9/91 percentiles",
+ "9/91 percentiles"
+ ]
+ ]
+ },
+ "treemap_ratio": {
+ "type": "TextField",
+ "label": "Ratio",
+ "isFloat": true,
+ "default": 1.618033988749895,
+ "description": "Target aspect ratio for treemap tiles."
+ },
+ "number_format": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Number format",
+ "default": [
+ ".3s",
+ ".3s | 12.3k"
+ ],
+ "choices": [
+ [
+ ".3s",
+ ".3s | 12.3k"
+ ],
+ [
+ ".3%",
+ ".3% | 1234543.210%"
+ ],
+ [
+ ".4r",
+ ".4r | 12350"
+ ],
+ [
+ ".3f",
+ ".3f | 12345.432"
+ ],
+ [
+ "+,",
+ "+, | +12,345.4321"
+ ],
+ [
+ "$,.2f",
+ "$,.2f | $12,345.43"
+ ]
+ ],
+ "description": "D3 format syntax: https://github.com/d3/d3-format"
+ },
+ "row_limit": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Row limit",
+ "default": null,
+ "choices": [
+ [
+ 10,
+ "10"
+ ],
+ [
+ 50,
+ "50"
+ ],
+ [
+ 100,
+ "100"
+ ],
+ [
+ 250,
+ "250"
+ ],
+ [
+ 500,
+ "500"
+ ],
+ [
+ 1000,
+ "1000"
+ ],
+ [
+ 5000,
+ "5000"
+ ],
+ [
+ 10000,
+ "10000"
+ ],
+ [
+ 50000,
+ "50000"
+ ]
+ ]
+ },
+ "limit": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Series limit",
+ "choices": [
+ [
+ 0,
+ "0"
+ ],
+ [
+ 5,
+ "5"
+ ],
+ [
+ 10,
+ "10"
+ ],
+ [
+ 25,
+ "25"
+ ],
+ [
+ 50,
+ "50"
+ ],
+ [
+ 100,
+ "100"
+ ],
+ [
+ 500,
+ "500"
+ ]
+ ],
+ "default": 50,
+ "description": "Limits the number of time series that get displayed"
+ },
+ "timeseries_limit_metric": {
+ "type": "SelectField",
+ "label": "Sort By",
+ "default": null,
+ "description": "Metric used to define the top series"
+ },
+ "rolling_type": {
+ "type": "SelectField",
+ "label": "Rolling",
+ "default": "None",
+ "choices": [
+ [
+ "None",
+ "None"
+ ],
+ [
+ "mean",
+ "mean"
+ ],
+ [
+ "sum",
+ "sum"
+ ],
+ [
+ "std",
+ "std"
+ ],
+ [
+ "cumsum",
+ "cumsum"
+ ]
+ ],
+ "description": "Defines a rolling window function to apply, works along with the [Periods] text box"
+ },
+ "rolling_periods": {
+ "type": "TextField",
+ "label": "Periods",
+ "isInt": true,
+ "description": "Defines the size of the rolling window function, relative to the time granularity selected"
+ },
+ "series": {
+ "type": "SelectField",
+ "label": "Series",
+ "default": null,
+ "description": "Defines the grouping of entities. Each series is shown as a specific color on the chart and has a legend toggle"
+ },
+ "entity": {
+ "type": "SelectField",
+ "label": "Entity",
+ "default": null,
+ "description": "This define the element to be plotted on the chart"
+ },
+ "x": {
+ "type": "SelectField",
+ "label": "X Axis",
+ "default": null,
+ "description": "Metric assigned to the [X] axis"
+ },
+ "y": {
+ "type": "SelectField",
+ "label": "Y Axis",
+ "default": null,
+ "description": "Metric assigned to the [Y] axis"
+ },
+ "size": {
+ "type": "SelectField",
+ "label": "Bubble Size",
+ "default": null
+ },
+ "url": {
+ "type": "TextField",
+ "label": "URL",
+ "description": "The URL, this field is templated, so you can integrate {{ width }} and/or {{ height }} in your URL string.",
+ "default": "https: //www.youtube.com/embed/JkI5rg_VcQ4"
+ },
+ "x_axis_label": {
+ "type": "TextField",
+ "label": "X Axis Label",
+ "renderTrigger": true,
+ "default": ""
+ },
+ "y_axis_label": {
+ "type": "TextField",
+ "label": "Y Axis Label",
+ "renderTrigger": true,
+ "default": ""
+ },
+ "where": {
+ "type": "TextField",
+ "label": "Custom WHERE clause",
+ "default": "",
+ "description": "The text in this box gets included in your query's WHERE clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards."
+ },
+ "having": {
+ "type": "TextField",
+ "label": "Custom HAVING clause",
+ "default": "",
+ "description": "The text in this box gets included in your query's HAVING clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards."
+ },
+ "compare_lag": {
+ "type": "TextField",
+ "label": "Comparison Period Lag",
+ "isInt": true,
+ "description": "Based on granularity, number of time periods to compare against"
+ },
+ "compare_suffix": {
+ "type": "TextField",
+ "label": "Comparison suffix",
+ "description": "Suffix to apply after the percentage display"
+ },
+ "table_timestamp_format": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Table Timestamp Format",
+ "default": "smart_date",
+ "choices": [
+ [
+ "smart_date",
+ "Adaptative formating"
+ ],
+ [
+ "%m/%d/%Y",
+ "%m/%d/%Y | 01/14/2019"
+ ],
+ [
+ "%Y-%m-%d",
+ "%Y-%m-%d | 2019-01-14"
+ ],
+ [
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10"
+ ],
+ [
+ "%H:%M:%S",
+ "%H:%M:%S | 01:32:10"
+ ]
+ ],
+ "description": "Timestamp Format"
+ },
+ "series_height": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Series Height",
+ "default": "25",
+ "choices": [
+ [
+ "10",
+ "10"
+ ],
+ [
+ "25",
+ "25"
+ ],
+ [
+ "40",
+ "40"
+ ],
+ [
+ "50",
+ "50"
+ ],
+ [
+ "75",
+ "75"
+ ],
+ [
+ "100",
+ "100"
+ ],
+ [
+ "150",
+ "150"
+ ],
+ [
+ "200",
+ "200"
+ ]
+ ],
+ "description": "Pixel height of each series"
+ },
+ "page_length": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Page Length",
+ "default": 0,
+ "choices": [
+ [
+ 0,
+ "0"
+ ],
+ [
+ 10,
+ "10"
+ ],
+ [
+ 25,
+ "25"
+ ],
+ [
+ 40,
+ "40"
+ ],
+ [
+ 50,
+ "50"
+ ],
+ [
+ 75,
+ "75"
+ ],
+ [
+ 100,
+ "100"
+ ],
+ [
+ 150,
+ "150"
+ ],
+ [
+ 200,
+ "200"
+ ]
+ ],
+ "description": "Rows per page, 0 means no pagination"
+ },
+ "x_axis_format": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "X axis format",
+ "renderTrigger": true,
+ "default": "smart_date",
+ "choices": [
+ [
+ "smart_date",
+ "Adaptative formating"
+ ],
+ [
+ "%m/%d/%Y",
+ "%m/%d/%Y | 01/14/2019"
+ ],
+ [
+ "%Y-%m-%d",
+ "%Y-%m-%d | 2019-01-14"
+ ],
+ [
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10"
+ ],
+ [
+ "%H:%M:%S",
+ "%H:%M:%S | 01:32:10"
+ ]
+ ],
+ "description": "D3 format syntax: https://github.com/d3/d3-format"
+ },
+ "y_axis_format": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Y axis format",
+ "renderTrigger": true,
+ "default": ".3s",
+ "choices": [
+ [
+ ".3s",
+ ".3s | 12.3k"
+ ],
+ [
+ ".3%",
+ ".3% | 1234543.210%"
+ ],
+ [
+ ".4r",
+ ".4r | 12350"
+ ],
+ [
+ ".3f",
+ ".3f | 12345.432"
+ ],
+ [
+ "+,",
+ "+, | +12,345.4321"
+ ],
+ [
+ "$,.2f",
+ "$,.2f | $12,345.43"
+ ]
+ ],
+ "description": "D3 format syntax: https://github.com/d3/d3-format"
+ },
+ "y_axis_2_format": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Right axis format",
+ "default": ".3s",
+ "choices": [
+ [
+ ".3s",
+ ".3s | 12.3k"
+ ],
+ [
+ ".3%",
+ ".3% | 1234543.210%"
+ ],
+ [
+ ".4r",
+ ".4r | 12350"
+ ],
+ [
+ ".3f",
+ ".3f | 12345.432"
+ ],
+ [
+ "+,",
+ "+, | +12,345.4321"
+ ],
+ [
+ "$,.2f",
+ "$,.2f | $12,345.43"
+ ]
+ ],
+ "description": "D3 format syntax: https://github.com/d3/d3-format"
+ },
+ "markup_type": {
+ "type": "SelectField",
+ "label": "Markup Type",
+ "choices": [
+ [
+ "markdown",
+ "markdown"
+ ],
+ [
+ "html",
+ "html"
+ ]
+ ],
+ "default": "markdown",
+ "description": "Pick your favorite markup language"
+ },
+ "rotation": {
+ "type": "SelectField",
+ "label": "Rotation",
+ "choices": [
+ [
+ "random",
+ "random"
+ ],
+ [
+ "flat",
+ "flat"
+ ],
+ [
+ "square",
+ "square"
+ ]
+ ],
+ "default": "random",
+ "description": "Rotation to apply to words in the cloud"
+ },
+ "line_interpolation": {
+ "type": "SelectField",
+ "label": "Line Style",
+ "renderTrigger": true,
+ "choices": [
+ [
+ "linear",
+ "linear"
+ ],
+ [
+ "basis",
+ "basis"
+ ],
+ [
+ "cardinal",
+ "cardinal"
+ ],
+ [
+ "monotone",
+ "monotone"
+ ],
+ [
+ "step-before",
+ "step-before"
+ ],
+ [
+ "step-after",
+ "step-after"
+ ]
+ ],
+ "default": "linear",
+ "description": "Line interpolation as defined by d3.js"
+ },
+ "pie_label_type": {
+ "type": "SelectField",
+ "label": "Label Type",
+ "default": "key",
+ "choices": [
+ [
+ "key",
+ "Category Name"
+ ],
+ [
+ "value",
+ "Value"
+ ],
+ [
+ "percent",
+ "Percentage"
+ ]
+ ],
+ "description": "What should be shown on the label?"
+ },
+ "code": {
+ "type": "TextAreaField",
+ "label": "Code",
+ "description": "Put your code here",
+ "default": ""
+ },
+ "pandas_aggfunc": {
+ "type": "SelectField",
+ "label": "Aggregation function",
+ "clearable": false,
+ "choices": [
+ [
+ "sum",
+ "sum"
+ ],
+ [
+ "mean",
+ "mean"
+ ],
+ [
+ "min",
+ "min"
+ ],
+ [
+ "max",
+ "max"
+ ],
+ [
+ "median",
+ "median"
+ ],
+ [
+ "stdev",
+ "stdev"
+ ],
+ [
+ "var",
+ "var"
+ ]
+ ],
+ "default": "sum",
+ "description": "Aggregate function to apply when pivoting and computing the total rows and columns"
+ },
+ "size_from": {
+ "type": "TextField",
+ "isInt": true,
+ "label": "Font Size From",
+ "default": "20",
+ "description": "Font size for the smallest value in the list"
+ },
+ "size_to": {
+ "type": "TextField",
+ "isInt": true,
+ "label": "Font Size To",
+ "default": "150",
+ "description": "Font size for the biggest value in the list"
+ },
+ "show_brush": {
+ "type": "CheckboxField",
+ "label": "Range Filter",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Whether to display the time range interactive selector"
+ },
+ "date_filter": {
+ "type": "CheckboxField",
+ "label": "Date Filter",
+ "default": false,
+ "description": "Whether to include a time filter"
+ },
+ "show_datatable": {
+ "type": "CheckboxField",
+ "label": "Data Table",
+ "default": false,
+ "description": "Whether to display the interactive data table"
+ },
+ "include_search": {
+ "type": "CheckboxField",
+ "label": "Search Box",
+ "renderTrigger": true,
+ "default": false,
+ "description": "Whether to include a client side search box"
+ },
+ "table_filter": {
+ "type": "CheckboxField",
+ "label": "Table Filter",
+ "default": false,
+ "description": "Whether to apply filter when table cell is clicked"
+ },
+ "show_bubbles": {
+ "type": "CheckboxField",
+ "label": "Show Bubbles",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Whether to display bubbles on top of countries"
+ },
+ "show_legend": {
+ "type": "CheckboxField",
+ "label": "Legend",
+ "renderTrigger": true,
+ "default": true,
+ "description": "Whether to display the legend (toggles)"
+ },
+ "x_axis_showminmax": {
+ "type": "CheckboxField",
+ "label": "X bounds",
+ "renderTrigger": true,
+ "default": true,
+ "description": "Whether to display the min and max values of the X axis"
+ },
+ "rich_tooltip": {
+ "type": "CheckboxField",
+ "label": "Rich Tooltip",
+ "renderTrigger": true,
+ "default": true,
+ "description": "The rich tooltip shows a list of all series for that point in time"
+ },
+ "y_axis_zero": {
+ "type": "CheckboxField",
+ "label": "Y Axis Zero",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Force the Y axis to start at 0 instead of the minimum value"
+ },
+ "y_log_scale": {
+ "type": "CheckboxField",
+ "label": "Y Log Scale",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Use a log scale for the Y axis"
+ },
+ "x_log_scale": {
+ "type": "CheckboxField",
+ "label": "X Log Scale",
+ "default": false,
+ "renderTrigger": true,
+ "description": "Use a log scale for the X axis"
+ },
+ "donut": {
+ "type": "CheckboxField",
+ "label": "Donut",
+ "default": false,
+ "description": "Do you want a donut or a pie?"
+ },
+ "labels_outside": {
+ "type": "CheckboxField",
+ "label": "Put labels outside",
+ "default": true,
+ "description": "Put the labels outside the pie?"
+ },
+ "contribution": {
+ "type": "CheckboxField",
+ "label": "Contribution",
+ "default": false,
+ "description": "Compute the contribution to the total"
+ },
+ "num_period_compare": {
+ "type": "TextField",
+ "label": "Period Ratio",
+ "default": "",
+ "isInt": true,
+ "description": "[integer] Number of period to compare against, this is relative to the granularity selected"
+ },
+ "period_ratio_type": {
+ "type": "SelectField",
+ "label": "Period Ratio Type",
+ "default": "growth",
+ "choices": [
+ [
+ "factor",
+ "factor"
+ ],
+ [
+ "growth",
+ "growth"
+ ],
+ [
+ "value",
+ "value"
+ ]
+ ],
+ "description": "`factor` means (new/previous), `growth` is ((new/previous) - 1), `value` is (new-previous)"
+ },
+ "time_compare": {
+ "type": "TextField",
+ "label": "Time Shift",
+ "default": null,
+ "description": "Overlay a timeseries from a relative time period. Expects relative time delta in natural language (example: 24 hours, 7 days, 56 weeks, 365 days"
+ },
+ "subheader": {
+ "type": "TextField",
+ "label": "Subheader",
+ "description": "Description text that shows up below your Big Number"
+ },
+ "mapbox_label": {
+ "type": "SelectField",
+ "multi": true,
+ "label": "label",
+ "default": [],
+ "description": "`count` is COUNT(*) if a group by is used. Numerical columns will be aggregated with the aggregator. Non-numerical columns will be used to label points. Leave empty to get a count of points in each cluster."
+ },
+ "mapbox_style": {
+ "type": "SelectField",
+ "label": "Map Style",
+ "choices": [
+ [
+ "mapbox://styles/mapbox/streets-v9",
+ "Streets"
+ ],
+ [
+ "mapbox://styles/mapbox/dark-v9",
+ "Dark"
+ ],
+ [
+ "mapbox://styles/mapbox/light-v9",
+ "Light"
+ ],
+ [
+ "mapbox://styles/mapbox/satellite-streets-v9",
+ "Satellite Streets"
+ ],
+ [
+ "mapbox://styles/mapbox/satellite-v9",
+ "Satellite"
+ ],
+ [
+ "mapbox://styles/mapbox/outdoors-v9",
+ "Outdoors"
+ ]
+ ],
+ "default": "mapbox://styles/mapbox/streets-v9",
+ "description": "Base layer map style"
+ },
+ "clustering_radius": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "Clustering Radius",
+ "default": "60",
+ "choices": [
+ [
+ "0",
+ "0"
+ ],
+ [
+ "20",
+ "20"
+ ],
+ [
+ "40",
+ "40"
+ ],
+ [
+ "60",
+ "60"
+ ],
+ [
+ "80",
+ "80"
+ ],
+ [
+ "100",
+ "100"
+ ],
+ [
+ "200",
+ "200"
+ ],
+ [
+ "500",
+ "500"
+ ],
+ [
+ "1000",
+ "1000"
+ ]
+ ],
+ "description": "The radius (in pixels) the algorithm uses to define a cluster. Choose 0 to turn off clustering, but beware that a large number of points (>1000) will cause lag."
+ },
+ "point_radius": {
+ "type": "SelectField",
+ "label": "Point Radius",
+ "default": "Auto",
+ "description": "The radius of individual points (ones that are not in a cluster). Either a numerical column or `Auto`, which scales the point based on the largest cluster"
+ },
+ "point_radius_unit": {
+ "type": "SelectField",
+ "label": "Point Radius Unit",
+ "default": "Pixels",
+ "choices": [
+ [
+ "Pixels",
+ "Pixels"
+ ],
+ [
+ "Miles",
+ "Miles"
+ ],
+ [
+ "Kilometers",
+ "Kilometers"
+ ]
+ ],
+ "description": "The unit of measure for the specified point radius"
+ },
+ "global_opacity": {
+ "type": "TextField",
+ "label": "Opacity",
+ "default": 1,
+ "isFloat": true,
+ "description": "Opacity of all clusters, points, and labels. Between 0 and 1."
+ },
+ "viewport_zoom": {
+ "type": "TextField",
+ "label": "Zoom",
+ "isFloat": true,
+ "default": 11,
+ "description": "Zoom level of the map",
+ "places": 8
+ },
+ "viewport_latitude": {
+ "type": "TextField",
+ "label": "Default latitude",
+ "default": 37.772123,
+ "isFloat": true,
+ "description": "Latitude of default viewport",
+ "places": 8
+ },
+ "viewport_longitude": {
+ "type": "TextField",
+ "label": "Default longitude",
+ "default": -122.405293,
+ "isFloat": true,
+ "description": "Longitude of default viewport",
+ "places": 8
+ },
+ "render_while_dragging": {
+ "type": "CheckboxField",
+ "label": "Live render",
+ "default": true,
+ "description": "Points and clusters will update as viewport is being changed"
+ },
+ "mapbox_color": {
+ "type": "SelectField",
+ "freeForm": true,
+ "label": "RGB Color",
+ "default": "rgb(0, 122, 135)",
+ "choices": [
+ [
+ "rgb(0, 139, 139)",
+ "Dark Cyan"
+ ],
+ [
+ "rgb(128, 0, 128)",
+ "Purple"
+ ],
+ [
+ "rgb(255, 215, 0)",
+ "Gold"
+ ],
+ [
+ "rgb(69, 69, 69)",
+ "Dim Gray"
+ ],
+ [
+ "rgb(220, 20, 60)",
+ "Crimson"
+ ],
+ [
+ "rgb(34, 139, 34)",
+ "Forest Green"
+ ]
+ ],
+ "description": "The color for points and clusters in RGB"
+ },
+ "ranges": {
+ "type": "TextField",
+ "label": "Ranges",
+ "default": "",
+ "description": "Ranges to highlight with shading"
+ },
+ "range_labels": {
+ "type": "TextField",
+ "label": "Range labels",
+ "default": "",
+ "description": "Labels for the ranges"
+ },
+ "markers": {
+ "type": "TextField",
+ "label": "Markers",
+ "default": "",
+ "description": "List of values to mark with triangles"
+ },
+ "marker_labels": {
+ "type": "TextField",
+ "label": "Marker labels",
+ "default": "",
+ "description": "Labels for the markers"
+ },
+ "marker_lines": {
+ "type": "TextField",
+ "label": "Marker lines",
+ "default": "",
+ "description": "List of values to mark with lines"
+ },
+ "marker_line_labels": {
+ "type": "TextField",
+ "label": "Marker line labels",
+ "default": "",
+ "description": "Labels for the marker lines"
+ },
+ "filters": {
+ "type": "FilterField",
+ "label": "",
+ "default": [],
+ "description": ""
+ },
+ "having_filters": {
+ "type": "FilterField",
+ "label": "",
+ "default": [],
+ "description": ""
+ },
+ "slice_id": {
+ "type": "HiddenField",
+ "label": "Slice ID",
+ "hidden": true,
+ "description": "The id of the active slice"
+ }
+ }
+}
\ No newline at end of file
diff --git a/superset/assets/javascripts/components/FaveStar.jsx b/superset/assets/javascripts/components/FaveStar.jsx
index 4e6afa2883..ce19fcb945 100644
--- a/superset/assets/javascripts/components/FaveStar.jsx
+++ b/superset/assets/javascripts/components/FaveStar.jsx
@@ -3,7 +3,7 @@ import cx from 'classnames';
import TooltipWrapper from './TooltipWrapper';
const propTypes = {
- sliceId: PropTypes.string.isRequired,
+ sliceId: PropTypes.number.isRequired,
actions: PropTypes.object.isRequired,
isStarred: PropTypes.bool.isRequired,
};
diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
index 08a1fe2b21..687910679d 100644
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/Dashboard.jsx
@@ -13,7 +13,6 @@ import Header from './components/Header';
require('bootstrap');
require('../../stylesheets/dashboard.css');
-require('../superset-select2.js');
export function getInitialState(dashboardData, context) {
const dashboard = Object.assign({ context }, utils.controllerInterface, dashboardData);
@@ -83,9 +82,6 @@ function initDashboardView(dashboard) {
);
$('div.grid-container').css('visibility', 'visible');
- $('.select2').select2({
- dropdownAutoWidth: true,
- });
$('div.widget').click(function (e) {
const $this = $(this);
const $target = $(e.target);
@@ -165,9 +161,7 @@ export function dashboardContainer(dashboard) {
}
},
effectiveExtraFilters(sliceId) {
- // Summarized filter, not defined by sliceId
- // returns k=field, v=array of values
- const f = {};
+ const f = [];
const immuneSlices = this.metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
// The slice is immune to dashboard fiterls
@@ -185,7 +179,11 @@ export function dashboardContainer(dashboard) {
for (const filteringSliceId in this.filters) {
for (const field in this.filters[filteringSliceId]) {
if (!immuneToFields.includes(field)) {
- f[field] = this.filters[filteringSliceId][field];
+ f.push({
+ col: field,
+ op: 'in',
+ val: this.filters[filteringSliceId][field],
+ });
}
}
}
diff --git a/superset/assets/javascripts/dashboard/components/GridLayout.jsx b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
index e2a118c6da..bfbd7f64b0 100644
--- a/superset/assets/javascripts/dashboard/components/GridLayout.jsx
+++ b/superset/assets/javascripts/dashboard/components/GridLayout.jsx
@@ -98,7 +98,7 @@ class GridLayout extends React.Component {
id={'slice_' + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
- className={`widget ${slice.viz_name}`}
+ className={`widget ${slice.form_data.viz_type}`}
>
-
+
{!this.props.dashboard.context.standalone_mode &&
}
diff --git a/superset/assets/javascripts/dashboard/components/SliceCell.jsx b/superset/assets/javascripts/dashboard/components/SliceCell.jsx
index 10abe1cafc..798753def8 100644
--- a/superset/assets/javascripts/dashboard/components/SliceCell.jsx
+++ b/superset/assets/javascripts/dashboard/components/SliceCell.jsx
@@ -67,13 +67,13 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
-
+
![loading](/static/assets/images/loading.gif)
-
+
diff --git a/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx b/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx
deleted file mode 100644
index 990c0930cd..0000000000
--- a/superset/assets/javascripts/explore/components/DisplayQueryButton.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React, { PropTypes } from 'react';
-import ModalTrigger from './../../components/ModalTrigger';
-
-const propTypes = {
- query: PropTypes.string,
-};
-
-const defaultProps = {
- query: '',
-};
-
-export default function DisplayQueryButton({ query }) {
- const modalBody = (
{query}
);
- return (
-
Query}
- modalTitle="Query"
- modalBody={modalBody}
- />
- );
-}
-
-DisplayQueryButton.propTypes = propTypes;
-DisplayQueryButton.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx b/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx
deleted file mode 100644
index b884100061..0000000000
--- a/superset/assets/javascripts/explore/components/ExploreActionButtons.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { PropTypes } from 'react';
-import cx from 'classnames';
-import URLShortLinkButton from './URLShortLinkButton';
-import EmbedCodeButton from './EmbedCodeButton';
-import DisplayQueryButton from './DisplayQueryButton';
-
-const propTypes = {
- canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
- slice: PropTypes.object.isRequired,
- query: PropTypes.string,
-};
-
-export default function ExploreActionButtons({ canDownload, slice, query }) {
- const exportToCSVClasses = cx('btn btn-default btn-sm', {
- 'disabled disabledButton': !canDownload,
- });
- return (
-
- );
-}
-
-ExploreActionButtons.propTypes = propTypes;
diff --git a/superset/assets/javascripts/explore/explore.jsx b/superset/assets/javascripts/explore/explore.jsx
deleted file mode 100644
index 67a837897a..0000000000
--- a/superset/assets/javascripts/explore/explore.jsx
+++ /dev/null
@@ -1,403 +0,0 @@
-// Javascript for the explorer page
-// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice
-// nb: to add a new vis, you must also add a Python fn in viz.py
-//
-// js
-const $ = window.$ = require('jquery');
-const px = require('./../modules/superset.js');
-const utils = require('./../modules/utils.js');
-const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
-import ExploreActionButtons from './components/ExploreActionButtons.jsx';
-
-require('jquery-ui');
-$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
-require('bootstrap');
-
-require('./../superset-select2.js');
-
-// css
-require('../../vendor/pygments.css');
-require('../../stylesheets/explore.css');
-
-let slice;
-
-const getPanelClass = function (fieldPrefix) {
- return (fieldPrefix === 'flt' ? 'filter' : 'having') + '_panel';
-};
-
-function prepForm() {
- // Assigning the right id to form elements in filters
- const fixId = function ($filter, fieldPrefix, i) {
- $filter.attr('id', function () {
- return fieldPrefix + '_' + i;
- });
-
- ['col', 'op', 'eq'].forEach(function (fieldMiddle) {
- const fieldName = fieldPrefix + '_' + fieldMiddle;
- $filter.find('[id^=' + fieldName + '_]')
- .attr('id', function () {
- return fieldName + '_' + i;
- })
- .attr('name', function () {
- return fieldName + '_' + i;
- });
- });
- };
-
- ['flt', 'having'].forEach(function (fieldPrefix) {
- let i = 1;
- $('#' + getPanelClass(fieldPrefix) + ' #filters > div').each(function () {
- fixId($(this), fieldPrefix, i);
- i++;
- });
- });
-}
-
-function query(forceUpdate, pushState) {
- let force = forceUpdate;
- if (force === undefined) {
- force = false;
- }
- $('.query-and-save button').attr('disabled', 'disabled');
- if (force) { // Don't hide the alert message when the page is just loaded
- $('div.alert').remove();
- }
- $('#is_cached').hide();
- prepForm();
-
- if (pushState !== false) {
- // update the url after prepForm() fix the field ids
- history.pushState({}, document.title, slice.querystring());
- }
- slice.container.html('');
- slice.render(force);
-}
-
-function saveSlice() {
- const action = $('input[name=rdo_save]:checked').val();
- if (action === 'saveas') {
- const sliceName = $('input[name=new_slice_name]').val();
- if (sliceName === '') {
- utils.showModal({
- title: 'Error',
- body: 'You must pick a name for the new slice',
- });
- return;
- }
- document.getElementById('slice_name').value = sliceName;
- }
- const addToDash = $('input[name=addToDash]:checked').val();
- if (addToDash === 'existing' && $('#save_to_dashboard_id').val() === '') {
- utils.showModal({
- title: 'Error',
- body: 'You must pick an existing dashboard',
- });
- return;
- } else if (addToDash === 'new' && $('input[name=new_dashboard_name]').val() === '') {
- utils.showModal({
- title: 'Error',
- body: 'Please enter a name for the new dashboard',
- });
- return;
- }
- $('#action').val(action);
- prepForm();
- $('#query').submit();
-}
-
-function initExploreView() {
- function getCollapsedFieldsets() {
- let collapsedFieldsets = $('#collapsedFieldsets').val();
-
- if (collapsedFieldsets !== undefined && collapsedFieldsets !== '') {
- collapsedFieldsets = collapsedFieldsets.split('||');
- } else {
- collapsedFieldsets = [];
- }
- return collapsedFieldsets;
- }
-
- function toggleFieldset(legend, animation) {
- const parent = legend.parent();
- const fieldset = parent.find('.legend_label').text();
- const collapsedFieldsets = getCollapsedFieldsets();
- let index;
-
- if (parent.hasClass('collapsed')) {
- if (animation) {
- parent.find('.panel-body').slideDown();
- } else {
- parent.find('.panel-body').show();
- }
- parent.removeClass('collapsed');
- parent.find('span.collapser').text('[-]');
-
- // removing from array, js is overcomplicated
- index = collapsedFieldsets.indexOf(fieldset);
- if (index !== -1) {
- collapsedFieldsets.splice(index, 1);
- }
- } else { // not collapsed
- if (animation) {
- parent.find('.panel-body').slideUp();
- } else {
- parent.find('.panel-body').hide();
- }
-
- parent.addClass('collapsed');
- parent.find('span.collapser').text('[+]');
- index = collapsedFieldsets.indexOf(fieldset);
- if (index === -1 && fieldset !== '' && fieldset !== undefined) {
- collapsedFieldsets.push(fieldset);
- }
- }
-
- $('#collapsedFieldsets').val(collapsedFieldsets.join('||'));
- }
-
- px.initFavStars();
-
- $('#viz_type').change(function () {
- $('#query').submit();
- });
-
- $('#datasource_id').change(function () {
- window.location = $(this).find('option:selected').attr('url');
- });
-
- const collapsedFieldsets = getCollapsedFieldsets();
- for (let i = 0; i < collapsedFieldsets.length; i++) {
- toggleFieldset($('legend:contains("' + collapsedFieldsets[i] + '")'), false);
- }
- function formatViz(viz) {
- const url = `/static/assets/images/viz_thumbnails/${viz.id}.png`;
- const noImg = '/static/assets/images/noimg.png';
- return $(
- `
` +
- `${viz.text}`
- );
- }
-
- $('.select2').select2({
- dropdownAutoWidth: true,
- });
- $('.select2Sortable').select2({
- dropdownAutoWidth: true,
- });
- $('.select2-with-images').select2({
- dropdownAutoWidth: true,
- dropdownCssClass: 'bigdrop',
- formatResult: formatViz,
- });
- $('.select2Sortable').select2Sortable({
- bindOrder: 'sortableStop',
- });
- $('form').show();
- $('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- $('.ui-helper-hidden-accessible').remove(); // jQuery-ui 1.11+ creates a div for every tooltip
-
- function addFilter(i, fieldPrefix) {
- const cp = $('#' + fieldPrefix + '0').clone();
- $(cp).appendTo('#' + getPanelClass(fieldPrefix) + ' #filters');
- $(cp).show();
- if (i !== undefined) {
- $(cp).find('#' + fieldPrefix + '_eq_0').val(px.getParam(fieldPrefix + '_eq_' + i));
- $(cp).find('#' + fieldPrefix + '_op_0').val(px.getParam(fieldPrefix + '_op_' + i));
- $(cp).find('#' + fieldPrefix + '_col_0').val(px.getParam(fieldPrefix + '_col_' + i));
- }
- $(cp).find('select').select2();
- $(cp).find('.remove').click(function () {
- $(this)
- .parent()
- .parent()
- .remove();
- });
- }
-
- function setFilters() {
- ['flt', 'having'].forEach(function (prefix) {
- for (let i = 1; i < 10; i++) {
- const col = px.getParam(prefix + '_col_' + i);
- if (col !== '') {
- addFilter(i, prefix);
- }
- }
- });
- }
- setFilters();
-
- $(window).bind('popstate', function () {
- // Browser back button
- const returnLocation = history.location || document.location;
- // Could do something more lightweight here, but we're not optimizing
- // for the use of the back button anyways
- returnLocation.reload();
- });
-
- $('#filter_panel #plus').click(function () {
- addFilter(undefined, 'flt');
- });
- $('#having_panel #plus').click(function () {
- addFilter(undefined, 'having');
- });
-
- function createChoices(term, data) {
- const filtered = $(data).filter(function () {
- return this.text.localeCompare(term) === 0;
- });
- if (filtered.length === 0) {
- return {
- id: term,
- text: term,
- };
- }
- return {};
- }
-
- function initSelectionToValue(element, callback) {
- callback({
- id: element.val(),
- text: element.val(),
- });
- }
-
- $('.select2_freeform').each(function () {
- const parent = $(this).parent();
- const name = $(this).attr('name');
- const l = [];
- let selected = '';
- for (let i = 0; i < this.options.length; i++) {
- l.push({
- id: this.options[i].value,
- text: this.options[i].text,
- });
- if (this.options[i].selected) {
- selected = this.options[i].value;
- }
- }
- parent.append(
- ``
- );
- $(`input[name='${name}']`).select2({
- createSearchChoice: createChoices,
- initSelection: initSelectionToValue,
- dropdownAutoWidth: true,
- multiple: false,
- data: l,
- });
- $(this).remove();
- });
-
- function prepSaveDialog() {
- const setButtonsState = function () {
- const addToDash = $('input[name=addToDash]:checked').val();
- if (addToDash === 'existing' || addToDash === 'new') {
- $('.gotodash').removeAttr('disabled');
- } else {
- $('.gotodash').prop('disabled', true);
- }
- };
- const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + $('#userid').val();
- $.get(url, function (data) {
- const choices = [];
- for (let i = 0; i < data.pks.length; i++) {
- choices.push({ id: data.pks[i], text: data.result[i].dashboard_title });
- }
- $('#save_to_dashboard_id').select2({
- data: choices,
- dropdownAutoWidth: true,
- }).on('select2-selecting', function () {
- $('#addToDash_existing').prop('checked', true);
- setButtonsState();
- });
- });
-
- $('input[name=addToDash]').change(setButtonsState);
- $("input[name='new_dashboard_name']").on('focus', function () {
- $('#add_to_new_dash').prop('checked', true);
- setButtonsState();
- });
- $("input[name='new_slice_name']").on('focus', function () {
- $('#save_as_new').prop('checked', true);
- setButtonsState();
- });
-
- $('#btn_modal_save').on('click', () => saveSlice());
-
- $('#btn_modal_save_goto_dash').click(() => {
- document.getElementById('goto_dash').value = 'true';
- saveSlice();
- });
- }
- prepSaveDialog();
-}
-
-function renderExploreActions() {
- const exploreActionsEl = document.getElementById('js-explore-actions');
- ReactDOM.render(
- ,
- exploreActionsEl
- );
-}
-
-function initComponents() {
- const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
- ReactDOM.render(
- query(true)}
- />,
- queryAndSaveBtnsEl
- );
- renderExploreActions();
-}
-
-let exploreController = {
- type: 'slice',
- done: (sliceObj) => {
- slice = sliceObj;
- renderExploreActions();
- const cachedSelector = $('#is_cached');
- if (slice.data !== undefined && slice.data.is_cached) {
- cachedSelector
- .attr(
- 'title',
- `Served from data cached at ${slice.data.cached_dttm}. Click [Query] to force refresh`)
- .show()
- .tooltip('fixTitle');
- } else {
- cachedSelector.hide();
- }
- },
- error: (sliceObj) => {
- slice = sliceObj;
- renderExploreActions();
- },
-};
-exploreController = Object.assign({}, utils.controllerInterface, exploreController);
-
-
-$(document).ready(function () {
- const data = $('.slice').data('slice');
-
- initExploreView();
-
- slice = px.Slice(data, exploreController);
-
- // call vis render method, which issues ajax
- // calls render on the slice for the first time
- query(false, false);
-
- slice.bindResizeToWindowResize();
-
- initComponents();
-});
diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js
index 29ea8e40df..7fba28a1e6 100644
--- a/superset/assets/javascripts/explorev2/actions/exploreActions.js
+++ b/superset/assets/javascripts/explorev2/actions/exploreActions.js
@@ -13,42 +13,88 @@ export function setDatasource(datasource) {
return { type: SET_DATASOURCE, datasource };
}
-export const FETCH_STARTED = 'FETCH_STARTED';
-export function fetchStarted() {
- return { type: FETCH_STARTED };
+export const SET_DATASOURCES = 'SET_DATASOURCES';
+export function setDatasources(datasources) {
+ return { type: SET_DATASOURCES, datasources };
}
-export const FETCH_SUCCEEDED = 'FETCH_SUCCEEDED';
-export function fetchSucceeded() {
- return { type: FETCH_SUCCEEDED };
+export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
+export function fetchDatasourceStarted() {
+ return { type: FETCH_DATASOURCE_STARTED };
}
-export const FETCH_FAILED = 'FETCH_FAILED';
-export function fetchFailed(error) {
- return { type: FETCH_FAILED, error };
+export const FETCH_DATASOURCE_SUCCEEDED = 'FETCH_DATASOURCE_SUCCEEDED';
+export function fetchDatasourceSucceeded() {
+ return { type: FETCH_DATASOURCE_SUCCEEDED };
}
-export function fetchDatasourceMetadata(datasourceId, datasourceType) {
+export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
+export function fetchDatasourceFailed(error) {
+ return { type: FETCH_DATASOURCE_FAILED, error };
+}
+
+export const FETCH_DATASOURCES_STARTED = 'FETCH_DATASOURCES_STARTED';
+export function fetchDatasourcesStarted() {
+ return { type: FETCH_DATASOURCES_STARTED };
+}
+
+export const FETCH_DATASOURCES_SUCCEEDED = 'FETCH_DATASOURCES_SUCCEEDED';
+export function fetchDatasourcesSucceeded() {
+ return { type: FETCH_DATASOURCES_SUCCEEDED };
+}
+
+export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED';
+export function fetchDatasourcesFailed(error) {
+ return { type: FETCH_DATASOURCES_FAILED, error };
+}
+
+export const RESET_FIELDS = 'RESET_FIELDS';
+export function resetFields() {
+ return { type: RESET_FIELDS };
+}
+
+export const TRIGGER_QUERY = 'TRIGGER_QUERY';
+export function triggerQuery() {
+ return { type: TRIGGER_QUERY };
+}
+
+export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) {
return function (dispatch) {
- dispatch(fetchStarted());
+ dispatch(fetchDatasourceStarted());
+ const url = `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`;
+ $.ajax({
+ type: 'GET',
+ url,
+ success: (data) => {
+ dispatch(setDatasource(data));
+ dispatch(fetchDatasourceSucceeded());
+ dispatch(resetFields());
+ if (alsoTriggerQuery) {
+ dispatch(triggerQuery());
+ }
+ },
+ error(error) {
+ dispatch(fetchDatasourceFailed(error.responseJSON.error));
+ },
+ });
+ };
+}
- if (datasourceId) {
- const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`];
- const url = '/superset/fetch_datasource_metadata?' + params.join('&');
- $.ajax({
- type: 'GET',
- url,
- success: (data) => {
- dispatch(setDatasource(data));
- dispatch(fetchSucceeded());
- },
- error(error) {
- dispatch(fetchFailed(error.responseJSON.error));
- },
- });
- } else {
- dispatch(fetchFailed('Please select a datasource'));
- }
+export function fetchDatasources() {
+ return function (dispatch) {
+ dispatch(fetchDatasourcesStarted());
+ const url = '/superset/datasources/';
+ $.ajax({
+ type: 'GET',
+ url,
+ success: (data) => {
+ dispatch(setDatasources(data));
+ dispatch(fetchDatasourcesSucceeded());
+ },
+ error(error) {
+ dispatch(fetchDatasourcesFailed(error.responseJSON.error));
+ },
+ });
};
}
@@ -85,8 +131,8 @@ export function setFieldValue(fieldName, value, validationErrors) {
}
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
-export function chartUpdateStarted() {
- return { type: CHART_UPDATE_STARTED };
+export function chartUpdateStarted(queryRequest) {
+ return { type: CHART_UPDATE_STARTED, queryRequest };
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
@@ -94,6 +140,14 @@ export function chartUpdateSucceeded(queryResponse) {
return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
}
+export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
+export function chartUpdateStopped(queryRequest) {
+ if (queryRequest) {
+ queryRequest.abort();
+ }
+ return { type: CHART_UPDATE_STOPPED };
+}
+
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
export function chartUpdateFailed(queryResponse) {
return { type: CHART_UPDATE_FAILED, queryResponse };
@@ -126,7 +180,7 @@ export function fetchDashboardsSucceeded(choices) {
export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED';
export function fetchDashboardsFailed(userId) {
- return { type: FETCH_FAILED, userId };
+ return { type: FETCH_DASHBOARDS_FAILED, userId };
}
export function fetchDashboards(userId) {
@@ -177,12 +231,19 @@ export function updateChartStatus(status) {
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, datasourceType) {
return function (dispatch) {
- dispatch(updateChartStatus('loading'));
const url = getExploreUrl(formData, datasourceType, 'json');
- $.getJSON(url, function (queryResponse) {
+ const queryRequest = $.getJSON(url, function (queryResponse) {
dispatch(chartUpdateSucceeded(queryResponse));
}).fail(function (err) {
- dispatch(chartUpdateFailed(err));
+ if (err.statusText !== 'abort') {
+ dispatch(chartUpdateFailed(err.responseJSON));
+ }
});
+ dispatch(chartUpdateStarted(queryRequest));
};
}
+
+export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
+export function renderTriggered() {
+ return { type: RENDER_TRIGGERED };
+}
diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
index a025d50c2d..22c5b7552a 100644
--- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
@@ -1,13 +1,15 @@
import $ from 'jquery';
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
-import { Panel, Alert } from 'react-bootstrap';
+import { Panel, Alert, Collapse } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
-import ExploreActionButtons from '../../explore/components/ExploreActionButtons';
+import ExploreActionButtons from './ExploreActionButtons';
import FaveStar from '../../components/FaveStar';
import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
+import { getExploreUrl } from '../exploreUtils';
+import { getFormDataFromFields } from '../stores/store';
const CHART_STATUS_MAP = {
failed: 'danger',
@@ -17,20 +19,20 @@ const CHART_STATUS_MAP = {
const propTypes = {
actions: PropTypes.object.isRequired,
- can_download: PropTypes.bool.isRequired,
- slice_id: PropTypes.string.isRequired,
- slice_name: PropTypes.string.isRequired,
- viz_type: PropTypes.string.isRequired,
- height: PropTypes.string.isRequired,
- containerId: PropTypes.string.isRequired,
- query: PropTypes.string,
- column_formats: PropTypes.object,
- chartStatus: PropTypes.string,
- isStarred: PropTypes.bool.isRequired,
- chartUpdateStartTime: PropTypes.number.isRequired,
- chartUpdateEndTime: PropTypes.number,
alert: PropTypes.string,
+ can_download: PropTypes.bool.isRequired,
+ chartStatus: PropTypes.string,
+ chartUpdateEndTime: PropTypes.number,
+ chartUpdateStartTime: PropTypes.number.isRequired,
+ column_formats: PropTypes.object,
+ containerId: PropTypes.string.isRequired,
+ height: PropTypes.string.isRequired,
+ isStarred: PropTypes.bool.isRequired,
+ slice: PropTypes.object,
table_name: PropTypes.string,
+ viz_type: PropTypes.string.isRequired,
+ formData: PropTypes.object,
+ latestQueryFormData: PropTypes.object,
};
class ChartContainer extends React.PureComponent {
@@ -38,14 +40,16 @@ class ChartContainer extends React.PureComponent {
super(props);
this.state = {
selector: `#${props.containerId}`,
+ showStackTrace: false,
};
}
renderViz() {
+ this.props.actions.renderTriggered();
const mockSlice = this.getMockedSliceObject();
+ this.setState({ mockSlice });
try {
visMap[this.props.viz_type](mockSlice, this.props.queryResponse);
- this.setState({ mockSlice });
} catch (e) {
this.props.actions.chartRenderingFailed(e);
}
@@ -53,8 +57,13 @@ class ChartContainer extends React.PureComponent {
componentDidUpdate(prevProps) {
if (
- prevProps.queryResponse !== this.props.queryResponse ||
- prevProps.height !== this.props.height
+ (
+ prevProps.queryResponse !== this.props.queryResponse ||
+ prevProps.height !== this.props.height ||
+ this.props.triggerRender
+ ) && !this.props.queryResponse.error
+ && this.props.chartStatus !== 'failed'
+ && this.props.chartStatus !== 'stopped'
) {
this.renderViz();
}
@@ -62,10 +71,15 @@ class ChartContainer extends React.PureComponent {
getMockedSliceObject() {
const props = this.props;
+ const getHeight = () => {
+ const headerHeight = this.props.standalone ? 0 : 100;
+ return parseInt(props.height, 10) - headerHeight;
+ };
return {
- viewSqlQuery: props.query,
+ viewSqlQuery: this.props.queryResponse.query,
containerId: props.containerId,
selector: this.state.selector,
+ formData: this.props.formData,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
@@ -77,7 +91,7 @@ class ChartContainer extends React.PureComponent {
// should call callback to adjust height of chart
$(this.state.selector).css(dim, size);
},
- height: () => parseInt(props.height, 10) - 100,
+ height: getHeight,
show: () => { },
get: (n) => ($(this.state.selector).get(n)),
find: (classname) => ($(this.state.selector).find(classname)),
@@ -85,7 +99,7 @@ class ChartContainer extends React.PureComponent {
width: () => this.chartContainerRef.getBoundingClientRect().width,
- height: () => parseInt(props.height, 10) - 100,
+ height: getHeight,
setFilter: () => {
// set filter according to data in store
@@ -111,9 +125,10 @@ class ChartContainer extends React.PureComponent {
},
data: {
- csv_endpoint: props.queryResponse.csv_endpoint,
- json_endpoint: props.queryResponse.json_endpoint,
- standalone_endpoint: props.queryResponse.standalone_endpoint,
+ csv_endpoint: getExploreUrl(this.props.formData, this.props.datasource_type, 'csv'),
+ json_endpoint: getExploreUrl(this.props.formData, this.props.datasource_type, 'json'),
+ standalone_endpoint: getExploreUrl(
+ this.props.formData, this.props.datasource_type, 'standalone'),
},
};
@@ -125,26 +140,45 @@ class ChartContainer extends React.PureComponent {
renderChartTitle() {
let title;
- if (this.props.slice_name) {
- title = this.props.slice_name;
+ if (this.props.slice) {
+ title = this.props.slice.slice_name;
} else {
title = `[${this.props.table_name}] - untitled`;
}
return title;
}
+ renderAlert() {
+ const msg = (
+
+ {this.props.alert}
+
+
);
+ return (
+
+
this.setState({ showStackTrace: !this.state.showStackTrace })}
+ >
+ {msg}
+
+ {this.props.queryResponse && this.props.queryResponse.stacktrace &&
+
+
+ {this.props.queryResponse.stacktrace}
+
+
+ }
+
);
+ }
+
renderChart() {
if (this.props.alert) {
- return (
-
- {this.props.alert}
-
-
- );
+ return this.renderAlert();
}
const loading = this.props.chartStatus === 'loading';
return (
@@ -170,6 +204,9 @@ class ChartContainer extends React.PureComponent {
}
render() {
+ if (this.props.standalone) {
+ return this.renderChart();
+ }
return (
{this.renderChartTitle()}
- {this.props.slice_id &&
+ {this.props.slice &&
@@ -195,7 +232,7 @@ class ChartContainer extends React.PureComponent {
>
@@ -208,16 +245,15 @@ class ChartContainer extends React.PureComponent {
startTime={this.props.chartUpdateStartTime}
endTime={this.props.chartUpdateEndTime}
isRunning={this.props.chartStatus === 'loading'}
- state={CHART_STATUS_MAP[this.props.chartStatus]}
+ status={CHART_STATUS_MAP[this.props.chartStatus]}
style={{ fontSize: '10px', marginRight: '5px' }}
/>
- {this.state.mockSlice &&
-
- }
+
}
@@ -232,21 +268,24 @@ class ChartContainer extends React.PureComponent {
ChartContainer.propTypes = propTypes;
function mapStateToProps(state) {
+ const formData = getFormDataFromFields(state.fields);
return {
- containerId: `slice-container-${state.viz.form_data.slice_id}`,
- slice_id: state.viz.form_data.slice_id,
- slice_name: state.viz.form_data.slice_name,
- viz_type: state.viz.form_data.viz_type,
- can_download: state.can_download,
- chartUpdateStartTime: state.chartUpdateStartTime,
- chartUpdateEndTime: state.chartUpdateEndTime,
- query: state.viz.query,
- column_formats: state.viz.column_formats,
- chartStatus: state.chartStatus,
- isStarred: state.isStarred,
alert: state.chartAlert,
- table_name: state.viz.form_data.datasource_name,
+ can_download: state.can_download,
+ chartStatus: state.chartStatus,
+ chartUpdateEndTime: state.chartUpdateEndTime,
+ chartUpdateStartTime: state.chartUpdateStartTime,
+ column_formats: state.datasource ? state.datasource.column_formats : null,
+ containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container',
+ formData,
+ latestQueryFormData: state.latestQueryFormData,
+ isStarred: state.isStarred,
queryResponse: state.queryResponse,
+ slice: state.slice,
+ standalone: state.standalone,
+ table_name: formData.datasource_name,
+ viz_type: formData.viz_type,
+ triggerRender: state.triggerRender,
};
}
diff --git a/superset/assets/javascripts/explorev2/components/ControlHeader.jsx b/superset/assets/javascripts/explorev2/components/ControlHeader.jsx
index 8d2ca210ce..66940da828 100644
--- a/superset/assets/javascripts/explorev2/components/ControlHeader.jsx
+++ b/superset/assets/javascripts/explorev2/components/ControlHeader.jsx
@@ -6,41 +6,72 @@ const propTypes = {
label: PropTypes.string.isRequired,
description: PropTypes.string,
validationErrors: PropTypes.array,
+ renderTrigger: PropTypes.bool,
+ rightNode: PropTypes.node,
};
const defaultProps = {
- description: null,
validationErrors: [],
+ renderTrigger: false,
};
-export default function ControlHeader({ label, description, validationErrors }) {
+export default function ControlHeader({
+ label, description, validationErrors, renderTrigger, rightNode }) {
const hasError = (validationErrors.length > 0);
return (
-
- {hasError ?
- {label} :
- {label}
- }
- {' '}
- {(validationErrors.length > 0) &&
-
-
- {validationErrors.join(' ')}
-
- }
- >
-
-
+
+
+
+ {hasError ?
+ {label} :
+ {label}
+ }
{' '}
-
+ {(validationErrors.length > 0) &&
+
+
+ {validationErrors.join(' ')}
+
+ }
+ >
+
+
+ {' '}
+
+ }
+ {description &&
+
+
+ {' '}
+
+ }
+ {renderTrigger &&
+
+
+ Takes effect on chart immediatly
+
+ }
+ >
+
+
+ {' '}
+
+ }
+
+
+ {rightNode &&
+
+ {rightNode}
+
}
- {description &&
-
- }
-
+
+
);
}
diff --git a/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx b/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx
index e5fdec909a..ee228a051e 100644
--- a/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx
@@ -4,10 +4,11 @@ import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import { Panel, Alert } from 'react-bootstrap';
-import visTypes, { sectionsToRender } from '../stores/visTypes';
+import { sectionsToRender } from '../stores/visTypes';
import ControlPanelSection from './ControlPanelSection';
import FieldSetRow from './FieldSetRow';
import FieldSet from './FieldSet';
+import fields from '../stores/fields';
const propTypes = {
datasource_type: PropTypes.string.isRequired,
@@ -23,44 +24,19 @@ const propTypes = {
class ControlPanelsContainer extends React.Component {
constructor(props) {
super(props);
- this.fieldOverrides = this.fieldOverrides.bind(this);
- this.getFieldData = this.getFieldData.bind(this);
this.removeAlert = this.removeAlert.bind(this);
+ this.getFieldData = this.getFieldData.bind(this);
}
- componentWillMount() {
- const datasource_id = this.props.form_data.datasource;
- const datasource_type = this.props.datasource_type;
- if (datasource_id) {
- this.props.actions.fetchDatasourceMetadata(datasource_id, datasource_type);
+ getFieldData(fieldName) {
+ const mapF = fields[fieldName].mapStateToProps;
+ if (mapF) {
+ return Object.assign({}, this.props.fields[fieldName], mapF(this.props.exploreState));
}
- }
- componentWillReceiveProps(nextProps) {
- if (nextProps.form_data.datasource !== this.props.form_data.datasource) {
- if (nextProps.form_data.datasource) {
- this.props.actions.fetchDatasourceMetadata(
- nextProps.form_data.datasource, nextProps.datasource_type);
- }
- }
- }
- getFieldData(fs) {
- const fieldOverrides = this.fieldOverrides();
- let fieldData = this.props.fields[fs] || {};
- if (fieldOverrides.hasOwnProperty(fs)) {
- const overrideData = fieldOverrides[fs];
- fieldData = Object.assign({}, fieldData, overrideData);
- }
- if (fieldData.mapStateToProps) {
- Object.assign(fieldData, fieldData.mapStateToProps(this.props.exploreState));
- }
- return fieldData;
+ return this.props.fields[fieldName];
}
sectionsToRender() {
return sectionsToRender(this.props.form_data.viz_type, this.props.datasource_type);
}
- fieldOverrides() {
- const viz = visTypes[this.props.form_data.viz_type];
- return viz.fieldOverrides || {};
- }
removeAlert() {
this.props.actions.removeControlPanelAlert();
}
@@ -78,7 +54,7 @@ class ControlPanelsContainer extends React.Component {
/>
}
- {!this.props.isDatasourceMetaLoading && this.sectionsToRender().map((section) => (
+ {this.sectionsToRender().map((section) => (
))}
diff --git a/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx b/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx
new file mode 100644
index 0000000000..769c064846
--- /dev/null
+++ b/superset/assets/javascripts/explorev2/components/DisplayQueryButton.jsx
@@ -0,0 +1,59 @@
+import React, { PropTypes } from 'react';
+import ModalTrigger from './../../components/ModalTrigger';
+import SyntaxHighlighter from 'react-syntax-highlighter';
+import { github } from 'react-syntax-highlighter/dist/styles';
+
+const $ = window.$ = require('jquery');
+
+const propTypes = {
+ queryEndpoint: PropTypes.string.isRequired,
+};
+
+export default class DisplayQueryButton extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ modalBody: ,
+ };
+ }
+ beforeOpen() {
+ this.setState({
+ modalBody:
+ (
),
+ });
+ $.ajax({
+ type: 'GET',
+ url: this.props.queryEndpoint,
+ success: (data) => {
+ const modalBody = data.language ?
+
+ {data.query}
+
+ :
+ {data.query}
;
+ this.setState({ modalBody });
+ },
+ error(data) {
+ this.setState({ modalBody: ({data.error}
) });
+ },
+ });
+ }
+ render() {
+ return (
+ Query}
+ modalTitle="Query"
+ bsSize="large"
+ beforeOpen={this.beforeOpen.bind(this)}
+ modalBody={this.state.modalBody}
+ />
+ );
+ }
+}
+
+DisplayQueryButton.propTypes = propTypes;
diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx b/superset/assets/javascripts/explorev2/components/EmbedCodeButton.jsx
similarity index 100%
rename from superset/assets/javascripts/explore/components/EmbedCodeButton.jsx
rename to superset/assets/javascripts/explorev2/components/EmbedCodeButton.jsx
diff --git a/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx b/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx
new file mode 100644
index 0000000000..5823c93bf6
--- /dev/null
+++ b/superset/assets/javascripts/explorev2/components/ExploreActionButtons.jsx
@@ -0,0 +1,53 @@
+import React, { PropTypes } from 'react';
+import cx from 'classnames';
+import URLShortLinkButton from './URLShortLinkButton';
+import EmbedCodeButton from './EmbedCodeButton';
+import DisplayQueryButton from './DisplayQueryButton';
+
+const propTypes = {
+ canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
+ slice: PropTypes.object,
+ queryEndpoint: PropTypes.string,
+};
+
+export default function ExploreActionButtons({ canDownload, slice, queryEndpoint }) {
+ const exportToCSVClasses = cx('btn btn-default btn-sm', {
+ 'disabled disabledButton': !canDownload,
+ });
+ if (slice) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
+
+ExploreActionButtons.propTypes = propTypes;
diff --git a/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
index 83bdf18eae..83117a6e36 100644
--- a/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
@@ -1,24 +1,27 @@
/* eslint camelcase: 0 */
-import React from 'react';
+import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import ChartContainer from './ChartContainer';
import ControlPanelsContainer from './ControlPanelsContainer';
import SaveModal from './SaveModal';
-import QueryAndSaveBtns from '../../explore/components/QueryAndSaveBtns';
-import { autoQueryFields } from '../stores/fields';
+import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrl } from '../exploreUtils';
+import { getFormDataFromFields } from '../stores/store';
const propTypes = {
- form_data: React.PropTypes.object.isRequired,
- actions: React.PropTypes.object.isRequired,
- datasource_type: React.PropTypes.string.isRequired,
- chartStatus: React.PropTypes.string.isRequired,
- fields: React.PropTypes.object.isRequired,
+ actions: PropTypes.object.isRequired,
+ datasource_type: PropTypes.string.isRequired,
+ chartStatus: PropTypes.string.isRequired,
+ fields: PropTypes.object.isRequired,
+ forcedHeight: PropTypes.string,
+ form_data: PropTypes.object.isRequired,
+ standalone: PropTypes.bool.isRequired,
+ triggerQuery: PropTypes.bool.isRequired,
+ queryRequest: PropTypes.object,
};
-
class ExploreViewContainer extends React.Component {
constructor(props) {
super(props);
@@ -29,17 +32,23 @@ class ExploreViewContainer extends React.Component {
}
componentDidMount() {
+ this.props.actions.fetchDatasources();
window.addEventListener('resize', this.handleResize.bind(this));
- this.runQuery();
}
- componentWillReceiveProps(nextProps) {
- const refreshChart = Object.keys(nextProps.form_data).some((field) => (
- nextProps.form_data[field] !== this.props.form_data[field]
- && autoQueryFields.indexOf(field) !== -1)
- );
- if (refreshChart) {
- this.onQuery();
+ componentWillReceiveProps(np) {
+ if (np.fields.viz_type.value !== this.props.fields.viz_type.value) {
+ this.props.actions.resetFields();
+ this.props.actions.triggerQuery();
+ }
+ if (np.fields.datasource.value !== this.props.fields.datasource.value) {
+ this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
+ }
+ }
+
+ componentDidUpdate() {
+ if (this.props.triggerQuery) {
+ this.runQuery();
}
}
@@ -48,19 +57,26 @@ class ExploreViewContainer extends React.Component {
}
onQuery() {
+ // remove alerts when query
+ this.props.actions.removeControlPanelAlert();
+ this.props.actions.removeChartAlert();
+
this.runQuery();
history.pushState(
{},
document.title,
- getExploreUrl(this.props.form_data, this.props.datasource_type)
- );
- // remove alerts when query
- this.props.actions.removeControlPanelAlert();
- this.props.actions.removeChartAlert();
+ getExploreUrl(this.props.form_data));
+ }
+
+ onStop() {
+ this.props.actions.chartUpdateStopped(this.props.queryRequest);
}
getHeight() {
- const navHeight = 90;
+ if (this.props.forcedHeight) {
+ return this.props.forcedHeight + 'px';
+ }
+ const navHeight = this.props.standalone ? 0 : 90;
return `${window.innerHeight - navHeight}px`;
}
@@ -101,8 +117,18 @@ class ExploreViewContainer extends React.Component {
}
return errorMessage;
}
+ renderChartContainer() {
+ return (
+ );
+ }
render() {
+ if (this.props.standalone) {
+ return this.renderChartContainer();
+ }
return (
}
@@ -126,7 +151,8 @@ class ExploreViewContainer extends React.Component {
canAdd="True"
onQuery={this.onQuery.bind(this)}
onSave={this.toggleModal.bind(this)}
- disabled={this.props.chartStatus === 'loading'}
+ onStop={this.onStop.bind(this)}
+ loading={this.props.chartStatus === 'loading'}
errorMessage={this.renderErrorMessage()}
/>
@@ -134,14 +160,10 @@ class ExploreViewContainer extends React.Component {
actions={this.props.actions}
form_data={this.props.form_data}
datasource_type={this.props.datasource_type}
- onQuery={this.onQuery.bind(this)}
/>
-
+ {this.renderChartContainer()}
@@ -152,11 +174,16 @@ class ExploreViewContainer extends React.Component {
ExploreViewContainer.propTypes = propTypes;
function mapStateToProps(state) {
+ const form_data = getFormDataFromFields(state.fields);
return {
chartStatus: state.chartStatus,
datasource_type: state.datasource_type,
fields: state.fields,
- form_data: state.viz.form_data,
+ form_data,
+ standalone: state.standalone,
+ triggerQuery: state.triggerQuery,
+ forcedHeight: state.forced_height,
+ queryRequest: state.queryRequest,
};
}
diff --git a/superset/assets/javascripts/explorev2/components/FieldSet.jsx b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
index 1c67eb9ae8..48a9d17301 100644
--- a/superset/assets/javascripts/explorev2/components/FieldSet.jsx
+++ b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
@@ -1,17 +1,19 @@
import React, { PropTypes } from 'react';
-import TextField from './TextField';
import CheckboxField from './CheckboxField';
-import TextAreaField from './TextAreaField';
-import SelectField from './SelectField';
-import FilterField from './FilterField';
import ControlHeader from './ControlHeader';
+import FilterField from './FilterField';
+import HiddenField from './HiddenField';
+import SelectField from './SelectField';
+import TextAreaField from './TextAreaField';
+import TextField from './TextField';
const fieldMap = {
- TextField,
CheckboxField,
- TextAreaField,
- SelectField,
FilterField,
+ HiddenField,
+ SelectField,
+ TextAreaField,
+ TextField,
};
const fieldTypes = Object.keys(fieldMap);
@@ -25,6 +27,8 @@ const propTypes = {
places: PropTypes.number,
validators: PropTypes.array,
validationErrors: PropTypes.array,
+ renderTrigger: PropTypes.bool,
+ rightNode: PropTypes.node,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
@@ -33,6 +37,7 @@ const propTypes = {
};
const defaultProps = {
+ renderTrigger: false,
validators: [],
validationErrors: [],
};
@@ -65,12 +70,15 @@ export default class FieldSet extends React.PureComponent {
}
render() {
const FieldType = fieldMap[this.props.type];
+ const divStyle = this.props.hidden ? { display: 'none' } : null;
return (
-
+
{},
removeFilter: () => {},
choices: [],
@@ -21,6 +22,11 @@ const defaultProps = {
};
export default class Filter extends React.Component {
+ constructor(props) {
+ super(props);
+ this.opChoices = this.props.having ? ['==', '!=', '>', '<', '>=', '<=']
+ : ['in', 'not in'];
+ }
fetchFilterValues(col) {
if (!this.props.datasource) {
return;
@@ -61,24 +67,27 @@ export default class Filter extends React.Component {
if (!filter.choices) {
this.fetchFilterValues(filter.col);
}
+ }
+ if (this.props.having) {
+ // druid having filter
return (
-
);
}
return (
-
);
}
@@ -102,7 +111,7 @@ export default class Filter extends React.Component {