diff --git a/docs/index.rst b/docs/index.rst index a2b65b1d9d440..1956dd1ccc9bf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,12 @@ +.. image:: _static/img/s.png + Superset's documentation '''''''''''''''''''''''' Superset is a data exploration platform designed to be visual, intuitive and interactive. + ---------------- .. warning:: This project was originally named Panoramix, was renamed to diff --git a/docs/sqllab.rst b/docs/sqllab.rst index 50dc5c78632e9..1a8923c1fc644 100644 --- a/docs/sqllab.rst +++ b/docs/sqllab.rst @@ -58,3 +58,5 @@ Superset's Jinja context: .. autoclass:: superset.jinja_context.PrestoTemplateProcessor :members: + +.. autofunction:: superset.jinja_context.url_param diff --git a/superset/assets/docs b/superset/assets/docs new file mode 120000 index 0000000000000..932170420302a --- /dev/null +++ b/superset/assets/docs @@ -0,0 +1 @@ +../../docs/_build/html/ \ No newline at end of file diff --git a/superset/assets/images/s.png b/superset/assets/images/s.png new file mode 100644 index 0000000000000..2031b86a9f522 Binary files /dev/null and b/superset/assets/images/s.png differ diff --git a/superset/assets/javascripts/SqlLab/actions.js b/superset/assets/javascripts/SqlLab/actions.js index ca12ac8be0c31..721de87923c7c 100644 --- a/superset/assets/javascripts/SqlLab/actions.js +++ b/superset/assets/javascripts/SqlLab/actions.js @@ -113,7 +113,6 @@ export function fetchQueryResults(query) { export function runQuery(query) { return function (dispatch) { dispatch(startQuery(query)); - const sqlJsonUrl = '/superset/sql_json/'; const sqlJsonRequest = { client_id: query.id, database_id: query.dbId, @@ -126,6 +125,7 @@ export function runQuery(query) { tmp_table_name: query.tempTableName, select_as_cta: query.ctas, }; + const sqlJsonUrl = '/superset/sql_json/' + location.search; $.ajax({ type: 'POST', dataType: 'json', diff --git a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx index e612c9f97e97e..3e97b3e3ee5d9 100644 --- a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx +++ b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx @@ -1,3 +1,4 @@ +/* global notify */ import React from 'react'; import { Alert, Button, Col, Modal } from 'react-bootstrap'; @@ -5,6 +6,7 @@ import Select from 'react-select'; import { Table } from 'reactable'; import shortid from 'shortid'; import $ from 'jquery'; +import { getExploreUrl } from '../../explorev2/exploreUtils'; const CHART_TYPES = [ { value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false }, @@ -89,7 +91,9 @@ class VisualizeModal extends React.PureComponent { } } if (!hasTime) { - hints.push('To use this chart type you need at least one column flagged as a date'); + hints.push( + 'To use this chart type you need at least one column ' + + 'flagged as a date'); } } this.setState({ hints }); @@ -113,9 +117,10 @@ class VisualizeModal extends React.PureComponent { chartType: this.state.chartType.value, datasourceName: this.state.datasourceName, columns: this.state.columns, - sql: this.props.query.executedSql, + sql: this.props.query.sql, dbId: this.props.query.dbId, }; + notify.info('Creating a data source and popping a new tab'); $.ajax({ type: 'POST', url: '/superset/sqllab_viz/', @@ -123,9 +128,28 @@ class VisualizeModal extends React.PureComponent { data: { data: JSON.stringify(vizOptions), }, - success: (url) => { - window.open(url); + dataType: 'json', + success: resp => { + const columns = Object.keys(this.state.columns).map(k => this.state.columns[k]); + const data = JSON.parse(resp); + const mainMetric = columns.filter(d => d.agg)[0]; + const mainGroupBy = columns.filter(d => d.is_dim)[0]; + const formData = { + datasource: `${data.table_id}__table`, + viz_type: this.state.chartType.value, + since: '100 years ago', + limit: '0', + }; + if (mainMetric) { + formData.metrics = [mainMetric.name]; + formData.metric = mainMetric.name; + } + if (mainGroupBy) { + formData.groupby = mainGroupBy.name; + } + window.open(getExploreUrl(formData)); }, + error: () => notify('An error occurred while creating the data source'), }); } changeDatasourceName(event) { diff --git a/superset/assets/javascripts/explorev2/exploreUtils.js b/superset/assets/javascripts/explorev2/exploreUtils.js index 59d83d2f14431..5951ff61a2e6c 100644 --- a/superset/assets/javascripts/explorev2/exploreUtils.js +++ b/superset/assets/javascripts/explorev2/exploreUtils.js @@ -1,26 +1,43 @@ /* eslint camelcase: 0 */ -export function getExploreUrl(form_data, endpoint = 'base', force = false) { +import URI from 'urijs'; + +export function getExploreUrl(form_data, endpointType = 'base', force = false, curUrl = null) { if (!form_data.datasource) { return null; } + + + // The search params from the window.location are carried through, + // but can be specified with curUrl (used for unit tests to spoof + // the window.location). + let uri = URI(window.location.search); + if (curUrl) { + uri = URI(URI(curUrl).search()); + } + + // Building the directory part of the URI + let directory = '/superset/explore/'; + if (['json', 'csv', 'query'].indexOf(endpointType) >= 0) { + directory = '/superset/explore_json/'; + } const [datasource_id, datasource_type] = form_data.datasource.split('__'); - let params = `${datasource_type}/${datasource_id}/`; - params += '?form_data=' + encodeURIComponent(JSON.stringify(form_data)); + directory += `${datasource_type}/${datasource_id}/`; + + // Building the querystring (search) part of the URI + const search = uri.search(true); + search.form_data = JSON.stringify(form_data); if (force) { - params += '&force=true'; + search.force = 'true'; + } + if (endpointType === 'csv') { + search.csv = 'true'; + } + if (endpointType === 'standalone') { + search.standalone = 'true'; } - switch (endpoint) { - case 'base': - return `/superset/explore/${params}`; - case 'json': - return `/superset/explore_json/${params}`; - case 'csv': - return `/superset/explore_json/${params}&csv=true`; - case 'standalone': - return `/superset/explore/${params}&standalone=true`; - case 'query': - return `/superset/explore_json/${params}&query=true`; - default: - return `/superset/explore/${params}`; + if (endpointType === 'query') { + search.query = 'true'; } + uri = uri.search(search).directory(directory); + return uri.toString(); } diff --git a/superset/assets/spec/javascripts/explorev2/utils_spec.jsx b/superset/assets/spec/javascripts/explorev2/utils_spec.jsx new file mode 100644 index 0000000000000..111dccc951d9a --- /dev/null +++ b/superset/assets/spec/javascripts/explorev2/utils_spec.jsx @@ -0,0 +1,59 @@ +import { it, describe } from 'mocha'; +import { expect } from 'chai'; +import URI from 'urijs'; +import { getExploreUrl } from '../../../javascripts/explorev2/exploreUtils.js'; + +describe('utils', () => { + const formData = { + datasource: '1__table', + }; + const sFormData = JSON.stringify(formData); + function compareURI(uri1, uri2) { + expect(uri1.toString()).to.equal(uri2.toString()); + } + + it('getExploreUrl generates proper base url', () => { + // This assertion is to show clearly the value of location.href + // in the context of unit tests. + expect(location.href).to.equal('about:blank'); + + compareURI( + URI(getExploreUrl(formData, 'base', false, 'http://superset.com')), + URI('/superset/explore/table/1/').search({ form_data: sFormData }) + ); + }); + it('getExploreUrl generates proper json url', () => { + compareURI( + URI(getExploreUrl(formData, 'json', false, 'superset.com')), + URI('/superset/explore_json/table/1/').search({ form_data: sFormData }) + ); + }); + it('getExploreUrl generates proper json forced url', () => { + compareURI( + URI(getExploreUrl(formData, 'json', true, 'superset.com')), + URI('/superset/explore_json/table/1/') + .search({ form_data: sFormData, force: 'true' }) + ); + }); + it('getExploreUrl generates proper csv URL', () => { + compareURI( + URI(getExploreUrl(formData, 'csv', false, 'superset.com')), + URI('/superset/explore_json/table/1/') + .search({ form_data: sFormData, csv: 'true' }) + ); + }); + it('getExploreUrl generates proper standalone URL', () => { + compareURI( + URI(getExploreUrl(formData, 'standalone', false, 'superset.com')), + URI('/superset/explore/table/1/') + .search({ form_data: sFormData, standalone: 'true' }) + ); + }); + it('getExploreUrl preserves main URLs params', () => { + compareURI( + URI(getExploreUrl(formData, 'json', false, 'superset.com?foo=bar')), + URI('/superset/explore_json/table/1/') + .search({ foo: 'bar', form_data: sFormData }) + ); + }); +}); diff --git a/superset/jinja_context.py b/superset/jinja_context.py index b35915bba6dea..ff1cb67b23e47 100644 --- a/superset/jinja_context.py +++ b/superset/jinja_context.py @@ -6,6 +6,7 @@ import inspect from jinja2.sandbox import SandboxedEnvironment +from flask import request from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta @@ -27,6 +28,18 @@ BASE_CONTEXT.update(config.get('JINJA_CONTEXT_ADDONS', {})) +def url_param(param, default=None): + """Get a url paramater + + :param param: the url parameter to lookup + :type param: str + :param default: the value to return in the absence of the parameter + :type default: str + """ + print(request.args) + return request.args.get(param, default) + + class BaseTemplateProcessor(object): """Base class for database-specific jinja context @@ -52,7 +65,9 @@ def __init__(self, database=None, query=None, table=None, **kwargs): self.schema = query.schema elif table: self.schema = table.schema - self.context = {} + self.context = { + 'url_param': url_param, + } self.context.update(kwargs) self.context.update(BASE_CONTEXT) if self.engine: diff --git a/superset/sql_parse.py b/superset/sql_parse.py index 61966c2a7829d..0faae28b9428a 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -1,6 +1,8 @@ +import logging + import sqlparse from sqlparse.sql import IdentifierList, Identifier -from sqlparse.tokens import DML, Keyword, Name +from sqlparse.tokens import Keyword, Name RESULT_OPERATIONS = {'UNION', 'INTERSECT', 'EXCEPT'} PRECEDES_TABLE_NAME = {'FROM', 'JOIN', 'DESC', 'DESCRIBE', 'WITH'} @@ -13,6 +15,7 @@ def __init__(self, sql_statement): self._table_names = set() self._alias_names = set() # TODO: multistatement support + logging.info("Parsing with sqlparse statement {}".format(self.sql)) self._parsed = sqlparse.parse(self.sql) for statement in self._parsed: self.__extract_from_token(statement) diff --git a/superset/views/core.py b/superset/views/core.py index 5f917c82c3f57..291cf5fb06aa8 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1710,7 +1710,6 @@ def sqllab_viz(self): SqlaTable = ConnectorRegistry.sources['table'] data = json.loads(request.form.get('data')) table_name = data.get('datasourceName') - viz_type = data.get('chartType') SqlaTable = ConnectorRegistry.sources['table'] table = ( db.session.query(SqlaTable) @@ -1762,16 +1761,9 @@ def sqllab_viz(self): table.columns = cols table.metrics = metrics db.session.commit() - params = { - 'viz_type': viz_type, - 'groupby': dims[0].column_name if dims else None, - 'metrics': metrics[0].metric_name if metrics else None, - 'metric': metrics[0].metric_name if metrics else None, - 'since': '100 years ago', - 'limit': '0', - } - params = "&".join([k + '=' + v for k, v in params.items() if v]) - return '/superset/explore/table/{table.id}/?{params}'.format(**locals()) + return self.json_response(json.dumps({ + 'table_id': table.id, + })) @has_access @expose("/table////")