From d758684ca9d3fbc18ad417c183a7b366dea7fff7 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Thu, 31 May 2018 21:18:36 -0700 Subject: [PATCH 01/14] Allow multiple time shifts (#5067) * Allow multiple time shifts * Handle old form data (cherry picked from commit 1d3e96b) --- superset/assets/src/explore/controls.jsx | 17 +++++++++++++---- superset/assets/src/explore/visTypes.js | 2 +- superset/viz.py | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 1aca3f7d1be6a..46ece7041d6aa 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -1577,11 +1577,20 @@ export const controls = { }, time_compare: { - type: 'TextControl', + type: 'SelectControl', + multi: true, + freeForm: true, label: t('Time Shift'), - default: null, - description: t('Overlay a timeseries from a ' + - 'relative time period. Expects relative time delta ' + + default: [], + choices: formatSelectOptions([ + '1 day', + '1 week', + '28 days', + '30 days', + '1 year', + ]), + description: t('Overlay one or more timeseries from a ' + + 'relative time period. Expects relative time deltas ' + 'in natural language (example: 24 hours, 7 days, ' + '56 weeks, 365 days)'), }, diff --git a/superset/assets/src/explore/visTypes.js b/superset/assets/src/explore/visTypes.js index 5fb541ab959bd..f771c38ad7817 100644 --- a/superset/assets/src/explore/visTypes.js +++ b/superset/assets/src/explore/visTypes.js @@ -74,7 +74,7 @@ export const sections = { 'of query results'), controlSetRows: [ ['rolling_type', 'rolling_periods', 'min_periods'], - ['time_compare', null], + ['time_compare'], ['num_period_compare', 'period_ratio_type'], ['resample_how', 'resample_rule', 'resample_fillmethod'], ], diff --git a/superset/viz.py b/superset/viz.py index a52fe8be095b7..b0ac9f31d5747 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -87,7 +87,7 @@ def __init__(self, datasource, form_data, force=False): self._some_from_cache = False self._any_cache_key = None self._any_cached_dttm = None - self._extra_chart_data = None + self._extra_chart_data = [] self.process_metrics() @@ -1201,10 +1201,15 @@ def process_data(self, df, aggregate=False): def run_extra_queries(self): fd = self.form_data - time_compare = fd.get('time_compare') - if time_compare: + + time_compare = fd.get('time_compare') or [] + # backwards compatibility + if not isinstance(time_compare, list): + time_compare = [time_compare] + + for option in time_compare: query_object = self.query_obj() - delta = utils.parse_human_timedelta(time_compare) + delta = utils.parse_human_timedelta(option) query_object['inner_from_dttm'] = query_object['from_dttm'] query_object['inner_to_dttm'] = query_object['to_dttm'] @@ -1217,10 +1222,11 @@ def run_extra_queries(self): df2 = self.get_df_payload(query_object).get('df') if df2 is not None: + label = '{} offset'. format(option) df2[DTTM_ALIAS] += delta df2 = self.process_data(df2) - self._extra_chart_data = self.to_series( - df2, classed='superset', title_suffix='---') + self._extra_chart_data.extend(self.to_series( + df2, classed='superset', title_suffix=label)) def get_data(self, df): df = self.process_data(df) From abae53b28aed0ae7b2d161c735aa3e40336c5f5d Mon Sep 17 00:00:00 2001 From: John Bodley <4567245+john-bodley@users.noreply.github.com> Date: Mon, 18 Jun 2018 15:43:18 -0700 Subject: [PATCH 02/14] [adhoc-filters] Adding adhoc-filters to all viz types (#5206) (cherry picked from commit d483ed1) --- babel-node | 0 package-lock.json | 743 ++++++++++++++++++ superset/assets/backendSync.json | 24 - .../components/AlteredSliceTag_spec.jsx | 80 +- .../components/AdhocFilterControl_spec.jsx | 52 -- .../ControlPanelsContainer_spec.jsx | 2 +- .../explore/components/FilterControl_spec.jsx | 248 ------ .../explore/components/Filter_spec.jsx | 115 --- .../assets/src/components/AlteredSliceTag.jsx | 12 +- .../controls/AdhocFilterControl.jsx | 78 +- .../explore/components/controls/Filter.jsx | 187 ----- .../components/controls/FilterControl.jsx | 155 ---- .../src/explore/components/controls/index.js | 2 - superset/assets/src/explore/controls.jsx | 50 -- superset/assets/src/explore/store.js | 8 - superset/assets/src/explore/visTypes.js | 128 ++- .../versions/bddc498dd179_adhoc_filters.py | 97 +++ .../translations/de/LC_MESSAGES/messages.po | 1 - .../translations/en/LC_MESSAGES/messages.po | 245 +++++- .../translations/es/LC_MESSAGES/messages.po | 1 - .../translations/fr/LC_MESSAGES/messages.po | 245 +++++- .../translations/it/LC_MESSAGES/messages.po | 1 - .../translations/ja/LC_MESSAGES/messages.po | 1 - superset/translations/messages.pot | 245 +++++- .../pt_BR/LC_MESSAGES/messages.po | 1 - .../translations/ru/LC_MESSAGES/messages.po | 1 - .../translations/zh/LC_MESSAGES/messages.po | 244 ++++++ superset/utils.py | 1 - 28 files changed, 1988 insertions(+), 979 deletions(-) create mode 100644 babel-node create mode 100644 package-lock.json delete mode 100644 superset/assets/spec/javascripts/explore/components/FilterControl_spec.jsx delete mode 100644 superset/assets/spec/javascripts/explore/components/Filter_spec.jsx delete mode 100644 superset/assets/src/explore/components/controls/Filter.jsx delete mode 100644 superset/assets/src/explore/components/controls/FilterControl.jsx create mode 100644 superset/migrations/versions/bddc498dd179_adhoc_filters.py diff --git a/babel-node b/babel-node new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000..dea4bad056ffd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,743 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-1.2.0.tgz", + "integrity": "sha512-yeDwKaLgGdTpXL7RgGt5r6T4LmnTza/hUn5Ul8uZSGGMtEjYo13Nxai7SQaGCTEzUtg9Zq9qJn0EjEr7SeSlTQ==", + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-preset-env": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", + "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-classes": "^6.23.0", + "babel-plugin-transform-es2015-computed-properties": "^6.22.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-es2015-function-name": "^6.22.0", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-umd": "^6.23.0", + "babel-plugin-transform-es2015-object-super": "^6.22.0", + "babel-plugin-transform-es2015-parameters": "^6.23.0", + "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", + "babel-plugin-transform-regenerator": "^6.22.0", + "browserslist": "^3.2.6", + "invariant": "^2.2.2", + "semver": "^5.3.0" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000844", + "electron-to-chromium": "^1.3.47" + } + }, + "caniuse-lite": { + "version": "1.0.30000856", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz", + "integrity": "sha512-x3mYcApHMQemyaHuH/RyqtKCGIYTgEA63fdi+VBvDz8xUSmRiVWTLeyKcoGQCGG6UPR9/+4qG4OKrTa6aSQRKg==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "electron-to-chromium": { + "version": "1.3.48", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", + "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "~0.5.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + } + } +} diff --git a/superset/assets/backendSync.json b/superset/assets/backendSync.json index 9145cc98175c7..9b825529e93ca 100644 --- a/superset/assets/backendSync.json +++ b/superset/assets/backendSync.json @@ -1683,18 +1683,6 @@ "renderTrigger": true, "default": "" }, - "where": { - "type": "TextControl", - "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": "TextControl", - "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": "TextControl", "label": "Comparison Period Lag", @@ -2628,12 +2616,6 @@ "default": "", "description": "Labels for the marker lines" }, - "filters": { - "type": "FilterControl", - "label": "", - "default": [], - "description": "" - }, "annotation_layers": { "type": "AnnotationLayerControl", "label": "", @@ -2641,12 +2623,6 @@ "description": "Annotation Layers", "renderTrigger": true }, - "having_filters": { - "type": "FilterControl", - "label": "", - "default": [], - "description": "" - }, "slice_id": { "type": "HiddenControl", "label": "Slice ID", diff --git a/superset/assets/spec/javascripts/components/AlteredSliceTag_spec.jsx b/superset/assets/spec/javascripts/components/AlteredSliceTag_spec.jsx index 79947201e604a..867006993e8ba 100644 --- a/superset/assets/spec/javascripts/components/AlteredSliceTag_spec.jsx +++ b/superset/assets/spec/javascripts/components/AlteredSliceTag_spec.jsx @@ -11,7 +11,15 @@ import TooltipWrapper from '../../../src/components/TooltipWrapper'; const defaultProps = { origFormData: { - filters: [{ col: 'a', op: '==', val: 'hello' }], + adhoc_filters: [ + { + clause: 'WHERE', + comparator: 'hello', + expressionType: 'SIMPLE', + operator: '==', + subject: 'a', + }, + ], y_axis_bounds: [10, 20], column_collection: [{ 1: 'a', b: ['6', 'g'] }], bool: false, @@ -21,7 +29,15 @@ const defaultProps = { ever: { a: 'b', c: 'd' }, }, currentFormData: { - filters: [{ col: 'b', op: 'in', val: ['hello', 'my', 'name'] }], + adhoc_filters: [ + { + clause: 'WHERE', + comparator: ['hello', 'my', 'name'], + expressionType: 'SIMPLE', + operator: 'in', + subject: 'b', + }, + ], y_axis_bounds: [15, 16], column_collection: [{ 1: 'a', b: [9, '15'], t: 'gggg' }], bool: true, @@ -33,9 +49,25 @@ const defaultProps = { }; const expectedDiffs = { - filters: { - before: [{ col: 'a', op: '==', val: 'hello' }], - after: [{ col: 'b', op: 'in', val: ['hello', 'my', 'name'] }], + adhoc_filters: { + before: [ + { + clause: 'WHERE', + comparator: 'hello', + expressionType: 'SIMPLE', + operator: '==', + subject: 'a', + }, + ], + after: [ + { + clause: 'WHERE', + comparator: ['hello', 'my', 'name'], + expressionType: 'SIMPLE', + operator: 'in', + subject: 'b', + }, + ], }, y_axis_bounds: { before: [10, 20], @@ -211,25 +243,49 @@ describe('AlteredSliceTag', () => { }); it('returns "[]" for empty filters', () => { - expect(wrapper.instance().formatValue([], 'filters')).to.equal('[]'); + expect(wrapper.instance().formatValue([], 'adhoc_filters')).to.equal('[]'); }); it('correctly formats filters with array values', () => { const filters = [ - { col: 'a', op: 'in', val: ['1', 'g', '7', 'ho'] }, - { col: 'b', op: 'not in', val: ['hu', 'ho', 'ha'] }, + { + clause: 'WHERE', + comparator: ['1', 'g', '7', 'ho'], + expressionType: 'SIMPLE', + operator: 'in', + subject: 'a', + }, + { + clause: 'WHERE', + comparator: ['hu', 'ho', 'ha'], + expressionType: 'SIMPLE', + operator: 'not in', + subject: 'b', + }, ]; const expected = 'a in [1, g, 7, ho], b not in [hu, ho, ha]'; - expect(wrapper.instance().formatValue(filters, 'filters')).to.equal(expected); + expect(wrapper.instance().formatValue(filters, 'adhoc_filters')).to.equal(expected); }); it('correctly formats filters with string values', () => { const filters = [ - { col: 'a', op: '==', val: 'gucci' }, - { col: 'b', op: 'LIKE', val: 'moshi moshi' }, + { + clause: 'WHERE', + comparator: 'gucci', + expressionType: 'SIMPLE', + operator: '==', + subject: 'a', + }, + { + clause: 'WHERE', + comparator: 'moshi moshi', + expressionType: 'SIMPLE', + operator: 'LIKE', + subject: 'b', + }, ]; const expected = 'a == gucci, b LIKE moshi moshi'; - expect(wrapper.instance().formatValue(filters, 'filters')).to.equal(expected); + expect(wrapper.instance().formatValue(filters, 'adhoc_filters')).to.equal(expected); }); }); }); diff --git a/superset/assets/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx index 4be8a2eba3c4c..2123ed7c2e09f 100644 --- a/superset/assets/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/AdhocFilterControl_spec.jsx @@ -33,18 +33,9 @@ const columns = [ { type: 'DOUBLE', column_name: 'value' }, ]; -const legacyFilter = { col: 'value', op: '>', val: '5' }; -const legacyHavingFilter = { col: 'SUM(value)', op: '>', val: '10' }; -const whereFilterText = 'target in (\'alpha\')'; -const havingFilterText = 'SUM(value) < 20'; - const formData = { - filters: [legacyFilter], - having: havingFilterText, - having_filters: [legacyHavingFilter], metric: undefined, metrics: [sumValueAdhocMetric, savedMetric.saved_metric_name], - where: whereFilterText, }; function setup(overrides) { @@ -68,49 +59,6 @@ describe('AdhocFilterControl', () => { expect(wrapper.find(OnPasteSelect)).to.have.lengthOf(1); }); - it('will translate legacy filters into adhoc filters if no adhoc filters are present', () => { - const { wrapper } = setup({ value: undefined }); - expect(wrapper.state('values')).to.have.lengthOf(4); - expect(wrapper.state('values')[0].equals(( - new AdhocFilter({ - expressionType: EXPRESSION_TYPES.SIMPLE, - subject: 'value', - operator: '>', - comparator: '5', - clause: CLAUSES.WHERE, - }) - ))).to.be.true; - expect(wrapper.state('values')[1].equals(( - new AdhocFilter({ - expressionType: EXPRESSION_TYPES.SIMPLE, - subject: 'SUM(value)', - operator: '>', - comparator: '10', - clause: CLAUSES.HAVING, - }) - ))).to.be.true; - expect(wrapper.state('values')[2].equals(( - new AdhocFilter({ - expressionType: EXPRESSION_TYPES.SQL, - sqlExpression: 'target in (\'alpha\')', - clause: CLAUSES.WHERE, - }) - ))).to.be.true; - expect(wrapper.state('values')[3].equals(( - new AdhocFilter({ - expressionType: EXPRESSION_TYPES.SQL, - sqlExpression: 'SUM(value) < 20', - clause: CLAUSES.HAVING, - }) - ))).to.be.true; - }); - - it('will ignore legacy filters if adhoc filters are present', () => { - const { wrapper } = setup(); - expect(wrapper.state('values')).to.have.lengthOf(1); - expect(wrapper.state('values')[0]).to.equal(simpleAdhocFilter); - }); - it('handles saved metrics being selected to filter on', () => { const { wrapper, onChange } = setup({ value: [] }); const select = wrapper.find(OnPasteSelect); diff --git a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx index fb1ea610fc989..64a657e1f7684 100644 --- a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx @@ -26,6 +26,6 @@ describe('ControlPanelsContainer', () => { }); it('renders ControlPanelSections', () => { - expect(wrapper.find(ControlPanelSection)).to.have.lengthOf(7); + expect(wrapper.find(ControlPanelSection)).to.have.lengthOf(6); }); }); diff --git a/superset/assets/spec/javascripts/explore/components/FilterControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/FilterControl_spec.jsx deleted file mode 100644 index 2b83fff457979..0000000000000 --- a/superset/assets/spec/javascripts/explore/components/FilterControl_spec.jsx +++ /dev/null @@ -1,248 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import { Button } from 'react-bootstrap'; -import sinon from 'sinon'; -import { expect } from 'chai'; -import { describe, it, beforeEach } from 'mocha'; -import { shallow } from 'enzyme'; -import FilterControl from '../../../../src/explore/components/controls/FilterControl'; -import Filter from '../../../../src/explore/components/controls/Filter'; - -const $ = window.$ = require('jquery'); - -const defaultProps = { - name: 'not_having_filters', - onChange: sinon.spy(), - value: [ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd'], - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - ], - datasource: { - id: 1, - type: 'qtable', - filter_select: true, - filterable_cols: [['col1', 'col2']], - metrics_combo: [ - ['m1', 'v1'], - ['m2', 'v2'], - ], - }, -}; - -describe('FilterControl', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - wrapper.setState({ - filters: [ - { - valuesLoading: false, - valueChoices: ['a', 'b', 'c', 'd', 'e', 'f'], - }, - { - valuesLoading: false, - valueChoices: ['X', 'Y', 'Z'], - }, - // Need a duplicate since onChange calls are not changing props - { - valuesLoading: false, - valueChoices: ['X', 'Y', 'Z'], - }, - ], - }); - }); - - it('renders Filters', () => { - expect( - React.isValidElement(), - ).to.equal(true); - }); - - it('renders one button and two filters', () => { - expect(wrapper.find(Filter)).to.have.lengthOf(2); - expect(wrapper.find(Button)).to.have.lengthOf(1); - }); - - it('adds filter when clicking Add Filter', () => { - const addButton = wrapper.find('#add-button'); - expect(addButton).to.have.lengthOf(1); - addButton.simulate('click'); - expect(defaultProps.onChange).to.have.property('callCount', 1); - expect(defaultProps.onChange.getCall(0).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd'], - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - { - col: 'col1', - op: 'in', - val: [], - }, - ]); - }); - - it('removes a the second filter when its delete button is clicked', () => { - expect(wrapper.find(Filter)).to.have.lengthOf(2); - wrapper.instance().removeFilter(1); - expect(defaultProps.onChange).to.have.property('callCount', 2); - expect(defaultProps.onChange.getCall(1).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd'], - }, - ]); - }); - - before(() => { - sinon.stub($, 'ajax'); - }); - - after(() => { - $.ajax.restore(); - }); - - it('makes a GET request to retrieve value choices', () => { - wrapper.instance().fetchFilterValues(0, 'col1'); - expect($.ajax.getCall(0).args[0].type).to.deep.equal('GET'); - expect($.ajax.getCall(0).args[0].url).to.deep.equal('/superset/filter/qtable/1/col1/'); - }); - - it('changes filter values when one is removed', () => { - wrapper.instance().changeFilter(0, 'val', ['a', 'b']); - expect(defaultProps.onChange).to.have.property('callCount', 3); - expect(defaultProps.onChange.getCall(2).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b'], - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - ]); - }); - - it('changes filter values when one is added', () => { - wrapper.instance().changeFilter(0, 'val', ['a', 'b', 'd', 'e']); - expect(defaultProps.onChange).to.have.property('callCount', 4); - expect(defaultProps.onChange.getCall(3).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd', 'e'], - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - ]); - }); - - it('changes op and transforms values', () => { - wrapper.instance().changeFilter(0, ['val', 'op'], ['a', '==']); - wrapper.instance().changeFilter(1, ['val', 'op'], [['Z'], 'in']); - expect(defaultProps.onChange).to.have.property('callCount', 6); - expect(defaultProps.onChange.getCall(4).args[0]).to.deep.equal([ - { - col: 'col1', - op: '==', - val: 'a', - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - ]); - expect(defaultProps.onChange.getCall(5).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd'], - }, - { - col: 'col2', - op: 'in', - val: ['Z'], - }, - ]); - }); - - it('changes column and clears invalid values', () => { - wrapper.instance().changeFilter(0, 'col', 'col2'); - expect(defaultProps.onChange).to.have.property('callCount', 7); - expect(defaultProps.onChange.getCall(6).args[0]).to.deep.equal([ - { - col: 'col2', - op: 'in', - val: [], - }, - { - col: 'col2', - op: '==', - val: 'Z', - }, - ]); - wrapper.instance().changeFilter(1, 'col', 'col1'); - expect(defaultProps.onChange).to.have.property('callCount', 8); - expect(defaultProps.onChange.getCall(7).args[0]).to.deep.equal([ - { - col: 'col1', - op: 'in', - val: ['a', 'b', 'd'], - }, - { - col: 'col1', - op: '==', - val: '', - }, - ]); - }); - - it('tracks an active filter select ajax request', () => { - const spyReq = sinon.spy(); - $.ajax.reset(); - $.ajax.onFirstCall().returns(spyReq); - wrapper.instance().fetchFilterValues(0, 'col1'); - expect(wrapper.state().activeRequest).to.equal(spyReq); - // Sets active to null after success - $.ajax.getCall(0).args[0].success(['opt1', 'opt2', null, '']); - expect(wrapper.state().filters[0].valuesLoading).to.equal(false); - expect(wrapper.state().filters[0].valueChoices).to.deep.equal(['opt1', 'opt2', null, '']); - expect(wrapper.state().activeRequest).to.equal(null); - }); - - - it('cancels active request if another is submitted', () => { - const spyReq = sinon.spy(); - spyReq.abort = sinon.spy(); - $.ajax.reset(); - $.ajax.onFirstCall().returns(spyReq); - wrapper.instance().fetchFilterValues(0, 'col1'); - expect(wrapper.state().activeRequest).to.equal(spyReq); - const spyReq1 = sinon.spy(); - $.ajax.onSecondCall().returns(spyReq1); - wrapper.instance().fetchFilterValues(1, 'col2'); - expect(spyReq.abort.called).to.equal(true); - expect(wrapper.state().activeRequest).to.equal(spyReq1); - }); -}); diff --git a/superset/assets/spec/javascripts/explore/components/Filter_spec.jsx b/superset/assets/spec/javascripts/explore/components/Filter_spec.jsx deleted file mode 100644 index 27425de9a3f6f..0000000000000 --- a/superset/assets/spec/javascripts/explore/components/Filter_spec.jsx +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import Select from 'react-select'; -import { Button } from 'react-bootstrap'; -import sinon from 'sinon'; -import { expect } from 'chai'; -import { describe, it, beforeEach } from 'mocha'; -import { shallow } from 'enzyme'; -import Filter from '../../../../src/explore/components/controls/Filter'; -import SelectControl from '../../../../src/explore/components/controls/SelectControl'; - -const defaultProps = { - changeFilter: sinon.spy(), - removeFilter: () => {}, - filter: { - col: null, - op: 'in', - value: ['val'], - }, - datasource: { - id: 1, - type: 'qtable', - filter_select: false, - filterable_cols: ['col1', 'col2'], - metrics_combo: [ - ['m1', 'v1'], - ['m2', 'v2'], - ], - }, -}; - -describe('Filter', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - }); - - it('renders Filters', () => { - expect( - React.isValidElement(), - ).to.equal(true); - }); - - it('renders two selects, one button and one input', () => { - expect(wrapper.find(Select)).to.have.lengthOf(2); - expect(wrapper.find(Button)).to.have.lengthOf(1); - expect(wrapper.find(SelectControl)).to.have.lengthOf(1); - expect(wrapper.find('#select-op').prop('options')).to.have.lengthOf(10); - }); - - it('renders five op choices for table datasource', () => { - const props = Object.assign({}, defaultProps); - props.datasource = { - id: 1, - type: 'druid', - filter_select: false, - filterable_cols: ['country_name'], - }; - const druidWrapper = shallow(); - expect(druidWrapper.find('#select-op').prop('options')).to.have.lengthOf(11); - }); - - it('renders six op choices for having filter', () => { - const props = Object.assign({}, defaultProps); - props.having = true; - const havingWrapper = shallow(); - expect(havingWrapper.find('#select-op').prop('options')).to.have.lengthOf(6); - }); - - it('calls changeFilter when select is changed', () => { - const selectCol = wrapper.find('#select-col'); - selectCol.simulate('change', { value: 'col' }); - const selectOp = wrapper.find('#select-op'); - selectOp.simulate('change', { value: 'in' }); - const selectVal = wrapper.find(SelectControl); - selectVal.simulate('change', { value: 'x' }); - expect(defaultProps.changeFilter).to.have.property('callCount', 3); - }); - - it('renders input for regex filters', () => { - const props = Object.assign({}, defaultProps); - props.filter = { - col: null, - op: 'regex', - value: 'val', - }; - const regexWrapper = shallow(); - expect(regexWrapper.find('input')).to.have.lengthOf(1); - }); - - it('renders `input` for text filters', () => { - const props = Object.assign({}, defaultProps); - ['>=', '>', '<=', '<'].forEach((op) => { - props.filter = { - col: 'col1', - op, - value: 'val', - }; - wrapper = shallow(); - expect(wrapper.find('input')).to.have.lengthOf(1); - }); - }); - - it('replaces null filter values with empty string in `input`', () => { - const props = Object.assign({}, defaultProps); - props.filter = { - col: 'col1', - op: '>=', - value: null, - }; - wrapper = shallow(); - expect(wrapper.find('input').props().value).to.equal(''); - }); -}); diff --git a/superset/assets/src/components/AlteredSliceTag.jsx b/superset/assets/src/components/AlteredSliceTag.jsx index 209e4a94bddd2..a317a041a6ad0 100644 --- a/superset/assets/src/components/AlteredSliceTag.jsx +++ b/superset/assets/src/components/AlteredSliceTag.jsx @@ -42,6 +42,10 @@ export default class AlteredSliceTag extends React.Component { if (!ofd[fdKey] && !cfd[fdKey]) { continue; } + // Ignore obsolete legacy filters + if (['filters', 'having', 'having_filters', 'where'].includes(fdKey)) { + continue; + } if (!isEqual(ofd[fdKey], cfd[fdKey])) { diffs[fdKey] = { before: ofd[fdKey], after: cfd[fdKey] }; } @@ -56,13 +60,15 @@ export default class AlteredSliceTag extends React.Component { return 'N/A'; } else if (value === null) { return 'null'; - } else if (controls[key] && controls[key].type === 'FilterControl') { + } else if (controls[key] && controls[key].type === 'AdhocFilterControl') { if (!value.length) { return '[]'; } return value.map((v) => { - const filterVal = v.val && v.val.constructor === Array ? `[${v.val.join(', ')}]` : v.val; - return `${v.col} ${v.op} ${filterVal}`; + const filterVal = v.comparator && v.comparator.constructor === Array ? + `[${v.comparator.join(', ')}]` : + v.comparator; + return `${v.subject} ${v.operator} ${filterVal}`; }).join(', '); } else if (controls[key] && controls[key].type === 'BoundsControl') { return `Min: ${value[0]}, Max: ${value[1]}`; diff --git a/superset/assets/src/explore/components/controls/AdhocFilterControl.jsx b/superset/assets/src/explore/components/controls/AdhocFilterControl.jsx index 1e88383359637..b51704d4b62af 100644 --- a/superset/assets/src/explore/components/controls/AdhocFilterControl.jsx +++ b/superset/assets/src/explore/components/controls/AdhocFilterControl.jsx @@ -16,12 +16,6 @@ import OnPasteSelect from '../../../components/OnPasteSelect'; import AdhocFilterOption from '../AdhocFilterOption'; import FilterDefinitionOption from '../FilterDefinitionOption'; -const legacyFilterShape = PropTypes.shape({ - col: PropTypes.string, - op: PropTypes.string, - val: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), -}); - const propTypes = { name: PropTypes.string, onChange: PropTypes.func, @@ -30,12 +24,8 @@ const propTypes = { columns: PropTypes.arrayOf(columnType), savedMetrics: PropTypes.arrayOf(savedMetricType), formData: PropTypes.shape({ - filters: PropTypes.arrayOf(legacyFilterShape), - having: PropTypes.string, - having_filters: PropTypes.arrayOf(legacyFilterShape), metric: PropTypes.oneOfType([PropTypes.string, adhocMetricType]), metrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, adhocMetricType])), - where: PropTypes.string, }), }; @@ -55,13 +45,15 @@ export default class AdhocFilterControl extends React.Component { constructor(props) { super(props); - this.coerceAdhocFilters = this.coerceAdhocFilters.bind(this); this.optionsForSelect = this.optionsForSelect.bind(this); this.onFilterEdit = this.onFilterEdit.bind(this); this.onChange = this.onChange.bind(this); this.getMetricExpression = this.getMetricExpression.bind(this); - const filters = this.coerceAdhocFilters(this.props.value, this.props.formData); + const filters = (this.props.value || []).map(filter => ( + isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter + )); + this.optionRenderer = VirtualizedRendererWrap(option => ( )); @@ -87,7 +79,11 @@ export default class AdhocFilterControl extends React.Component { this.setState({ options: this.optionsForSelect(nextProps) }); } if (this.props.value !== nextProps.value) { - this.setState({ values: this.coerceAdhocFilters(nextProps.value, nextProps.formData) }); + this.setState({ + values: (nextProps.value || []).map( + filter => (isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter + )), + }); } } @@ -147,62 +143,6 @@ export default class AdhocFilterControl extends React.Component { )).expression; } - coerceAdhocFilters(propsValues, formData) { - // this converts filters from the four legacy filter controls into adhoc filters in the case - // someone loads an old slice in the explore view - if (propsValues) { - return propsValues.map(filter => ( - isDictionaryForAdhocFilter(filter) ? new AdhocFilter(filter) : filter - )); - } - return [ - ...(formData.filters || []).map(filter => ( - new AdhocFilter({ - subject: filter.col, - operator: filter.op, - comparator: filter.val, - clause: CLAUSES.WHERE, - expressionType: EXPRESSION_TYPES.SIMPLE, - filterOptionName: this.generateConvertedFilterOptionName(), - }) - )), - ...(formData.having_filters || []).map(filter => ( - new AdhocFilter({ - subject: filter.col, - operator: filter.op, - comparator: filter.val, - clause: CLAUSES.HAVING, - expressionType: EXPRESSION_TYPES.SIMPLE, - filterOptionName: this.generateConvertedFilterOptionName(), - }) - )), - ...[ - formData.where ? - new AdhocFilter({ - sqlExpression: formData.where, - clause: CLAUSES.WHERE, - expressionType: EXPRESSION_TYPES.SQL, - filterOptionName: this.generateConvertedFilterOptionName(), - }) : - null, - ], - ...[ - formData.having ? - new AdhocFilter({ - sqlExpression: formData.having, - clause: CLAUSES.HAVING, - expressionType: EXPRESSION_TYPES.SQL, - filterOptionName: this.generateConvertedFilterOptionName(), - }) : - null, - ], - ].filter(option => option); - } - - generateConvertedFilterOptionName() { - return `form_filter_${Math.random().toString(36).substring(2, 15)}_${Math.random().toString(36).substring(2, 15)}`; - } - optionsForSelect(props) { const options = [ ...props.columns, diff --git a/superset/assets/src/explore/components/controls/Filter.jsx b/superset/assets/src/explore/components/controls/Filter.jsx deleted file mode 100644 index 539a4133aed88..0000000000000 --- a/superset/assets/src/explore/components/controls/Filter.jsx +++ /dev/null @@ -1,187 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Select from 'react-select'; -import { Button, Row, Col } from 'react-bootstrap'; -import { t } from '../../../locales'; -import SelectControl from './SelectControl'; - -const operatorsArr = [ - { val: 'in', type: 'array', useSelect: true, multi: true }, - { val: 'not in', type: 'array', useSelect: true, multi: true }, - { val: '==', type: 'string', useSelect: true, multi: false, havingOnly: true }, - { val: '!=', type: 'string', useSelect: true, multi: false, havingOnly: true }, - { val: '>=', type: 'string', havingOnly: true }, - { val: '<=', type: 'string', havingOnly: true }, - { val: '>', type: 'string', havingOnly: true }, - { val: '<', type: 'string', havingOnly: true }, - { val: 'regex', type: 'string', datasourceTypes: ['druid'] }, - { val: 'LIKE', type: 'string', datasourceTypes: ['table'] }, - { val: 'IS NULL', type: null }, - { val: 'IS NOT NULL', type: null }, -]; -const operators = {}; -operatorsArr.forEach((op) => { - operators[op.val] = op; -}); - -const propTypes = { - changeFilter: PropTypes.func, - removeFilter: PropTypes.func, - filter: PropTypes.object.isRequired, - datasource: PropTypes.object, - having: PropTypes.bool, - valuesLoading: PropTypes.bool, - valueChoices: PropTypes.array, -}; - -const defaultProps = { - changeFilter: () => {}, - removeFilter: () => {}, - datasource: null, - having: false, - valuesLoading: false, - valueChoices: [], -}; - -export default class Filter extends React.Component { - - switchFilterValue(prevOp, nextOp) { - if (operators[prevOp].type !== operators[nextOp].type) { - // Switch from array to string or vice versa - const val = this.props.filter.val; - let newVal; - if (operators[nextOp].type === 'string') { - if (!val || !val.length) { - newVal = ''; - } else { - newVal = val[0]; - } - } else if (operators[nextOp].type === 'array') { - if (!val || !val.length) { - newVal = []; - } else { - newVal = [val]; - } - } - this.props.changeFilter(['val', 'op'], [newVal, nextOp]); - } else { - // No value type change - this.props.changeFilter('op', nextOp); - } - } - - changeText(event) { - this.props.changeFilter('val', event.target.value); - } - - changeSelect(value) { - this.props.changeFilter('val', value); - } - - changeColumn(event) { - this.props.changeFilter('col', event.value); - } - - changeOp(event) { - this.switchFilterValue(this.props.filter.op, event.value); - } - - removeFilter(filter) { - this.props.removeFilter(filter); - } - - renderFilterFormControl(filter) { - const operator = operators[filter.op]; - if (operator.type === null) { - // IS NULL or IS NOT NULL - return null; - } - if (operator.useSelect && !this.props.having) { - // TODO should use a simple Select, not a control here... - return ( - - ); - } - return ( - - ); - } - render() { - const datasource = this.props.datasource; - const filter = this.props.filter; - const opsChoices = operatorsArr - .filter((o) => { - if (this.props.having) { - return !!o.havingOnly; - } - return (!o.datasourceTypes || o.datasourceTypes.indexOf(datasource.type) >= 0); - }) - .map(o => ({ value: o.val, label: o.val })); - let colChoices; - if (datasource) { - if (this.props.having) { - colChoices = datasource.metrics_combo.map(c => ({ value: c[0], label: c[1] })); - } else { - colChoices = datasource.filterable_cols.map(c => ({ value: c[0], label: c[1] })); - } - } - return ( -
- - - - - - {this.renderFilterFormControl(filter)} - - - - - -
- ); - } -} - -Filter.propTypes = propTypes; -Filter.defaultProps = defaultProps; diff --git a/superset/assets/src/explore/components/controls/FilterControl.jsx b/superset/assets/src/explore/components/controls/FilterControl.jsx deleted file mode 100644 index 041dd6fe2c181..0000000000000 --- a/superset/assets/src/explore/components/controls/FilterControl.jsx +++ /dev/null @@ -1,155 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, Row, Col } from 'react-bootstrap'; -import Filter from './Filter'; -import { t } from '../../../locales'; - -const $ = window.$ = require('jquery'); - -const propTypes = { - name: PropTypes.string, - onChange: PropTypes.func, - value: PropTypes.array, - datasource: PropTypes.object, -}; - -const defaultProps = { - onChange: () => {}, - value: [], -}; - -export default class FilterControl extends React.Component { - - constructor(props) { - super(props); - const initialFilters = props.value.map(() => ({ - valuesLoading: false, - valueChoices: [], - })); - this.state = { - filters: initialFilters, - activeRequest: null, - }; - } - - componentDidMount() { - this.state.filters.forEach((filter, index) => this.fetchFilterValues(index)); - } - - fetchFilterValues(index, column) { - const datasource = this.props.datasource; - const col = column || this.props.value[index].col; - const having = this.props.name === 'having_filters'; - if (col && this.props.datasource && this.props.datasource.filter_select && !having) { - this.setState((prevState) => { - const newStateFilters = Object.assign([], prevState.filters); - newStateFilters[index].valuesLoading = true; - return { filters: newStateFilters }; - }); - // if there is an outstanding request to fetch values, cancel it. - if (this.state.activeRequest) { - this.state.activeRequest.abort(); - } - this.setState({ - activeRequest: $.ajax({ - type: 'GET', - url: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`, - success: (data) => { - this.setState((prevState) => { - const newStateFilters = Object.assign([], prevState.filters); - newStateFilters[index] = { valuesLoading: false, valueChoices: data }; - return { filters: newStateFilters, activeRequest: null }; - }); - }, - }), - }); - } - } - - addFilter() { - const newFilters = Object.assign([], this.props.value); - const col = this.props.datasource && this.props.datasource.filterable_cols.length > 0 ? - this.props.datasource.filterable_cols[0][0] : - null; - newFilters.push({ - col, - op: 'in', - val: this.props.datasource.filter_select ? [] : '', - }); - this.props.onChange(newFilters); - const nextIndex = this.state.filters.length; - this.setState((prevState) => { - const newStateFilters = Object.assign([], prevState.filters); - newStateFilters.push({ valuesLoading: false, valueChoices: [] }); - return { filters: newStateFilters }; - }); - this.fetchFilterValues(nextIndex, col); - } - - changeFilter(index, control, value) { - const newFilters = Object.assign([], this.props.value); - const modifiedFilter = Object.assign({}, newFilters[index]); - if (typeof control === 'string') { - modifiedFilter[control] = value; - } else { - control.forEach((c, i) => { - modifiedFilter[c] = value[i]; - }); - } - // Clear selected values and refresh upon column change - if (control === 'col') { - if (modifiedFilter.val.constructor === Array) { - modifiedFilter.val = []; - } else if (typeof modifiedFilter.val === 'string') { - modifiedFilter.val = ''; - } - this.fetchFilterValues(index, value); - } - newFilters.splice(index, 1, modifiedFilter); - this.props.onChange(newFilters); - } - - removeFilter(index) { - this.props.onChange(this.props.value.filter((f, i) => i !== index)); - this.setState((prevState) => { - const newStateFilters = Object.assign([], prevState.filters); - newStateFilters.splice(index, 1); - return { filters: newStateFilters }; - }); - } - - render() { - const filters = this.props.value.map((filter, i) => ( -
- -
- )); - return ( -
- {filters} - - - - - -
- ); - } -} - -FilterControl.propTypes = propTypes; -FilterControl.defaultProps = defaultProps; diff --git a/superset/assets/src/explore/components/controls/index.js b/superset/assets/src/explore/components/controls/index.js index 81991275eace2..4a2df4237bc2e 100644 --- a/superset/assets/src/explore/components/controls/index.js +++ b/superset/assets/src/explore/components/controls/index.js @@ -6,7 +6,6 @@ import ColorPickerControl from './ColorPickerControl'; import ColorSchemeControl from './ColorSchemeControl'; import DatasourceControl from './DatasourceControl'; import DateFilterControl from './DateFilterControl'; -import FilterControl from './FilterControl'; import FixedOrMetricControl from './FixedOrMetricControl'; import HiddenControl from './HiddenControl'; import SelectAsyncControl from './SelectAsyncControl'; @@ -29,7 +28,6 @@ const controlMap = { ColorSchemeControl, DatasourceControl, DateFilterControl, - FilterControl, FixedOrMetricControl, HiddenControl, SelectAsyncControl, diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 46ece7041d6aa..68dcdc5152992 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -1128,34 +1128,6 @@ export const controls = { default: '', }, - where: { - type: 'TextAreaControl', - label: t('Custom WHERE clause'), - default: '', - language: 'sql', - minLines: 2, - maxLines: 10, - offerEditInModal: false, - description: t('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: 'TextAreaControl', - label: t('Custom HAVING clause'), - default: '', - language: 'sql', - minLines: 2, - maxLines: 10, - offerEditInModal: false, - description: t('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: 'TextControl', label: t('Comparison Period Lag'), @@ -1818,16 +1790,6 @@ export const controls = { description: t('Labels for the marker lines'), }, - filters: { - type: 'FilterControl', - label: '', - default: [], - description: '', - mapStateToProps: state => ({ - datasource: state.datasource, - }), - }, - annotation_layers: { type: 'AnnotationLayerControl', label: '', @@ -1850,18 +1812,6 @@ export const controls = { provideFormDataToProps: true, }, - having_filters: { - type: 'FilterControl', - label: '', - default: [], - description: '', - mapStateToProps: state => ({ - choices: (state.datasource) ? state.datasource.metrics_combo - .concat(state.datasource.filterable_cols) : [], - datasource: state.datasource, - }), - }, - slice_id: { type: 'HiddenControl', label: t('Slice ID'), diff --git a/superset/assets/src/explore/store.js b/superset/assets/src/explore/store.js index 32c132fd42a50..48a24df511232 100644 --- a/superset/assets/src/explore/store.js +++ b/superset/assets/src/explore/store.js @@ -68,14 +68,6 @@ export function getControlsState(state, form_data) { delete formData[k]; } } - // Removing invalid filters that point to a now inexisting column - if (control.type === 'FilterControl' && control.choices) { - if (!formData[k]) { - formData[k] = []; - } - const choiceValues = control.choices.map(c => c[0]); - formData[k] = formData[k].filter(flt => choiceValues.indexOf(flt.col) >= 0); - } if (typeof control.default === 'function') { control.default = control.default(control); diff --git a/superset/assets/src/explore/visTypes.js b/superset/assets/src/explore/visTypes.js index f771c38ad7817..b0037ec5f496c 100644 --- a/superset/assets/src/explore/visTypes.js +++ b/superset/assets/src/explore/visTypes.js @@ -40,14 +40,6 @@ export const sections = { ['since', 'until'], ], }, - sqlClause: { - label: t('SQL'), - controlSetRows: [ - ['where'], - ['having'], - ], - description: t('This section exposes ways to include snippets of SQL in your query'), - }, annotations: { label: t('Annotations and Layers'), expanded: true, @@ -80,20 +72,6 @@ export const sections = { ], }, ], - filters: [ - { - label: t('Filters'), - expanded: true, - controlSetRows: [['filters']], - }, - { - label: t('Result Filters'), - expanded: true, - description: t('The filters to apply after post-aggregation.' + - 'Leave the value control empty to filter empty strings or nulls'), - controlSetRows: [['having_filters']], - }, - ], }; const timeGrainSqlaAnimationOverrides = { @@ -263,6 +241,13 @@ export const visTypes = { ['line_charts_2', 'y_axis_2_format'], ], }, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['adhoc_filters'], + ], + }, sections.annotations, ], controlOverrides: { @@ -279,8 +264,6 @@ export const visTypes = { }, }, sectionOverrides: { - sqlClause: [], - filters: [[]], datasourceAndVizType: { label: t('Chart Type'), controlSetRows: [ @@ -310,7 +293,9 @@ export const visTypes = { label: t('Query'), expanded: true, controlSetRows: [ - ['metric', 'freq'], + ['metric'], + ['adhoc_filters'], + ['freq'], ], }, { @@ -373,6 +358,13 @@ export const visTypes = { ['metric_2', 'y_axis_2_format'], ], }, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['adhoc_filters'], + ], + }, sections.annotations, ], controlOverrides: { @@ -489,6 +481,13 @@ export const visTypes = { ['deck_slices', null], ], }, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['adhoc_filters'], + ], + }, ], }, @@ -502,6 +501,7 @@ export const visTypes = { controlSetRows: [ ['spatial', 'size'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -540,6 +540,7 @@ export const visTypes = { controlSetRows: [ ['spatial', 'size'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -579,6 +580,7 @@ export const visTypes = { controlSetRows: [ ['line_column', 'line_type'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -612,6 +614,7 @@ export const visTypes = { controlSetRows: [ ['spatial', 'size'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -656,6 +659,7 @@ export const visTypes = { expanded: true, controlSetRows: [ ['geojson', 'row_limit'], + ['adhoc_filters'], ], }, { @@ -696,6 +700,7 @@ export const visTypes = { controlSetRows: [ ['line_column', 'line_type'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -736,6 +741,7 @@ export const visTypes = { controlSetRows: [ ['start_spatial', 'end_spatial'], ['row_limit', null], + ['adhoc_filters'], ], }, { @@ -784,6 +790,7 @@ export const visTypes = { expanded: true, controlSetRows: [ ['spatial', 'row_limit'], + ['adhoc_filters'], ], }, { @@ -901,6 +908,13 @@ export const visTypes = { ['row_limit', null], ], }, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['adhoc_filters'], + ], + }, { label: t('Options'), expanded: true, @@ -929,7 +943,9 @@ export const visTypes = { label: t('Query'), expanded: true, controlSetRows: [ - ['groupby', 'metrics'], + ['metrics'], + ['adhoc_filters'], + ['groupby'], ['limit'], ['column_collection'], ['url'], @@ -969,8 +985,10 @@ export const visTypes = { label: t('Query'), expanded: true, controlSetRows: [ - ['groupby', 'columns'], ['metrics'], + ['adhoc_filters'], + ['groupby'], + ['columns'], ['row_limit', null], ], }, @@ -1017,7 +1035,9 @@ export const visTypes = { label: t('Query'), expanded: true, controlSetRows: [ - ['series', 'metric'], + ['metric'], + ['adhoc_filters'], + ['series'], ['row_limit', null], ], }, @@ -1040,6 +1060,7 @@ export const visTypes = { expanded: true, controlSetRows: [ ['metrics'], + ['adhoc_filters'], ['groupby'], ], }, @@ -1070,6 +1091,7 @@ export const visTypes = { controlSetRows: [ ['domain_granularity', 'subdomain_granularity'], ['metrics'], + ['adhoc_filters'], ], }, { @@ -1106,7 +1128,9 @@ export const visTypes = { expanded: true, controlSetRows: [ ['metrics'], - ['groupby', 'limit'], + ['adhoc_filters'], + ['groupby'], + ['limit'], ], }, { @@ -1128,8 +1152,11 @@ export const visTypes = { expanded: true, controlSetRows: [ ['series', 'entity'], - ['x', 'y'], - ['size', 'max_bubble_size'], + ['x'], + ['y'], + ['adhoc_filters'], + ['size'], + ['max_bubble_size'], ['limit', null], ], }, @@ -1179,6 +1206,7 @@ export const visTypes = { expanded: true, controlSetRows: [ ['metric'], + ['adhoc_filters'], ], }, { @@ -1256,6 +1284,7 @@ export const visTypes = { expanded: true, controlSetRows: [ ['all_columns_x'], + ['adhoc_filters'], ['row_limit'], ['groupby'], ], @@ -1298,7 +1327,9 @@ export const visTypes = { expanded: true, controlSetRows: [ ['groupby'], - ['metric', 'secondary_metric'], + ['metric'], + ['secondary_metric'], + ['adhoc_filters'], ['row_limit'], ], }, @@ -1338,6 +1369,7 @@ export const visTypes = { controlSetRows: [ ['groupby'], ['metric'], + ['adhoc_filters'], ['row_limit'], ], }, @@ -1366,6 +1398,7 @@ export const visTypes = { controlSetRows: [ ['groupby'], ['metric'], + ['adhoc_filters'], ['row_limit'], ], }, @@ -1391,8 +1424,11 @@ export const visTypes = { label: t('Query'), expanded: true, controlSetRows: [ - ['groupby', 'columns'], - ['metric', 'row_limit'], + ['groupby'], + ['columns'], + ['metric'], + ['adhoc_filters'], + ['row_limit'], ], }, { @@ -1432,6 +1468,7 @@ export const visTypes = { controlSetRows: [ ['entity'], ['metric'], + ['adhoc_filters'], ], }, { @@ -1467,6 +1504,7 @@ export const visTypes = { ['entity'], ['country_fieldtype'], ['metric'], + ['adhoc_filters'], ], }, { @@ -1503,6 +1541,7 @@ export const visTypes = { controlSetRows: [ ['groupby'], ['metric'], + ['adhoc_filters'], ['date_filter', 'instant_filtering'], ['show_sqla_time_granularity', 'show_sqla_time_column'], ['show_druid_time_granularity', 'show_druid_time_origin'], @@ -1544,6 +1583,7 @@ export const visTypes = { ['series'], ['metrics'], ['secondary_metric'], + ['adhoc_filters'], ['limit'], ], }, @@ -1564,7 +1604,9 @@ export const visTypes = { expanded: true, controlSetRows: [ ['all_columns_x', 'all_columns_y'], - ['metric', 'row_limit'], + ['metric'], + ['adhoc_filters'], + ['row_limit'], ], }, { @@ -1627,6 +1669,7 @@ export const visTypes = { ['all_columns_x', 'all_columns_y'], ['clustering_radius'], ['row_limit'], + ['adhoc_filters'], ['groupby'], ], }, @@ -1701,6 +1744,13 @@ export const visTypes = { ['min_leaf_node_event_count'], ], }, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['adhoc_filters'], + ], + }, { label: t('Additional meta data'), controlSetRows: [ @@ -1799,12 +1849,6 @@ export const visTypes = { export default visTypes; -function adhocFilterEnabled(viz) { - return viz.controlPanelSections.find(( - section => section.controlSetRows.find(row => row.find(control => control === 'adhoc_filters')) - )); -} - export function sectionsToRender(vizType, datasourceType) { const viz = visTypes[vizType]; @@ -1826,7 +1870,5 @@ export function sectionsToRender(vizType, datasourceType) { sectionsCopy.datasourceAndVizType, datasourceType === 'table' ? sectionsCopy.sqlaTimeSeries : sectionsCopy.druidTimeSeries, viz.controlPanelSections, - !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sectionsCopy.sqlClause : []), - !adhocFilterEnabled(viz) && (datasourceType === 'table' ? sectionsCopy.filters[0] : sectionsCopy.filters), ).filter(section => section); } diff --git a/superset/migrations/versions/bddc498dd179_adhoc_filters.py b/superset/migrations/versions/bddc498dd179_adhoc_filters.py new file mode 100644 index 0000000000000..55503f00719bd --- /dev/null +++ b/superset/migrations/versions/bddc498dd179_adhoc_filters.py @@ -0,0 +1,97 @@ +"""adhoc filters + +Revision ID: bddc498dd179 +Revises: afb7730f6a9c +Create Date: 2018-06-13 14:54:47.086507 + +""" + +# revision identifiers, used by Alembic. +revision = 'bddc498dd179' +down_revision = '80a67c5192fa' + + +from collections import defaultdict +import json +import uuid + +from alembic import op +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, Text + +from superset import db +from superset import utils + + +Base = declarative_base() + + +class Slice(Base): + __tablename__ = 'slices' + + id = Column(Integer, primary_key=True) + params = Column(Text) + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + mapping = {'having': 'having_filters', 'where': 'filters'} + + for slc in session.query(Slice).all(): + try: + params = json.loads(slc.params) + + if not 'adhoc_filters' in params: + params['adhoc_filters'] = [] + + for clause, filters in mapping.items(): + if clause in params and params[clause] != '': + params['adhoc_filters'].append({ + 'clause': clause.upper(), + 'expressionType': 'SQL', + 'filterOptionName': str(uuid.uuid4()), + 'sqlExpression': params[clause], + }) + + if filters in params: + for filt in params[filters]: + params['adhoc_filters'].append({ + 'clause': clause.upper(), + 'comparator': filt['val'], + 'expressionType': 'SIMPLE', + 'filterOptionName': str(uuid.uuid4()), + 'operator': filt['op'], + 'subject': filt['col'], + }) + + for key in ('filters', 'having', 'having_filters', 'where'): + if key in params: + del params[key] + + slc.params = json.dumps(params, sort_keys=True) + except Exception: + pass + + session.commit() + session.close() + + +def downgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + for slc in session.query(Slice).all(): + try: + params = json.loads(slc.params) + utils.split_adhoc_filters_into_base_filters(params) + + if 'adhoc_filters' in params: + del params['adhoc_filters'] + + slc.params = json.dumps(params, sort_keys=True) + except Exception: + pass + + session.commit() + session.close() diff --git a/superset/translations/de/LC_MESSAGES/messages.po b/superset/translations/de/LC_MESSAGES/messages.po index 7db79cda09975..59077e21abbe0 100644 --- a/superset/translations/de/LC_MESSAGES/messages.po +++ b/superset/translations/de/LC_MESSAGES/messages.po @@ -1193,7 +1193,6 @@ msgstr "" msgid "Select operator" msgstr "" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:138 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "" diff --git a/superset/translations/en/LC_MESSAGES/messages.po b/superset/translations/en/LC_MESSAGES/messages.po index 80f9112cc248a..3c32531869745 100644 --- a/superset/translations/en/LC_MESSAGES/messages.po +++ b/superset/translations/en/LC_MESSAGES/messages.po @@ -2636,7 +2636,250 @@ msgstr "" msgid "An unknown error occurred. (Status: %s )" msgstr "" -#: superset/assets/javascripts/profile/components/App.jsx:24 +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:228 +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:166 +#, python-format +msgid "%s column(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:229 +msgid "To filter on a metric, use Custom SQL tab." +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:235 +#, python-format +msgid "%s operators(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:275 +msgid "type a value here" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:284 +#: superset/assets/src/explore/components/controls/Filter.jsx:120 +msgid "Filter value" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx:91 +msgid "choose WHERE or HAVING..." +msgstr "" + +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:179 +#, python-format +msgid "%s aggregates(s)" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:35 +msgid "description" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:46 +msgid "bolt" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:47 +msgid "Changing this control takes effect instantly" +msgstr "" + +#: superset/assets/src/explore/components/DisplayQueryButton.jsx:75 +msgid "Error..." +msgstr "" + +#: superset/assets/src/explore/components/EmbedCodeButton.jsx:91 +msgid "Width" +msgstr "" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:42 +msgid "Export to .json" +msgstr "" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:52 +msgid "Export to .csv format" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:64 +#, python-format +msgid "%s - untitled" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:100 +msgid "Edit chart properties" +msgstr "" + +#: superset/assets/src/explore/components/RowCountLabel.jsx:25 +msgid "Limit reached" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:74 +msgid "Please enter a chart name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:89 +msgid "Please select a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:97 +msgid "Please enter a dashboard name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:133 +msgid "Save A Chart" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:154 +#, python-format +msgid "Overwrite chart %s" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:167 +msgid "[chart name]" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:180 +msgid "Do not add to a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:188 +msgid "Add chart to existing dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:204 +msgid "Add to new dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:230 +msgid "Save & go to dashboard" +msgstr "" + +#: superset/assets/src/explore/components/controls/AdhocFilterControl.jsx:241 +msgid "choose a column or metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/AnnotationLayerControl.jsx:147 +msgid "Add Annotation Layer" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:50 +msgid "`Min` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:53 +msgid "`Max` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:70 +#: superset/connectors/druid/views.py:57 +msgid "Min" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:78 +#: superset/connectors/druid/views.py:58 +msgid "Max" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:79 +msgid "Something went wrong while fetching the datasource list" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:110 +msgid "Select a datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:120 +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:120 +msgid "Search / Filter" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:180 +msgid "Click to point to another datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:191 +msgid "Edit the datasource's configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:203 +msgid "Show datasource configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select column" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:161 +msgid "Select operator" +msgstr "" + +#: superset/templates/appbuilder/general/widgets/search.html:6 +msgid "Add Filter" +msgstr "" + +#: superset/assets/src/explore/components/controls/MetricsControl.jsx:239 +msgid "choose a column or aggregate function" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectAsyncControl.jsx:25 +msgid "Error while fetching data" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:48 +msgid "No results found" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:114 +#, python-format +msgid "%s option(s)" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:62 +msgid "Invalid lat/long configuration." +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:128 +msgid "Longitude & Latitude columns" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:144 +msgid "Delimited long & lat single column" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:145 +msgid "" +"Multiple formats accepted, look the geopy.points Python library for more " +"details" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:157 +msgid "Reverse lat/long " +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:163 +msgid "Geohash" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:70 +msgid "textarea" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "Edit" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "in modal" +msgstr "" + +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:110 +msgid "Select a visualization type" +msgstr "" + +#: superset/assets/src/profile/components/App.jsx:24 +#: superset/assets/src/welcome/App.jsx:56 +#: superset/assets/src/welcome/App.jsx:59 msgid "Favorites" msgstr "" diff --git a/superset/translations/es/LC_MESSAGES/messages.po b/superset/translations/es/LC_MESSAGES/messages.po index 4742f76379b11..1dbd4649ce5f7 100644 --- a/superset/translations/es/LC_MESSAGES/messages.po +++ b/superset/translations/es/LC_MESSAGES/messages.po @@ -1250,7 +1250,6 @@ msgstr "Selecciona la columna" msgid "Select operator" msgstr "Selecciona el operador" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:138 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "Añadir Filtro" diff --git a/superset/translations/fr/LC_MESSAGES/messages.po b/superset/translations/fr/LC_MESSAGES/messages.po index 33ce07e9a5ca1..7c75eb4e54217 100644 --- a/superset/translations/fr/LC_MESSAGES/messages.po +++ b/superset/translations/fr/LC_MESSAGES/messages.po @@ -2636,7 +2636,250 @@ msgstr "" msgid "An unknown error occurred. (Status: %s )" msgstr "" -#: superset/assets/javascripts/profile/components/App.jsx:24 +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:228 +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:166 +#, python-format +msgid "%s column(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:229 +msgid "To filter on a metric, use Custom SQL tab." +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:235 +#, python-format +msgid "%s operators(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:275 +msgid "type a value here" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:284 +#: superset/assets/src/explore/components/controls/Filter.jsx:120 +msgid "Filter value" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx:91 +msgid "choose WHERE or HAVING..." +msgstr "" + +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:179 +#, python-format +msgid "%s aggregates(s)" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:35 +msgid "description" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:46 +msgid "bolt" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:47 +msgid "Changing this control takes effect instantly" +msgstr "" + +#: superset/assets/src/explore/components/DisplayQueryButton.jsx:75 +msgid "Error..." +msgstr "" + +#: superset/assets/src/explore/components/EmbedCodeButton.jsx:91 +msgid "Width" +msgstr "Largeur" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:42 +msgid "Export to .json" +msgstr "" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:52 +msgid "Export to .csv format" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:64 +#, python-format +msgid "%s - untitled" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:100 +msgid "Edit chart properties" +msgstr "" + +#: superset/assets/src/explore/components/RowCountLabel.jsx:25 +msgid "Limit reached" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:74 +msgid "Please enter a chart name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:89 +msgid "Please select a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:97 +msgid "Please enter a dashboard name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:133 +msgid "Save A Chart" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:154 +#, python-format +msgid "Overwrite chart %s" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:167 +msgid "[chart name]" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:180 +msgid "Do not add to a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:188 +msgid "Add chart to existing dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:204 +msgid "Add to new dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:230 +msgid "Save & go to dashboard" +msgstr "" + +#: superset/assets/src/explore/components/controls/AdhocFilterControl.jsx:241 +msgid "choose a column or metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/AnnotationLayerControl.jsx:147 +msgid "Add Annotation Layer" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:50 +msgid "`Min` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:53 +msgid "`Max` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:70 +#: superset/connectors/druid/views.py:57 +msgid "Min" +msgstr "Min" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:78 +#: superset/connectors/druid/views.py:58 +msgid "Max" +msgstr "Max" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:79 +msgid "Something went wrong while fetching the datasource list" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:110 +msgid "Select a datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:120 +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:120 +msgid "Search / Filter" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:180 +msgid "Click to point to another datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:191 +msgid "Edit the datasource's configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:203 +msgid "Show datasource configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select column" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:161 +msgid "Select operator" +msgstr "" + +#: superset/templates/appbuilder/general/widgets/search.html:6 +msgid "Add Filter" +msgstr "Ajouter un filtre" + +#: superset/assets/src/explore/components/controls/MetricsControl.jsx:239 +msgid "choose a column or aggregate function" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectAsyncControl.jsx:25 +msgid "Error while fetching data" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:48 +msgid "No results found" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:114 +#, python-format +msgid "%s option(s)" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:62 +msgid "Invalid lat/long configuration." +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:128 +msgid "Longitude & Latitude columns" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:144 +msgid "Delimited long & lat single column" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:145 +msgid "" +"Multiple formats accepted, look the geopy.points Python library for more " +"details" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:157 +msgid "Reverse lat/long " +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:163 +msgid "Geohash" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:70 +msgid "textarea" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "Edit" +msgstr "Éditer" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "in modal" +msgstr "" + +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:110 +msgid "Select a visualization type" +msgstr "" + +#: superset/assets/src/profile/components/App.jsx:24 +#: superset/assets/src/welcome/App.jsx:56 +#: superset/assets/src/welcome/App.jsx:59 msgid "Favorites" msgstr "" diff --git a/superset/translations/it/LC_MESSAGES/messages.po b/superset/translations/it/LC_MESSAGES/messages.po index 5ef5b4b28e0e4..f383e14d51877 100644 --- a/superset/translations/it/LC_MESSAGES/messages.po +++ b/superset/translations/it/LC_MESSAGES/messages.po @@ -1598,7 +1598,6 @@ msgstr "Seleziona una colonna" msgid "Select operator" msgstr "Seleziona operatore" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:145 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "Aggiungi filtro" diff --git a/superset/translations/ja/LC_MESSAGES/messages.po b/superset/translations/ja/LC_MESSAGES/messages.po index 5f6b714f5c01b..df1618511f6c4 100644 --- a/superset/translations/ja/LC_MESSAGES/messages.po +++ b/superset/translations/ja/LC_MESSAGES/messages.po @@ -1202,7 +1202,6 @@ msgstr "列を選択" msgid "Select operator" msgstr "オペレータを選択" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:138 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "フィルターを追加" diff --git a/superset/translations/messages.pot b/superset/translations/messages.pot index 85a2c76402995..b30afc8d11e80 100644 --- a/superset/translations/messages.pot +++ b/superset/translations/messages.pot @@ -2934,7 +2934,250 @@ msgstr "" msgid "An unknown error occurred. (Status: %s )" msgstr "" -#: superset/assets/javascripts/profile/components/App.jsx:24 +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:228 +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:166 +#, python-format +msgid "%s column(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:229 +msgid "To filter on a metric, use Custom SQL tab." +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:235 +#, python-format +msgid "%s operators(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:275 +msgid "type a value here" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:284 +#: superset/assets/src/explore/components/controls/Filter.jsx:120 +msgid "Filter value" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx:91 +msgid "choose WHERE or HAVING..." +msgstr "" + +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:179 +#, python-format +msgid "%s aggregates(s)" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:35 +msgid "description" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:46 +msgid "bolt" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:47 +msgid "Changing this control takes effect instantly" +msgstr "" + +#: superset/assets/src/explore/components/DisplayQueryButton.jsx:75 +msgid "Error..." +msgstr "" + +#: superset/assets/src/explore/components/EmbedCodeButton.jsx:91 +msgid "Width" +msgstr "" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:42 +msgid "Export to .json" +msgstr "" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:52 +msgid "Export to .csv format" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:64 +#, python-format +msgid "%s - untitled" +msgstr "" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:100 +msgid "Edit chart properties" +msgstr "" + +#: superset/assets/src/explore/components/RowCountLabel.jsx:25 +msgid "Limit reached" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:74 +msgid "Please enter a chart name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:89 +msgid "Please select a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:97 +msgid "Please enter a dashboard name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:133 +msgid "Save A Chart" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:154 +#, python-format +msgid "Overwrite chart %s" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:167 +msgid "[chart name]" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:180 +msgid "Do not add to a dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:188 +msgid "Add chart to existing dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:204 +msgid "Add to new dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:230 +msgid "Save & go to dashboard" +msgstr "" + +#: superset/assets/src/explore/components/controls/AdhocFilterControl.jsx:241 +msgid "choose a column or metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/AnnotationLayerControl.jsx:147 +msgid "Add Annotation Layer" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:50 +msgid "`Min` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:53 +msgid "`Max` value should be numeric or empty" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:70 +#: superset/connectors/druid/views.py:57 +msgid "Min" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:78 +#: superset/connectors/druid/views.py:58 +msgid "Max" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:79 +msgid "Something went wrong while fetching the datasource list" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:110 +msgid "Select a datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:120 +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:120 +msgid "Search / Filter" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:180 +msgid "Click to point to another datasource" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:191 +msgid "Edit the datasource's configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:203 +msgid "Show datasource configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select column" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:161 +msgid "Select operator" +msgstr "" + +#: superset/templates/appbuilder/general/widgets/search.html:6 +msgid "Add Filter" +msgstr "" + +#: superset/assets/src/explore/components/controls/MetricsControl.jsx:239 +msgid "choose a column or aggregate function" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectAsyncControl.jsx:25 +msgid "Error while fetching data" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:48 +msgid "No results found" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:114 +#, python-format +msgid "%s option(s)" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:62 +msgid "Invalid lat/long configuration." +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:128 +msgid "Longitude & Latitude columns" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:144 +msgid "Delimited long & lat single column" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:145 +msgid "" +"Multiple formats accepted, look the geopy.points Python library for more " +"details" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:157 +msgid "Reverse lat/long " +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:163 +msgid "Geohash" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:70 +msgid "textarea" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "Edit" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "in modal" +msgstr "" + +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:110 +msgid "Select a visualization type" +msgstr "" + +#: superset/assets/src/profile/components/App.jsx:24 +#: superset/assets/src/welcome/App.jsx:56 +#: superset/assets/src/welcome/App.jsx:59 msgid "Favorites" msgstr "" diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.po b/superset/translations/pt_BR/LC_MESSAGES/messages.po index b2a5b3474b470..87100212351ce 100644 --- a/superset/translations/pt_BR/LC_MESSAGES/messages.po +++ b/superset/translations/pt_BR/LC_MESSAGES/messages.po @@ -1256,7 +1256,6 @@ msgstr "Selecione a coluna" msgid "Select operator" msgstr "Selecione o operador" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:138 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "Adicionar filtro" diff --git a/superset/translations/ru/LC_MESSAGES/messages.po b/superset/translations/ru/LC_MESSAGES/messages.po index 79e5713a445a8..5ea2cc1b5172a 100644 --- a/superset/translations/ru/LC_MESSAGES/messages.po +++ b/superset/translations/ru/LC_MESSAGES/messages.po @@ -1599,7 +1599,6 @@ msgstr "Выбрать столбец" msgid "Select operator" msgstr "Выбрать оператор" -#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:145 #: superset/templates/appbuilder/general/widgets/search.html:6 msgid "Add Filter" msgstr "Добавить фильтр" diff --git a/superset/translations/zh/LC_MESSAGES/messages.po b/superset/translations/zh/LC_MESSAGES/messages.po index 7283f82d44b23..eb1c551b48566 100644 --- a/superset/translations/zh/LC_MESSAGES/messages.po +++ b/superset/translations/zh/LC_MESSAGES/messages.po @@ -2569,6 +2569,250 @@ msgid "An unknown error occurred. (Status: %s )" msgstr "出现未知错误。(状态:%s)" #: superset/assets/javascripts/profile/components/App.jsx:24 +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:228 +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:166 +#, python-format +msgid "%s column(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:229 +msgid "To filter on a metric, use Custom SQL tab." +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:235 +#, python-format +msgid "%s operators(s)" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:275 +msgid "type a value here" +msgstr "" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSimpleTabContent.jsx:284 +#: superset/assets/src/explore/components/controls/Filter.jsx:120 +msgid "Filter value" +msgstr "过滤值" + +#: superset/assets/src/explore/components/AdhocFilterEditPopoverSqlTabContent.jsx:91 +msgid "choose WHERE or HAVING..." +msgstr "" + +#: superset/assets/src/explore/components/AdhocMetricEditPopover.jsx:179 +#, python-format +msgid "%s aggregates(s)" +msgstr "" + +#: superset/assets/src/explore/components/ControlHeader.jsx:35 +msgid "description" +msgstr "描述" + +#: superset/assets/src/explore/components/ControlHeader.jsx:46 +msgid "bolt" +msgstr "螺栓" + +#: superset/assets/src/explore/components/ControlHeader.jsx:47 +msgid "Changing this control takes effect instantly" +msgstr "" + +#: superset/assets/src/explore/components/DisplayQueryButton.jsx:75 +msgid "Error..." +msgstr "错误 ..." + +#: superset/assets/src/explore/components/EmbedCodeButton.jsx:91 +msgid "Width" +msgstr "宽度" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:42 +msgid "Export to .json" +msgstr "导出到 .json" + +#: superset/assets/src/explore/components/ExploreActionButtons.jsx:52 +msgid "Export to .csv format" +msgstr "导出为 .csv 格式" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:64 +#, python-format +msgid "%s - untitled" +msgstr "%s - 无标题" + +#: superset/assets/src/explore/components/ExploreChartHeader.jsx:100 +msgid "Edit chart properties" +msgstr "" + +#: superset/assets/src/explore/components/RowCountLabel.jsx:25 +msgid "Limit reached" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:74 +msgid "Please enter a chart name" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:89 +msgid "Please select a dashboard" +msgstr "请选择一个仪表板" + +#: superset/assets/src/explore/components/SaveModal.jsx:97 +msgid "Please enter a dashboard name" +msgstr "请输入仪表板名称" + +#: superset/assets/src/explore/components/SaveModal.jsx:133 +msgid "Save A Chart" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:154 +#, python-format +msgid "Overwrite chart %s" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:167 +msgid "[chart name]" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:180 +msgid "Do not add to a dashboard" +msgstr "不要添加到仪表板" + +#: superset/assets/src/explore/components/SaveModal.jsx:188 +msgid "Add chart to existing dashboard" +msgstr "" + +#: superset/assets/src/explore/components/SaveModal.jsx:204 +msgid "Add to new dashboard" +msgstr "添加到新的仪表板" + +#: superset/assets/src/explore/components/SaveModal.jsx:230 +msgid "Save & go to dashboard" +msgstr "保存并转到仪表板" + +#: superset/assets/src/explore/components/controls/AdhocFilterControl.jsx:241 +msgid "choose a column or metric" +msgstr "" + +#: superset/assets/src/explore/components/controls/AnnotationLayerControl.jsx:147 +msgid "Add Annotation Layer" +msgstr "" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:50 +msgid "`Min` value should be numeric or empty" +msgstr "最小值应该是数字或空的" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:53 +msgid "`Max` value should be numeric or empty" +msgstr "最大值应该是数字或空的" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:70 +#: superset/connectors/druid/views.py:57 +msgid "Min" +msgstr "最小值" + +#: superset/assets/src/explore/components/controls/BoundsControl.jsx:78 +#: superset/connectors/druid/views.py:58 +msgid "Max" +msgstr "最大值" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:79 +msgid "Something went wrong while fetching the datasource list" +msgstr "提取数据源列表时出错" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:110 +msgid "Select a datasource" +msgstr "选择一个数据源" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:120 +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:120 +msgid "Search / Filter" +msgstr "搜索 / 过滤" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:180 +msgid "Click to point to another datasource" +msgstr "点击指向另一个数据源" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:191 +msgid "Edit the datasource's configuration" +msgstr "编辑数据源的配置" + +#: superset/assets/src/explore/components/controls/DatasourceControl.jsx:203 +msgid "Show datasource configuration" +msgstr "" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select metric" +msgstr "选择指标" + +#: superset/assets/src/explore/components/controls/Filter.jsx:149 +msgid "Select column" +msgstr "选择列" + +#: superset/assets/src/explore/components/controls/Filter.jsx:161 +msgid "Select operator" +msgstr "选择运算符" + +#: superset/templates/appbuilder/general/widgets/search.html:6 +msgid "Add Filter" +msgstr "增加过滤条件" + +#: superset/assets/src/explore/components/controls/MetricsControl.jsx:239 +msgid "choose a column or aggregate function" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectAsyncControl.jsx:25 +msgid "Error while fetching data" +msgstr "获取数据时出错" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:48 +msgid "No results found" +msgstr "" + +#: superset/assets/src/explore/components/controls/SelectControl.jsx:114 +#, python-format +msgid "%s option(s)" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:62 +msgid "Invalid lat/long configuration." +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:128 +msgid "Longitude & Latitude columns" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:144 +msgid "Delimited long & lat single column" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:145 +msgid "" +"Multiple formats accepted, look the geopy.points Python library for more " +"details" +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:157 +msgid "Reverse lat/long " +msgstr "" + +#: superset/assets/src/explore/components/controls/SpatialControl.jsx:163 +msgid "Geohash" +msgstr "" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:70 +msgid "textarea" +msgstr "文本区域" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "Edit" +msgstr "编辑" + +#: superset/assets/src/explore/components/controls/TextAreaControl.jsx:98 +msgid "in modal" +msgstr "在模态中" + +#: superset/assets/src/explore/components/controls/VizTypeControl.jsx:110 +msgid "Select a visualization type" +msgstr "选择一个可视化类型" + +#: superset/assets/src/profile/components/App.jsx:24 +#: superset/assets/src/welcome/App.jsx:56 +#: superset/assets/src/welcome/App.jsx:59 msgid "Favorites" msgstr "收藏" diff --git a/superset/utils.py b/superset/utils.py index 08ce0d2f385af..eec253037698a 100644 --- a/superset/utils.py +++ b/superset/utils.py @@ -881,4 +881,3 @@ def split_adhoc_filters_into_base_filters(fd): fd['having'] = ' AND '.join(['({})'.format(sql) for sql in sql_having_filters]) fd['having_filters'] = simple_having_filters fd['filters'] = simple_where_filters - del fd['adhoc_filters'] From 8c03fabc9cc4f545249dcc0b28845a15ce7ea48b Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Mon, 16 Jul 2018 16:30:11 -0700 Subject: [PATCH 03/14] Time shift difference (#5177) * Allow different comparisons when using time shift * Remove test code * Remove lead/trail NaN and improve code * Add headers/subheader * Update yarn * Migration script * Fix migration * Small fixes * Trigger tests * Fix lint * Fix javascript (cherry picked from commit 7b4e6c7) --- superset/assets/package.json | 1 + superset/assets/src/SqlLab/actions.js | 4 +- .../components/ControlPanelsContainer.jsx | 22 ++- superset/assets/src/explore/controls.jsx | 47 +++--- superset/assets/src/explore/main.css | 21 +++ superset/assets/src/explore/store.js | 5 + .../src/explore/{visTypes.js => visTypes.jsx} | 8 +- .../assets/src/visualizations/nvd3_vis.js | 4 +- superset/assets/yarn.lock | 17 +- ..._migrate_num_period_compare_and_period_.py | 157 ++++++++++++++++++ superset/viz.py | 56 ++++--- 11 files changed, 285 insertions(+), 57 deletions(-) rename superset/assets/src/explore/{visTypes.js => visTypes.jsx} (99%) create mode 100644 superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py diff --git a/superset/assets/package.json b/superset/assets/package.json index fb96f73473742..14b66f480fa06 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -67,6 +67,7 @@ "geojson-extent": "^0.3.2", "geolib": "^2.0.24", "immutable": "^3.8.2", + "is-react": "^1.1.1", "jed": "^1.1.1", "jquery": "3.1.1", "lodash.throttle": "^4.1.1", diff --git a/superset/assets/src/SqlLab/actions.js b/superset/assets/src/SqlLab/actions.js index 644947023bcb8..1a2566c493398 100644 --- a/superset/assets/src/SqlLab/actions.js +++ b/superset/assets/src/SqlLab/actions.js @@ -401,7 +401,9 @@ export function popSavedQuery(saveQueryId) { }; dispatch(addQueryEditor(queryEditorProps)); }, - error: () => notify.error(t('The query couldn\'t be loaded')), + error: () => { + dispatch(addDangerToast(t('The query couldn\'t be loaded'))); + }, }); }; } diff --git a/superset/assets/src/explore/components/ControlPanelsContainer.jsx b/superset/assets/src/explore/components/ControlPanelsContainer.jsx index 1bf653f9386e5..af74324911399 100644 --- a/superset/assets/src/explore/components/ControlPanelsContainer.jsx +++ b/superset/assets/src/explore/components/ControlPanelsContainer.jsx @@ -1,5 +1,6 @@ /* eslint camelcase: 0 */ import React from 'react'; +import isReact from 'is-react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -29,6 +30,10 @@ class ControlPanelsContainer extends React.Component { this.renderControlPanelSection = this.renderControlPanelSection.bind(this); } getControlData(controlName) { + if (isReact.element(controlName)) { + return controlName; + } + const control = this.props.controls[controlName]; // Identifying mapStateToProps function to apply (logic can't be in store) let mapF = controls[controlName].mapStateToProps; @@ -69,10 +74,13 @@ class ControlPanelsContainer extends React.Component { ( - controlName && - ctrls[controlName] && - { + if (!controlName) { + return null; + } else if (isReact.element(controlName)) { + return controlName; + } else if (ctrls[controlName]) { + return ( - ))} + />); + } + return null; + })} /> ))} diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 68dcdc5152992..d43b371d97cdb 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -857,7 +857,7 @@ export const controls = { resample_rule: { type: 'SelectControl', freeForm: true, - label: t('Resample Rule'), + label: t('Rule'), default: null, choices: formatSelectOptions(['', '1T', '1H', '1D', '7D', '1M', '1AS']), description: t('Pandas resample rule'), @@ -866,7 +866,7 @@ export const controls = { resample_how: { type: 'SelectControl', freeForm: true, - label: t('Resample How'), + label: t('How'), default: null, choices: formatSelectOptions(['', 'mean', 'sum', 'median']), description: t('Pandas resample how'), @@ -875,7 +875,7 @@ export const controls = { resample_fillmethod: { type: 'SelectControl', freeForm: true, - label: t('Resample Fill Method'), + label: t('Fill Method'), default: null, choices: formatSelectOptions(['', 'ffill', 'bfill']), description: t('Pandas resample fill method'), @@ -1203,11 +1203,12 @@ export const controls = { mapStateToProps: (state) => { const showWarning = ( state.controls && - state.controls.num_period_compare && - state.controls.num_period_compare.value !== ''); + state.controls.comparison_type && + state.controls.comparison_type.value === 'percentage'); return { warning: showWarning ? - t('When `Period Ratio` is set, the Y Axis Format is forced to `.1%`') : null, + t('When `Calculation type` is set to "Percentage change", the Y ' + + 'Axis Format is forced to `.1%`') : null, disabled: showWarning, }; }, @@ -1530,30 +1531,11 @@ export const controls = { description: t('Compute the contribution to the total'), }, - num_period_compare: { - type: 'TextControl', - label: t('Period Ratio'), - default: '', - isInt: true, - description: t('[integer] Number of period to compare against, ' + - 'this is relative to the granularity selected'), - }, - - period_ratio_type: { - type: 'SelectControl', - label: t('Period Ratio Type'), - default: 'growth', - choices: formatSelectOptions(['factor', 'growth', 'value']), - description: t('`factor` means (new/previous), `growth` is ' + - '((new/previous) - 1), `value` is (new-previous)'), - }, - time_compare: { type: 'SelectControl', multi: true, freeForm: true, label: t('Time Shift'), - default: [], choices: formatSelectOptions([ '1 day', '1 week', @@ -1567,6 +1549,21 @@ export const controls = { '56 weeks, 365 days)'), }, + comparison_type: { + type: 'SelectControl', + label: t('Calculation type'), + default: 'values', + choices: [ + ['values', 'Actual Values'], + ['absolute', 'Absolute difference'], + ['percentage', 'Percentage change'], + ['ratio', 'Ratio'], + ], + description: t('How to display time shifts: as individual lines; as the ' + + 'absolute difference between the main time series and each time shift; ' + + 'as the percentage change; or as the ratio between series and time shifts.'), + }, + subheader: { type: 'TextControl', label: t('Subheader'), diff --git a/superset/assets/src/explore/main.css b/superset/assets/src/explore/main.css index 9ab50882dc95a..32680e1e385c0 100644 --- a/superset/assets/src/explore/main.css +++ b/superset/assets/src/explore/main.css @@ -217,3 +217,24 @@ text-align: center; margin-top: 60px; } + +h1.section-header { + font-size: 14px; + font-weight: bold; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 5px; + margin-left: -16px; +} + +h2.section-header { + font-size: 13px; + font-weight: bold; + margin-bottom: 0; + margin-top: 0; + padding-bottom: 5px; +} + +.Select { + margin-bottom: 15px; +} diff --git a/superset/assets/src/explore/store.js b/superset/assets/src/explore/store.js index 48a24df511232..784100599c803 100644 --- a/superset/assets/src/explore/store.js +++ b/superset/assets/src/explore/store.js @@ -1,4 +1,5 @@ /* eslint camelcase: 0 */ +import isReact from 'is-react'; import controls from './controls'; import visTypes, { sectionsToRender } from './visTypes'; @@ -50,6 +51,10 @@ export function getControlsState(state, form_data) { const controlOverrides = viz.controlOverrides || {}; const controlsState = {}; controlNames.forEach((k) => { + if (isReact.element(k)) { + // no state + return; + } const control = Object.assign({}, controls[k], controlOverrides[k]); if (control.mapStateToProps) { Object.assign(control, control.mapStateToProps(state, control)); diff --git a/superset/assets/src/explore/visTypes.js b/superset/assets/src/explore/visTypes.jsx similarity index 99% rename from superset/assets/src/explore/visTypes.js rename to superset/assets/src/explore/visTypes.jsx index b0037ec5f496c..692a898183398 100644 --- a/superset/assets/src/explore/visTypes.js +++ b/superset/assets/src/explore/visTypes.jsx @@ -2,6 +2,7 @@ * This file defines how controls (defined in controls.js) are structured into sections * and associated with each and every visualization type. */ +import React from 'react'; import { D3_TIME_FORMAT_OPTIONS } from './controls'; import * as v from './validators'; import { t } from '../locales'; @@ -65,9 +66,12 @@ export const sections = { 'that allow for advanced analytical post processing ' + 'of query results'), controlSetRows: [ + [

Moving Average

], ['rolling_type', 'rolling_periods', 'min_periods'], - ['time_compare'], - ['num_period_compare', 'period_ratio_type'], + [

Time Comparison

], + ['time_compare', 'comparison_type'], + [

Python Functions

], + [

pandas.resample

], ['resample_how', 'resample_rule', 'resample_fillmethod'], ], }, diff --git a/superset/assets/src/visualizations/nvd3_vis.js b/superset/assets/src/visualizations/nvd3_vis.js index c5954bb9d17f2..a9f38b5c53093 100644 --- a/superset/assets/src/visualizations/nvd3_vis.js +++ b/superset/assets/src/visualizations/nvd3_vis.js @@ -400,8 +400,8 @@ export default function nvd3Vis(slice, payload) { const yAxisFormatter = d3FormatPreset(fd.y_axis_format); if (chart.yAxis && chart.yAxis.tickFormat) { - if (fd.num_period_compare || fd.contribution) { - // When computing a "Period Ratio" or "Contribution" selected, we force a percentage format + if (fd.contribution || fd.comparison_type === 'percentage') { + // When computing a "Percentage" or "Contribution" selected, we force a percentage format const percentageFormat = d3.format('.1%'); chart.yAxis.tickFormat(percentageFormat); } else { diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock index 37ee898cd1a14..8fc8bdf1747cd 100644 --- a/superset/assets/yarn.lock +++ b/superset/assets/yarn.lock @@ -3600,7 +3600,7 @@ fault@^1.0.2: dependencies: format "^0.2.2" -fbjs@^0.8.1, fbjs@^0.8.12, fbjs@^0.8.4, fbjs@^0.8.9: +fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" dependencies: @@ -4981,6 +4981,12 @@ is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" +is-react@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-react/-/is-react-1.1.1.tgz#304d5541e191190f2389bd588a33da0756bfa0c7" + dependencies: + react "^16.0.0" + is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" @@ -8074,6 +8080,15 @@ react@^15.6.2: object-assign "^4.1.0" prop-types "^15.5.10" +react@^16.0.0: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + reactable@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/reactable/-/reactable-1.0.2.tgz#67a579fee3af68b991b5f04df921a4a40ece0b72" diff --git a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py new file mode 100644 index 0000000000000..8bb6f100683a8 --- /dev/null +++ b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py @@ -0,0 +1,157 @@ +"""Migrate num_period_compare and period_ratio_type + +Revision ID: 3dda56f1c4c6 +Revises: bddc498dd179 +Create Date: 2018-07-05 15:19:14.609299 + +""" + +from __future__ import division + +# revision identifiers, used by Alembic. +revision = '3dda56f1c4c6' +down_revision = 'bddc498dd179' + +import json + +from alembic import op +import isodate +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, Text + +from superset import db + + +Base = declarative_base() + + +class Slice(Base): + __tablename__ = 'slices' + + id = Column(Integer, primary_key=True) + params = Column(Text) + + +comparison_type_map = { + 'factor': 'ratio', + 'growth': 'percentage', + 'value': 'absolute', +} + +db_engine_specs_map = { + 'second': 'PT1S', + 'minute': 'PT1M', + '5 minute': 'PT5M', + '10 minute': 'PT10M', + 'half hour': 'PT0.5H', + 'hour': 'PT1H', + 'day': 'P1D', + 'week': 'P1W', + 'month': 'P1M', + 'quarter': 'P0.25Y', + 'year': 'P1Y', +} + + +def isodate_duration_to_string(obj): + if obj.tdelta: + if not obj.months and not obj.years: + return format_seconds(obj.tdelta.total_seconds()) + raise Exception('Unable to convert: {0}'.format(obj)) + + if obj.months % 12 != 0: + months = obj.months + 12 * obj.years + return '{0} months'.format(months) + + return '{0} years'.format(obj.years + obj.months // 12) + + +def timedelta_to_string(obj): + if obj.microseconds: + raise Exception('Unable to convert: {0}'.format(obj)) + elif obj.seconds: + return format_seconds(obj.total_seconds()) + elif obj.days % 7 == 0: + return '{0} weeks'.format(obj.days // 7) + else: + return '{0} days'.format(obj.days) + + +def format_seconds(value): + periods = [ + ('minute', 60), + ('hour', 3600), + ('day', 86400), + ('week', 604800), + ] + for period, multiple in periods: + if value % multiple == 0: + value //= multiple + break + else: + period = 'second' + + return '{0} {1}{2}'.format(value, period, 's' if value > 1 else '') + + +def compute_time_compare(granularity, periods): + # convert old db_engine_spec granularity to ISO duration + if granularity in db_engine_specs_map: + granularity = db_engine_specs_map[granularity] + + try: + obj = isodate.parse_duration(granularity) * periods + except isodate.isoerror.ISO8601Error: + # if parse_human_timedelta can parse it, return it directly + delta = '{0} {1}{2}'.format(periods, granularity, 's' if periods > 1 else '') + obj = parse_human_timedelta(delta) + if obj: + return delta + raise Exception('Unable to parse: {0}'.format(granularity)) + + if isinstance(obj, isodate.duration.Duration): + return isodate_duration_to_string(obj) + elif isinstance(obj, datetime.timedelta): + return timedelta_to_string(obj) + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + for chart in session.query(Slice): + params = json.loads(chart.params) + + if not params.get('num_period_compare'): + continue + + num_period_compare = int(params.get('num_period_compare')) + granularity = params.get('granularity') or params.get('time_grain_sqla') + period_ratio_type = params.get('period_ratio_type', 'growth') + + time_compare = compute_time_compare(granularity, num_period_compare) + comparison_type = comparison_type_map[period_ratio_type.lower()] + + params['time_compare'] = [time_compare] + params['comparison_type'] = comparison_type + chart.params = json.dumps(params, sort_keys=True) + + session.commit() + session.close() + + +def downgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + for chart in session.query(Slice): + params = json.loads(chart.params) + + if 'time_compare' in params or 'comparison_type' in params: + params.pop('time_compare', None) + params.pop('comparison_type', None) + chart.params = json.dumps(params, sort_keys=True) + + session.commit() + session.close() diff --git a/superset/viz.py b/superset/viz.py index b0ac9f31d5747..8a953a763c1cd 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1185,18 +1185,6 @@ def process_data(self, df, aggregate=False): if min_periods: df = df[min_periods:] - num_period_compare = fd.get('num_period_compare') - if num_period_compare: - num_period_compare = int(num_period_compare) - prt = fd.get('period_ratio_type') - if prt and prt == 'growth': - df = (df / df.shift(num_period_compare)) - 1 - elif prt and prt == 'value': - df = df - df.shift(num_period_compare) - else: - df = df / df.shift(num_period_compare) - - df = df[num_period_compare:] return df def run_extra_queries(self): @@ -1221,22 +1209,50 @@ def run_extra_queries(self): query_object['to_dttm'] -= delta df2 = self.get_df_payload(query_object).get('df') - if df2 is not None: + if df2 is not None and DTTM_ALIAS in df2: label = '{} offset'. format(option) df2[DTTM_ALIAS] += delta df2 = self.process_data(df2) - self._extra_chart_data.extend(self.to_series( - df2, classed='superset', title_suffix=label)) + self._extra_chart_data.append((label, df2)) def get_data(self, df): + fd = self.form_data + comparison_type = fd.get('comparison_type') or 'values' df = self.process_data(df) - chart_data = self.to_series(df) - if self._extra_chart_data: - chart_data += self._extra_chart_data - chart_data = sorted(chart_data, key=lambda x: tuple(x['key'])) + if comparison_type == 'values': + chart_data = self.to_series(df) + for i, (label, df2) in enumerate(self._extra_chart_data): + chart_data.extend( + self.to_series( + df2, classed='time-shift-{}'.format(i), title_suffix=label)) + else: + chart_data = [] + for i, (label, df2) in enumerate(self._extra_chart_data): + # reindex df2 into the df2 index + combined_index = df.index.union(df2.index) + df2 = df2.reindex(combined_index) \ + .interpolate(method='time') \ + .reindex(df.index) + + if comparison_type == 'absolute': + diff = df - df2 + elif comparison_type == 'percentage': + diff = (df - df2) / df2 + elif comparison_type == 'ratio': + diff = df / df2 + else: + raise Exception( + 'Invalid `comparison_type`: {0}'.format(comparison_type)) - return chart_data + # remove leading/trailing NaNs from the time shift difference + diff = diff[diff.first_valid_index():diff.last_valid_index()] + + chart_data.extend( + self.to_series( + diff, classed='time-shift-{}'.format(i), title_suffix=label)) + + return sorted(chart_data, key=lambda x: tuple(x['key'])) class MultiLineViz(NVD3Viz): From c9867a6f27990a8f60f3415bc992288b155f20dc Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 17 Jul 2018 15:30:18 -0700 Subject: [PATCH 04/14] Fix db migration 3dda56f1c4c6 (#5415) (cherry picked from commit 73ec526) --- ...a56f1c4c6_migrate_num_period_compare_and_period_.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py index 8bb6f100683a8..c2a0d9af27962 100644 --- a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py +++ b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py @@ -9,18 +9,20 @@ from __future__ import division # revision identifiers, used by Alembic. -revision = '3dda56f1c4c6' -down_revision = 'bddc498dd179' +import datetime import json from alembic import op import isodate -import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, Text from superset import db +from superset.utils import parse_human_timedelta + +revision = '3dda56f1c4c6' +down_revision = 'bddc498dd179' Base = declarative_base() @@ -96,6 +98,8 @@ def format_seconds(value): def compute_time_compare(granularity, periods): + if not granularity: + return None # convert old db_engine_spec granularity to ISO duration if granularity in db_engine_specs_map: granularity = db_engine_specs_map[granularity] From d3c6a83bd839ecf528cf554392e09ef93d8b48fa Mon Sep 17 00:00:00 2001 From: timifasubaa <30888507+timifasubaa@users.noreply.github.com> Date: Tue, 17 Jul 2018 15:38:16 -0700 Subject: [PATCH 05/14] allow selection of dbs where csv can be uploaded to (#5393) (cherry picked from commit 7f8eaee) --- superset/forms.py | 8 +++-- superset/migrations/versions/1d9e835a84f9_.py | 29 +++++++++++++++++++ superset/models/core.py | 3 +- superset/views/core.py | 8 ++--- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 superset/migrations/versions/1d9e835a84f9_.py diff --git a/superset/forms.py b/superset/forms.py index 0537ded3e4167..5983989787532 100644 --- a/superset/forms.py +++ b/superset/forms.py @@ -49,8 +49,10 @@ def filter_not_empty_values(value): class CsvToDatabaseForm(DynamicForm): # pylint: disable=E0211 - def all_db_items(): - return db.session.query(models.Database) + def csv_enabled_dbs(): + return db.session.query( + models.Database).filter_by( + allow_csv_upload=True).all() name = StringField( _('Table Name'), @@ -64,7 +66,7 @@ def all_db_items(): FileRequired(), FileAllowed(['csv'], _('CSV Files Only!'))]) con = QuerySelectField( _('Database'), - query_factory=all_db_items, + query_factory=csv_enabled_dbs, get_pk=lambda a: a.id, get_label=lambda a: a.database_name) sep = StringField( _('Delimiter'), diff --git a/superset/migrations/versions/1d9e835a84f9_.py b/superset/migrations/versions/1d9e835a84f9_.py new file mode 100644 index 0000000000000..0a5a63fc819e6 --- /dev/null +++ b/superset/migrations/versions/1d9e835a84f9_.py @@ -0,0 +1,29 @@ +"""empty message + +Revision ID: 1d9e835a84f9 +Revises: 3dda56f1c4c6 +Create Date: 2018-07-16 18:04:07.764659 + +""" + +# revision identifiers, used by Alembic. +revision = '1d9e835a84f9' +down_revision = '3dda56f1c4c6' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import expression + + +def upgrade(): + op.add_column( + 'dbs', + sa.Column( + 'allow_csv_upload', + sa.Boolean(), + nullable=False, + server_default=expression.true())) + +def downgrade(): + op.drop_column('dbs', 'allow_csv_upload') + \ No newline at end of file diff --git a/superset/models/core.py b/superset/models/core.py index b35e2943ee904..09da731c23b10 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -621,6 +621,7 @@ class Database(Model, AuditMixinNullable, ImportMixin): expose_in_sqllab = Column(Boolean, default=False) allow_run_sync = Column(Boolean, default=True) allow_run_async = Column(Boolean, default=False) + allow_csv_upload = Column(Boolean, default=True) allow_ctas = Column(Boolean, default=False) allow_dml = Column(Boolean, default=False) force_ctas_schema = Column(String(250)) @@ -636,7 +637,7 @@ class Database(Model, AuditMixinNullable, ImportMixin): impersonate_user = Column(Boolean, default=False) export_fields = ('database_name', 'sqlalchemy_uri', 'cache_timeout', 'expose_in_sqllab', 'allow_run_sync', 'allow_run_async', - 'allow_ctas', 'extra') + 'allow_ctas', 'allow_csv_upload', 'extra') export_children = ['tables'] def __repr__(self): diff --git a/superset/views/core.py b/superset/views/core.py index 926d55f337403..d47e8ac44e0d4 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -193,14 +193,14 @@ class DatabaseView(SupersetModelView, DeleteMixin, YamlExportMixin): # noqa list_columns = [ 'database_name', 'backend', 'allow_run_sync', 'allow_run_async', - 'allow_dml', 'creator', 'modified'] + 'allow_dml', 'allow_csv_upload', 'creator', 'modified'] order_columns = [ 'database_name', 'allow_run_sync', 'allow_run_async', 'allow_dml', - 'modified', + 'modified', 'allow_csv_upload', ] add_columns = [ 'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra', - 'expose_in_sqllab', 'allow_run_sync', 'allow_run_async', + 'expose_in_sqllab', 'allow_run_sync', 'allow_run_async', 'allow_csv_upload', 'allow_ctas', 'allow_dml', 'force_ctas_schema', 'impersonate_user', 'allow_multi_schema_metadata_fetch', ] @@ -325,7 +325,7 @@ class DatabaseAsync(DatabaseView): 'id', 'database_name', 'expose_in_sqllab', 'allow_ctas', 'force_ctas_schema', 'allow_run_async', 'allow_run_sync', 'allow_dml', - 'allow_multi_schema_metadata_fetch', + 'allow_multi_schema_metadata_fetch', 'allow_csv_upload', ] From 7554d5973f7de3ec573146adeb7c5b2f9c981a3e Mon Sep 17 00:00:00 2001 From: John Bodley <4567245+john-bodley@users.noreply.github.com> Date: Sat, 21 Jul 2018 12:01:26 -0700 Subject: [PATCH 06/14] [sql] Correct SQL parameter formatting (#5178) (cherry picked from commit 7fcc2af) --- .pylintrc | 2 +- superset/connectors/sqla/models.py | 9 +- superset/db_engine_specs.py | 21 ++++- .../4451805bbaa1_remove_double_percents.py | 86 +++++++++++++++++++ superset/models/core.py | 43 +++++++--- superset/sql_lab.py | 3 +- tests/core_tests.py | 9 ++ tests/sqllab_tests.py | 2 +- tox.ini | 2 +- 9 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 superset/migrations/versions/4451805bbaa1_remove_double_percents.py diff --git a/.pylintrc b/.pylintrc index 820637dbd0980..016b04e367e45 100644 --- a/.pylintrc +++ b/.pylintrc @@ -282,7 +282,7 @@ ignored-modules=numpy,pandas,alembic.op,sqlalchemy,alembic.context,flask_appbuil # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,sqlalchemy.orm.scoping.scoped_session +ignored-classes=contextlib.closing,optparse.Values,thread._local,_thread._local,sqlalchemy.orm.scoping.scoped_session # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 2cb0e95a174e1..01afb94fde23f 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -12,7 +12,6 @@ from flask_appbuilder import Model from flask_babel import lazy_gettext as _ import pandas as pd -import six import sqlalchemy as sa from sqlalchemy import ( and_, asc, Boolean, Column, DateTime, desc, ForeignKey, Integer, or_, @@ -420,14 +419,8 @@ def get_template_processor(self, **kwargs): table=self, database=self.database, **kwargs) def get_query_str(self, query_obj): - engine = self.database.get_sqla_engine() qry = self.get_sqla_query(**query_obj) - sql = six.text_type( - qry.compile( - engine, - compile_kwargs={'literal_binds': True}, - ), - ) + sql = self.database.compile_sqla_query(qry) logging.info(sql) sql = sqlparse.format(sql, reindent=True) if query_obj['is_prequery']: diff --git a/superset/db_engine_specs.py b/superset/db_engine_specs.py index 728d1d68310e7..a63bb0461e977 100644 --- a/superset/db_engine_specs.py +++ b/superset/db_engine_specs.py @@ -62,7 +62,6 @@ class BaseEngineSpec(object): """Abstract class for database engine specific configurations""" engine = 'base' # str as defined in sqlalchemy.engine.engine - cursor_execute_kwargs = {} time_grains = tuple() time_groupby_inline = False limit_method = LimitMethod.FETCH_MANY @@ -281,6 +280,19 @@ def get_configuration_for_impersonation(cls, uri, impersonate_user, username): """ return {} + @classmethod + def get_normalized_column_names(cls, cursor_description): + columns = cursor_description if cursor_description else [] + return [cls.normalize_column_name(col[0]) for col in columns] + + @staticmethod + def normalize_column_name(column_name): + return column_name + + @staticmethod + def execute(cursor, query, async=False): + cursor.execute(query) + class PostgresBaseEngineSpec(BaseEngineSpec): """ Abstract class for Postgres 'like' databases """ @@ -492,7 +504,6 @@ def get_table_names(cls, schema, inspector): class MySQLEngineSpec(BaseEngineSpec): engine = 'mysql' - cursor_execute_kwargs = {'args': {}} time_grains = ( Grain('Time Column', _('Time Column'), '{col}', None), Grain('second', _('second'), 'DATE_ADD(DATE({col}), ' @@ -555,7 +566,6 @@ def extract_error_message(cls, e): class PrestoEngineSpec(BaseEngineSpec): engine = 'presto' - cursor_execute_kwargs = {'parameters': None} time_grains = ( Grain('Time Column', _('Time Column'), '{col}', None), @@ -854,7 +864,6 @@ class HiveEngineSpec(PrestoEngineSpec): """Reuses PrestoEngineSpec functionality.""" engine = 'hive' - cursor_execute_kwargs = {'async': True} # Scoping regex at class level to avoid recompiling # 17/02/07 19:36:38 INFO ql.Driver: Total jobs = 5 @@ -1108,6 +1117,10 @@ def get_configuration_for_impersonation(cls, uri, impersonate_user, username): configuration['hive.server2.proxy.user'] = username return configuration + @staticmethod + def execute(cursor, query, async=False): + cursor.execute(query, async=async) + class MssqlEngineSpec(BaseEngineSpec): engine = 'mssql' diff --git a/superset/migrations/versions/4451805bbaa1_remove_double_percents.py b/superset/migrations/versions/4451805bbaa1_remove_double_percents.py new file mode 100644 index 0000000000000..2e57b39d3f67f --- /dev/null +++ b/superset/migrations/versions/4451805bbaa1_remove_double_percents.py @@ -0,0 +1,86 @@ +"""remove double percents + +Revision ID: 4451805bbaa1 +Revises: afb7730f6a9c +Create Date: 2018-06-13 10:20:35.846744 + +""" + +# revision identifiers, used by Alembic. +revision = '4451805bbaa1' +down_revision = 'bddc498dd179' + + +from alembic import op +import json +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, create_engine, ForeignKey, Integer, String, Text + +from superset import db + +Base = declarative_base() + + +class Slice(Base): + __tablename__ = 'slices' + + id = Column(Integer, primary_key=True) + datasource_id = Column(Integer, ForeignKey('tables.id')) + datasource_type = Column(String(200)) + params = Column(Text) + + +class Table(Base): + __tablename__ = 'tables' + + id = Column(Integer, primary_key=True) + database_id = Column(Integer, ForeignKey('dbs.id')) + + +class Database(Base): + __tablename__ = 'dbs' + + id = Column(Integer, primary_key=True) + sqlalchemy_uri = Column(String(1024)) + + +def replace(source, target): + bind = op.get_bind() + session = db.Session(bind=bind) + + query = ( + session.query(Slice, Database) + .join(Table) + .join(Database) + .filter(Slice.datasource_type == 'table') + .all() + ) + + for slc, database in query: + try: + engine = create_engine(database.sqlalchemy_uri) + + if engine.dialect.identifier_preparer._double_percents: + params = json.loads(slc.params) + + if 'adhoc_filters' in params: + for filt in params['adhoc_filters']: + if 'sqlExpression' in filt: + filt['sqlExpression'] = ( + filt['sqlExpression'].replace(source, target) + ) + + slc.params = json.dumps(params, sort_keys=True) + except Exception: + pass + + session.commit() + session.close() + + +def upgrade(): + replace('%%', '%') + + +def downgrade(): + replace('%', '%%') diff --git a/superset/models/core.py b/superset/models/core.py index 09da731c23b10..c9018db4babb0 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -6,6 +6,7 @@ from __future__ import print_function from __future__ import unicode_literals +from contextlib import closing from copy import copy, deepcopy from datetime import datetime import functools @@ -19,6 +20,7 @@ from future.standard_library import install_aliases import numpy import pandas as pd +import six import sqlalchemy as sqla from sqlalchemy import ( Boolean, Column, create_engine, DateTime, ForeignKey, Integer, @@ -750,12 +752,7 @@ def get_quoter(self): def get_df(self, sql, schema): sqls = [str(s).strip().strip(';') for s in sqlparse.parse(sql)] - eng = self.get_sqla_engine(schema=schema) - - for i in range(len(sqls) - 1): - eng.execute(sqls[i]) - - df = pd.read_sql_query(sqls[-1], eng) + engine = self.get_sqla_engine(schema=schema) def needs_conversion(df_series): if df_series.empty: @@ -764,15 +761,35 @@ def needs_conversion(df_series): return True return False - for k, v in df.dtypes.items(): - if v.type == numpy.object_ and needs_conversion(df[k]): - df[k] = df[k].apply(utils.json_dumps_w_dates) - return df + with closing(engine.raw_connection()) as conn: + with closing(conn.cursor()) as cursor: + for sql in sqls: + self.db_engine_spec.execute(cursor, sql) + df = pd.DataFrame.from_records( + data=list(cursor.fetchall()), + columns=[col_desc[0] for col_desc in cursor.description], + coerce_float=True, + ) + + for k, v in df.dtypes.items(): + if v.type == numpy.object_ and needs_conversion(df[k]): + df[k] = df[k].apply(utils.json_dumps_w_dates) + return df def compile_sqla_query(self, qry, schema=None): - eng = self.get_sqla_engine(schema=schema) - compiled = qry.compile(eng, compile_kwargs={'literal_binds': True}) - return '{}'.format(compiled) + engine = self.get_sqla_engine(schema=schema) + + sql = six.text_type( + qry.compile( + engine, + compile_kwargs={'literal_binds': True}, + ), + ) + + if engine.dialect.identifier_preparer._double_percents: + sql = sql.replace('%%', '%') + + return sql def select_star( self, table_name, schema=None, limit=100, show_cols=False, diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 856ea4880fb71..7f90aad0e38ff 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -216,8 +216,7 @@ def handle_error(msg): cursor = conn.cursor() logging.info('Running query: \n{}'.format(executed_sql)) logging.info(query.executed_sql) - cursor.execute(query.executed_sql, - **db_engine_spec.cursor_execute_kwargs) + db_engine_spec.execute(cursor, query.executed_sql, async=True) logging.info('Handling cursor') db_engine_spec.handle_cursor(cursor, query, session) logging.info('Fetching data: {}'.format(query.to_dict())) diff --git a/tests/core_tests.py b/tests/core_tests.py index 8b1bd7d0e3235..7041875a82631 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -440,6 +440,15 @@ def test_csv_endpoint(self): expected_data = csv.reader( io.StringIO('first_name,last_name\nadmin, user\n')) + sql = "SELECT first_name FROM ab_user WHERE first_name LIKE '%admin%'" + client_id = '{}'.format(random.getrandbits(64))[:10] + self.run_sql(sql, client_id, raise_on_error=True) + + resp = self.get_resp('/superset/csv/{}'.format(client_id)) + data = csv.reader(io.StringIO(resp)) + expected_data = csv.reader( + io.StringIO('first_name\nadmin\n')) + self.assertEqual(list(expected_data), list(data)) self.logout() diff --git a/tests/sqllab_tests.py b/tests/sqllab_tests.py index 4626f53093559..63ccb9c56673a 100644 --- a/tests/sqllab_tests.py +++ b/tests/sqllab_tests.py @@ -249,7 +249,7 @@ def test_sqllab_viz(self): 'sql': """\ SELECT viz_type, count(1) as ccount FROM slices - WHERE viz_type LIKE '%%a%%' + WHERE viz_type LIKE '%a%' GROUP BY viz_type""", 'dbId': 1, } diff --git a/tox.ini b/tox.ini index 2b2678eae4465..29026147ae7bc 100644 --- a/tox.ini +++ b/tox.ini @@ -36,7 +36,7 @@ setenv = SUPERSET_CONFIG = tests.superset_test_config SUPERSET_HOME = {envtmpdir} py27-mysql: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8 - py34-mysql: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset + py{34,36}-mysql: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset py{27,34,36}-postgres: SUPERSET__SQLALCHEMY_DATABASE_URI = postgresql+psycopg2://postgresuser:pguserpassword@localhost/superset py{27,34,36}-sqlite: SUPERSET__SQLALCHEMY_DATABASE_URI = sqlite:////{envtmpdir}/superset.db whitelist_externals = From 5091b9c7d068b60095e482a7377a98e813e5f258 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Mon, 23 Jul 2018 09:43:09 -0700 Subject: [PATCH 07/14] Fix the build by merging both db migrations heads (#5464) (cherry picked from commit 971e9f0) --- CONTRIBUTING.md | 18 +++++++++++++++ superset/migrations/versions/705732c70154_.py | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 superset/migrations/versions/705732c70154_.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 519644a4af0f1..7463bc1a5771e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -505,3 +505,21 @@ https://github.com/apache/incubator-superset/pull/3013 with a PGP key and providing MD5, Apache voting, as well as publishing to Apache's SVN repository. View the ASF docs for more information. + + +## Merging DB migrations + +When 2 db migrations collide, you'll get an error message like this one: + +``` + alembic.util.exc.CommandError: Multiple head revisions are present for + given argument 'head'; please specify a specific target + revision, '@head' to narrow to a specific head, + or 'heads' for all heads` +``` + +To fix it, first run `superset db heads`, this should list 2 or more +migration hashes. Then run +`superset db merge {PASTE_SHA1_HERE} {PASTE_SHA2_HERE}`. This will create +a new merge migration. You can then `superset db upgrade` to this new +checkpoint. diff --git a/superset/migrations/versions/705732c70154_.py b/superset/migrations/versions/705732c70154_.py new file mode 100644 index 0000000000000..212f69a854272 --- /dev/null +++ b/superset/migrations/versions/705732c70154_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 705732c70154 +Revises: ('4451805bbaa1', '1d9e835a84f9') +Create Date: 2018-07-22 21:51:19.235558 + +""" + +# revision identifiers, used by Alembic. +revision = '705732c70154' +down_revision = ('4451805bbaa1', '1d9e835a84f9') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass From 1604d8bd35ef9710872d6ec0e8c4daac87536827 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Mon, 23 Jul 2018 12:29:21 -0700 Subject: [PATCH 08/14] Move flake8-related packages deps to reqs-dev.txt (#5460) * Move flake8-related packages deps to reqs-dev.txt My VIM which is integrated with flake8 wouldn't match the output from travis and would often miss things related to the flake8 plugins installed using Tox. By moving this to requirements-dev.txt, we can expect developers would have the proper configuration locally and get matching results with Travis when running flake8 or in their IDEs if its integrated with flake8.. * merging migratinos * sorting packages * Specify folder for flake8 processing * pin pycodestyle==2.3.1 * merge db migrations (cherry picked from commit fee5023) --- requirements-dev.txt | 14 +++++++++--- superset/migrations/versions/46ba6aaaac97_.py | 22 +++++++++++++++++++ superset/migrations/versions/e3970889f38e_.py | 22 +++++++++++++++++++ tox.ini | 9 ++------ 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 superset/migrations/versions/46ba6aaaac97_.py create mode 100644 superset/migrations/versions/e3970889f38e_.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 7f9616395e683..fdb627d96207d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,16 @@ +console_log==0.2.10 +flake8-coding==1.3.0 +flake8-commas==2.0.0 +flake8-future-import==0.4.5 +flake8-import-order==0.18 +flake8-quotes==1.0.0 +flake8==3.5.0 flask-cors==3.0.3 ipdb==0.11 mysqlclient==1.3.12 -psycopg2==2.7.4 +psycopg2-binary==2.7.5 +pycodestyle==2.3.1 +pylint==1.9.2 redis==2.10.6 statsd==3.2.2 -tox==2.9.1 -console_log==0.2.10 +tox==3.1.2 diff --git a/superset/migrations/versions/46ba6aaaac97_.py b/superset/migrations/versions/46ba6aaaac97_.py new file mode 100644 index 0000000000000..eb0a7aacaa34b --- /dev/null +++ b/superset/migrations/versions/46ba6aaaac97_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 46ba6aaaac97 +Revises: ('705732c70154', 'e3970889f38e') +Create Date: 2018-07-23 11:20:54.929246 + +""" + +# revision identifiers, used by Alembic. +revision = '46ba6aaaac97' +down_revision = ('705732c70154', 'e3970889f38e') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/migrations/versions/e3970889f38e_.py b/superset/migrations/versions/e3970889f38e_.py new file mode 100644 index 0000000000000..abbee363d84a1 --- /dev/null +++ b/superset/migrations/versions/e3970889f38e_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: e3970889f38e +Revises: ('4451805bbaa1', '1d9e835a84f9') +Create Date: 2018-07-22 09:32:36.986561 + +""" + +# revision identifiers, used by Alembic. +revision = 'e3970889f38e' +down_revision = ('4451805bbaa1', '1d9e835a84f9') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/tox.ini b/tox.ini index 29026147ae7bc..743930c6958d7 100644 --- a/tox.ini +++ b/tox.ini @@ -50,14 +50,9 @@ deps = [testenv:flake8] commands = - flake8 + flake8 {toxinidir}/ deps = - flake8 - flake8-coding - flake8-commas - flake8-future-import - flake8-import-order - flake8-quotes + -rrequirements-dev.txt [testenv:javascript] commands = From 4f138e542767164541f2f47bc1f969cf86f08aae Mon Sep 17 00:00:00 2001 From: timifasubaa <30888507+timifasubaa@users.noreply.github.com> Date: Mon, 23 Jul 2018 16:27:41 -0700 Subject: [PATCH 09/14] fix migration 3dda56f1c (#5468) * fix migration 3dda56f1c * add isodate to setup.py: (cherry picked from commit bea0a0a) --- requirements.txt | 1 + setup.py | 1 + .../3dda56f1c4c6_migrate_num_period_compare_and_period_.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1f42d8c8c07ba..fbc69c51d6243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ geopy==1.11.0 gunicorn==19.8.0 humanize==0.5.1 idna==2.6 +isodate==0.6.0 markdown==2.6.11 pandas==0.22.0 parsedatetime==2.0.0 diff --git a/setup.py b/setup.py index f463f624a8561..e4fb35aa23367 100644 --- a/setup.py +++ b/setup.py @@ -69,6 +69,7 @@ def get_git_sha(): 'gunicorn', # deprecated 'humanize', 'idna', + 'isodate', 'markdown', 'pandas', 'parsedatetime', diff --git a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py index c2a0d9af27962..fef8bb81a11d2 100644 --- a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py +++ b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py @@ -125,7 +125,7 @@ def upgrade(): session = db.Session(bind=bind) for chart in session.query(Slice): - params = json.loads(chart.params) + params = json.loads(chart.params or '{}') if not params.get('num_period_compare'): continue @@ -150,7 +150,7 @@ def downgrade(): session = db.Session(bind=bind) for chart in session.query(Slice): - params = json.loads(chart.params) + params = json.loads(chart.params or '{}') if 'time_compare' in params or 'comparison_type' in params: params.pop('time_compare', None) From 246fc031384ce2f3e1b4a2fe337a9e11010882d4 Mon Sep 17 00:00:00 2001 From: John Bodley <4567245+john-bodley@users.noreply.github.com> Date: Tue, 24 Jul 2018 14:05:00 -0700 Subject: [PATCH 10/14] [migration] Fix migration 3dda56f1c (#5471) (cherry picked from commit bfcc3a6) --- ...4c6_migrate_num_period_compare_and_period_.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py index fef8bb81a11d2..475252fbf2bbe 100644 --- a/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py +++ b/superset/migrations/versions/3dda56f1c4c6_migrate_num_period_compare_and_period_.py @@ -16,7 +16,7 @@ from alembic import op import isodate from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy import Column, Integer, Text +from sqlalchemy import Column, Integer, String, Text from superset import db from superset.utils import parse_human_timedelta @@ -32,6 +32,7 @@ class Slice(Base): __tablename__ = 'slices' id = Column(Integer, primary_key=True) + datasource_type = Column(String(200)) params = Column(Text) @@ -50,6 +51,12 @@ class Slice(Base): 'hour': 'PT1H', 'day': 'P1D', 'week': 'P1W', + 'week_ending_saturday': 'P1W', + 'week_start_sunday': 'P1W', + 'week_start_monday': 'P1W', + 'week_starting_sunday': 'P1W', + 'P1W/1970-01-03T00:00:00Z': 'P1W', + '1969-12-28T00:00:00Z/P1W': 'P1W', 'month': 'P1M', 'quarter': 'P0.25Y', 'year': 'P1Y', @@ -131,10 +138,11 @@ def upgrade(): continue num_period_compare = int(params.get('num_period_compare')) - granularity = params.get('granularity') or params.get('time_grain_sqla') - period_ratio_type = params.get('period_ratio_type', 'growth') - + granularity = (params.get('granularity') if chart.datasource_type == 'druid' + else params.get('time_grain_sqla')) time_compare = compute_time_compare(granularity, num_period_compare) + + period_ratio_type = params.get('period_ratio_type') or 'growth' comparison_type = comparison_type_map[period_ratio_type.lower()] params['time_compare'] = [time_compare] From 8eada37f9a3e1bf91256a8c92f6765f6cbff634d Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Tue, 24 Jul 2018 15:14:11 -0700 Subject: [PATCH 11/14] Migrate dashboard positions data from v1 to v2 format (#5463) * Migrate dashboard positions data from v1 to v2 format * UPDATING.md * rebase onto master (cherry picked from commit fd2d4b0) --- UPDATING.md | 14 + ...f3fed1fe_convert_dashboard_v1_positions.py | 663 ++++++++++++++++++ superset/migrations/versions/c18bd4186f15_.py | 22 + superset/migrations/versions/ec1f88a35cc6_.py | 22 + superset/migrations/versions/fc480c87706c_.py | 22 + 5 files changed, 743 insertions(+) create mode 100644 superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py create mode 100644 superset/migrations/versions/c18bd4186f15_.py create mode 100644 superset/migrations/versions/ec1f88a35cc6_.py create mode 100644 superset/migrations/versions/fc480c87706c_.py diff --git a/UPDATING.md b/UPDATING.md index 78607ffee01b9..753ec05d0ebc8 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -3,6 +3,20 @@ This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. +## Superset 0.27.0 +* Superset 0.27 start to use nested layout for dashboard builder, which is not +backward-compatible with earlier dashboard grid data. We provide migration script +to automatically convert dashboard grid to nested layout data. To be safe, please +take a database backup prior to this upgrade. It's the only way people could go +back to a previous state. + + +## Superset 0.26.0 +* Superset 0.26.0 deprecates the `superset worker` CLI, which is a simple +wrapper around the `celery worker` command, forcing you into crafting +your own native `celery worker` command. Your command should look something +like `celery worker --app=superset.sql_lab:celery_app --pool=gevent -Ofair` + ## Superset 0.25.0 Superset 0.25.0 contains a backwards incompatible changes. If you run a production system you should schedule downtime for this diff --git a/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py b/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py new file mode 100644 index 0000000000000..aa2d95d0d5144 --- /dev/null +++ b/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py @@ -0,0 +1,663 @@ +"""Migrate dashboard position_json data from V1 to V2 + +Revision ID: bebcf3fed1fe +Revises: fc480c87706c +Create Date: 2018-07-22 11:59:07.025119 + +""" + +# revision identifiers, used by Alembic. +import collections +import json +import sys +from functools import reduce +import uuid + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import ( + Table, Column, + Integer, String, Text, ForeignKey, +) +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +from superset import db + +revision = 'bebcf3fed1fe' +down_revision = 'fc480c87706c' + +Base = declarative_base() + +BACKGROUND_TRANSPARENT = 'BACKGROUND_TRANSPARENT' +CHART_TYPE = 'DASHBOARD_CHART_TYPE' +COLUMN_TYPE = 'DASHBOARD_COLUMN_TYPE' +DASHBOARD_GRID_ID = 'DASHBOARD_GRID_ID' +DASHBOARD_GRID_TYPE = 'DASHBOARD_GRID_TYPE' +DASHBOARD_HEADER_ID = 'DASHBOARD_HEADER_ID' +DASHBOARD_HEADER_TYPE = 'DASHBOARD_HEADER_TYPE' +DASHBOARD_ROOT_ID = 'DASHBOARD_ROOT_ID' +DASHBOARD_ROOT_TYPE = 'DASHBOARD_ROOT_TYPE' +DASHBOARD_VERSION_KEY = 'DASHBOARD_VERSION_KEY' +MARKDOWN_TYPE = 'DASHBOARD_MARKDOWN_TYPE' +ROW_TYPE = 'DASHBOARD_ROW_TYPE' + +GRID_COLUMN_COUNT = 12 +GRID_MIN_COLUMN_COUNT = 1 +GRID_MIN_ROW_UNITS = 5 +GRID_RATIO = 4.0 +NUMBER_OF_CHARTS_PER_ROW = 3 +MAX_RECURSIVE_LEVEL = 6 +ROW_HEIGHT = 8 +TOTAL_COLUMNS = 48 +DEFAULT_CHART_WIDTH = int(TOTAL_COLUMNS / NUMBER_OF_CHARTS_PER_ROW) +MAX_VALUE = sys.maxsize + + +class Slice(Base): + """Declarative class to do query in upgrade""" + __tablename__ = 'slices' + id = Column(Integer, primary_key=True) + slice_name = Column(String(250)) + params = Column(Text) + viz_type = Column(String(250)) + + +dashboard_slices = Table( + 'dashboard_slices', Base.metadata, + Column('id', Integer, primary_key=True), + Column('dashboard_id', Integer, ForeignKey('dashboards.id')), + Column('slice_id', Integer, ForeignKey('slices.id')), +) + + +class Dashboard(Base): + """Declarative class to do query in upgrade""" + __tablename__ = 'dashboards' + id = sa.Column(sa.Integer, primary_key=True) + dashboard_title = sa.Column(String(500)) + position_json = sa.Column(sa.Text) + slices = relationship( + 'Slice', secondary=dashboard_slices, backref='dashboards') + + +def is_v2_dash(positions): + return ( + isinstance(positions, dict) and + positions.get('DASHBOARD_VERSION_KEY') == 'v2' + ) + + +def get_boundary(positions): + top = MAX_VALUE + left = MAX_VALUE + bottom = 0 + right = 0 + + for position in positions: + top = min(position['row'], top) + left = min(position['col'], left) + bottom = max(position['row'] + position['size_y'], bottom) + right = max(position['col'] + position['size_x'], right) + + return { + 'top': top, + 'bottom': bottom, + 'left': left, + 'right': right, + } + + +def generate_id(): + return uuid.uuid4().hex[:8] + + +def has_overlap(positions, xAxis=True): + sorted_positions = \ + sorted(positions[:], key=lambda pos: pos['col']) \ + if xAxis else sorted(positions[:], key=lambda pos: pos['row']) + + result = False + for idx, position in enumerate(sorted_positions): + if idx < len(sorted_positions) - 1: + if xAxis: + result = position['col'] + position['size_x'] > \ + sorted_positions[idx + 1]['col'] + else: + result = position['row'] + position['size_y'] > \ + sorted_positions[idx + 1]['row'] + if result: + break + + return result + + +def get_empty_layout(): + return { + DASHBOARD_VERSION_KEY: 'v2', + DASHBOARD_ROOT_ID: { + 'type': DASHBOARD_ROOT_TYPE, + 'id': DASHBOARD_ROOT_ID, + 'children': [DASHBOARD_GRID_ID], + }, + DASHBOARD_GRID_ID: { + 'type': DASHBOARD_GRID_TYPE, + 'id': DASHBOARD_GRID_ID, + 'children': [], + }, + } + + +def get_header_component(title): + return { + 'id': DASHBOARD_HEADER_ID, + 'type': DASHBOARD_HEADER_TYPE, + 'meta': { + 'text': title, + }, + } + + +def get_row_container(): + return { + 'type': ROW_TYPE, + 'id': 'DASHBOARD_ROW_TYPE-{}'.format(generate_id()), + 'children': [], + 'meta': { + 'background': BACKGROUND_TRANSPARENT, + }, + } + + +def get_col_container(): + return { + 'type': COLUMN_TYPE, + 'id': 'DASHBOARD_COLUMN_TYPE-{}'.format(generate_id()), + 'children': [], + 'meta': { + 'background': BACKGROUND_TRANSPARENT, + }, + } + + +def get_chart_holder(position): + size_x = position['size_x'] + size_y = position['size_y'] + slice_id = position['slice_id'] + slice_name = position.get('slice_name') + code = position.get('code') + + width = max( + GRID_MIN_COLUMN_COUNT, + int(round(size_x / GRID_RATIO)) + ) + height = max( + GRID_MIN_ROW_UNITS, + int(round(((size_y / GRID_RATIO) * 100) / ROW_HEIGHT)) + ) + if code is not None: + markdown_content = ' ' # white-space markdown + if len(code): + markdown_content = code + elif slice_name.strip(): + markdown_content = '##### {}'.format(slice_name) + + return { + 'type': MARKDOWN_TYPE, + 'id': 'DASHBOARD_MARKDOWN_TYPE-{}'.format(generate_id()), + 'children': [], + 'meta': { + 'width': width, + 'height': height, + 'code': markdown_content, + } + } + + return { + 'type': CHART_TYPE, + 'id': 'DASHBOARD_CHART_TYPE-{}'.format(generate_id()), + 'children': [], + 'meta': { + 'width': width, + 'height': height, + 'chartId': int(slice_id), + }, + } + + +def get_children_max(children, attr, root): + return max([root[childId]['meta'][attr] for childId in children]) + + +def get_children_sum(children, attr, root): + return reduce( + (lambda sum, childId: sum + root[childId]['meta'][attr]), + children, + 0 + ) + + +# find column that: width > 2 and +# each row has at least 1 chart can reduce width +def get_wide_column_ids(children, root): + return list( + filter( + lambda childId: can_reduce_column_width(root[childId], root), + children + ) + ) + + +def is_wide_leaf_component(component): + return ( + component['type'] in [CHART_TYPE, MARKDOWN_TYPE] and + component['meta']['width'] > GRID_MIN_COLUMN_COUNT + ) + + +def can_reduce_column_width(column_component, root): + return ( + column_component['type'] == COLUMN_TYPE and + column_component['meta']['width'] > GRID_MIN_COLUMN_COUNT and + all([ + is_wide_leaf_component(root[childId]) or ( + root[childId]['type'] == ROW_TYPE and + all([ + is_wide_leaf_component(root[id]) for id in root[childId]['children'] + ]) + ) for childId in column_component['children'] + ]) + ) + + +def reduce_row_width(row_component, root): + wide_leaf_component_ids = list( + filter( + lambda childId: is_wide_leaf_component(root[childId]), + row_component['children'] + ) + ) + + widest_chart_id = None + widest_width = 0 + for component_id in wide_leaf_component_ids: + if root[component_id]['meta']['width'] > widest_width: + widest_width = root[component_id]['meta']['width'] + widest_chart_id = component_id + + if widest_chart_id: + root[widest_chart_id]['meta']['width'] -= 1 + + return get_children_sum(row_component['children'], 'width', root) + + +def reduce_component_width(component): + if is_wide_leaf_component(component): + component['meta']['width'] -= 1 + return component['meta']['width'] + + +def convert(positions, level, parent, root): + if len(positions) == 0: + return + + if len(positions) == 1 or level >= MAX_RECURSIVE_LEVEL: + # special treatment for single chart dash: + # always wrap chart inside a row + if parent['type'] == DASHBOARD_GRID_TYPE: + row_container = get_row_container() + root[row_container['id']] = row_container + parent['children'].append(row_container['id']) + parent = row_container + + chart_holder = get_chart_holder(positions[0]) + root[chart_holder['id']] = chart_holder + parent['children'].append(chart_holder['id']) + return + + current_positions = positions[:] + boundary = get_boundary(current_positions) + top = boundary['top'] + bottom = boundary['bottom'] + left = boundary['left'] + right = boundary['right'] + + # find row dividers + layers = [] + current_row = top + 1 + while len(current_positions) and current_row <= bottom: + upper = [] + lower = [] + + is_row_divider = True + for position in current_positions: + row = position['row'] + size_y = position['size_y'] + if row + size_y <= current_row: + lower.append(position) + continue + elif row >= current_row: + upper.append(position) + continue + is_row_divider = False + break + + if is_row_divider: + current_positions = upper[:] + layers.append(lower) + current_row += 1 + + # Each layer is a list of positions belong to same row section + # they can be a list of charts, or arranged in columns, or mixed + for layer in layers: + if len(layer) == 0: + return + + if len(layer) == 1 and parent['type'] == COLUMN_TYPE: + chart_holder = get_chart_holder(layer[0]) + root[chart_holder['id']] = chart_holder + parent['children'].append(chart_holder['id']) + return + + # create a new row + row_container = get_row_container() + root[row_container['id']] = row_container + parent['children'].append(row_container['id']) + + current_positions = layer[:] + if not has_overlap(current_positions): + # this is a list of charts in the same row + sorted_by_col = sorted( + current_positions, + key=lambda pos: pos['col'], + ) + for position in sorted_by_col: + chart_holder = get_chart_holder(position) + root[chart_holder['id']] = chart_holder + row_container['children'].append(chart_holder['id']) + else: + # this row has columns, find col dividers + current_col = left + 1 + while len(current_positions) and current_col <= right: + upper = [] + lower = [] + + is_col_divider = True + for position in current_positions: + col = position['col'] + size_x = position['size_x'] + if col + size_x <= current_col: + lower.append(position) + continue + elif col >= current_col: + upper.append(position) + continue + is_col_divider = False + break + + if is_col_divider: + # is single chart in the column: + # add to parent container without create new column container + if len(lower) == 1: + chart_holder = get_chart_holder(lower[0]) + root[chart_holder['id']] = chart_holder + row_container['children'].append(chart_holder['id']) + else: + # create new col container + col_container = get_col_container() + root[col_container['id']] = col_container + + if not has_overlap(lower, False): + sorted_by_row = sorted( + lower, + key=lambda pos: pos['row'], + ) + for position in sorted_by_row: + chart_holder = get_chart_holder(position) + root[chart_holder['id']] = chart_holder + col_container['children'].append(chart_holder['id']) + else: + convert(lower, level + 2, col_container, root) + + # add col meta + if len(col_container['children']): + row_container['children'].append(col_container['id']) + col_container['meta']['width'] = get_children_max( + col_container['children'], + 'width', + root, + ) + + current_positions = upper[:] + current_col += 1 + + # add row meta + row_container['meta']['width'] = get_children_sum( + row_container['children'], + 'width', + root, + ) + + +def convert_to_layout(positions): + root = get_empty_layout() + + convert(positions, 0, root[DASHBOARD_GRID_ID], root) + + # remove row's width, height and col's height from its meta data + # and make sure every row's width <= GRID_COLUMN_COUNT + # Each item is a dashboard component: + # row_container, or col_container, or chart_holder + for item in root.values(): + if not isinstance(item, dict): + continue + + if ROW_TYPE == item['type']: + meta = item['meta'] + if meta.get('width', 0) > GRID_COLUMN_COUNT: + current_width = meta['width'] + while ( + current_width > GRID_COLUMN_COUNT and + len(list(filter( + lambda childId: is_wide_leaf_component(root[childId]), + item['children'], + ))) + ): + current_width = reduce_row_width(item, root) + + # because we round v1 chart size to nearest v2 grids count, result + # in there might be overall row width > GRID_COLUMN_COUNT. + # So here is an extra step to check row width, and reduce chart + # or column width if needed and if possible. + if current_width > GRID_COLUMN_COUNT: + has_wide_columns = True + while has_wide_columns: + col_ids = get_wide_column_ids(item['children'], root) + idx = 0 + # need 2nd loop since same column may reduce multiple times + while idx < len(col_ids) and current_width > GRID_COLUMN_COUNT: + current_column = col_ids[idx] + for childId in root[current_column]['children']: + if root[childId]['type'] == ROW_TYPE: + root[childId]['meta']['width'] = reduce_row_width( + root[childId], root + ) + else: + root[childId]['meta']['width'] = \ + reduce_component_width(root[childId]) + + root[current_column]['meta']['width'] = get_children_max( + root[current_column]['children'], + 'width', + root + ) + current_width = get_children_sum( + item['children'], + 'width', + root + ) + idx += 1 + + has_wide_columns = ( + len(get_wide_column_ids(item['children'], root)) and + current_width > GRID_COLUMN_COUNT + ) + + meta.pop('width', None) + + return root + + +def merge_position(position, bottom_line, last_column_start): + col = position['col'] + size_x = position['size_x'] + size_y = position['size_y'] + end_column = len(bottom_line) \ + if col + size_x > last_column_start \ + else col + size_x + + # finding index where index >= col and bottom_line value > bottom_line[col] + taller_indexes = [i for i, value in enumerate(bottom_line) + if (i >= col and value > bottom_line[col])] + + current_row_value = bottom_line[col] + # if no enough space to fit current position, will start from taller row value + if len(taller_indexes) > 0 and (taller_indexes[0] - col + 1) < size_x: + current_row_value = max(bottom_line[col:col + size_x]) + + # add current row value with size_y of this position + for i in range(col, end_column): + bottom_line[i] = current_row_value + size_y + + +# In original position data, a lot of position's row attribute are problematic, +# for example, same positions are assigned to more than 1 chart. +# The convert function depends on row id, col id to split the whole dashboard into +# nested rows and columns. Bad row id will lead to many empty spaces, or a few charts +# are overlapped in the same row. +# This function read positions by row first. +# Then based on previous col id, width and height attribute, +# re-calculate next position's row id. +def scan_dashboard_positions_data(positions): + positions_by_row_id = {} + for position in positions: + row = position['row'] + position['col'] = min(position['col'], TOTAL_COLUMNS) + if not positions_by_row_id.get(row): + positions_by_row_id[row] = [] + positions_by_row_id[row].append(position) + + bottom_line = [0] * (TOTAL_COLUMNS + 1) + # col index always starts from 1, set a large number for [0] as placeholder + bottom_line[0] = MAX_VALUE + last_column_start = max([position['col'] for position in positions]) + + # ordered_raw_positions are arrays of raw positions data sorted by row id + ordered_raw_positions = [] + row_ids = sorted(positions_by_row_id.keys()) + for row_id in row_ids: + ordered_raw_positions.append(positions_by_row_id[row_id]) + updated_positions = [] + + while len(ordered_raw_positions): + next_row = ordered_raw_positions.pop(0) + next_col = 1 + while len(next_row): + # special treatment for same (row, col) assigned to more than 1 chart: + # add one additional row and display wider chart first + available_columns_index = [i for i, e in enumerate( + list(filter(lambda x: x['col'] == next_col, next_row)))] + + if len(available_columns_index): + idx = available_columns_index[0] + if len(available_columns_index) > 1: + idx = sorted( + available_columns_index, + key=lambda x: next_row[x]['size_x'], + reverse=True + )[0] + + next_position = next_row.pop(idx) + merge_position(next_position, bottom_line, last_column_start + 1) + next_position['row'] = \ + bottom_line[next_position['col']] - next_position['size_y'] + updated_positions.append(next_position) + next_col += next_position['size_x'] + else: + next_col = next_row[0]['col'] + + return updated_positions + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + dashboards = session.query(Dashboard).all() + for i, dashboard in enumerate(dashboards): + print('scanning dashboard ({}/{}) >>>>'.format(i + 1, len(dashboards))) + position_json = json.loads(dashboard.position_json or '[]') + if not is_v2_dash(position_json): + print('Converting dashboard... dash_id: {}'.format(dashboard.id)) + position_dict = {} + positions = [] + slices = dashboard.slices + + if position_json: + # scan and fix positions data: extra spaces, dup rows, .etc + position_json = scan_dashboard_positions_data(position_json) + position_dict = \ + {str(position['slice_id']): position for position in position_json} + + last_row_id = max([pos['row'] + pos['size_y'] for pos in position_json]) \ + if position_json else 0 + new_slice_counter = 0 + for slice in slices: + position = position_dict.get(str(slice.id)) + + # some dashboard didn't have position_json + # place 3 charts in a row + if not position: + position = { + 'col': ( + new_slice_counter % NUMBER_OF_CHARTS_PER_ROW * + DEFAULT_CHART_WIDTH + 1 + ), + 'row': ( + last_row_id + + int(new_slice_counter / NUMBER_OF_CHARTS_PER_ROW) * + DEFAULT_CHART_WIDTH + ), + 'size_x': DEFAULT_CHART_WIDTH, + 'size_y': DEFAULT_CHART_WIDTH, + 'slice_id': str(slice.id), + } + new_slice_counter += 1 + + # attach additional parameters to position dict, + # prepare to replace markup and separator viz_type + # to dashboard UI component + form_data = json.loads(slice.params or '{}') + viz_type = slice.viz_type + if form_data and viz_type in ['markup', 'separator']: + position['code'] = form_data.get('code') + position['slice_name'] = slice.slice_name + + positions.append(position) + + v2_layout = convert_to_layout(positions) + v2_layout[DASHBOARD_HEADER_ID] = get_header_component(dashboard.dashboard_title) + + sorted_by_key = collections.OrderedDict(sorted(v2_layout.items())) + # print('converted position_json:\n {}'.format(json.dumps(sorted_by_key, indent=2))) + dashboard.position_json = json.dumps(sorted_by_key, indent=2) + session.merge(dashboard) + session.commit() + else: + print('Skip converted dash_id: {}'.format(dashboard.id)) + + session.close() + + +def downgrade(): + print('downgrade is done') diff --git a/superset/migrations/versions/c18bd4186f15_.py b/superset/migrations/versions/c18bd4186f15_.py new file mode 100644 index 0000000000000..cecb6f5422aa2 --- /dev/null +++ b/superset/migrations/versions/c18bd4186f15_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: c18bd4186f15 +Revises: ('46ba6aaaac97', 'ec1f88a35cc6') +Create Date: 2018-07-24 14:29:41.341098 + +""" + +# revision identifiers, used by Alembic. +revision = 'c18bd4186f15' +down_revision = ('46ba6aaaac97', 'ec1f88a35cc6') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/migrations/versions/ec1f88a35cc6_.py b/superset/migrations/versions/ec1f88a35cc6_.py new file mode 100644 index 0000000000000..e84c6215a9426 --- /dev/null +++ b/superset/migrations/versions/ec1f88a35cc6_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: ec1f88a35cc6 +Revises: ('bebcf3fed1fe', '705732c70154') +Create Date: 2018-07-23 11:18:11.866106 + +""" + +# revision identifiers, used by Alembic. +revision = 'ec1f88a35cc6' +down_revision = ('bebcf3fed1fe', '705732c70154') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/migrations/versions/fc480c87706c_.py b/superset/migrations/versions/fc480c87706c_.py new file mode 100644 index 0000000000000..85fbdf2fc192b --- /dev/null +++ b/superset/migrations/versions/fc480c87706c_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: fc480c87706c +Revises: ('4451805bbaa1', '1d9e835a84f9') +Create Date: 2018-07-22 11:50:54.174443 + +""" + +# revision identifiers, used by Alembic. +revision = 'fc480c87706c' +down_revision = ('4451805bbaa1', '1d9e835a84f9') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass From 9f4a4f26b3327126392eebc5f5c38759e24f395b Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Tue, 24 Jul 2018 15:23:30 -0700 Subject: [PATCH 12/14] retire dashboard v1 (js and python) (#5418) (cherry picked from commit 3f2fc8f) --- superset/assets/package.json | 1 - .../dashboard/fixtures/mockDashboardState.js | 1 - .../dashboard/reducers/dashboardState_spec.js | 1 - .../src/dashboard/components/Dashboard.jsx | 7 +- .../src/dashboard/components/Header.jsx | 46 +- .../components/HeaderActionsDropdown.jsx | 7 +- .../src/dashboard/components/SaveModal.jsx | 9 +- .../dashboard/containers/DashboardHeader.jsx | 1 - .../deprecated/PromptV2ConversionModal.jsx | 102 ---- .../dashboard/deprecated/V2PreviewModal.jsx | 148 ----- .../src/dashboard/deprecated/chart/Chart.jsx | 259 --------- .../dashboard/deprecated/chart/ChartBody.jsx | 55 -- .../deprecated/chart/ChartContainer.jsx | 29 - .../src/dashboard/deprecated/chart/chart.css | 4 - .../dashboard/deprecated/chart/chartAction.js | 195 ------- .../deprecated/chart/chartReducer.js | 158 ------ .../src/dashboard/deprecated/v1/actions.js | 127 ----- .../deprecated/v1/components/CodeModal.jsx | 48 -- .../deprecated/v1/components/Controls.jsx | 215 -------- .../deprecated/v1/components/CssEditor.jsx | 91 ---- .../deprecated/v1/components/Dashboard.jsx | 441 --------------- .../v1/components/DashboardContainer.jsx | 31 -- .../deprecated/v1/components/GridCell.jsx | 157 ------ .../deprecated/v1/components/GridLayout.jsx | 198 ------- .../deprecated/v1/components/Header.jsx | 169 ------ .../v1/components/RefreshIntervalModal.jsx | 64 --- .../deprecated/v1/components/SaveModal.jsx | 161 ------ .../deprecated/v1/components/SliceAdder.jsx | 216 -------- .../deprecated/v1/components/SliceHeader.jsx | 194 ------- .../src/dashboard/deprecated/v1/index.jsx | 28 - .../src/dashboard/deprecated/v1/reducers.js | 272 ---------- .../src/dashboard/reducers/dashboardState.js | 1 - .../src/dashboard/reducers/getInitialState.js | 24 +- .../src/dashboard/stylesheets/dashboard.less | 22 - .../util/dashboardLayoutConverter.js | 513 ------------------ .../src/explore/components/SaveModal.jsx | 9 +- superset/assets/src/explore/constants.js | 2 + superset/assets/src/logger.js | 13 - superset/assets/webpack.config.js | 1 - superset/models/core.py | 50 +- superset/views/core.py | 116 +--- tests/dashboard_tests.py | 43 +- tests/import_export_tests.py | 37 +- 43 files changed, 106 insertions(+), 4160 deletions(-) delete mode 100644 superset/assets/src/dashboard/deprecated/PromptV2ConversionModal.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/chart/Chart.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/chart/chart.css delete mode 100644 superset/assets/src/dashboard/deprecated/chart/chartAction.js delete mode 100644 superset/assets/src/dashboard/deprecated/chart/chartReducer.js delete mode 100644 superset/assets/src/dashboard/deprecated/v1/actions.js delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/Dashboard.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/DashboardContainer.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/GridCell.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/Header.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/SaveModal.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/components/SliceHeader.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/index.jsx delete mode 100644 superset/assets/src/dashboard/deprecated/v1/reducers.js delete mode 100644 superset/assets/src/dashboard/util/dashboardLayoutConverter.js diff --git a/superset/assets/package.json b/superset/assets/package.json index 14b66f480fa06..8d41cd9151447 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -100,7 +100,6 @@ "react-dnd-html5-backend": "^2.5.4", "react-dom": "^15.6.2", "react-gravatar": "^2.6.1", - "react-grid-layout": "0.16.5", "react-map-gl": "^3.0.4", "react-markdown": "^3.3.0", "react-redux": "^5.0.2", diff --git a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js index fd640d1f8b2b8..d405ccf327218 100644 --- a/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js +++ b/superset/assets/spec/javascripts/dashboard/fixtures/mockDashboardState.js @@ -11,5 +11,4 @@ export default { maxUndoHistoryExceeded: false, isStarred: true, css: '', - isV2Preview: false, // @TODO remove upon v1 deprecation }; diff --git a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js index f8095cd875972..7772f71015515 100644 --- a/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js +++ b/superset/assets/spec/javascripts/dashboard/reducers/dashboardState_spec.js @@ -135,7 +135,6 @@ describe('dashboardState reducer', () => { hasUnsavedChanges: false, maxUndoHistoryExceeded: false, editMode: false, - isV2Preview: false, // @TODO remove upon v1 deprecation }); }); diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index 5f5479e81ce66..9bc80b892d7c0 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -87,9 +87,6 @@ class Dashboard extends React.PureComponent { componentWillReceiveProps(nextProps) { if (!nextProps.dashboardState.editMode) { - const version = nextProps.dashboardState.isV2Preview - ? 'v2-preview' - : 'v2'; // log pane loads const loadedPaneIds = []; let minQueryStartTime = Infinity; @@ -108,7 +105,7 @@ class Dashboard extends React.PureComponent { Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, { ...restStats, duration: new Date().getTime() - paneMinQueryStart, - version, + version: 'v2', }); if (!this.isFirstLoad) { @@ -129,7 +126,7 @@ class Dashboard extends React.PureComponent { Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, { pane_ids: loadedPaneIds, duration: new Date().getTime() - minQueryStartTime, - version, + version: 'v2', }); Logger.send(this.actionLog); this.isFirstLoad = false; diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 3b1b6b1f3690c..0c1951b8d7838 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -7,7 +7,6 @@ import EditableTitle from '../../components/EditableTitle'; import Button from '../../components/Button'; import FaveStar from '../../components/FaveStar'; import UndoRedoKeylisteners from './UndoRedoKeylisteners'; -import V2PreviewModal from '../deprecated/V2PreviewModal'; import { chartPropShape } from '../util/propShapes'; import { t } from '../../locales'; @@ -32,7 +31,6 @@ const propTypes = { startPeriodicRender: PropTypes.func.isRequired, updateDashboardTitle: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, - isV2Preview: PropTypes.bool.isRequired, setEditMode: PropTypes.func.isRequired, showBuilderPane: PropTypes.bool.isRequired, toggleBuilderPane: PropTypes.func.isRequired, @@ -60,7 +58,6 @@ class Header extends React.PureComponent { didNotifyMaxUndoHistoryToast: false, emphasizeUndo: false, hightlightRedo: false, - showV2PreviewModal: props.isV2Preview, }; this.handleChangeText = this.handleChangeText.bind(this); @@ -69,7 +66,6 @@ class Header extends React.PureComponent { this.toggleEditMode = this.toggleEditMode.bind(this); this.forceRefresh = this.forceRefresh.bind(this); this.overwriteDashboard = this.overwriteDashboard.bind(this); - this.toggleShowV2PreviewModal = this.toggleShowV2PreviewModal.bind(this); } componentWillReceiveProps(nextProps) { @@ -129,10 +125,6 @@ class Header extends React.PureComponent { this.props.setEditMode(!this.props.editMode); } - toggleShowV2PreviewModal() { - this.setState({ showV2PreviewModal: !this.state.showV2PreviewModal }); - } - overwriteDashboard() { const { dashboardTitle, @@ -161,7 +153,6 @@ class Header extends React.PureComponent { filters, expandedSlices, css, - isV2Preview, onUndo, onRedo, undoLength, @@ -177,7 +168,7 @@ class Header extends React.PureComponent { const userCanEdit = dashboardInfo.dash_edit_perm; const userCanSaveAs = dashboardInfo.dash_save_perm; - const popButton = hasUnsavedChanges || isV2Preview; + const popButton = hasUnsavedChanges; return (
@@ -196,20 +187,6 @@ class Header extends React.PureComponent { isStarred={this.props.isStarred} /> - {isV2Preview && ( -
- {t('v2 Preview')} - -
- )} - {isV2Preview && - this.state.showV2PreviewModal && ( - - )}
{userCanSaveAs && ( @@ -245,32 +222,17 @@ class Header extends React.PureComponent { )} {editMode && - (hasUnsavedChanges || isV2Preview) && ( + hasUnsavedChanges && ( - )} - - {!editMode && - isV2Preview && ( - )} {!editMode && - !isV2Preview && !hasUnsavedChanges && ( - - - - ); -} - -PromptV2ConversionModal.propTypes = propTypes; -PromptV2ConversionModal.defaultProps = defaultProps; - -export default PromptV2ConversionModal; diff --git a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx b/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx deleted file mode 100644 index 828651fbde004..0000000000000 --- a/superset/assets/src/dashboard/deprecated/V2PreviewModal.jsx +++ /dev/null @@ -1,148 +0,0 @@ -/* eslint-env browser */ -import moment from 'moment'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { Modal, Button } from 'react-bootstrap'; -import { connect } from 'react-redux'; -import { - Logger, - LOG_ACTIONS_READ_ABOUT_V2_CHANGES, - LOG_ACTIONS_FALLBACK_TO_V1, -} from '../../logger'; - -import { t } from '../../locales'; - -const propTypes = { - v2FeedbackUrl: PropTypes.string, - v2AutoConvertDate: PropTypes.string, - forceV2Edit: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, -}; - -const defaultProps = { - v2FeedbackUrl: null, - v2AutoConvertDate: null, - handleFallbackToV1: null, -}; - -// This is a gross component but it is temporary! -class V2PreviewModal extends React.Component { - static logReadAboutV2Changes() { - Logger.append( - LOG_ACTIONS_READ_ABOUT_V2_CHANGES, - { version: 'v2-preview' }, - true, - ); - } - - constructor(props) { - super(props); - this.handleFallbackToV1 = this.handleFallbackToV1.bind(this); - } - - handleFallbackToV1() { - Logger.append( - LOG_ACTIONS_FALLBACK_TO_V1, - { - force_v2_edit: this.props.forceV2Edit, - }, - true, - ); - const url = new URL(window.location); // eslint-disable-line - url.searchParams.set('version', 'v1'); - url.searchParams.delete('edit'); // remove JIC they were editing and v1 editing is not allowed - window.location = url; - } - - render() { - const { v2FeedbackUrl, v2AutoConvertDate, onClose } = this.props; - - const timeUntilAutoConversion = v2AutoConvertDate - ? `approximately ${moment(v2AutoConvertDate).toNow( - true, - )} (${v2AutoConvertDate})` // eg 2 weeks (MM-DD-YYYY) - : 'a limited amount of time'; - - return ( - - -
- {t('Welcome to the new Dashboard v2 experience! 🎉')} -
-
- -

{t('Who')}

-

- {t( - "As this dashboard's owner or a Superset Admin, we're soliciting your help to ensure a successful transition to the new dashboard experience. You can learn more about these changes ", - )} - - here - - {v2FeedbackUrl ? t(' or ') : ''} - {v2FeedbackUrl ? ( - - {t('provide feedback')} - - ) : ( - '' - )}. -

-
-

{t('What')}

-

- {t('You are ')} - {t('previewing')} - {t( - ' an auto-converted v2 version of your v1 dashboard. This conversion may have introduced regressions, such as minor layout variation or incompatible custom CSS. ', - )} - - {t( - 'To persist your dashboard as v2, please make any necessary changes and save the dashboard', - )} - - {t( - '. Note that non-owners/-admins will continue to see the original version until you take this action.', - )} -

-
-

{t('When')}

-

- {t('You have ')} - - {timeUntilAutoConversion} - {t(' to edit and save this version ')} - - {t( - ' before it is auto-persisted to this preview. Upon save you will no longer be able to use the v1 experience.', - )} -

-
- - - - -
- ); - } -} - -V2PreviewModal.propTypes = propTypes; -V2PreviewModal.defaultProps = defaultProps; - -export default connect(({ dashboardInfo }) => ({ - v2FeedbackUrl: dashboardInfo.v2FeedbackUrl, - v2AutoConvertDate: dashboardInfo.v2AutoConvertDate, - forceV2Edit: dashboardInfo.forceV2Edit, -}))(V2PreviewModal); diff --git a/superset/assets/src/dashboard/deprecated/chart/Chart.jsx b/superset/assets/src/dashboard/deprecated/chart/Chart.jsx deleted file mode 100644 index bade493203a0b..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/Chart.jsx +++ /dev/null @@ -1,259 +0,0 @@ -/* eslint camelcase: 0 */ -import React from 'react'; -import PropTypes from 'prop-types'; -import Mustache from 'mustache'; -import { Tooltip } from 'react-bootstrap'; - -import { d3format } from '../../../modules/utils'; -import ChartBody from './ChartBody'; -import Loading from '../../../components/Loading'; -import { Logger, LOG_ACTIONS_RENDER_CHART } from '../../../logger'; -import StackTraceMessage from '../../../components/StackTraceMessage'; -import RefreshChartOverlay from '../../../components/RefreshChartOverlay'; -import visMap from '../../../visualizations'; -import sandboxedEval from '../../../modules/sandbox'; -import './chart.css'; - -const propTypes = { - annotationData: PropTypes.object, - actions: PropTypes.object, - chartKey: PropTypes.string.isRequired, - containerId: PropTypes.string.isRequired, - datasource: PropTypes.object.isRequired, - formData: PropTypes.object.isRequired, - headerHeight: PropTypes.number, - height: PropTypes.number, - width: PropTypes.number, - setControlValue: PropTypes.func, - timeout: PropTypes.number, - vizType: PropTypes.string.isRequired, - // state - chartAlert: PropTypes.string, - chartStatus: PropTypes.string, - chartUpdateEndTime: PropTypes.number, - chartUpdateStartTime: PropTypes.number, - latestQueryFormData: PropTypes.object, - queryRequest: PropTypes.object, - queryResponse: PropTypes.object, - lastRendered: PropTypes.number, - triggerQuery: PropTypes.bool, - refreshOverlayVisible: PropTypes.bool, - errorMessage: PropTypes.node, - // dashboard callbacks - addFilter: PropTypes.func, - getFilters: PropTypes.func, - clearFilter: PropTypes.func, - removeFilter: PropTypes.func, - onQuery: PropTypes.func, - onDismissRefreshOverlay: PropTypes.func, -}; - -const defaultProps = { - addFilter: () => ({}), - getFilters: () => ({}), - clearFilter: () => ({}), - removeFilter: () => ({}), -}; - -class Chart extends React.PureComponent { - constructor(props) { - super(props); - this.state = {}; - // these properties are used by visualizations - this.annotationData = props.annotationData; - this.containerId = props.containerId; - this.selector = `#${this.containerId}`; - this.formData = props.formData; - this.datasource = props.datasource; - this.addFilter = this.addFilter.bind(this); - this.getFilters = this.getFilters.bind(this); - this.clearFilter = this.clearFilter.bind(this); - this.removeFilter = this.removeFilter.bind(this); - this.headerHeight = this.headerHeight.bind(this); - this.height = this.height.bind(this); - this.width = this.width.bind(this); - } - - componentDidMount() { - if (this.props.triggerQuery) { - this.props.actions.runQuery(this.props.formData, false, - this.props.timeout, - this.props.chartKey, - ); - } - } - - componentWillReceiveProps(nextProps) { - this.annotationData = nextProps.annotationData; - this.containerId = nextProps.containerId; - this.selector = `#${this.containerId}`; - this.formData = nextProps.formData; - this.datasource = nextProps.datasource; - } - - componentDidUpdate(prevProps) { - if ( - this.props.queryResponse && - ['success', 'rendered'].indexOf(this.props.chartStatus) > -1 && - !this.props.queryResponse.error && ( - prevProps.annotationData !== this.props.annotationData || - prevProps.queryResponse !== this.props.queryResponse || - prevProps.height !== this.props.height || - prevProps.width !== this.props.width || - prevProps.lastRendered !== this.props.lastRendered) - ) { - this.renderViz(); - } - } - - getFilters() { - return this.props.getFilters(); - } - - setTooltip(tooltip) { - this.setState({ tooltip }); - } - - addFilter(col, vals, merge = true, refresh = true) { - this.props.addFilter(col, vals, merge, refresh); - } - - clearFilter() { - this.props.clearFilter(); - } - - removeFilter(col, vals, refresh = true) { - this.props.removeFilter(col, vals, refresh); - } - - clearError() { - this.setState({ errorMsg: null }); - } - - width() { - return this.props.width || this.container.el.offsetWidth; - } - - headerHeight() { - return this.props.headerHeight || 0; - } - - height() { - return this.props.height || this.container.el.offsetHeight; - } - - d3format(col, number) { - const { datasource } = this.props; - const format = (datasource.column_formats && datasource.column_formats[col]) || '0.3s'; - - return d3format(format, number); - } - - error(e) { - this.props.actions.chartRenderingFailed(e, this.props.chartKey); - } - - verboseMetricName(metric) { - return this.props.datasource.verbose_map[metric] || metric; - } - - render_template(s) { - const context = { - width: this.width(), - height: this.height(), - }; - return Mustache.render(s, context); - } - - renderTooltip() { - if (this.state.tooltip) { - /* eslint-disable react/no-danger */ - return ( - -
- - ); - /* eslint-enable react/no-danger */ - } - return null; - } - - renderViz() { - const viz = visMap[this.props.vizType]; - const fd = this.props.formData; - const qr = this.props.queryResponse; - const renderStart = Logger.getTimestamp(); - try { - // Executing user-defined data mutator function - if (fd.js_data) { - qr.data = sandboxedEval(fd.js_data)(qr.data); - } - // [re]rendering the visualization - viz(this, qr, this.props.setControlValue); - Logger.append(LOG_ACTIONS_RENDER_CHART, { - slice_id: this.props.chartKey, - viz_type: this.props.vizType, - start_offset: renderStart, - duration: Logger.getTimestamp() - renderStart, - }); - this.props.actions.chartRenderingSucceeded(this.props.chartKey); - } catch (e) { - this.props.actions.chartRenderingFailed(e, this.props.chartKey); - } - } - - render() { - const isLoading = this.props.chartStatus === 'loading'; - return ( -
- {this.renderTooltip()} - {isLoading && - - } - {this.props.chartAlert && - - } - - {!isLoading && - !this.props.chartAlert && - this.props.refreshOverlayVisible && - !this.props.errorMessage && - this.container && - - } - {!isLoading && !this.props.chartAlert && - { - this.container = inner; - }} - /> - } -
- ); - } -} - -Chart.propTypes = propTypes; -Chart.defaultProps = defaultProps; - -export default Chart; diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx deleted file mode 100644 index b459f4418207d..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/ChartBody.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import $ from 'jquery'; - -const propTypes = { - containerId: PropTypes.string.isRequired, - vizType: PropTypes.string.isRequired, - height: PropTypes.func.isRequired, - width: PropTypes.func.isRequired, - faded: PropTypes.bool, -}; - -class ChartBody extends React.PureComponent { - html(data) { - this.el.innerHTML = data; - } - - css(property, value) { - this.el.style[property] = value; - } - - get(n) { - return $(this.el).get(n); - } - - find(classname) { - return $(this.el).find(classname); - } - - show() { - return $(this.el).show(); - } - - height() { - return this.props.height(); - } - - width() { - return this.props.width(); - } - - render() { - return ( -
{ this.el = el; }} - /> - ); - } -} - -ChartBody.propTypes = propTypes; - -export default ChartBody; diff --git a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx b/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx deleted file mode 100644 index b731412fc5ff7..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/ChartContainer.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import * as Actions from './chartAction'; -import Chart from './Chart'; - -function mapStateToProps({ charts }, ownProps) { - const chart = charts[ownProps.chartKey]; - return { - annotationData: chart.annotationData, - chartAlert: chart.chartAlert, - chartStatus: chart.chartStatus, - chartUpdateEndTime: chart.chartUpdateEndTime, - chartUpdateStartTime: chart.chartUpdateStartTime, - latestQueryFormData: chart.latestQueryFormData, - lastRendered: chart.lastRendered, - queryResponse: chart.queryResponse, - queryRequest: chart.queryRequest, - triggerQuery: chart.triggerQuery, - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators(Actions, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Chart); diff --git a/superset/assets/src/dashboard/deprecated/chart/chart.css b/superset/assets/src/dashboard/deprecated/chart/chart.css deleted file mode 100644 index eda2054f92af9..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chart.css +++ /dev/null @@ -1,4 +0,0 @@ -.chart-tooltip { - opacity: 0.75; - font-size: 12px; -} diff --git a/superset/assets/src/dashboard/deprecated/chart/chartAction.js b/superset/assets/src/dashboard/deprecated/chart/chartAction.js deleted file mode 100644 index 52f9c47076214..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chartAction.js +++ /dev/null @@ -1,195 +0,0 @@ -import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../../../explore/exploreUtils'; -import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../../../modules/AnnotationTypes'; -import { Logger, LOG_ACTIONS_LOAD_CHART } from '../../../logger'; -import { COMMON_ERR_MESSAGES } from '../../../common'; -import { t } from '../../../locales'; - -const $ = window.$ = require('jquery'); - -export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; -export function chartUpdateStarted(queryRequest, latestQueryFormData, key) { - return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key }; -} - -export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED'; -export function chartUpdateSucceeded(queryResponse, key) { - return { type: CHART_UPDATE_SUCCEEDED, queryResponse, key }; -} - -export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED'; -export function chartUpdateStopped(key) { - return { type: CHART_UPDATE_STOPPED, key }; -} - -export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT'; -export function chartUpdateTimeout(statusText, timeout, key) { - return { type: CHART_UPDATE_TIMEOUT, statusText, timeout, key }; -} - -export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED'; -export function chartUpdateFailed(queryResponse, key) { - return { type: CHART_UPDATE_FAILED, queryResponse, key }; -} - -export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED'; -export function chartRenderingFailed(error, key) { - return { type: CHART_RENDERING_FAILED, error, key }; -} - -export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED'; -export function chartRenderingSucceeded(key) { - return { type: CHART_RENDERING_SUCCEEDED, key }; -} - -export const REMOVE_CHART = 'REMOVE_CHART'; -export function removeChart(key) { - return { type: REMOVE_CHART, key }; -} - -export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS'; -export function annotationQuerySuccess(annotation, queryResponse, key) { - return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key }; -} - -export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED'; -export function annotationQueryStarted(annotation, queryRequest, key) { - return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key }; -} - -export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED'; -export function annotationQueryFailed(annotation, queryResponse, key) { - return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key }; -} - -export function runAnnotationQuery(annotation, timeout = 60, formData = null, key) { - return function (dispatch, getState) { - const sliceKey = key || Object.keys(getState().charts)[0]; - const fd = formData || getState().charts[sliceKey].latestQueryFormData; - - if (!requiresQuery(annotation.sourceType)) { - return Promise.resolve(); - } - - const granularity = fd.time_grain_sqla || fd.granularity; - fd.time_grain_sqla = granularity; - fd.granularity = granularity; - - const sliceFormData = Object.keys(annotation.overrides) - .reduce((d, k) => ({ - ...d, - [k]: annotation.overrides[k] || fd[k], - }), {}); - const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE; - const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative); - const queryRequest = $.ajax({ - url, - dataType: 'json', - timeout: timeout * 1000, - }); - dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey)); - return queryRequest - .then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey))) - .catch((err) => { - if (err.statusText === 'timeout') { - dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey)); - } else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) { - dispatch(annotationQuerySuccess(annotation, err, sliceKey)); - } else if (err.statusText !== 'abort') { - dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey)); - } - }); - }; -} - -export const TRIGGER_QUERY = 'TRIGGER_QUERY'; -export function triggerQuery(value = true, key) { - return { type: TRIGGER_QUERY, value, key }; -} - -// this action is used for forced re-render without fetch data -export const RENDER_TRIGGERED = 'RENDER_TRIGGERED'; -export function renderTriggered(value, key) { - return { type: RENDER_TRIGGERED, value, key }; -} - -export const UPDATE_QUERY_FORM_DATA = 'UPDATE_QUERY_FORM_DATA'; -export function updateQueryFormData(value, key) { - return { type: UPDATE_QUERY_FORM_DATA, value, key }; -} - -export const RUN_QUERY = 'RUN_QUERY'; -export function runQuery(formData, force = false, timeout = 60, key) { - return (dispatch) => { - const { url, payload } = getExploreUrlAndPayload({ - formData, - endpointType: 'json', - force, - }); - const logStart = Logger.getTimestamp(); - const queryRequest = $.ajax({ - type: 'POST', - url, - dataType: 'json', - data: { - form_data: JSON.stringify(payload), - }, - timeout: timeout * 1000, - }); - const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key))) - .then(() => queryRequest) - .then((queryResponse) => { - Logger.append(LOG_ACTIONS_LOAD_CHART, { - slice_id: 'slice_' + key, - is_cached: queryResponse.is_cached, - force_refresh: force, - row_count: queryResponse.rowcount, - datasource: formData.datasource, - start_offset: logStart, - duration: Logger.getTimestamp() - logStart, - has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0, - viz_type: formData.viz_type, - }); - return dispatch(chartUpdateSucceeded(queryResponse, key)); - }) - .catch((err) => { - Logger.append(LOG_ACTIONS_LOAD_CHART, { - slice_id: 'slice_' + key, - has_err: true, - datasource: formData.datasource, - start_offset: logStart, - duration: Logger.getTimestamp() - logStart, - }); - if (err.statusText === 'timeout') { - dispatch(chartUpdateTimeout(err.statusText, timeout, key)); - } else if (err.statusText === 'abort') { - dispatch(chartUpdateStopped(key)); - } else { - let errObject; - if (err.responseJSON) { - errObject = err.responseJSON; - } else if (err.stack) { - errObject = { - error: t('Unexpected error: ') + err.description, - stacktrace: err.stack, - }; - } else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) { - errObject = { - error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT, - }; - } else { - errObject = { - error: t('Unexpected error.'), - }; - } - dispatch(chartUpdateFailed(errObject, key)); - } - }); - const annotationLayers = formData.annotation_layers || []; - return Promise.all([ - queryPromise, - dispatch(triggerQuery(false, key)), - dispatch(updateQueryFormData(payload, key)), - ...annotationLayers.map(x => dispatch(runAnnotationQuery(x, timeout, formData, key))), - ]); - }; -} diff --git a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js b/superset/assets/src/dashboard/deprecated/chart/chartReducer.js deleted file mode 100644 index 8d11249598470..0000000000000 --- a/superset/assets/src/dashboard/deprecated/chart/chartReducer.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint camelcase: 0 */ -import PropTypes from 'prop-types'; - -import { now } from '../../../modules/dates'; -import * as actions from './chartAction'; -import { t } from '../../../locales'; - -export const chartPropType = { - chartKey: PropTypes.string.isRequired, - chartAlert: PropTypes.string, - chartStatus: PropTypes.string, - chartUpdateEndTime: PropTypes.number, - chartUpdateStartTime: PropTypes.number, - latestQueryFormData: PropTypes.object, - queryRequest: PropTypes.object, - queryResponse: PropTypes.object, - triggerQuery: PropTypes.bool, - lastRendered: PropTypes.number, -}; - -export const chart = { - chartKey: '', - chartAlert: null, - chartStatus: 'loading', - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - latestQueryFormData: {}, - queryRequest: null, - queryResponse: null, - triggerQuery: true, - lastRendered: 0, -}; - -export default function chartReducer(charts = {}, action) { - const actionHandlers = { - [actions.CHART_UPDATE_SUCCEEDED](state) { - return { ...state, - chartStatus: 'success', - queryResponse: action.queryResponse, - chartUpdateEndTime: now(), - }; - }, - [actions.CHART_UPDATE_STARTED](state) { - return { ...state, - chartStatus: 'loading', - chartAlert: null, - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - queryRequest: action.queryRequest, - }; - }, - [actions.CHART_UPDATE_STOPPED](state) { - return { ...state, - chartStatus: 'stopped', - chartAlert: t('Updating chart was stopped'), - }; - }, - [actions.CHART_RENDERING_SUCCEEDED](state) { - return { ...state, - chartStatus: 'rendered', - }; - }, - [actions.CHART_RENDERING_FAILED](state) { - return { ...state, - chartStatus: 'failed', - chartAlert: t('An error occurred while rendering the visualization: %s', action.error), - }; - }, - [actions.CHART_UPDATE_TIMEOUT](state) { - return { ...state, - chartStatus: 'failed', - chartAlert: ( - `${t('Query timeout')} - ` + - t(`visualization queries are set to timeout at ${action.timeout} seconds. `) + - t('Perhaps your data has grown, your database is under unusual load, ' + - 'or you are simply querying a data source that is too 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](state) { - return { ...state, - chartStatus: 'failed', - chartAlert: action.queryResponse ? action.queryResponse.error : t('Network error.'), - chartUpdateEndTime: now(), - queryResponse: action.queryResponse, - }; - }, - [actions.TRIGGER_QUERY](state) { - return { ...state, triggerQuery: action.value }; - }, - [actions.RENDER_TRIGGERED](state) { - return { ...state, lastRendered: action.value }; - }, - [actions.UPDATE_QUERY_FORM_DATA](state) { - return { ...state, latestQueryFormData: action.value }; - }, - [actions.ANNOTATION_QUERY_STARTED](state) { - if (state.annotationQuery && - state.annotationQuery[action.annotation.name]) { - state.annotationQuery[action.annotation.name].abort(); - } - const annotationQuery = { - ...state.annotationQuery, - [action.annotation.name]: action.queryRequest, - }; - return { - ...state, - annotationQuery, - }; - }, - [actions.ANNOTATION_QUERY_SUCCESS](state) { - const annotationData = { - ...state.annotationData, - [action.annotation.name]: action.queryResponse.data, - }; - const annotationError = { ...state.annotationError }; - delete annotationError[action.annotation.name]; - const annotationQuery = { ...state.annotationQuery }; - delete annotationQuery[action.annotation.name]; - return { - ...state, - annotationData, - annotationError, - annotationQuery, - }; - }, - [actions.ANNOTATION_QUERY_FAILED](state) { - const annotationData = { ...state.annotationData }; - delete annotationData[action.annotation.name]; - const annotationError = { - ...state.annotationError, - [action.annotation.name]: action.queryResponse ? - action.queryResponse.error : t('Network error.'), - }; - const annotationQuery = { ...state.annotationQuery }; - delete annotationQuery[action.annotation.name]; - return { - ...state, - annotationData, - annotationError, - annotationQuery, - }; - }, - }; - - /* eslint-disable no-param-reassign */ - if (action.type === actions.REMOVE_CHART) { - delete charts[action.key]; - return charts; - } - - if (action.type in actionHandlers) { - return { ...charts, [action.key]: actionHandlers[action.type](charts[action.key], action) }; - } - - return charts; -} diff --git a/superset/assets/src/dashboard/deprecated/v1/actions.js b/superset/assets/src/dashboard/deprecated/v1/actions.js deleted file mode 100644 index 7381486f24c76..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/actions.js +++ /dev/null @@ -1,127 +0,0 @@ -/* global notify */ -import $ from 'jquery'; -import { getExploreUrlAndPayload } from '../../../explore/exploreUtils'; - -export const ADD_FILTER = 'ADD_FILTER'; -export function addFilter(sliceId, col, vals, merge = true, refresh = true) { - return { type: ADD_FILTER, sliceId, col, vals, merge, refresh }; -} - -export const CLEAR_FILTER = 'CLEAR_FILTER'; -export function clearFilter(sliceId) { - return { type: CLEAR_FILTER, sliceId }; -} - -export const REMOVE_FILTER = 'REMOVE_FILTER'; -export function removeFilter(sliceId, col, vals, refresh = true) { - return { type: REMOVE_FILTER, sliceId, col, vals, refresh }; -} - -export const UPDATE_DASHBOARD_LAYOUT = 'UPDATE_DASHBOARD_LAYOUT'; -export function updateDashboardLayout(layout) { - return { type: UPDATE_DASHBOARD_LAYOUT, layout }; -} - -export const UPDATE_DASHBOARD_TITLE = 'UPDATE_DASHBOARD_TITLE'; -export function updateDashboardTitle(title) { - return { type: UPDATE_DASHBOARD_TITLE, title }; -} - -export function addSlicesToDashboard(dashboardId, sliceIds) { - return () => ( - $.ajax({ - type: 'POST', - url: `/superset/add_slices/${dashboardId}/`, - data: { - data: JSON.stringify({ slice_ids: sliceIds }), - }, - }) - .done(() => { - // Refresh page to allow for slices to re-render - window.location.reload(); - }) - ); -} - -export const REMOVE_SLICE = 'REMOVE_SLICE'; -export function removeSlice(slice) { - return { type: REMOVE_SLICE, slice }; -} - -export const UPDATE_SLICE_NAME = 'UPDATE_SLICE_NAME'; -export function updateSliceName(slice, sliceName) { - return { type: UPDATE_SLICE_NAME, slice, sliceName }; -} -export function saveSlice(slice, sliceName) { - const oldName = slice.slice_name; - return (dispatch) => { - const sliceParams = {}; - sliceParams.slice_id = slice.slice_id; - sliceParams.action = 'overwrite'; - sliceParams.slice_name = sliceName; - - const { url, payload } = getExploreUrlAndPayload({ - formData: slice.form_data, - endpointType: 'base', - force: false, - curUrl: null, - requestParams: sliceParams, - }); - return $.ajax({ - url, - type: 'POST', - data: { - form_data: JSON.stringify(payload), - }, - success: () => { - dispatch(updateSliceName(slice, sliceName)); - notify.success('This slice name was saved successfully.'); - }, - error: () => { - // if server-side reject the overwrite action, - // revert to old state - dispatch(updateSliceName(slice, oldName)); - notify.error("You don't have the rights to alter this slice"); - }, - }); - }; -} - -const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard'; -export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; -export function toggleFaveStar(isStarred) { - return { type: TOGGLE_FAVE_STAR, isStarred }; -} - -export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR'; -export function fetchFaveStar(id) { - return function (dispatch) { - const url = `${FAVESTAR_BASE_URL}/${id}/count`; - return $.get(url) - .done((data) => { - if (data.count > 0) { - dispatch(toggleFaveStar(true)); - } - }); - }; -} - -export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR'; -export function saveFaveStar(id, isStarred) { - return function (dispatch) { - const urlSuffix = isStarred ? 'unselect' : 'select'; - const url = `${FAVESTAR_BASE_URL}/${id}/${urlSuffix}/`; - $.get(url); - dispatch(toggleFaveStar(!isStarred)); - }; -} - -export const TOGGLE_EXPAND_SLICE = 'TOGGLE_EXPAND_SLICE'; -export function toggleExpandSlice(slice, isExpanded) { - return { type: TOGGLE_EXPAND_SLICE, slice, isExpanded }; -} - -export const SET_EDIT_MODE = 'SET_EDIT_MODE'; -export function setEditMode(editMode) { - return { type: SET_EDIT_MODE, editMode }; -} diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx deleted file mode 100644 index 3f802c3471a95..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/CodeModal.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import ModalTrigger from '../../../../components/ModalTrigger'; -import { t } from '../../../../locales'; - -const propTypes = { - triggerNode: PropTypes.node.isRequired, - code: PropTypes.string, - codeCallback: PropTypes.func, -}; - -const defaultProps = { - codeCallback: () => {}, -}; - -export default class CodeModal extends React.PureComponent { - constructor(props) { - super(props); - this.state = { code: props.code }; - } - beforeOpen() { - let code = this.props.code; - if (!code && this.props.codeCallback) { - code = this.props.codeCallback(); - } - this.setState({ code }); - } - render() { - return ( - -
-              {this.state.code}
-            
-
- } - /> - ); - } -} -CodeModal.propTypes = propTypes; -CodeModal.defaultProps = defaultProps; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx deleted file mode 100644 index 435c762c0ec4d..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/Controls.jsx +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DropdownButton, MenuItem } from 'react-bootstrap'; - -import CssEditor from './CssEditor'; -import RefreshIntervalModal from './RefreshIntervalModal'; -import SaveModal from './SaveModal'; -import SliceAdder from './SliceAdder'; -import { t } from '../../../../locales'; -import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger'; - -const $ = window.$ = require('jquery'); - -const propTypes = { - dashboard: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired, - slices: PropTypes.array, - userId: PropTypes.string.isRequired, - addSlicesToDashboard: PropTypes.func, - onSave: PropTypes.func, - onChange: PropTypes.func, - renderSlices: PropTypes.func, - serialize: PropTypes.func, - startPeriodicRender: PropTypes.func, - editMode: PropTypes.bool, -}; - -function MenuItemContent({ faIcon, text, tooltip, children }) { - return ( - - {text} {''} - - {children} - - ); -} -MenuItemContent.propTypes = { - faIcon: PropTypes.string.isRequired, - text: PropTypes.string, - tooltip: PropTypes.string, - children: PropTypes.node, -}; - -function ActionMenuItem(props) { - return ( - - - - ); -} -ActionMenuItem.propTypes = { - onClick: PropTypes.func, -}; - -class Controls extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - css: props.dashboard.css || '', - cssTemplates: [], - }; - this.refresh = this.refresh.bind(this); - this.toggleModal = this.toggleModal.bind(this); - this.updateDom = this.updateDom.bind(this); - } - componentWillMount() { - this.updateDom(this.state.css); - - $.get('/csstemplateasyncmodelview/api/read', (data) => { - const cssTemplates = data.result.map(row => ({ - value: row.template_name, - css: row.css, - label: row.template_name, - })); - this.setState({ cssTemplates }); - }); - } - refresh() { - // Force refresh all slices - this.props.renderSlices(true); - } - toggleModal(modal) { - let currentModal; - if (modal !== this.state.currentModal) { - currentModal = modal; - } - this.setState({ currentModal }); - } - changeCss(css) { - this.setState({ css }, () => { - this.updateDom(css); - }); - this.props.onChange(); - } - updateDom(css) { - const className = 'CssEditor-css'; - const head = document.head || document.getElementsByTagName('head')[0]; - let style = document.querySelector('.' + className); - - if (!style) { - style = document.createElement('style'); - style.className = className; - style.type = 'text/css'; - head.appendChild(style); - } - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.innerHTML = css; - } - } - render() { - const { dashboard, userId, filters, - addSlicesToDashboard, startPeriodicRender, - serialize, onSave, editMode } = this.props; - const emailBody = t('Checkout this dashboard: %s', window.location.href); - const emailLink = 'mailto:?Subject=Superset%20Dashboard%20' - + `${dashboard.dashboard_title}&Body=${emailBody}`; - let saveText = t('Save as'); - if (editMode) { - saveText = t('Save'); - } - return ( - - - - startPeriodicRender(refreshInterval * 1000)} - triggerNode={ - - } - /> - {dashboard.dash_save_perm && - !dashboard.forceV2Edit && - - } - /> - } - {editMode && - { window.location = `/dashboardmodelview/edit/${dashboard.id}`; }} - /> - } - {editMode && - { window.location = emailLink; }} - faIcon="envelope" - /> - } - {editMode && - - } - /> - } - {editMode && - - } - initialCss={this.state.css} - templates={this.state.cssTemplates} - onChange={this.changeCss.bind(this)} - /> - } - - - ); - } -} -Controls.propTypes = propTypes; - -export default Controls; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx b/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx deleted file mode 100644 index ee11ff26d626b..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/CssEditor.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Select from 'react-select'; - -import AceEditor from 'react-ace'; -import 'brace/mode/css'; -import 'brace/theme/github'; - -import ModalTrigger from '../../../../components/ModalTrigger'; -import { t } from '../../../../locales'; - -const propTypes = { - initialCss: PropTypes.string, - triggerNode: PropTypes.node.isRequired, - onChange: PropTypes.func, - templates: PropTypes.array, -}; - -const defaultProps = { - initialCss: '', - onChange: () => {}, - templates: [], -}; - -class CssEditor extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - css: props.initialCss, - cssTemplateOptions: [], - }; - } - changeCss(css) { - this.setState({ css }, () => { - this.props.onChange(css); - }); - } - changeCssTemplate(opt) { - this.changeCss(opt.css); - } - renderTemplateSelector() { - if (this.props.templates) { - return ( -
-
{t('Load a template')}
- - -
-
- ); - } -} - -GridCell.propTypes = propTypes; -GridCell.defaultProps = defaultProps; - -export default GridCell; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx b/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx deleted file mode 100644 index ef0ec24796de4..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/GridLayout.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Responsive, WidthProvider } from 'react-grid-layout'; - -import GridCell from './GridCell'; - -require('react-grid-layout/css/styles.css'); -require('react-resizable/css/styles.css'); - -const ResponsiveReactGridLayout = WidthProvider(Responsive); - -const propTypes = { - dashboard: PropTypes.object.isRequired, - datasources: PropTypes.object, - charts: PropTypes.object.isRequired, - filters: PropTypes.object, - timeout: PropTypes.number, - onChange: PropTypes.func, - getFormDataExtra: PropTypes.func, - exploreChart: PropTypes.func, - exportCSV: PropTypes.func, - fetchSlice: PropTypes.func, - saveSlice: PropTypes.func, - removeSlice: PropTypes.func, - removeChart: PropTypes.func, - updateDashboardLayout: PropTypes.func, - toggleExpandSlice: PropTypes.func, - addFilter: PropTypes.func, - getFilters: PropTypes.func, - clearFilter: PropTypes.func, - removeFilter: PropTypes.func, - editMode: PropTypes.bool.isRequired, -}; - -const defaultProps = { - onChange: () => ({}), - getFormDataExtra: () => ({}), - exploreChart: () => ({}), - exportCSV: () => ({}), - fetchSlice: () => ({}), - saveSlice: () => ({}), - removeSlice: () => ({}), - removeChart: () => ({}), - updateDashboardLayout: () => ({}), - toggleExpandSlice: () => ({}), - addFilter: () => ({}), - getFilters: () => ({}), - clearFilter: () => ({}), - removeFilter: () => ({}), -}; - -class GridLayout extends React.Component { - constructor(props) { - super(props); - - this.onResizeStop = this.onResizeStop.bind(this); - this.onDragStop = this.onDragStop.bind(this); - this.forceRefresh = this.forceRefresh.bind(this); - this.removeSlice = this.removeSlice.bind(this); - this.updateSliceName = this.props.dashboard.dash_edit_perm ? - this.updateSliceName.bind(this) : null; - } - - onResizeStop(layout) { - this.props.updateDashboardLayout(layout); - this.props.onChange(); - } - - onDragStop(layout) { - this.props.updateDashboardLayout(layout); - this.props.onChange(); - } - - getWidgetId(slice) { - return 'widget_' + slice.slice_id; - } - - getWidgetHeight(slice) { - const widgetId = this.getWidgetId(slice); - if (!widgetId || !this.refs[widgetId]) { - return 400; - } - return this.refs[widgetId].offsetHeight; - } - - getWidgetWidth(slice) { - const widgetId = this.getWidgetId(slice); - if (!widgetId || !this.refs[widgetId]) { - return 400; - } - return this.refs[widgetId].offsetWidth; - } - - findSliceIndexById(sliceId) { - return this.props.dashboard.slices - .map(slice => (slice.slice_id)).indexOf(sliceId); - } - - forceRefresh(sliceId) { - return this.props.fetchSlice(this.props.charts['slice_' + sliceId], true); - } - - removeSlice(slice) { - if (!slice) { - return; - } - - // remove slice dashboard and charts - this.props.removeSlice(slice); - this.props.removeChart(this.props.charts['slice_' + slice.slice_id].chartKey); - this.props.onChange(); - } - - updateSliceName(sliceId, sliceName) { - const index = this.findSliceIndexById(sliceId); - if (index === -1) { - return; - } - - const currentSlice = this.props.dashboard.slices[index]; - if (currentSlice.slice_name === sliceName) { - return; - } - - this.props.saveSlice(currentSlice, sliceName); - } - - isExpanded(slice) { - return this.props.dashboard.metadata.expanded_slices && - this.props.dashboard.metadata.expanded_slices[slice.slice_id]; - } - - render() { - const cells = this.props.dashboard.slices.map((slice) => { - const chartKey = `slice_${slice.slice_id}`; - const currentChart = this.props.charts[chartKey]; - const queryResponse = currentChart.queryResponse || {}; - return ( -
- -
); - }); - - return ( - - {cells} - - ); - } -} - -GridLayout.propTypes = propTypes; -GridLayout.defaultProps = defaultProps; - -export default GridLayout; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx b/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx deleted file mode 100644 index c801c0aa0d19c..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/Header.jsx +++ /dev/null @@ -1,169 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Controls from './Controls'; -import EditableTitle from '../../../../components/EditableTitle'; -import Button from '../../../../components/Button'; -import FaveStar from '../../../../components/FaveStar'; -import InfoTooltipWithTrigger from '../../../../components/InfoTooltipWithTrigger'; -import PromptV2ConversionModal from '../../PromptV2ConversionModal'; -import { - Logger, - LOG_ACTIONS_DISMISS_V2_PROMPT, - LOG_ACTIONS_SHOW_V2_INFO_PROMPT, -} from '../../../../logger'; -import { t } from '../../../../locales'; - -const propTypes = { - dashboard: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired, - userId: PropTypes.string.isRequired, - isStarred: PropTypes.bool, - addSlicesToDashboard: PropTypes.func, - onSave: PropTypes.func, - onChange: PropTypes.func, - fetchFaveStar: PropTypes.func, - renderSlices: PropTypes.func, - saveFaveStar: PropTypes.func, - serialize: PropTypes.func, - startPeriodicRender: PropTypes.func, - updateDashboardTitle: PropTypes.func, - editMode: PropTypes.bool.isRequired, - setEditMode: PropTypes.func.isRequired, - handleConvertToV2: PropTypes.func.isRequired, - unsavedChanges: PropTypes.bool.isRequired, -}; - -class Header extends React.PureComponent { - constructor(props) { - super(props); - this.handleSaveTitle = this.handleSaveTitle.bind(this); - this.toggleEditMode = this.toggleEditMode.bind(this); - this.state = { - showV2PromptModal: props.dashboard.promptV2Conversion, - }; - this.toggleShowV2PromptModal = this.toggleShowV2PromptModal.bind(this); - } - handleSaveTitle(title) { - this.props.updateDashboardTitle(title); - } - toggleEditMode() { - this.props.setEditMode(!this.props.editMode); - } - toggleShowV2PromptModal() { - const nextShowModal = !this.state.showV2PromptModal; - this.setState({ showV2PromptModal: nextShowModal }); - if (nextShowModal) { - Logger.append( - LOG_ACTIONS_SHOW_V2_INFO_PROMPT, - { - force_v2_edit: this.props.dashboard.forceV2Edit, - }, - true, - ); - } else { - Logger.append( - LOG_ACTIONS_DISMISS_V2_PROMPT, - { - force_v2_edit: this.props.dashboard.forceV2Edit, - }, - true, - ); - } - } - renderUnsaved() { - if (!this.props.unsavedChanges) { - return null; - } - return ( - - ); - } - renderEditButton() { - if (!this.props.dashboard.dash_save_perm) { - return null; - } - const btnText = this.props.editMode ? 'Switch to View Mode' : 'Edit Dashboard'; - return ( - ); - } - render() { - const dashboard = this.props.dashboard; - return ( -
-
-

- - - - - {dashboard.promptV2Conversion && ( - - {t('Convert to v2')} - - - )} - {this.renderUnsaved()} -

-
-
- {this.renderEditButton()} - -
-
- {this.state.showV2PromptModal && - dashboard.promptV2Conversion && - !this.props.editMode && ( - - )} -
- ); - } -} -Header.propTypes = propTypes; - -export default Header; diff --git a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx b/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx deleted file mode 100644 index 3e43f9365ecc9..0000000000000 --- a/superset/assets/src/dashboard/deprecated/v1/components/RefreshIntervalModal.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Select from 'react-select'; -import ModalTrigger from '../../../../components/ModalTrigger'; -import { t } from '../../../../locales'; - -const propTypes = { - triggerNode: PropTypes.node.isRequired, - initialRefreshFrequency: PropTypes.number, - onChange: PropTypes.func, -}; - -const defaultProps = { - initialRefreshFrequency: 0, - onChange: () => {}, -}; - -const options = [ - [0, t('Don\'t refresh')], - [10, t('10 seconds')], - [30, t('30 seconds')], - [60, t('1 minute')], - [300, t('5 minutes')], - [1800, t('30 minutes')], - [3600, t('1 hour')], - [21600, t('6 hours')], - [43200, t('12 hours')], - [86400, t('24 hours')], -].map(o => ({ value: o[0], label: o[1] })); - -class RefreshIntervalModal extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - refreshFrequency: props.initialRefreshFrequency, - }; - } - render() { - return ( - - {t('Choose the refresh frequency for this dashboard')} - {t('Add to new dashboard')}   @@ -224,7 +231,7 @@ class SaveModal extends React.Component { type="button" id="btn_modal_save_goto_dash" className="btn btn-primary pull-left gotodash" - disabled={this.state.addToDash === 'noSave'} + disabled={this.state.addToDash === 'noSave' || canNotSaveToDash} onClick={this.saveOrOverwrite.bind(this, true)} > {t('Save & go to dashboard')} diff --git a/superset/assets/src/explore/constants.js b/superset/assets/src/explore/constants.js index 39d70637829e4..aac9c13907743 100644 --- a/superset/assets/src/explore/constants.js +++ b/superset/assets/src/explore/constants.js @@ -35,3 +35,5 @@ export const MULTI_OPERATORS = [OPERATORS.in, OPERATORS['not in']]; export const sqlaAutoGeneratedMetricNameRegex = /^(sum|min|max|avg|count|count_distinct)__.*$/i; export const sqlaAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|AVG|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i; export const druidAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i; + +export const EXPLORE_ONLY_VIZ_TYPE = ['separator', 'markup']; diff --git a/superset/assets/src/logger.js b/superset/assets/src/logger.js index ea8e0fbf9d8f4..06059b28b1a8e 100644 --- a/superset/assets/src/logger.js +++ b/superset/assets/src/logger.js @@ -141,13 +141,6 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart'; export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart'; export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter'; -// @TODO remove upon v1 deprecation -export const LOG_ACTIONS_PREVIEW_V2 = 'preview_dashboard_v2'; -export const LOG_ACTIONS_FALLBACK_TO_V1 = 'fallback_to_dashboard_v1'; -export const LOG_ACTIONS_READ_ABOUT_V2_CHANGES = 'read_about_v2_changes'; -export const LOG_ACTIONS_DISMISS_V2_PROMPT = 'dismiss_v2_conversion_prompt'; -export const LOG_ACTIONS_SHOW_V2_INFO_PROMPT = 'show_v2_conversion_prompt'; - export const DASHBOARD_EVENT_NAMES = [ LOG_ACTIONS_MOUNT_DASHBOARD, LOG_ACTIONS_FIRST_DASHBOARD_LOAD, @@ -159,12 +152,6 @@ export const DASHBOARD_EVENT_NAMES = [ LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART, LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, LOG_ACTIONS_REFRESH_DASHBOARD, - - LOG_ACTIONS_PREVIEW_V2, - LOG_ACTIONS_FALLBACK_TO_V1, - LOG_ACTIONS_READ_ABOUT_V2_CHANGES, - LOG_ACTIONS_DISMISS_V2_PROMPT, - LOG_ACTIONS_SHOW_V2_INFO_PROMPT, ]; export const EXPLORE_EVENT_NAMES = [ diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js index 96554d6461b3f..27bafa84f828a 100644 --- a/superset/assets/webpack.config.js +++ b/superset/assets/webpack.config.js @@ -20,7 +20,6 @@ const config = { addSlice: ['babel-polyfill', APP_DIR + '/src/addSlice/index.jsx'], explore: ['babel-polyfill', APP_DIR + '/src/explore/index.jsx'], dashboard: ['babel-polyfill', APP_DIR + '/src/dashboard/index.jsx'], - dashboard_deprecated: ['babel-polyfill', APP_DIR + '/src/dashboard/deprecated/v1/index.jsx'], sqllab: ['babel-polyfill', APP_DIR + '/src/SqlLab/index.jsx'], welcome: ['babel-polyfill', APP_DIR + '/src/welcome/index.jsx'], profile: ['babel-polyfill', APP_DIR + '/src/profile/index.jsx'], diff --git a/superset/models/core.py b/superset/models/core.py index c9018db4babb0..425fbde498bd0 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -403,10 +403,10 @@ def params(self, value): self.json_metadata = value @property - def position_array(self): + def position(self): if self.position_json: return json.loads(self.position_json) - return [] + return {} @classmethod def import_obj(cls, dashboard_to_import, import_time=None): @@ -422,16 +422,7 @@ def import_obj(cls, dashboard_to_import, import_time=None): def alter_positions(dashboard, old_to_new_slc_id_dict): """ Updates slice_ids in the position json. - Sample position json v1: - [{ - "col": 5, - "row": 10, - "size_x": 4, - "size_y": 2, - "slice_id": "3610" - }] - - Sample position json v2: + Sample position_json data: { "DASHBOARD_VERSION_KEY": "v2", "DASHBOARD_ROOT_ID": { @@ -457,32 +448,17 @@ def alter_positions(dashboard, old_to_new_slc_id_dict): } """ position_data = json.loads(dashboard.position_json) - is_v2_dash = ( - isinstance(position_data, dict) and - position_data.get('DASHBOARD_VERSION_KEY') == 'v2' - ) - if is_v2_dash: - position_json = position_data.values() - for value in position_json: - if (isinstance(value, dict) and value.get('meta') and - value.get('meta').get('chartId')): - old_slice_id = value.get('meta').get('chartId') - - if old_slice_id in old_to_new_slc_id_dict: - value['meta']['chartId'] = ( - old_to_new_slc_id_dict[old_slice_id] - ) - dashboard.position_json = json.dumps(position_data) - else: - position_array = dashboard.position_array - for position in position_array: - if 'slice_id' not in position: - continue - old_slice_id = int(position['slice_id']) + position_json = position_data.values() + for value in position_json: + if (isinstance(value, dict) and value.get('meta') and + value.get('meta').get('chartId')): + old_slice_id = value.get('meta').get('chartId') + if old_slice_id in old_to_new_slc_id_dict: - position['slice_id'] = '{}'.format( - old_to_new_slc_id_dict[old_slice_id]) - dashboard.position_json = json.dumps(position_array) + value['meta']['chartId'] = ( + old_to_new_slc_id_dict[old_slice_id] + ) + dashboard.position_json = json.dumps(position_data) logging.info('Started import of the dashboard: {}' .format(dashboard_to_import.to_json())) diff --git a/superset/views/core.py b/superset/views/core.py index d47e8ac44e0d4..8e58c92dbcf43 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1550,7 +1550,6 @@ def copy_dash(self, dashboard_id): dash.owners = [g.user] if g.user else [] dash.dashboard_title = data['dashboard_title'] - is_v2_dash = Superset._is_v2_dash(data['positions']) if data['duplicate_slices']: # Duplicating slices as well, mapping old ids to new ones old_to_new_sliceids = {} @@ -1566,18 +1565,14 @@ def copy_dash(self, dashboard_id): # update chartId of layout entities # in v2_dash positions json data, chartId should be integer, # while in older version slice_id is string type - if is_v2_dash: - for value in data['positions'].values(): - if ( - isinstance(value, dict) and value.get('meta') and - value.get('meta').get('chartId') - ): - old_id = '{}'.format(value.get('meta').get('chartId')) - new_id = int(old_to_new_sliceids[old_id]) - value['meta']['chartId'] = new_id - else: - for d in data['positions']: - d['slice_id'] = old_to_new_sliceids[d['slice_id']] + for value in data['positions'].values(): + if ( + isinstance(value, dict) and value.get('meta') and + value.get('meta').get('chartId') + ): + old_id = '{}'.format(value.get('meta').get('chartId')) + new_id = int(old_to_new_sliceids[old_id]) + value['meta']['chartId'] = new_id else: dash.slices = original_dash.slices dash.params = original_dash.params @@ -1606,43 +1601,9 @@ def save_dash(self, dashboard_id): session.close() return 'SUCCESS' - @staticmethod - def _is_v2_dash(positions): - return ( - isinstance(positions, dict) and - positions.get('DASHBOARD_VERSION_KEY') == 'v2' - ) - @staticmethod def _set_dash_metadata(dashboard, data): positions = data['positions'] - is_v2_dash = Superset._is_v2_dash(positions) - - # @TODO remove upon v1 deprecation - if not is_v2_dash: - positions = data['positions'] - slice_ids = [int(d['slice_id']) for d in positions] - dashboard.slices = [o for o in dashboard.slices if o.id in slice_ids] - positions = sorted(data['positions'], key=lambda x: int(x['slice_id'])) - dashboard.position_json = json.dumps(positions, indent=4, sort_keys=True) - md = dashboard.params_dict - dashboard.css = data['css'] - dashboard.dashboard_title = data['dashboard_title'] - - if 'filter_immune_slices' not in md: - md['filter_immune_slices'] = [] - if 'timed_refresh_immune_slices' not in md: - md['timed_refresh_immune_slices'] = [] - if 'filter_immune_slice_fields' not in md: - md['filter_immune_slice_fields'] = {} - md['expanded_slices'] = data['expanded_slices'] - default_filters_data = json.loads(data.get('default_filters', '{}')) - applicable_filters =\ - {key: v for key, v in default_filters_data.items() - if int(key) in slice_ids} - md['default_filters'] = json.dumps(applicable_filters) - dashboard.json_metadata = json.dumps(md, indent=4) - return # find slices in the position data slice_ids = [] @@ -2129,57 +2090,14 @@ def dashboard(self, dashboard_id): standalone_mode = request.args.get('standalone') == 'true' edit_mode = request.args.get('edit') == 'true' - # TODO remove switch upon v1 deprecation 🎉 - # during v2 rollout, multiple factors determine whether we show v1 or v2 - # if layout == v1 - # view = v1 for non-editors - # view = v1 or v2 for editors depending on config + request (force) - # edit = v1 or v2 for editors depending on config + request (force) - # - # if layout == v2 (not backwards compatible) - # view = v2 - # edit = v2 - dashboard_layout = dash.data.get('position_json', {}) - is_v2_dash = ( - isinstance(dashboard_layout, dict) and - dashboard_layout.get('DASHBOARD_VERSION_KEY') == 'v2' - ) - - force_v1 = request.args.get('version') == 'v1' and not is_v2_dash - force_v2 = request.args.get('version') == 'v2' - force_v2_edit = ( - is_v2_dash or - not app.config.get('CAN_FALLBACK_TO_DASH_V1_EDIT_MODE') - ) - v2_is_default_view = app.config.get('DASH_V2_IS_DEFAULT_VIEW_FOR_EDITORS') - prompt_v2_conversion = False - if is_v2_dash: - dashboard_view = 'v2' - elif not dash_edit_perm: - dashboard_view = 'v1' - else: - if force_v2 or (v2_is_default_view and not force_v1): - dashboard_view = 'v2' - else: - dashboard_view = 'v1' - prompt_v2_conversion = not force_v1 - if force_v2_edit: - dash_edit_perm = False - # Hack to log the dashboard_id properly, even when getting a slug @log_this def dashboard(**kwargs): # noqa pass - - # TODO remove extra logging upon v1 deprecation 🎉 dashboard( dashboard_id=dash.id, - dashboard_version='v2' if is_v2_dash else 'v1', - dashboard_view=dashboard_view, + dashboard_version='v2', dash_edit_perm=dash_edit_perm, - force_v1=force_v1, - force_v2=force_v2, - force_v2_edit=force_v2_edit, edit_mode=edit_mode) dashboard_data = dash.data @@ -2197,26 +2115,14 @@ def dashboard(**kwargs): # noqa 'datasources': {ds.uid: ds.data for ds in datasources}, 'common': self.common_bootsrap_payload(), 'editMode': edit_mode, - # TODO remove the following upon v1 deprecation 🎉 - 'force_v2_edit': force_v2_edit, - 'prompt_v2_conversion': prompt_v2_conversion, - 'v2_auto_convert_date': app.config.get('PLANNED_V2_AUTO_CONVERT_DATE'), - 'v2_feedback_url': app.config.get('V2_FEEDBACK_URL'), } if request.args.get('json') == 'true': return json_success(json.dumps(bootstrap_data)) - if dashboard_view == 'v2': - entry = 'dashboard' - template = 'superset/dashboard.html' - else: - entry = 'dashboard_deprecated' - template = 'superset/dashboard_v1_deprecated.html' - return self.render_template( - template, - entry=entry, + 'superset/dashboard.html', + entry='dashboard', standalone_mode=standalone_mode, title=dash.dashboard_title, bootstrap_data=json.dumps(bootstrap_data), diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index 6cc575368935a..a2cebea47053e 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -33,6 +33,25 @@ def setUp(self): def tearDown(self): pass + def get_mock_positions(self, dash): + positions = { + 'DASHBOARD_VERSION_KEY': 'v2', + } + for i, slc in enumerate(dash.slices): + id = 'DASHBOARD_CHART_TYPE-{}'.format(i) + d = { + 'type': 'DASHBOARD_CHART_TYPE', + 'id': id, + 'children': [], + 'meta': { + 'width': 4, + 'height': 50, + 'chartId': slc.id, + }, + } + positions[id] = d + return positions + def test_dashboard(self): self.login(username='admin') urls = {} @@ -61,10 +80,11 @@ def test_save_dash(self, username='admin'): self.login(username=username) dash = db.session.query(models.Dashboard).filter_by( slug='births').first() + positions = self.get_mock_positions(dash) data = { 'css': '', 'expanded_slices': {}, - 'positions': dash.position_array, + 'positions': positions, 'dashboard_title': dash.dashboard_title, } url = '/superset/save_dash/{}/'.format(dash.id) @@ -76,12 +96,13 @@ def test_save_dash_with_filter(self, username='admin'): dash = db.session.query(models.Dashboard).filter_by( slug='world_health').first() + positions = self.get_mock_positions(dash) filters = {str(dash.slices[0].id): {'region': ['North America']}} default_filters = json.dumps(filters) data = { 'css': '', 'expanded_slices': {}, - 'positions': dash.position_array, + 'positions': positions, 'dashboard_title': dash.dashboard_title, 'default_filters': default_filters, } @@ -104,12 +125,13 @@ def test_save_dash_with_invalid_filters(self, username='admin'): slug='world_health').first() # add an invalid filter slice + positions = self.get_mock_positions(dash) filters = {str(99999): {'region': ['North America']}} default_filters = json.dumps(filters) data = { 'css': '', 'expanded_slices': {}, - 'positions': dash.position_array, + 'positions': positions, 'dashboard_title': dash.dashboard_title, 'default_filters': default_filters, } @@ -131,10 +153,11 @@ def test_save_dash_with_dashboard_title(self, username='admin'): .first() ) origin_title = dash.dashboard_title + positions = self.get_mock_positions(dash) data = { 'css': '', 'expanded_slices': {}, - 'positions': dash.position_array, + 'positions': positions, 'dashboard_title': 'new title', } url = '/superset/save_dash/{}/'.format(dash.id) @@ -153,11 +176,12 @@ def test_copy_dash(self, username='admin'): self.login(username=username) dash = db.session.query(models.Dashboard).filter_by( slug='births').first() + positions = self.get_mock_positions(dash) data = { 'css': '', 'duplicate_slices': False, 'expanded_slices': {}, - 'positions': dash.position_array, + 'positions': positions, 'dashboard_title': 'Copy Of Births', } @@ -211,9 +235,16 @@ def test_remove_slices(self, username='admin'): self.login(username=username) dash = db.session.query(models.Dashboard).filter_by( slug='births').first() - positions = dash.position_array[:-1] origin_slices_length = len(dash.slices) + positions = self.get_mock_positions(dash) + # remove one chart + chart_keys = [] + for key in positions.keys(): + if key.startswith('DASHBOARD_CHART_TYPE'): + chart_keys.append(key) + positions.pop(chart_keys[0]) + data = { 'css': '', 'expanded_slices': {}, diff --git a/tests/import_export_tests.py b/tests/import_export_tests.py index dc9c4ade5b023..4b2ba74d73568 100644 --- a/tests/import_export_tests.py +++ b/tests/import_export_tests.py @@ -22,6 +22,8 @@ class ImportExportTests(SupersetTestCase): """Testing export import functionality for dashboards""" + requires_examples = True + def __init__(self, *args, **kwargs): super(ImportExportTests, self).__init__(*args, **kwargs) @@ -155,9 +157,9 @@ def assert_dash_equals(self, expected_dash, actual_dash, self.assertEquals( len(expected_dash.slices), len(actual_dash.slices)) expected_slices = sorted( - expected_dash.slices, key=lambda s: s.slice_name) + expected_dash.slices, key=lambda s: s.slice_name or '') actual_slices = sorted( - actual_dash.slices, key=lambda s: s.slice_name) + actual_dash.slices, key=lambda s: s.slice_name or '') for e_slc, a_slc in zip(expected_slices, actual_slices): self.assert_slice_equals(e_slc, a_slc) if check_position: @@ -191,7 +193,10 @@ def assert_datasource_equals(self, expected_ds, actual_ds): set([m.metric_name for m in actual_ds.metrics])) def assert_slice_equals(self, expected_slc, actual_slc): - self.assertEquals(expected_slc.slice_name, actual_slc.slice_name) + # to avoid bad slice data (no slice_name) + expected_slc_name = expected_slc.slice_name or '' + actual_slc_name = actual_slc.slice_name or '' + self.assertEquals(expected_slc_name, actual_slc_name) self.assertEquals( expected_slc.datasource_type, actual_slc.datasource_type) self.assertEquals(expected_slc.viz_type, actual_slc.viz_type) @@ -209,6 +214,7 @@ def test_export_1_dashboard(self): resp.data.decode('utf-8'), object_hook=utils.decode_dashboards, )['dashboards'] + self.assert_dash_equals(birth_dash, exported_dashboards[0]) self.assertEquals( birth_dash.id, @@ -320,13 +326,18 @@ def test_import_dashboard_1_slice(self): dash_with_1_slice = self.create_dashboard( 'dash_with_1_slice', slcs=[slc], id=10002) dash_with_1_slice.position_json = """ - [{{ - "col": 5, - "row": 10, - "size_x": 4, - "size_y": 2, - "slice_id": "{}" - }}] + {{"DASHBOARD_VERSION_KEY": "v2", + "DASHBOARD_CHART_TYPE-{0}": {{ + "type": "DASHBOARD_CHART_TYPE", + "id": {0}, + "children": [], + "meta": {{ + "width": 4, + "height": 50, + "chartId": {0} + }} + }} + }} """.format(slc.id) imported_dash_id = models.Dashboard.import_obj( dash_with_1_slice, import_time=1990) @@ -340,10 +351,8 @@ def test_import_dashboard_1_slice(self): self.assertEquals({'remote_id': 10002, 'import_time': 1990}, json.loads(imported_dash.json_metadata)) - expected_position = dash_with_1_slice.position_array - expected_position[0]['slice_id'] = '{}'.format( - imported_dash.slices[0].id) - self.assertEquals(expected_position, imported_dash.position_array) + expected_position = dash_with_1_slice.position + self.assertEquals(expected_position, imported_dash.position) def test_import_dashboard_2_slices(self): e_slc = self.create_slice('e_slc', id=10007, table_name='energy_usage') From 6c9a60e6176743a0c6b8dc130e08b69fddfb1701 Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Wed, 25 Jul 2018 20:16:16 -0700 Subject: [PATCH 13/14] fix tox.ini --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 743930c6958d7..a0ecfa79a3e08 100644 --- a/tox.ini +++ b/tox.ini @@ -66,7 +66,6 @@ commands = deps = -rrequirements.txt -rrequirements-dev.txt - pylint [tox] envlist = From 83aeade2d70ebd8ea3945144bf281edcbab0d770 Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Thu, 26 Jul 2018 13:15:09 -0700 Subject: [PATCH 14/14] [migration] bug fix in dashboard migration (bebcf3fed1fe_) (#5497) fix a bug in handle empty rows (cherry picked from commit 7ff02c0) --- .../versions/bebcf3fed1fe_convert_dashboard_v1_positions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py b/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py index aa2d95d0d5144..5767401c996e8 100644 --- a/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py +++ b/superset/migrations/versions/bebcf3fed1fe_convert_dashboard_v1_positions.py @@ -351,13 +351,13 @@ def convert(positions, level, parent, root): # they can be a list of charts, or arranged in columns, or mixed for layer in layers: if len(layer) == 0: - return + continue if len(layer) == 1 and parent['type'] == COLUMN_TYPE: chart_holder = get_chart_holder(layer[0]) root[chart_holder['id']] = chart_holder parent['children'].append(chart_holder['id']) - return + continue # create a new row row_container = get_row_container()