From 20915457ff5e9236155de718dd859ab943729428 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 9 Aug 2017 17:18:10 +0200 Subject: [PATCH 01/10] import logging (#3264) --- superset/migrations/versions/65903709c321_allow_dml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superset/migrations/versions/65903709c321_allow_dml.py b/superset/migrations/versions/65903709c321_allow_dml.py index d14c6a98235cd..9860c503a9ba3 100644 --- a/superset/migrations/versions/65903709c321_allow_dml.py +++ b/superset/migrations/versions/65903709c321_allow_dml.py @@ -6,6 +6,8 @@ """ +import logging + # revision identifiers, used by Alembic. revision = '65903709c321' down_revision = '4500485bde7d' From be01851ef7a694b38f2205414b7c7e11d5968a14 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 9 Aug 2017 09:04:29 -0700 Subject: [PATCH 02/10] Relying on FAB for font-awesome.min.css (#3261) --- MANIFEST.in | 2 -- superset/templates/superset/basic.html | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index e57373a46e545..a1b3ffac9b566 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,5 @@ recursive-include superset/templates * recursive-include superset/static * -recursive-exclude superset/static/assets/node_modules * -recursive-include superset/static/assets/node_modules/font-awesome * recursive-exclude superset/static/docs * recursive-exclude superset/static/spec * recursive-exclude tests * diff --git a/superset/templates/superset/basic.html b/superset/templates/superset/basic.html index e146c51758b00..d4f2e1f55776c 100644 --- a/superset/templates/superset/basic.html +++ b/superset/templates/superset/basic.html @@ -12,7 +12,7 @@ {% block head_meta %}{% endblock %} {% block head_css %} - + From 6da68ab271d3afff12203b9bbae01f07c6435a85 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Wed, 9 Aug 2017 18:09:02 +0200 Subject: [PATCH 03/10] Explicitly add Flask as dependancy (#3252) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ad86b08959ae0..afc767c2c5cd6 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ def get_git_sha(): 'celery==3.1.25', 'colorama==0.3.9', 'cryptography==1.9', + 'flask==0.12.2', 'flask-appbuilder==1.9.1', 'flask-cache==0.13.1', 'flask-migrate==2.0.3', From cc36428260f979d78e69d86dac0e6a4ba6b17780 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 9 Aug 2017 09:10:12 -0700 Subject: [PATCH 04/10] Modernize SQLA pessimistic handling (#3256) Looks like SQLAlchemy has redefined the best practice around pessimistic connection handling. --- superset/__init__.py | 2 +- superset/utils.py | 49 ++++++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/superset/__init__.py b/superset/__init__.py index fceff86159ff8..b8fccd9934f17 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -69,7 +69,7 @@ def get_js_manifest(): if conf.get('WTF_CSRF_ENABLED'): csrf = CSRFProtect(app) -utils.pessimistic_connection_handling(db.engine.pool) +utils.pessimistic_connection_handling(db.engine) cache = utils.setup_cache(app, conf.get('CACHE_CONFIG')) tables_cache = utils.setup_cache(app, conf.get('TABLE_NAMES_CACHE_CONFIG')) diff --git a/superset/utils.py b/superset/utils.py index 49a0f47355f31..ed841862ea72e 100644 --- a/superset/utils.py +++ b/superset/utils.py @@ -40,7 +40,7 @@ import markdown as md from past.builtins import basestring from pydruid.utils.having import Having -from sqlalchemy import event, exc +from sqlalchemy import event, exc, select from sqlalchemy.types import TypeDecorator, TEXT logging.getLogger('MARKDOWN').setLevel(logging.INFO) @@ -436,19 +436,42 @@ def __exit__(self, type, value, traceback): logging.warning("timeout can't be used in the current context") logging.exception(e) -def pessimistic_connection_handling(target): - @event.listens_for(target, "checkout") - def ping_connection(dbapi_connection, connection_record, connection_proxy): - """ - Disconnect Handling - Pessimistic, taken from: - http://docs.sqlalchemy.org/en/rel_0_9/core/pooling.html - """ - cursor = dbapi_connection.cursor() + +def pessimistic_connection_handling(some_engine): + @event.listens_for(some_engine, "engine_connect") + def ping_connection(connection, branch): + if branch: + # "branch" refers to a sub-connection of a connection, + # we don't want to bother pinging on these. + return + + # turn off "close with result". This flag is only used with + # "connectionless" execution, otherwise will be False in any case + save_should_close_with_result = connection.should_close_with_result + connection.should_close_with_result = False + try: - cursor.execute("SELECT 1") - except: - raise exc.DisconnectionError() - cursor.close() + # run a SELECT 1. use a core select() so that + # the SELECT of a scalar value without a table is + # appropriately formatted for the backend + connection.scalar(select([1])) + except exc.DBAPIError as err: + # catch SQLAlchemy's DBAPIError, which is a wrapper + # for the DBAPI's exception. It includes a .connection_invalidated + # attribute which specifies if this connection is a "disconnect" + # condition, which is based on inspection of the original exception + # by the dialect in use. + if err.connection_invalidated: + # run the same SELECT again - the connection will re-validate + # itself and establish a new connection. The disconnect detection + # here also causes the whole connection pool to be invalidated + # so that all stale connections are discarded. + connection.scalar(select([1])) + else: + raise + finally: + # restore "close with result" + connection.should_close_with_result = save_should_close_with_result class QueryStatus(object): From 033ba2cb66c4decbbdf6f534a2351c2193196477 Mon Sep 17 00:00:00 2001 From: eeve Date: Thu, 10 Aug 2017 00:12:21 +0800 Subject: [PATCH 05/10] Improve the chart type of Visualize in sqllab (#3241) * Improve the chart type of Visualize in sqllab & Add some css & Fix the link address in the navbar * add vizTypes filter --- .../SqlLab/components/VisualizeModal.jsx | 17 +++++++++++------ .../javascripts/explore/stores/visTypes.js | 4 ++++ superset/assets/stylesheets/superset.css | 19 +++++++++++++++++++ superset/templates/appbuilder/navbar.html | 4 ++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx index dce820cd677a5..56a66d3a66ea0 100644 --- a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx +++ b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx @@ -13,13 +13,18 @@ import { getExploreUrl } from '../../explore/exploreUtils'; import * as actions from '../actions'; import { VISUALIZE_VALIDATION_ERRORS } from '../constants'; import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; +import visTypes from '../../explore/stores/visTypes'; -const CHART_TYPES = [ - { value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false }, - { value: 'pie', label: 'Pie Chart', requiresTime: false }, - { value: 'line', label: 'Time Series - Line Chart', requiresTime: true }, - { value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true }, -]; +const CHART_TYPES = Object.keys(visTypes) + .filter(typeName => !!visTypes[typeName].showOnExplore) + .map((typeName) => { + const vis = visTypes[typeName]; + return { + value: typeName, + label: vis.label, + requiresTime: !!vis.requiresTime, + }; + }); const propTypes = { actions: PropTypes.object.isRequired, diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js index 1df8e11c7267e..e937b036ab2db 100644 --- a/superset/assets/javascripts/explore/stores/visTypes.js +++ b/superset/assets/javascripts/explore/stores/visTypes.js @@ -78,6 +78,7 @@ export const sections = { export const visTypes = { dist_bar: { label: 'Distribution - Bar Chart', + showOnExplore: true, controlPanelSections: [ { label: 'Chart Options', @@ -108,6 +109,7 @@ export const visTypes = { pie: { label: 'Pie Chart', + showOnExplore: true, controlPanelSections: [ { label: null, @@ -124,6 +126,7 @@ export const visTypes = { line: { label: 'Time Series - Line Chart', + showOnExplore: true, requiresTime: true, controlPanelSections: [ sections.NVD3TimeSeries[0], @@ -194,6 +197,7 @@ export const visTypes = { bar: { label: 'Time Series - Bar Chart', + showOnExplore: true, requiresTime: true, controlPanelSections: [ sections.NVD3TimeSeries[0], diff --git a/superset/assets/stylesheets/superset.css b/superset/assets/stylesheets/superset.css index 20041330c9434..aa5678cea4080 100644 --- a/superset/assets/stylesheets/superset.css +++ b/superset/assets/stylesheets/superset.css @@ -237,3 +237,22 @@ div.widget .slice_container { .Select-menu-outer { z-index: 10 !important; } + +/** not found record **/ +.panel b { + display: inline-block; + width: 98%; + padding: 2rem; + margin: 0 1% 20px 1%; + background: #f8f8f8; +} + +/** table on both sides of the gap **/ +.panel .table-responsive{ + margin: 0 1%; +} +@media screen and (max-width: 767px) { + .panel .table-responsive{ + width: 98%; + } +} \ No newline at end of file diff --git a/superset/templates/appbuilder/navbar.html b/superset/templates/appbuilder/navbar.html index b5c3d0a074beb..0ea2daec5fb15 100644 --- a/superset/templates/appbuilder/navbar.html +++ b/superset/templates/appbuilder/navbar.html @@ -34,12 +34,12 @@
  • - +  
  • - +  
  • From 327c052456c7c4a3aa8e5e90eb2874915d539913 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 9 Aug 2017 09:52:43 -0700 Subject: [PATCH 06/10] [webpack] break CSS and JS files while webpackin' (#3262) * [webpack] break CSS and JS files while webpackin' * cleaning up some templates * Fix pylint issue --- superset/__init__.py | 22 ++++++++------- superset/assets/javascripts/SqlLab/index.jsx | 2 +- .../SqlLab/{main.css => main.less} | 1 - superset/assets/javascripts/explore/index.jsx | 2 +- .../javascripts/{css-theme.js => theme.js} | 1 + superset/assets/package.json | 1 + .../{superset.css => superset.less} | 0 superset/assets/webpack.config.js | 27 ++++++++++++------- superset/templates/superset/base.html | 15 +++-------- superset/templates/superset/basic.html | 16 ++++++----- superset/templates/superset/dashboard.html | 8 ------ superset/templates/superset/explore.html | 23 ---------------- superset/templates/superset/index.html | 6 ----- .../superset/partials/_script_tag.html | 2 +- superset/templates/superset/profile.html | 8 ------ superset/templates/superset/sqllab.html | 8 ------ superset/templates/superset/welcome.html | 7 ----- superset/views/core.py | 24 +++++++++++------ 18 files changed, 64 insertions(+), 109 deletions(-) rename superset/assets/javascripts/SqlLab/{main.css => main.less} (99%) rename superset/assets/javascripts/{css-theme.js => theme.js} (70%) rename superset/assets/stylesheets/{superset.css => superset.less} (100%) delete mode 100644 superset/templates/superset/explore.html delete mode 100644 superset/templates/superset/index.html delete mode 100644 superset/templates/superset/profile.html delete mode 100644 superset/templates/superset/sqllab.html diff --git a/superset/__init__.py b/superset/__init__.py index b8fccd9934f17..9576458804337 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -32,19 +32,21 @@ app.config.from_object(CONFIG_MODULE) conf = app.config +# Handling manifest file logic at app start +MANIFEST_FILE = APP_DIR + '/static/assets/dist/manifest.json' +get_manifest_file = lambda x: x +manifest = {} +try: + with open(MANIFEST_FILE, 'r') as f: + manifest = json.load(f) + get_manifest_file = lambda x: '/static/assets/dist/' + manifest.get(x, '') +except Exception: + print("no manifest file found at " + MANIFEST_FILE) + @app.context_processor def get_js_manifest(): - manifest = {} - try: - with open(APP_DIR + '/static/assets/dist/manifest.json', 'r') as f: - manifest = json.load(f) - except Exception as e: - print( - "no manifest file found at " + - APP_DIR + "/static/assets/dist/manifest.json" - ) - return dict(js_manifest=manifest) + return dict(js_manifest=get_manifest_file) for bp in conf.get('BLUEPRINTS'): diff --git a/superset/assets/javascripts/SqlLab/index.jsx b/superset/assets/javascripts/SqlLab/index.jsx index ba09924720177..4e2eae898c6db 100644 --- a/superset/assets/javascripts/SqlLab/index.jsx +++ b/superset/assets/javascripts/SqlLab/index.jsx @@ -10,7 +10,7 @@ import { initJQueryAjax } from '../modules/utils'; import App from './components/App'; import { appSetup } from '../common'; -import './main.css'; +import './main.less'; import '../../stylesheets/reactable-pagination.css'; import '../components/FilterableTable/FilterableTableStyles.css'; diff --git a/superset/assets/javascripts/SqlLab/main.css b/superset/assets/javascripts/SqlLab/main.less similarity index 99% rename from superset/assets/javascripts/SqlLab/main.css rename to superset/assets/javascripts/SqlLab/main.less index ad2bb37c0e3e6..d5dab4c03cc09 100644 --- a/superset/assets/javascripts/SqlLab/main.css +++ b/superset/assets/javascripts/SqlLab/main.less @@ -161,7 +161,6 @@ div.Workspace { margin: 0px; border: none; font-size: 12px; - line-height: @line-height-base; background-color: transparent !important; } diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx index 0fe4fcaba9524..8d29d9eaf0da9 100644 --- a/superset/assets/javascripts/explore/index.jsx +++ b/superset/assets/javascripts/explore/index.jsx @@ -19,7 +19,7 @@ import '../../stylesheets/reactable-pagination.css'; appSetup(); initJQueryAjax(); -const exploreViewContainer = document.getElementById('js-explore-view-container'); +const exploreViewContainer = document.getElementById('app'); const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap')); const controls = getControlsState(bootstrapData, bootstrapData.form_data); delete bootstrapData.form_data; diff --git a/superset/assets/javascripts/css-theme.js b/superset/assets/javascripts/theme.js similarity index 70% rename from superset/assets/javascripts/css-theme.js rename to superset/assets/javascripts/theme.js index 8fab234f65f6f..68a7a8ac5f021 100644 --- a/superset/assets/javascripts/css-theme.js +++ b/superset/assets/javascripts/theme.js @@ -1,2 +1,3 @@ import '../stylesheets/less/index.less'; import '../stylesheets/react-select/select.less'; +import '../stylesheets/superset.less'; diff --git a/superset/assets/package.json b/superset/assets/package.json index 1422492bfea58..2dde6b337a95d 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -108,6 +108,7 @@ "eslint-plugin-jsx-a11y": "^5.0.3", "eslint-plugin-react": "^7.0.1", "exports-loader": "^0.6.3", + "extract-text-webpack-plugin": "2.1.2", "file-loader": "^0.11.1", "github-changes": "^1.0.4", "ignore-styles": "^5.0.1", diff --git a/superset/assets/stylesheets/superset.css b/superset/assets/stylesheets/superset.less similarity index 100% rename from superset/assets/stylesheets/superset.css rename to superset/assets/stylesheets/superset.less diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js index e3413e5d14c2a..10e41c96bd8ff 100644 --- a/superset/assets/webpack.config.js +++ b/superset/assets/webpack.config.js @@ -2,6 +2,7 @@ const webpack = require('webpack'); const path = require('path'); const ManifestPlugin = require('webpack-manifest-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); // input dir const APP_DIR = path.resolve(__dirname, './'); @@ -14,7 +15,7 @@ const config = { fs: 'empty', }, entry: { - 'css-theme': APP_DIR + '/javascripts/css-theme.js', + theme: APP_DIR + '/javascripts/theme.js', common: APP_DIR + '/javascripts/common.js', addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'], dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'], @@ -64,11 +65,24 @@ const config = { include: APP_DIR + '/node_modules/mapbox-gl/js', loader: 'babel-loader', }, - /* for require('*.css') */ + // Extract css files { test: /\.css$/, include: APP_DIR, - loader: 'style-loader!css-loader', + loader: ExtractTextPlugin.extract({ + use: ['css-loader'], + fallback: 'style-loader', + }), + }, + // Optionally extract less files + // or any other compile-to-css language + { + test: /\.less$/, + include: APP_DIR, + loader: ExtractTextPlugin.extract({ + use: ['css-loader', 'less-loader'], + fallback: 'style-loader', + }), }, /* for css linking images */ { @@ -92,12 +106,6 @@ const config = { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader', }, - /* for require('*.less') */ - { - test: /\.less$/, - include: APP_DIR, - loader: 'style-loader!css-loader!less-loader', - }, /* for mapbox */ { test: /\.json$/, @@ -123,6 +131,7 @@ const config = { NODE_ENV: JSON.stringify(process.env.NODE_ENV), }, }), + new ExtractTextPlugin('[name].[chunkhash].css'), ], }; if (process.env.NODE_ENV === 'production') { diff --git a/superset/templates/superset/base.html b/superset/templates/superset/base.html index 1a9c42cd89ca3..b47104957a80d 100644 --- a/superset/templates/superset/base.html +++ b/superset/templates/superset/base.html @@ -1,21 +1,12 @@ {% extends "appbuilder/baselayout.html" %} {% block head_css %} - - {{super()}} - {% endblock %} - - {% block head_js %} - {{super()}} - {% with filename="css-theme" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} + + {% endblock %} {% block tail_js %} {{super()}} - {% with filename="common" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} + {% endblock %} diff --git a/superset/templates/superset/basic.html b/superset/templates/superset/basic.html index d4f2e1f55776c..87b058d13eff5 100644 --- a/superset/templates/superset/basic.html +++ b/superset/templates/superset/basic.html @@ -12,15 +12,16 @@ {% block head_meta %}{% endblock %} {% block head_css %} - - - + + + + {% if entry %} + + {% endif %} {% endblock %} {% block head_js %} - {% with filename="css-theme" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} + {% endblock %} {% block tail_js %} + {% if entry %} + + {% endif %} {% endblock %} diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html index 8fe4a6ed5bfc1..e297e5116a604 100644 --- a/superset/templates/superset/dashboard.html +++ b/superset/templates/superset/dashboard.html @@ -1,14 +1,6 @@ {% extends "superset/basic.html" %} -{% block head_js %} - {{ super() }} - {% with filename="dashboard" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} -{% block title %}[dashboard] {{ dashboard_title }}{% endblock %} {% block body %} -
    -{% endblock %} - -{% block tail_js %} - {{ super() }} - {% with filename="explore" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} diff --git a/superset/templates/superset/index.html b/superset/templates/superset/index.html deleted file mode 100644 index bd0455745e5d0..0000000000000 --- a/superset/templates/superset/index.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "superset/basic.html" %} - -{% block tail_js %} - {{ super() }} - -{% endblock %} diff --git a/superset/templates/superset/partials/_script_tag.html b/superset/templates/superset/partials/_script_tag.html index e7bee3fede0f3..d8d6f6fd382ae 100644 --- a/superset/templates/superset/partials/_script_tag.html +++ b/superset/templates/superset/partials/_script_tag.html @@ -1,5 +1,5 @@ {% block tail_js %} {% endblock %} diff --git a/superset/templates/superset/profile.html b/superset/templates/superset/profile.html deleted file mode 100644 index df5233219bbec..0000000000000 --- a/superset/templates/superset/profile.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "superset/basic.html" %} - -{% block tail_js %} - {{ super() }} - {% with filename="profile" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} diff --git a/superset/templates/superset/sqllab.html b/superset/templates/superset/sqllab.html deleted file mode 100644 index bd41b1480cad5..0000000000000 --- a/superset/templates/superset/sqllab.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "superset/basic.html" %} - -{% block tail_js %} - {{ super() }} - {% with filename="sqllab" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} diff --git a/superset/templates/superset/welcome.html b/superset/templates/superset/welcome.html index 09bc8b2eaa1d3..4db2cd3ab5e9a 100644 --- a/superset/templates/superset/welcome.html +++ b/superset/templates/superset/welcome.html @@ -1,12 +1,5 @@ {% extends "superset/basic.html" %} -{% block head_js %} - {{ super() }} - {% with filename="welcome" %} - {% include "superset/partials/_script_tag.html" %} - {% endwith %} -{% endblock %} - {% block title %}{{ _("Welcome!") }}{% endblock %} {% block body %} diff --git a/superset/views/core.py b/superset/views/core.py index a10e8848e2de0..4b20e3cea8ea0 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1093,12 +1093,16 @@ def explore(self, datasource_type, datasource_id): table_name = datasource.table_name \ if datasource_type == 'table' \ else datasource.datasource_name + if slc: + title = "[slice] " + slc.slice_name + else: + title = "[explore] " + table_name return self.render_template( - "superset/explore.html", + "superset/basic.html", bootstrap_data=json.dumps(bootstrap_data), - slice=slc, - standalone_mode=standalone, - table_name=table_name) + entry='explore', + title=title, + standalone_mode=standalone) @api @has_access_api @@ -1723,7 +1727,8 @@ def dashboard(**kwargs): # noqa return self.render_template( "superset/dashboard.html", - dashboard_title=dash.dashboard_title, + entry='dashboard', + title='[dashboard] ' + dash.dashboard_title, bootstrap_data=json.dumps(bootstrap_data), ) @@ -2232,7 +2237,8 @@ def welcome(self): """Personalized welcome page""" if not g.user or not g.user.get_id(): return redirect(appbuilder.get_url_for_login) - return self.render_template('superset/welcome.html', utils=utils) + return self.render_template( + 'superset/welcome.html', entry='welcome', utils=utils) @has_access @expose("/profile//") @@ -2273,9 +2279,10 @@ def profile(self, username): } } return self.render_template( - 'superset/profile.html', + 'superset/basic.html', title=user.username + "'s profile", navbar_container=True, + entry='profile', bootstrap_data=json.dumps(payload, default=utils.json_iso_dttm_ser) ) @@ -2287,7 +2294,8 @@ def sqllab(self): 'defaultDbId': config.get('SQLLAB_DEFAULT_DBID'), } return self.render_template( - 'superset/sqllab.html', + 'superset/basic.html', + entry='sqllab', bootstrap_data=json.dumps(d, default=utils.json_iso_dttm_ser) ) appbuilder.add_view_no_menu(Superset) From 0cf0860a3d059af379e0c2ee3e08ff5adf6c7cac Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Thu, 10 Aug 2017 02:25:00 +0200 Subject: [PATCH 07/10] Set default ports Druid (#3266) For Druid set the default port for the broker and coordinator. --- superset/connectors/druid/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py index 335f9d26b1fba..6340331ba785a 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -69,11 +69,11 @@ class DruidCluster(Model, AuditMixinNullable): # short unique name, used in permissions cluster_name = Column(String(250), unique=True) coordinator_host = Column(String(255)) - coordinator_port = Column(Integer) + coordinator_port = Column(Integer, default=8081) coordinator_endpoint = Column( String(255), default='druid/coordinator/v1/metadata') broker_host = Column(String(255)) - broker_port = Column(Integer) + broker_port = Column(Integer, default=8082) broker_endpoint = Column(String(255), default='druid/v2') metadata_last_refreshed = Column(DateTime) cache_timeout = Column(Integer) From 57421d14d081695ad6eba3e68f9567793725b270 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 9 Aug 2017 18:06:18 -0700 Subject: [PATCH 08/10] [bugfix] preserve order in groupby (#3268) Recently in https://github.com/apache/incubator-superset/commit/4c3313b01cb508ced8519a68f6479db423974929 I introduced an issue where the order of groupby fields might change. This addresses this issue and will preserve ordering. --- superset/viz.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/superset/viz.py b/superset/viz.py index 6606f012459b7..b7b72ba3f50f4 100755 --- a/superset/viz.py +++ b/superset/viz.py @@ -112,10 +112,13 @@ def get_extra_filters(self): def query_obj(self): """Building a query object""" form_data = self.form_data - groupby = form_data.get("groupby") or [] + gb = form_data.get("groupby") or [] metrics = form_data.get("metrics") or [] columns = form_data.get("columns") or [] - groupby = list(set(groupby + columns)) + groupby = [] + for o in gb + columns: + if o not in groupby: + groupby.append(o) is_timeseries = self.is_timeseries if DTTM_ALIAS in groupby: From 08b7e891a7dd65616aef4177b72b1e8770310d98 Mon Sep 17 00:00:00 2001 From: Alex Guziel Date: Wed, 9 Aug 2017 22:34:39 -0700 Subject: [PATCH 09/10] Use sane Celery defaults to prevent tasks from being delayed (#3267) --- superset/cli.py | 2 +- superset/config.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/superset/cli.py b/superset/cli.py index f6163bb140b09..e4165e6716be7 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -194,7 +194,7 @@ def worker(workers): celery_app.conf.update( CELERYD_CONCURRENCY=config.get("SUPERSET_CELERY_WORKERS")) - worker = celery_worker.worker(app=celery_app) + worker = celery_app.Worker(optimization='fair') worker.run() diff --git a/superset/config.py b/superset/config.py index bc91edd07b097..94d8dfc56a673 100644 --- a/superset/config.py +++ b/superset/config.py @@ -245,6 +245,8 @@ class CeleryConfig(object): CELERY_RESULT_BACKEND = 'db+sqlite:///celery_results.sqlite' CELERY_ANNOTATIONS = {'tasks.add': {'rate_limit': '10/s'}} CELERYD_LOG_LEVEL = 'DEBUG' + CELERYD_PREFETCH_MULTIPLIER = 1 + CELERY_ACKS_LATE = True CELERY_CONFIG = CeleryConfig """ CELERY_CONFIG = None From b3107bb603d2ddae38fec97d81878b2dae339624 Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Thu, 10 Aug 2017 14:21:45 -0700 Subject: [PATCH 10/10] [explore] Split large reducer logic in ExploreViewContainer (#3088) * split reducer logic for ExploreViewContainer * fix saveModal component and unit tests * revert changes in SaveModal_spec. will make another commit just to improve test coverage for SaveModal component. * remove comment-out code * fix merge confilicts --- .../explore/actions/chartActions.js | 70 ++++++++++ .../explore/actions/exploreActions.js | 123 +----------------- .../explore/actions/saveModalActions.js | 57 ++++++++ .../explore/components/ChartContainer.jsx | 38 +++--- .../components/ControlPanelsContainer.jsx | 10 +- .../components/ExploreViewContainer.jsx | 28 ++-- .../explore/components/SaveModal.jsx | 26 ++-- superset/assets/javascripts/explore/index.jsx | 24 ++-- .../explore/reducers/chartReducer.js | 72 ++++++++++ .../explore/reducers/exploreReducer.js | 102 +++------------ .../javascripts/explore/reducers/index.js | 11 ++ .../explore/reducers/saveModalReducer.js | 28 ++++ .../javascripts/explore/chartActions_spec.js | 36 +++++ .../explore/exploreActions_spec.js | 68 +--------- 14 files changed, 364 insertions(+), 329 deletions(-) create mode 100644 superset/assets/javascripts/explore/actions/chartActions.js create mode 100644 superset/assets/javascripts/explore/actions/saveModalActions.js create mode 100644 superset/assets/javascripts/explore/reducers/chartReducer.js create mode 100644 superset/assets/javascripts/explore/reducers/index.js create mode 100644 superset/assets/javascripts/explore/reducers/saveModalReducer.js create mode 100644 superset/assets/spec/javascripts/explore/chartActions_spec.js diff --git a/superset/assets/javascripts/explore/actions/chartActions.js b/superset/assets/javascripts/explore/actions/chartActions.js new file mode 100644 index 0000000000000..6c9291dc41ece --- /dev/null +++ b/superset/assets/javascripts/explore/actions/chartActions.js @@ -0,0 +1,70 @@ +import { getExploreUrl } from '../exploreUtils'; +import { getFormDataFromControls } from '../stores/store'; +import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; +import { triggerQuery } from './exploreActions'; + +const $ = window.$ = require('jquery'); + +export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; +export function chartUpdateStarted(queryRequest, latestQueryFormData) { + return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData }; +} + +export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED'; +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_TIMEOUT = 'CHART_UPDATE_TIMEOUT'; +export function chartUpdateTimeout(statusText) { + return { type: CHART_UPDATE_TIMEOUT, statusText }; +} + +export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED'; +export function chartUpdateFailed(queryResponse) { + return { type: CHART_UPDATE_FAILED, queryResponse }; +} + +export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS'; +export function updateChartStatus(status) { + return { type: UPDATE_CHART_STATUS, status }; +} + +export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED'; +export function chartRenderingFailed(error) { + return { type: CHART_RENDERING_FAILED, error }; +} + +export const RUN_QUERY = 'RUN_QUERY'; +export function runQuery(formData, force = false) { + return function (dispatch, getState) { + const { explore } = getState(); + const lastQueryFormData = getFormDataFromControls(explore.controls); + const url = getExploreUrl(formData, 'json', force); + const queryRequest = $.ajax({ + url, + dataType: 'json', + success(queryResponse) { + dispatch(chartUpdateSucceeded(queryResponse)); + }, + error(err) { + if (err.statusText === 'timeout') { + dispatch(chartUpdateTimeout(err.statusText)); + } else if (err.statusText !== 'abort') { + dispatch(chartUpdateFailed(err.responseJSON)); + } + }, + timeout: QUERY_TIMEOUT_THRESHOLD, + }); + dispatch(chartUpdateStarted(queryRequest, lastQueryFormData)); + dispatch(triggerQuery(false)); + }; +} diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js index d45acd5834b9e..32fa3c5b4745f 100644 --- a/superset/assets/javascripts/explore/actions/exploreActions.js +++ b/superset/assets/javascripts/explore/actions/exploreActions.js @@ -1,6 +1,4 @@ /* eslint camelcase: 0 */ -import { getExploreUrl } from '../exploreUtils'; -import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; const $ = window.$ = require('jquery'); @@ -37,8 +35,8 @@ export function resetControls() { } export const TRIGGER_QUERY = 'TRIGGER_QUERY'; -export function triggerQuery() { - return { type: TRIGGER_QUERY }; +export function triggerQuery(value = true) { + return { type: TRIGGER_QUERY, value }; } export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) { @@ -95,39 +93,6 @@ export function setControlValue(controlName, value, validationErrors) { return { type: SET_FIELD_VALUE, controlName, value, validationErrors }; } -export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; -export function chartUpdateStarted(queryRequest) { - return { type: CHART_UPDATE_STARTED, queryRequest }; -} - -export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED'; -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_TIMEOUT = 'CHART_UPDATE_TIMEOUT'; -export function chartUpdateTimeout(statusText) { - return { type: CHART_UPDATE_TIMEOUT, statusText }; -} - -export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED'; -export function chartUpdateFailed(queryResponse) { - return { type: CHART_UPDATE_FAILED, queryResponse }; -} - -export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED'; -export function chartRenderingFailed(error) { - return { type: CHART_RENDERING_FAILED, error }; -} - export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS'; export function updateExploreEndpoints(jsonUrl, csvUrl, standaloneUrl) { return { type: UPDATE_EXPLORE_ENDPOINTS, jsonUrl, csvUrl, standaloneUrl }; @@ -143,95 +108,11 @@ export function removeChartAlert() { return { type: REMOVE_CHART_ALERT }; } -export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED'; -export function fetchDashboardsSucceeded(choices) { - return { type: FETCH_DASHBOARDS_SUCCEEDED, choices }; -} - -export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED'; -export function fetchDashboardsFailed(userId) { - return { type: FETCH_DASHBOARDS_FAILED, userId }; -} - -export function fetchDashboards(userId) { - return function (dispatch) { - const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userId; - $.ajax({ - type: 'GET', - url, - success: (data) => { - const choices = []; - for (let i = 0; i < data.pks.length; i++) { - choices.push({ value: data.pks[i], label: data.result[i].dashboard_title }); - } - dispatch(fetchDashboardsSucceeded(choices)); - }, - error: () => { - dispatch(fetchDashboardsFailed(userId)); - }, - }); - }; -} - -export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED'; -export function saveSliceFailed() { - return { type: SAVE_SLICE_FAILED }; -} -export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS'; -export function saveSliceSuccess(data) { - return { type: SAVE_SLICE_SUCCESS, data }; -} - -export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT'; -export function removeSaveModalAlert() { - return { type: REMOVE_SAVE_MODAL_ALERT }; -} - -export function saveSlice(url) { - return function (dispatch) { - return $.get(url, (data, status) => { - if (status === 'success') { - dispatch(saveSliceSuccess(data)); - } else { - dispatch(saveSliceFailed()); - } - }); - }; -} - export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE'; export function updateChartTitle(slice_name) { return { type: UPDATE_CHART_TITLE, slice_name }; } -export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS'; -export function updateChartStatus(status) { - return { type: UPDATE_CHART_STATUS, status }; -} - -export const RUN_QUERY = 'RUN_QUERY'; -export function runQuery(formData, force = false) { - return function (dispatch) { - const url = getExploreUrl(formData, 'json', force); - const queryRequest = $.ajax({ - url, - dataType: 'json', - success(queryResponse) { - dispatch(chartUpdateSucceeded(queryResponse)); - }, - error(err) { - if (err.statusText === 'timeout') { - dispatch(chartUpdateTimeout(err.statusText)); - } else if (err.statusText !== 'abort') { - dispatch(chartUpdateFailed(err.responseJSON)); - } - }, - timeout: QUERY_TIMEOUT_THRESHOLD, - }); - dispatch(chartUpdateStarted(queryRequest)); - }; -} - export const RENDER_TRIGGERED = 'RENDER_TRIGGERED'; export function renderTriggered() { return { type: RENDER_TRIGGERED }; diff --git a/superset/assets/javascripts/explore/actions/saveModalActions.js b/superset/assets/javascripts/explore/actions/saveModalActions.js new file mode 100644 index 0000000000000..b1111287f288c --- /dev/null +++ b/superset/assets/javascripts/explore/actions/saveModalActions.js @@ -0,0 +1,57 @@ +const $ = window.$ = require('jquery'); + +export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED'; +export function fetchDashboardsSucceeded(choices) { + return { type: FETCH_DASHBOARDS_SUCCEEDED, choices }; +} + +export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED'; +export function fetchDashboardsFailed(userId) { + return { type: FETCH_DASHBOARDS_FAILED, userId }; +} + +export function fetchDashboards(userId) { + return function (dispatch) { + const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userId; + return $.ajax({ + type: 'GET', + url, + success: (data) => { + const choices = []; + for (let i = 0; i < data.pks.length; i++) { + choices.push({ value: data.pks[i], label: data.result[i].dashboard_title }); + } + dispatch(fetchDashboardsSucceeded(choices)); + }, + error: () => { + dispatch(fetchDashboardsFailed(userId)); + }, + }); + }; +} + +export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED'; +export function saveSliceFailed() { + return { type: SAVE_SLICE_FAILED }; +} +export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS'; +export function saveSliceSuccess(data) { + return { type: SAVE_SLICE_SUCCESS, data }; +} + +export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT'; +export function removeSaveModalAlert() { + return { type: REMOVE_SAVE_MODAL_ALERT }; +} + +export function saveSlice(url) { + return function (dispatch) { + return $.get(url, (data, status) => { + if (status === 'success') { + dispatch(saveSliceSuccess(data)); + } else { + dispatch(saveSliceFailed()); + } + }); + }; +} diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx index ab2be2fcdf24c..3e93b9e6a4be7 100644 --- a/superset/assets/javascripts/explore/components/ChartContainer.jsx +++ b/superset/assets/javascripts/explore/components/ChartContainer.jsx @@ -322,29 +322,29 @@ class ChartContainer extends React.PureComponent { ChartContainer.propTypes = propTypes; -function mapStateToProps(state) { - const formData = getFormDataFromControls(state.controls); +function mapStateToProps({ explore, chart }) { + const formData = getFormDataFromControls(explore.controls); return { - alert: state.chartAlert, - can_overwrite: state.can_overwrite, - can_download: state.can_download, - chartStatus: state.chartStatus, - chartUpdateEndTime: state.chartUpdateEndTime, - chartUpdateStartTime: state.chartUpdateStartTime, - datasource: state.datasource, - column_formats: state.datasource ? state.datasource.column_formats : null, - containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container', + alert: explore.chartAlert, + can_overwrite: explore.can_overwrite, + can_download: explore.can_download, + datasource: explore.datasource, + column_formats: explore.datasource ? explore.datasource.column_formats : null, + containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container', formData, - latestQueryFormData: state.latestQueryFormData, - isStarred: state.isStarred, - queryResponse: state.queryResponse, - slice: state.slice, - standalone: state.standalone, + isStarred: explore.isStarred, + slice: explore.slice, + standalone: explore.standalone, table_name: formData.datasource_name, viz_type: formData.viz_type, - triggerRender: state.triggerRender, - datasourceType: state.datasource.type, - datasourceId: state.datasource_id, + triggerRender: explore.triggerRender, + datasourceType: explore.datasource.type, + datasourceId: explore.datasource_id, + chartStatus: chart.chartStatus, + chartUpdateEndTime: chart.chartUpdateEndTime, + chartUpdateStartTime: chart.chartUpdateStartTime, + latestQueryFormData: chart.latestQueryFormData, + queryResponse: chart.queryResponse, }; } diff --git a/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx b/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx index e3b29855a8075..8a8c2d8802b80 100644 --- a/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx +++ b/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx @@ -96,12 +96,12 @@ class ControlPanelsContainer extends React.Component { ControlPanelsContainer.propTypes = propTypes; -function mapStateToProps(state) { +function mapStateToProps({ explore }) { return { - alert: state.controlPanelAlert, - isDatasourceMetaLoading: state.isDatasourceMetaLoading, - controls: state.controls, - exploreState: state, + alert: explore.controlPanelAlert, + isDatasourceMetaLoading: explore.isDatasourceMetaLoading, + controls: explore.controls, + exploreState: explore, }; } diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx index ae94f6a02efcc..bb96dbc3a3270 100644 --- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx +++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx @@ -8,12 +8,15 @@ import ControlPanelsContainer from './ControlPanelsContainer'; import SaveModal from './SaveModal'; import QueryAndSaveBtns from './QueryAndSaveBtns'; import { getExploreUrl } from '../exploreUtils'; -import * as actions from '../actions/exploreActions'; import { getFormDataFromControls } from '../stores/store'; +import * as exploreActions from '../actions/exploreActions'; +import * as saveModalActions from '../actions/saveModalActions'; +import * as chartActions from '../actions/chartActions'; const propTypes = { actions: PropTypes.object.isRequired, datasource_type: PropTypes.string.isRequired, + isDatasourceMetaLoading: PropTypes.bool.isRequired, chartStatus: PropTypes.string, controls: PropTypes.object.isRequired, forcedHeight: PropTypes.string, @@ -85,7 +88,6 @@ class ExploreViewContainer extends React.Component { return `${window.innerHeight - navHeight}px`; } - triggerQueryIfNeeded() { if (this.props.triggerQuery && !this.hasErrors()) { this.props.actions.runQuery(this.props.form_data); @@ -172,7 +174,9 @@ class ExploreViewContainer extends React.Component {
    @@ -186,21 +190,23 @@ class ExploreViewContainer extends React.Component { ExploreViewContainer.propTypes = propTypes; -function mapStateToProps(state) { - const form_data = getFormDataFromControls(state.controls); +function mapStateToProps({ explore, chart }) { + const form_data = getFormDataFromControls(explore.controls); return { - chartStatus: state.chartStatus, - datasource_type: state.datasource.type, - controls: state.controls, + isDatasourceMetaLoading: explore.isDatasourceMetaLoading, + datasource_type: explore.datasource.type, + controls: explore.controls, form_data, - standalone: state.standalone, - triggerQuery: state.triggerQuery, - forcedHeight: state.forced_height, - queryRequest: state.queryRequest, + standalone: explore.standalone, + triggerQuery: explore.triggerQuery, + forcedHeight: explore.forced_height, + queryRequest: chart.queryRequest, + chartStatus: chart.chartStatus, }; } function mapDispatchToProps(dispatch) { + const actions = Object.assign({}, exploreActions, saveModalActions, chartActions); return { actions: bindActionCreators(actions, dispatch), }; diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx b/superset/assets/javascripts/explore/components/SaveModal.jsx index 4dbff36acfea6..beb07b27ea4d2 100644 --- a/superset/assets/javascripts/explore/components/SaveModal.jsx +++ b/superset/assets/javascripts/explore/components/SaveModal.jsx @@ -1,10 +1,11 @@ /* eslint camelcase: 0 */ import React from 'react'; import PropTypes from 'prop-types'; -import $ from 'jquery'; +import { connect } from 'react-redux'; + import { Modal, Alert, Button, Radio } from 'react-bootstrap'; import Select from 'react-select'; -import { connect } from 'react-redux'; +import { getExploreUrl } from '../exploreUtils'; const propTypes = { can_overwrite: PropTypes.bool, @@ -102,12 +103,7 @@ class SaveModal extends React.Component { } sliceParams.goto_dash = gotodash; - const baseUrl = `/superset/explore/${this.props.datasource.type}/${this.props.datasource.id}/`; - sliceParams.datasource_name = this.props.datasource.name; - - const saveUrl = `${baseUrl}?form_data=` + - `${encodeURIComponent(JSON.stringify(this.props.form_data))}` + - `&${$.param(sliceParams, true)}`; + const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, sliceParams); this.props.actions.saveSlice(saveUrl) .then((data) => { // Go to new slice url or dashboard url @@ -234,14 +230,14 @@ class SaveModal extends React.Component { SaveModal.propTypes = propTypes; -function mapStateToProps(state) { +function mapStateToProps({ explore, saveModal }) { return { - datasource: state.datasource, - slice: state.slice, - can_overwrite: state.can_overwrite, - user_id: state.user_id, - dashboards: state.dashboards, - alert: state.saveModalAlert, + datasource: explore.datasource, + slice: explore.slice, + can_overwrite: explore.can_overwrite, + user_id: explore.user_id, + dashboards: saveModal.dashboards, + alert: explore.saveModalAlert, }; } diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx index 8d29d9eaf0da9..2f6e898ee276f 100644 --- a/superset/assets/javascripts/explore/index.jsx +++ b/superset/assets/javascripts/explore/index.jsx @@ -11,7 +11,8 @@ import AlertsWrapper from '../components/AlertsWrapper'; import { getControlsState, getFormDataFromControls } from './stores/store'; import { initJQueryAjax } from '../modules/utils'; import ExploreViewContainer from './components/ExploreViewContainer'; -import { exploreReducer } from './reducers/exploreReducer'; +import rootReducer from './reducers/index'; + import { appSetup } from '../common'; import './main.css'; import '../../stylesheets/reactable-pagination.css'; @@ -28,23 +29,30 @@ delete bootstrapData.form_data; // Initial state const bootstrappedState = Object.assign( bootstrapData, { - chartStatus: null, - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - dashboards: [], controls, - latestQueryFormData: getFormDataFromControls(controls), filterColumnOpts: [], isDatasourceMetaLoading: false, isStarred: false, - queryResponse: null, triggerQuery: true, triggerRender: false, alert: null, }, ); -const store = createStore(exploreReducer, bootstrappedState, +const initState = { + chart: { + chartStatus: null, + chartUpdateEndTime: null, + chartUpdateStartTime: now(), + latestQueryFormData: getFormDataFromControls(controls), + queryResponse: null, + }, + saveModal: { + dashboards: [], + }, + explore: bootstrappedState, +}; +const store = createStore(rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)), ); diff --git a/superset/assets/javascripts/explore/reducers/chartReducer.js b/superset/assets/javascripts/explore/reducers/chartReducer.js new file mode 100644 index 0000000000000..c41771b44c313 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/chartReducer.js @@ -0,0 +1,72 @@ +/* eslint camelcase: 0 */ +import { now } from '../../modules/dates'; +import * as actions from '../actions/chartActions'; +import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; + +export default function chartReducer(state = {}, action) { + const actionHandlers = { + [actions.CHART_UPDATE_SUCCEEDED]() { + return Object.assign( + {}, + state, + { + chartStatus: 'success', + queryResponse: action.queryResponse, + }, + ); + }, + [actions.CHART_UPDATE_STARTED]() { + return Object.assign({}, state, + { + chartStatus: 'loading', + chartUpdateEndTime: null, + chartUpdateStartTime: now(), + queryRequest: action.queryRequest, + latestQueryFormData: action.latestQueryFormData, + }); + }, + [actions.CHART_UPDATE_STOPPED]() { + return Object.assign({}, state, + { + chartStatus: 'stopped', + chartAlert: 'Updating chart was stopped', + }); + }, + [actions.CHART_RENDERING_FAILED]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: 'An error occurred while rendering the visualization: ' + action.error, + }); + }, + [actions.CHART_UPDATE_TIMEOUT]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: 'Query timeout - visualization query are set to timeout at ' + + `${QUERY_TIMEOUT_THRESHOLD / 1000} seconds. ` + + 'Perhaps your data has grown, your database is under unusual load, ' + + 'or you are simply querying a data source that is to large to be processed within the timeout range. ' + + 'If that is the case, we recommend that you summarize your data further.', + }); + }, + [actions.CHART_UPDATE_FAILED]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.', + chartUpdateEndTime: now(), + queryResponse: action.queryResponse, + }); + }, + [actions.UPDATE_CHART_STATUS]() { + const newState = Object.assign({}, state, { chartStatus: action.status }); + if (action.status === 'success' || action.status === 'failed') { + newState.chartUpdateEndTime = now(); + } + return newState; + }, + }; + + if (action.type in actionHandlers) { + return actionHandlers[action.type](); + } + return state; +} diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js b/superset/assets/javascripts/explore/reducers/exploreReducer.js index 96e36e3765754..bc1072f49163f 100644 --- a/superset/assets/javascripts/explore/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js @@ -1,23 +1,18 @@ /* eslint camelcase: 0 */ import { getControlsState, getFormDataFromControls } from '../stores/store'; import * as actions from '../actions/exploreActions'; -import { now } from '../../modules/dates'; -import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; -export const exploreReducer = function (state, action) { +export default function exploreReducer(state = {}, action) { const actionHandlers = { [actions.TOGGLE_FAVE_STAR]() { return Object.assign({}, state, { isStarred: action.isStarred }); }, - [actions.FETCH_DATASOURCE_STARTED]() { return Object.assign({}, state, { isDatasourceMetaLoading: true }); }, - [actions.FETCH_DATASOURCE_SUCCEEDED]() { return Object.assign({}, state, { isDatasourceMetaLoading: false }); }, - [actions.FETCH_DATASOURCE_FAILED]() { // todo(alanna) handle failure/error state return Object.assign({}, state, @@ -29,16 +24,25 @@ export const exploreReducer = function (state, action) { [actions.SET_DATASOURCE]() { return Object.assign({}, state, { datasource: action.datasource }); }, - [actions.REMOVE_CONTROL_PANEL_ALERT]() { - return Object.assign({}, state, { controlPanelAlert: null }); + [actions.FETCH_DATASOURCES_STARTED]() { + return Object.assign({}, state, { isDatasourcesLoading: true }); }, - [actions.FETCH_DASHBOARDS_SUCCEEDED]() { - return Object.assign({}, state, { dashboards: action.choices }); + [actions.FETCH_DATASOURCES_SUCCEEDED]() { + return Object.assign({}, state, { isDatasourcesLoading: false }); }, - - [actions.FETCH_DASHBOARDS_FAILED]() { + [actions.FETCH_DATASOURCES_FAILED]() { + // todo(alanna) handle failure/error state return Object.assign({}, state, - { saveModalAlert: `fetching dashboards failed for ${action.userId}` }); + { + isDatasourcesLoading: false, + controlPanelAlert: action.error, + }); + }, + [actions.SET_DATASOURCES]() { + return Object.assign({}, state, { datasources: action.datasources }); + }, + [actions.REMOVE_CONTROL_PANEL_ALERT]() { + return Object.assign({}, state, { controlPanelAlert: null }); }, [actions.SET_FIELD_VALUE]() { const controls = Object.assign({}, state.controls); @@ -52,70 +56,11 @@ export const exploreReducer = function (state, action) { } return Object.assign({}, state, changes); }, - [actions.CHART_UPDATE_SUCCEEDED]() { - return Object.assign( - {}, - state, - { - chartStatus: 'success', - queryResponse: action.queryResponse, - }, - ); - }, - [actions.CHART_UPDATE_STARTED]() { - return Object.assign({}, state, - { - chartStatus: 'loading', - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - triggerQuery: false, - queryRequest: action.queryRequest, - latestQueryFormData: getFormDataFromControls(state.controls), - }); - }, - [actions.CHART_UPDATE_STOPPED]() { - return Object.assign({}, state, - { - chartStatus: 'stopped', - chartAlert: 'Updating chart was stopped', - }); - }, - [actions.CHART_RENDERING_FAILED]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: 'An error occurred while rendering the visualization: ' + action.error, - }); - }, [actions.TRIGGER_QUERY]() { return Object.assign({}, state, { - triggerQuery: true, + triggerQuery: action.value, }); }, - [actions.CHART_UPDATE_TIMEOUT]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: 'Query timeout - visualization query are set to timeout at ' + - `${QUERY_TIMEOUT_THRESHOLD / 1000} seconds. ` + - 'Perhaps your data has grown, your database is under unusual load, ' + - 'or you are simply querying a data source that is to large to be processed within the timeout range. ' + - 'If that is the case, we recommend that you summarize your data further.', - }); - }, - [actions.CHART_UPDATE_FAILED]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.', - chartUpdateEndTime: now(), - queryResponse: action.queryResponse, - }); - }, - [actions.UPDATE_CHART_STATUS]() { - const newState = Object.assign({}, state, { chartStatus: action.status }); - if (action.status === 'success' || action.status === 'failed') { - newState.chartUpdateEndTime = now(); - } - return newState; - }, [actions.UPDATE_CHART_TITLE]() { const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name }); return Object.assign({}, state, { slice: updatedSlice }); @@ -126,15 +71,6 @@ export const exploreReducer = function (state, action) { } return state; }, - [actions.SAVE_SLICE_FAILED]() { - return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' }); - }, - [actions.SAVE_SLICE_SUCCESS](data) { - return Object.assign({}, state, { data }); - }, - [actions.REMOVE_SAVE_MODAL_ALERT]() { - return Object.assign({}, state, { saveModalAlert: null }); - }, [actions.RESET_FIELDS]() { const controls = getControlsState(state, getFormDataFromControls(state.controls)); return Object.assign({}, state, { controls }); @@ -147,4 +83,4 @@ export const exploreReducer = function (state, action) { return actionHandlers[action.type](); } return state; -}; +} diff --git a/superset/assets/javascripts/explore/reducers/index.js b/superset/assets/javascripts/explore/reducers/index.js new file mode 100644 index 0000000000000..0d5acb04c7e88 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/index.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux'; + +import chart from './chartReducer'; +import saveModal from './saveModalReducer'; +import explore from './exploreReducer'; + +export default combineReducers({ + chart, + saveModal, + explore, +}); diff --git a/superset/assets/javascripts/explore/reducers/saveModalReducer.js b/superset/assets/javascripts/explore/reducers/saveModalReducer.js new file mode 100644 index 0000000000000..912d5315f3049 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/saveModalReducer.js @@ -0,0 +1,28 @@ +/* eslint camelcase: 0 */ +import * as actions from '../actions/saveModalActions'; + +export default function saveModalReducer(state = {}, action) { + const actionHandlers = { + [actions.FETCH_DASHBOARDS_SUCCEEDED]() { + return Object.assign({}, state, { dashboards: action.choices }); + }, + [actions.FETCH_DASHBOARDS_FAILED]() { + return Object.assign({}, state, + { saveModalAlert: `fetching dashboards failed for ${action.userId}` }); + }, + [actions.SAVE_SLICE_FAILED]() { + return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' }); + }, + [actions.SAVE_SLICE_SUCCESS](data) { + return Object.assign({}, state, { data }); + }, + [actions.REMOVE_SAVE_MODAL_ALERT]() { + return Object.assign({}, state, { saveModalAlert: null }); + }, + }; + + if (action.type in actionHandlers) { + return actionHandlers[action.type](); + } + return state; +} diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js new file mode 100644 index 0000000000000..b2e069ab971a8 --- /dev/null +++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js @@ -0,0 +1,36 @@ +import { it, describe } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import $ from 'jquery'; +import * as exploreUtils from '../../../javascripts/explore/exploreUtils'; +import * as actions from '../../../javascripts/explore/actions/chartActions'; + +describe('chart actions', () => { + let dispatch; + let urlStub; + let ajaxStub; + let request; + + beforeEach(() => { + dispatch = sinon.spy(); + urlStub = sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL')); + ajaxStub = sinon.stub($, 'ajax'); + }); + + afterEach(() => { + urlStub.restore(); + ajaxStub.restore(); + }); + + it('should handle query timeout', () => { + ajaxStub.yieldsTo('error', { statusText: 'timeout' }); + request = actions.runQuery({}); + request(dispatch, sinon.stub().returns({ + explore: { + controls: [], + }, + })); + expect(dispatch.callCount).to.equal(3); + expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT); + }); +}); diff --git a/superset/assets/spec/javascripts/explore/exploreActions_spec.js b/superset/assets/spec/javascripts/explore/exploreActions_spec.js index 9fa02e4b12484..5d2926de2ef12 100644 --- a/superset/assets/spec/javascripts/explore/exploreActions_spec.js +++ b/superset/assets/spec/javascripts/explore/exploreActions_spec.js @@ -4,9 +4,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import $ from 'jquery'; import * as actions from '../../../javascripts/explore/actions/exploreActions'; -import * as exploreUtils from '../../../javascripts/explore/exploreUtils'; import { defaultState } from '../../../javascripts/explore/stores/store'; -import { exploreReducer } from '../../../javascripts/explore/reducers/exploreReducer'; +import exploreReducer from '../../../javascripts/explore/reducers/exploreReducer'; describe('reducers', () => { it('sets correct control value given a key and value', () => { @@ -81,69 +80,4 @@ describe('fetching actions', () => { expect(dispatch.getCall(4).args[0].type).to.equal(actions.TRIGGER_QUERY); }); }); - - describe('fetchDashboards', () => { - const userID = 1; - const mockDashboardData = { - pks: ['value'], - result: [ - { dashboard_title: 'dashboard title' }, - ], - }; - const makeRequest = () => { - request = actions.fetchDashboards(userID); - request(dispatch); - }; - - it('makes the ajax request', () => { - makeRequest(); - expect(ajaxStub.calledOnce).to.be.true; - }); - - it('calls correct url', () => { - const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userID; - makeRequest(); - expect(ajaxStub.getCall(0).args[0].url).to.equal(url); - }); - - it('calls correct actions on error', () => { - ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } }); - makeRequest(); - expect(dispatch.callCount).to.equal(1); - expect(dispatch.getCall(0).args[0].type).to.equal(actions.FETCH_DASHBOARDS_FAILED); - }); - - it('calls correct actions on success', () => { - ajaxStub.yieldsTo('success', mockDashboardData); - makeRequest(); - expect(dispatch.callCount).to.equal(1); - expect(dispatch.getCall(0).args[0].type).to.equal(actions.FETCH_DASHBOARDS_SUCCEEDED); - }); - }); -}); - -describe('runQuery', () => { - let dispatch; - let urlStub; - let ajaxStub; - let request; - - beforeEach(() => { - dispatch = sinon.spy(); - urlStub = sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL')); - ajaxStub = sinon.stub($, 'ajax'); - }); - - afterEach(() => { - urlStub.restore(); - ajaxStub.restore(); - }); - - it('should handle query timeout', () => { - ajaxStub.yieldsTo('error', { statusText: 'timeout' }); - request = actions.runQuery({}); - request(dispatch); - expect(dispatch.callCount).to.equal(2); - expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT); - }); });