diff --git a/superset/assets/javascripts/components/CachedLabel.jsx b/superset/assets/javascripts/components/CachedLabel.jsx
new file mode 100644
index 00000000000..e649fad3b2b
--- /dev/null
+++ b/superset/assets/javascripts/components/CachedLabel.jsx
@@ -0,0 +1,68 @@
+import React, { PropTypes } from 'react';
+import { Label } from 'react-bootstrap';
+import moment from 'moment';
+import TooltipWrapper from './TooltipWrapper';
+
+const propTypes = {
+ onClick: PropTypes.func,
+ cachedTimestamp: PropTypes.string,
+ className: PropTypes.string,
+};
+
+class CacheLabel extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ tooltipContent: '',
+ hovered: false,
+ };
+ }
+
+ updateTooltipContent() {
+ const cachedText = this.props.cachedTimestamp ? (
+
+ Loaded data cached {moment(this.props.cachedTimestamp).fromNow()}
+ ) :
+ 'Loaded from cache';
+
+ const tooltipContent = (
+
+ {cachedText}.
+ Click to force-refresh
+
+ );
+ this.setState({ tooltipContent });
+ }
+
+ mouseOver() {
+ this.updateTooltipContent();
+ this.setState({ hovered: true });
+ }
+
+ mouseOut() {
+ this.setState({ hovered: false });
+ }
+
+ render() {
+ const labelStyle = this.state.hovered ? 'primary' : 'default';
+ return (
+
+
+ );
+ }
+}
+CacheLabel.propTypes = propTypes;
+
+export default CacheLabel;
diff --git a/superset/assets/javascripts/components/TooltipWrapper.jsx b/superset/assets/javascripts/components/TooltipWrapper.jsx
index 56f0c148e2a..fb476c410c7 100644
--- a/superset/assets/javascripts/components/TooltipWrapper.jsx
+++ b/superset/assets/javascripts/components/TooltipWrapper.jsx
@@ -4,7 +4,7 @@ import { slugify } from '../modules/utils';
const propTypes = {
label: PropTypes.string.isRequired,
- tooltip: PropTypes.string.isRequired,
+ tooltip: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
placement: PropTypes.string,
};
diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
index 2094fb2ddc4..59e8e8874e1 100644
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/Dashboard.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import { render } from 'react-dom';
import d3 from 'd3';
import { Alert } from 'react-bootstrap';
+import moment from 'moment';
import GridLayout from './components/GridLayout';
import Header from './components/Header';
@@ -143,13 +144,15 @@ export function dashboardContainer(dashboard, datasources) {
done(slice) {
const refresh = slice.getWidgetHeader().find('.refresh');
const data = slice.data;
+ const cachedWhen = moment(data.cached_dttm).fromNow();
if (data !== undefined && data.is_cached) {
refresh
.addClass('danger')
- .attr('title',
- 'Served from data cached at ' + data.cached_dttm +
- '. Click to force refresh')
- .tooltip('fixTitle');
+ .attr(
+ 'title',
+ `Served from data cached ${cachedWhen}. ` +
+ 'Click to force refresh')
+ .tooltip('fixTitle');
} else {
refresh
.removeClass('danger')
diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
index f6ea3469fb0..cc0524251b8 100644
--- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
@@ -2,7 +2,7 @@ import $ from 'jquery';
import Mustache from 'mustache';
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
-import { Alert, Collapse, Label, Panel } from 'react-bootstrap';
+import { Alert, Collapse, Panel } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
import ExploreActionButtons from './ExploreActionButtons';
@@ -11,6 +11,7 @@ import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromControls } from '../stores/store';
+import CachedLabel from '../../components/CachedLabel';
const CHART_STATUS_MAP = {
failed: 'danger',
@@ -265,17 +266,10 @@ class ChartContainer extends React.PureComponent {
{this.props.chartStatus === 'success' &&
this.props.queryResponse &&
this.props.queryResponse.is_cached &&
-
-
-
+
}
{
try {
vizMap[formData.viz_type](this, queryResponse);
diff --git a/superset/assets/spec/javascripts/components/CachedLabel_spec.jsx b/superset/assets/spec/javascripts/components/CachedLabel_spec.jsx
new file mode 100644
index 00000000000..a720a8ce575
--- /dev/null
+++ b/superset/assets/spec/javascripts/components/CachedLabel_spec.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+import { shallow } from 'enzyme';
+import { Label } from 'react-bootstrap';
+
+import CachedLabel from '../../../javascripts/components/CachedLabel';
+
+describe('CachedLabel', () => {
+ const defaultProps = {
+ onClick: () => {},
+ cachedTimestamp: '2017-01-01',
+ };
+
+ it('is valid', () => {
+ expect(
+ React.isValidElement(),
+ ).to.equal(true);
+ });
+ it('renders', () => {
+ const wrapper = shallow(
+ ,
+ );
+ expect(wrapper.find(Label)).to.have.length(1);
+ });
+});