Compare commits

...

8 Commits

Author SHA1 Message Date
Maxime Beauchemin
c0c9763975 0.23.1 2018-03-05 22:41:49 -08:00
Maxime Beauchemin
0fcdfcfd8c 0.23.0 2018-03-05 16:59:08 -08:00
Maxime Beauchemin
663fb0f584 0.23.0rc4 2018-02-21 18:16:13 -08:00
Maxime Beauchemin
05367a0c10 Allowing config flag to turn off javascript controls (#4400)
* Allowing config flag to turn off javascript controls

* lint

* one line, avoiding mutation

* Setting JS fields as readOnly

(cherry picked from commit a373db24f0)
2018-02-21 18:15:54 -08:00
Grace Guo
f99c22af0f for 48 columns layout, adjust default size and layout for newly added slices (#4446)
(cherry picked from commit 5768a1fe5e)
2018-02-21 18:15:48 -08:00
Maxime Beauchemin
a1173f4346 [explore] allow URL shortner even if no slice exist (#4457)
recent regression perhaps from the PR that moved to using POST .

(cherry picked from commit 0eecec10cd)
2018-02-21 18:15:41 -08:00
Maxime Beauchemin
91c5ce9080 [bugfix] address issue 4206 (#4452)
closes 4206

(cherry picked from commit 177d7c07e6)
2018-02-21 18:15:34 -08:00
Maxime Beauchemin
16787ee8f8 0.23.0rc3 2018-02-15 21:30:08 -08:00
12 changed files with 79 additions and 35 deletions

View File

@@ -2,6 +2,9 @@ recursive-include superset/data *
recursive-include superset/migrations * recursive-include superset/migrations *
recursive-include superset/static * recursive-include superset/static *
recursive-exclude superset/static/docs * recursive-exclude superset/static/docs *
recursive-exclude superset/static/assets/docs *
recursive-exclude superset/static/assets/visualizations *
recursive-exclude superset/static/assets/images/viz_thumbnails_large *
recursive-exclude superset/static/spec * recursive-exclude superset/static/spec *
recursive-exclude superset/static/images/viz_thumbnails_large * recursive-exclude superset/static/images/viz_thumbnails_large *
recursive-exclude superset/static/assets/node_modules * recursive-exclude superset/static/assets/node_modules *

View File

@@ -39,16 +39,21 @@ export function getInitialState(bootstrapData) {
dashboard.posDict[position.slice_id] = position; dashboard.posDict[position.slice_id] = position;
}); });
} }
dashboard.slices.forEach((slice, index) => { const lastRowId = Math.max.apply(null,
dashboard.position_json.map(pos => (pos.row + pos.size_y)));
let newSliceCounter = 0;
dashboard.slices.forEach((slice) => {
const sliceId = slice.slice_id; const sliceId = slice.slice_id;
let pos = dashboard.posDict[sliceId]; let pos = dashboard.posDict[sliceId];
if (!pos) { if (!pos) {
// append new slices to dashboard bottom, 3 slices per row
pos = { pos = {
col: (index * 4 + 1) % 12, col: (newSliceCounter % 3) * 16 + 1,
row: Math.floor((index) / 3) * 4, row: lastRowId + Math.floor(newSliceCounter / 3) * 16,
size_x: 4, size_x: 16,
size_y: 4, size_y: 16,
}; };
newSliceCounter++;
} }
dashboard.layout.push({ dashboard.layout.push({

View File

@@ -14,6 +14,7 @@ const propTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
hovered: PropTypes.bool, hovered: PropTypes.bool,
tooltipOnClick: PropTypes.func, tooltipOnClick: PropTypes.func,
warning: PropTypes.string,
}; };
const defaultProps = { const defaultProps = {
@@ -75,6 +76,19 @@ export default class ControlHeader extends React.Component {
{this.props.label} {this.props.label}
</span> </span>
{' '} {' '}
{(this.props.warning) &&
<span>
<OverlayTrigger
placement="top"
overlay={
<Tooltip id={'error-tooltip'}>{this.props.warning}</Tooltip>
}
>
<i className="fa fa-exclamation-circle text-danger" />
</OverlayTrigger>
{' '}
</span>
}
{(this.props.validationErrors.length > 0) && {(this.props.validationErrors.length > 0) &&
<span> <span>
<OverlayTrigger <OverlayTrigger

View File

@@ -9,27 +9,28 @@ import { exportChart } from '../exploreUtils';
const propTypes = { const propTypes = {
canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired, canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
slice: PropTypes.object,
chartStatus: PropTypes.string, chartStatus: PropTypes.string,
latestQueryFormData: PropTypes.object, latestQueryFormData: PropTypes.object,
queryResponse: PropTypes.object, queryResponse: PropTypes.object,
}; };
export default function ExploreActionButtons({ export default function ExploreActionButtons({
canDownload, slice, chartStatus, latestQueryFormData, queryResponse }) { canDownload, chartStatus, latestQueryFormData, queryResponse }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', { const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload, 'disabled disabledButton': !canDownload,
}); });
const doExportCSV = exportChart.bind(this, latestQueryFormData, 'csv'); const doExportCSV = exportChart.bind(this, latestQueryFormData, 'csv');
const doExportChart = exportChart.bind(this, latestQueryFormData, 'json'); const doExportChart = exportChart.bind(this, latestQueryFormData, 'json');
if (slice) { return (
return ( <div className="btn-group results" role="group">
<div className="btn-group results" role="group"> {latestQueryFormData &&
<URLShortLinkButton latestQueryFormData={latestQueryFormData} /> <URLShortLinkButton latestQueryFormData={latestQueryFormData} />}
<EmbedCodeButton latestQueryFormData={latestQueryFormData} /> {latestQueryFormData &&
<EmbedCodeButton latestQueryFormData={latestQueryFormData} />}
{latestQueryFormData &&
<a <a
onClick={doExportChart} onClick={doExportChart}
className="btn btn-default btn-sm" className="btn btn-default btn-sm"
@@ -38,8 +39,8 @@ export default function ExploreActionButtons({
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i className="fa fa-file-code-o" /> .json <i className="fa fa-file-code-o" /> .json
</a> </a>}
{latestQueryFormData &&
<a <a
onClick={doExportCSV} onClick={doExportCSV}
className={exportToCSVClasses} className={exportToCSVClasses}
@@ -48,18 +49,13 @@ export default function ExploreActionButtons({
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i className="fa fa-file-text-o" /> .csv <i className="fa fa-file-text-o" /> .csv
</a> </a>}
<DisplayQueryButton
<DisplayQueryButton queryResponse={queryResponse}
queryResponse={queryResponse} latestQueryFormData={latestQueryFormData}
latestQueryFormData={latestQueryFormData} chartStatus={chartStatus}
chartStatus={chartStatus} />
/> </div>
</div>
);
}
return (
<DisplayQueryButton latestQueryFormData={latestQueryFormData} />
); );
} }

View File

@@ -25,6 +25,7 @@ const propTypes = {
offerEditInModal: PropTypes.bool, offerEditInModal: PropTypes.bool,
language: PropTypes.oneOf([null, 'json', 'html', 'sql', 'markdown', 'javascript']), language: PropTypes.oneOf([null, 'json', 'html', 'sql', 'markdown', 'javascript']),
aboveEditorSection: PropTypes.node, aboveEditorSection: PropTypes.node,
readOnly: PropTypes.bool,
}; };
const defaultProps = { const defaultProps = {
@@ -34,6 +35,7 @@ const defaultProps = {
minLines: 3, minLines: 3,
maxLines: 10, maxLines: 10,
offerEditInModal: true, offerEditInModal: true,
readOnly: false,
}; };
export default class TextAreaControl extends React.Component { export default class TextAreaControl extends React.Component {
@@ -57,6 +59,7 @@ export default class TextAreaControl extends React.Component {
editorProps={{ $blockScrolling: true }} editorProps={{ $blockScrolling: true }}
enableLiveAutocompletion enableLiveAutocompletion
value={this.props.value} value={this.props.value}
readOnly={this.props.readOnly}
/> />
); );
} }
@@ -67,6 +70,7 @@ export default class TextAreaControl extends React.Component {
placeholder={t('textarea')} placeholder={t('textarea')}
onChange={this.onControlChange.bind(this)} onChange={this.onControlChange.bind(this)}
value={this.props.value} value={this.props.value}
disabled={this.props.readOnly}
style={{ height: this.props.height }} style={{ height: this.props.height }}
/> />
</FormGroup>); </FormGroup>);

View File

@@ -97,6 +97,11 @@ function jsFunctionControl(label, description, extraDescr = null, height = 100,
{extraDescr} {extraDescr}
</div> </div>
), ),
mapStateToProps: state => ({
warning: !state.common.conf.ENABLE_JAVASCRIPT_CONTROLS ?
t('This functionality is disabled in your environment for security reasons.') : null,
readOnly: !state.common.conf.ENABLE_JAVASCRIPT_CONTROLS,
}),
}; };
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "superset", "name": "superset",
"version": "0.23.0dev", "version": "0.23.1",
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.", "description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
"license": "Apache-2.0", "license": "Apache-2.0",
"directories": { "directories": {

View File

@@ -8,12 +8,7 @@ import ExploreActionButtons from
describe('ExploreActionButtons', () => { describe('ExploreActionButtons', () => {
const defaultProps = { const defaultProps = {
canDownload: 'True', canDownload: 'True',
slice: { latestQueryFormData: {},
data: {
csv_endpoint: '',
json_endpoint: '',
},
},
queryEndpoint: 'localhost', queryEndpoint: 'localhost',
}; };

View File

@@ -370,6 +370,12 @@ TRACKING_URL_TRANSFORMER = lambda x: x # noqa: E731
# Interval between consecutive polls when using Hive Engine # Interval between consecutive polls when using Hive Engine
HIVE_POLL_INTERVAL = 5 HIVE_POLL_INTERVAL = 5
# Allow for javascript controls components
# this enables programmers to customize certain charts (like the
# geospatial ones) by inputing javascript in controls. This exposes
# an XSS security vulnerability
ENABLE_JAVASCRIPT_CONTROLS = False
try: try:
if CONFIG_PATH_ENV_VAR in os.environ: if CONFIG_PATH_ENV_VAR in os.environ:
# Explicitly import config module that is not in pythonpath; useful # Explicitly import config module that is not in pythonpath; useful

View File

@@ -279,6 +279,8 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
__('Refresh column metadata'), __('Refresh column metadata'),
'fa-refresh') 'fa-refresh')
def refresh(self, tables): def refresh(self, tables):
if not isinstance(tables, list):
tables = [tables]
for t in tables: for t in tables:
t.fetch_metadata() t.fetch_metadata()
msg = _( msg = _(

View File

@@ -19,7 +19,10 @@ from superset.connectors.connector_registry import ConnectorRegistry
from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.models import SqlaTable
from superset.translations.utils import get_language_pack from superset.translations.utils import get_language_pack
FRONTEND_CONF_KEYS = ('SUPERSET_WEBSERVER_TIMEOUT',) FRONTEND_CONF_KEYS = (
'SUPERSET_WEBSERVER_TIMEOUT',
'ENABLE_JAVASCRIPT_CONTROLS',
)
def get_error_msg(): def get_error_msg():

View File

@@ -73,6 +73,14 @@ if perms_instruction_link:
else: else:
DATASOURCE_ACCESS_ERR = __("You don't have access to this datasource") DATASOURCE_ACCESS_ERR = __("You don't have access to this datasource")
FORM_DATA_KEY_BLACKLIST = []
if not config.get('ENABLE_JAVASCRIPT_CONTROLS'):
FORM_DATA_KEY_BLACKLIST = [
'js_tooltip',
'js_onclick_href',
'js_data_mutator',
]
def get_database_access_error_msg(database_name): def get_database_access_error_msg(database_name):
return __('This view requires the database %(name)s or ' return __('This view requires the database %(name)s or '
@@ -948,7 +956,10 @@ class Superset(BaseSupersetView):
if request.args.get('viz_type'): if request.args.get('viz_type'):
# Converting old URLs # Converting old URLs
d = cast_form_data(request.args) d = cast_form_data(d)
d = {k: v for k, v in d.items() if k not in FORM_DATA_KEY_BLACKLIST}
return d return d
def get_viz( def get_viz(