From e7205b9f2c1ec10a4e93b811eed8f9ec2d07e437 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Fri, 14 Apr 2017 22:29:38 -0700 Subject: [PATCH] A nice CacheLabel React component Introducing a nice component as a label that show when data was loaded from cache, when the cache was taken (in humanize duration as in `a few minutes ago`) in a tooltip, and it can act as a button that can trigger a force-refresh. While working on it, it became clear that it was going to be hard to use this component in the Dashboard view since it's not pure React. I'm planning on refactoring the dashboard view with proper React/Redux and introducing the CachedLabel component at that point. While digging around in the Dashboard view I realized that there was a bunch on unused code around managing timers that was used in explorev1 and decided to rip it out. --- .../javascripts/components/CachedLabel.jsx | 68 +++++++++++++++++++ .../javascripts/components/TooltipWrapper.jsx | 2 +- .../javascripts/dashboard/Dashboard.jsx | 11 +-- .../explorev2/components/ChartContainer.jsx | 18 ++--- .../assets/javascripts/modules/superset.js | 17 ----- .../components/CachedLabel_spec.jsx | 26 +++++++ 6 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 superset/assets/javascripts/components/CachedLabel.jsx create mode 100644 superset/assets/spec/javascripts/components/CachedLabel_spec.jsx diff --git a/superset/assets/javascripts/components/CachedLabel.jsx b/superset/assets/javascripts/components/CachedLabel.jsx new file mode 100644 index 0000000000000..e649fad3b2b4a --- /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 56f0c148e2a25..fb476c410c7eb 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 2094fb2ddc478..59e8e8874e1c8 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 f6ea3469fb091..cc0524251b8fe 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 0000000000000..a720a8ce575d4 --- /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); + }); +});