diff --git a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx index b8b52e36fdb3f..30b47994eb3bf 100644 --- a/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx +++ b/superset/assets/javascripts/explore/components/ExploreChartHeader.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { chartPropType } from '../../chart/chartReducer'; import ExploreActionButtons from './ExploreActionButtons'; +import RowCountLabel from './RowCountLabel'; import EditableTitle from '../../components/EditableTitle'; import AlteredSliceTag from '../../components/AlteredSliceTag'; import FaveStar from '../../components/FaveStar'; @@ -66,11 +67,12 @@ class ExploreChartHeader extends React.PureComponent { } render() { + const formData = this.props.form_data; const queryResponse = this.props.chart.queryResponse; const data = { - csv_endpoint: getExploreUrl(this.props.form_data, 'csv'), - json_endpoint: getExploreUrl(this.props.form_data, 'json'), - standalone_endpoint: getExploreUrl(this.props.form_data, 'standalone'), + csv_endpoint: getExploreUrl(formData, 'csv'), + json_endpoint: getExploreUrl(formData, 'json'), + standalone_endpoint: getExploreUrl(formData, 'standalone'), }; return ( @@ -109,13 +111,20 @@ class ExploreChartHeader extends React.PureComponent { {this.props.chart.sliceFormData && }
+ {this.props.chart.chartStatus === 'success' && queryResponse && + + } {this.props.chart.chartStatus === 'success' && queryResponse && queryResponse.is_cached && +
diff --git a/superset/assets/javascripts/explore/components/RowCountLabel.jsx b/superset/assets/javascripts/explore/components/RowCountLabel.jsx new file mode 100644 index 0000000000000..1b29a0309ec37 --- /dev/null +++ b/superset/assets/javascripts/explore/components/RowCountLabel.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Label } from 'react-bootstrap'; + +import { t } from '../../locales'; +import { defaultNumberFormatter } from '../../modules/utils'; +import TooltipWrapper from '../../components/TooltipWrapper'; + + +const propTypes = { + rowcount: PropTypes.number, + limit: PropTypes.number, +}; + +const defaultProps = { +}; + +export default function RowCountLabel({ rowcount, limit }) { + const limitReached = rowcount === limit; + const bsStyle = (limitReached || rowcount === 0) ? 'warning' : 'default'; + const formattedRowCount = defaultNumberFormatter(rowcount); + const tooltip = ( + + {limitReached && +
{t('Limit reached')}
} + {rowcount} +
+ ); + return ( + + + + ); +} + +RowCountLabel.propTypes = propTypes; +RowCountLabel.defaultProps = defaultProps; diff --git a/superset/assets/javascripts/modules/utils.js b/superset/assets/javascripts/modules/utils.js index e7757d4b06d8a..b5590f0e79cee 100644 --- a/superset/assets/javascripts/modules/utils.js +++ b/superset/assets/javascripts/modules/utils.js @@ -4,6 +4,17 @@ import $ from 'jquery'; import { formatDate, UTC } from './dates'; +const siFormatter = d3.format('.3s'); + +export function defaultNumberFormatter(n) { + let si = siFormatter(n); + // Removing trailing `.00` if any + if (si.slice(-1) < 'A') { + si = parseFloat(si).toString(); + } + return si; +} + export function d3FormatPreset(format) { // like d3.format, but with support for presets like 'smart_date' if (format === 'smart_date') { @@ -12,7 +23,7 @@ export function d3FormatPreset(format) { if (format) { return d3.format(format); } - return d3.format('.3s'); + return defaultNumberFormatter; } export const d3TimeFormatPreset = function (format) { const effFormat = format || 'smart_date'; diff --git a/superset/assets/spec/javascripts/explore/components/RowCountLabel_spec.jsx b/superset/assets/spec/javascripts/explore/components/RowCountLabel_spec.jsx new file mode 100644 index 0000000000000..1642fd7df680f --- /dev/null +++ b/superset/assets/spec/javascripts/explore/components/RowCountLabel_spec.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { shallow } from 'enzyme'; +import { Label } from 'react-bootstrap'; + +import TooltipWrapper from './../../../../javascripts/components/TooltipWrapper'; + +import RowCountLabel from '../../../../javascripts/explore/components/RowCountLabel'; + +describe('RowCountLabel', () => { + const defaultProps = { + rowcount: 51, + limit: 100, + }; + + it('is valid', () => { + expect(React.isValidElement()).to.equal(true); + }); + it('renders a Label and a TooltipWrapper', () => { + const wrapper = shallow(); + expect(wrapper.find(Label)).to.have.lengthOf(1); + expect(wrapper.find(TooltipWrapper)).to.have.lengthOf(1); + }); + it('renders a warning when limit is reached', () => { + const props = { + rowcount: 100, + limit: 100, + }; + const wrapper = shallow(); + expect(wrapper.find(Label).first().props().bsStyle).to.equal('warning'); + }); +}); diff --git a/superset/assets/spec/javascripts/modules/utils_spec.jsx b/superset/assets/spec/javascripts/modules/utils_spec.jsx index 1e3f2d4007454..174e0e1e61db8 100644 --- a/superset/assets/spec/javascripts/modules/utils_spec.jsx +++ b/superset/assets/spec/javascripts/modules/utils_spec.jsx @@ -2,7 +2,7 @@ import { it, describe } from 'mocha'; import { expect } from 'chai'; import { tryNumify, slugify, formatSelectOptionsForRange, d3format, - d3FormatPreset, d3TimeFormatPreset, + d3FormatPreset, d3TimeFormatPreset, defaultNumberFormatter, } from '../../../javascripts/modules/utils'; describe('utils', () => { @@ -52,4 +52,21 @@ describe('utils', () => { expect(d3FormatPreset('smart_date')(0)).to.equal('1970'); }); }); + describe('d3TimeFormatPreset', () => { + expect(defaultNumberFormatter(10)).to.equal('10'); + expect(defaultNumberFormatter(1)).to.equal('1'); + expect(defaultNumberFormatter(1.0)).to.equal('1'); + expect(defaultNumberFormatter(10.0)).to.equal('10'); + expect(defaultNumberFormatter(10001)).to.equal('10.0k'); + expect(defaultNumberFormatter(111000000)).to.equal('111M'); + expect(defaultNumberFormatter(0.23)).to.equal('230m'); + + expect(defaultNumberFormatter(-10)).to.equal('-10'); + expect(defaultNumberFormatter(-1)).to.equal('-1'); + expect(defaultNumberFormatter(-1.0)).to.equal('-1'); + expect(defaultNumberFormatter(-10.0)).to.equal('-10'); + expect(defaultNumberFormatter(-10001)).to.equal('-10.0k'); + expect(defaultNumberFormatter(-111000000)).to.equal('-111M'); + expect(defaultNumberFormatter(-0.23)).to.equal('-230m'); + }); }); diff --git a/superset/viz.py b/superset/viz.py index 2ea85d26a8b25..6551577de15c4 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -273,11 +273,13 @@ def get_payload(self, force=False): cache_timeout = self.cache_timeout stacktrace = None annotations = [] + rowcount = None try: df = self.get_df() if not self.error_message: data = self.get_data(df) annotations = self.get_annotations() + rowcount = len(df.index) except Exception as e: logging.exception(e) if not self.error_message: @@ -295,6 +297,7 @@ def get_payload(self, force=False): 'status': self.status, 'stacktrace': stacktrace, 'annotations': annotations, + 'rowcount': rowcount, } payload['cached_dttm'] = datetime.utcnow().isoformat().split('.')[0] logging.info('Caching for the next {} seconds'.format(