mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
A nice CacheLabel React component (#2628)
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.
This commit is contained in:
parent
23aeee5a9c
commit
787daf6005
68
superset/assets/javascripts/components/CachedLabel.jsx
Normal file
68
superset/assets/javascripts/components/CachedLabel.jsx
Normal file
@ -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 ? (
|
||||||
|
<span>
|
||||||
|
Loaded data cached <b>{moment(this.props.cachedTimestamp).fromNow()}</b>
|
||||||
|
</span>) :
|
||||||
|
'Loaded from cache';
|
||||||
|
|
||||||
|
const tooltipContent = (
|
||||||
|
<span>
|
||||||
|
{cachedText}.
|
||||||
|
Click to force-refresh
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
this.setState({ tooltipContent });
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseOver() {
|
||||||
|
this.updateTooltipContent();
|
||||||
|
this.setState({ hovered: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseOut() {
|
||||||
|
this.setState({ hovered: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const labelStyle = this.state.hovered ? 'primary' : 'default';
|
||||||
|
return (
|
||||||
|
<TooltipWrapper
|
||||||
|
tooltip={this.state.tooltipContent}
|
||||||
|
label="cache-desc"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
className={this.props.className}
|
||||||
|
bsStyle={labelStyle}
|
||||||
|
style={{ fontSize: '10px', marginRight: '5px', cursor: 'pointer' }}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onMouseOver={this.mouseOver.bind(this)}
|
||||||
|
onMouseOut={this.mouseOut.bind(this)}
|
||||||
|
>
|
||||||
|
cached <i className="fa fa-refresh" />
|
||||||
|
</Label>
|
||||||
|
</TooltipWrapper>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CacheLabel.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default CacheLabel;
|
@ -4,7 +4,7 @@ import { slugify } from '../modules/utils';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
tooltip: PropTypes.string.isRequired,
|
tooltip: PropTypes.node.isRequired,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import { Alert } from 'react-bootstrap';
|
import { Alert } from 'react-bootstrap';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import GridLayout from './components/GridLayout';
|
import GridLayout from './components/GridLayout';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
@ -143,13 +144,15 @@ export function dashboardContainer(dashboard, datasources) {
|
|||||||
done(slice) {
|
done(slice) {
|
||||||
const refresh = slice.getWidgetHeader().find('.refresh');
|
const refresh = slice.getWidgetHeader().find('.refresh');
|
||||||
const data = slice.data;
|
const data = slice.data;
|
||||||
|
const cachedWhen = moment(data.cached_dttm).fromNow();
|
||||||
if (data !== undefined && data.is_cached) {
|
if (data !== undefined && data.is_cached) {
|
||||||
refresh
|
refresh
|
||||||
.addClass('danger')
|
.addClass('danger')
|
||||||
.attr('title',
|
.attr(
|
||||||
'Served from data cached at ' + data.cached_dttm +
|
'title',
|
||||||
'. Click to force refresh')
|
`Served from data cached ${cachedWhen}. ` +
|
||||||
.tooltip('fixTitle');
|
'Click to force refresh')
|
||||||
|
.tooltip('fixTitle');
|
||||||
} else {
|
} else {
|
||||||
refresh
|
refresh
|
||||||
.removeClass('danger')
|
.removeClass('danger')
|
||||||
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
|||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { connect } from 'react-redux';
|
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 visMap from '../../../visualizations/main';
|
||||||
import { d3format } from '../../modules/utils';
|
import { d3format } from '../../modules/utils';
|
||||||
import ExploreActionButtons from './ExploreActionButtons';
|
import ExploreActionButtons from './ExploreActionButtons';
|
||||||
@ -11,6 +11,7 @@ import TooltipWrapper from '../../components/TooltipWrapper';
|
|||||||
import Timer from '../../components/Timer';
|
import Timer from '../../components/Timer';
|
||||||
import { getExploreUrl } from '../exploreUtils';
|
import { getExploreUrl } from '../exploreUtils';
|
||||||
import { getFormDataFromControls } from '../stores/store';
|
import { getFormDataFromControls } from '../stores/store';
|
||||||
|
import CachedLabel from '../../components/CachedLabel';
|
||||||
|
|
||||||
const CHART_STATUS_MAP = {
|
const CHART_STATUS_MAP = {
|
||||||
failed: 'danger',
|
failed: 'danger',
|
||||||
@ -265,17 +266,10 @@ class ChartContainer extends React.PureComponent {
|
|||||||
{this.props.chartStatus === 'success' &&
|
{this.props.chartStatus === 'success' &&
|
||||||
this.props.queryResponse &&
|
this.props.queryResponse &&
|
||||||
this.props.queryResponse.is_cached &&
|
this.props.queryResponse.is_cached &&
|
||||||
<TooltipWrapper
|
<CachedLabel
|
||||||
tooltip="Loaded from cache. Click to force refresh"
|
onClick={this.runQuery.bind(this)}
|
||||||
label="cache-desc"
|
cachedTimestamp={queryResponse.cached_dttm}
|
||||||
>
|
/>
|
||||||
<Label
|
|
||||||
style={{ fontSize: '10px', marginRight: '5px', cursor: 'pointer' }}
|
|
||||||
onClick={this.runQuery.bind(this)}
|
|
||||||
>
|
|
||||||
cached
|
|
||||||
</Label>
|
|
||||||
</TooltipWrapper>
|
|
||||||
}
|
}
|
||||||
<Timer
|
<Timer
|
||||||
startTime={this.props.chartUpdateStartTime}
|
startTime={this.props.chartUpdateStartTime}
|
||||||
|
@ -56,19 +56,12 @@ const px = function () {
|
|||||||
.tooltip();
|
.tooltip();
|
||||||
}
|
}
|
||||||
const Slice = function (data, datasource, controller) {
|
const Slice = function (data, datasource, controller) {
|
||||||
let timer;
|
|
||||||
const token = $('#token_' + data.slice_id);
|
const token = $('#token_' + data.slice_id);
|
||||||
const containerId = 'con_' + data.slice_id;
|
const containerId = 'con_' + data.slice_id;
|
||||||
const selector = '#' + containerId;
|
const selector = '#' + containerId;
|
||||||
const container = $(selector);
|
const container = $(selector);
|
||||||
const sliceId = data.slice_id;
|
const sliceId = data.slice_id;
|
||||||
const formData = applyDefaultFormData(data.form_data);
|
const formData = applyDefaultFormData(data.form_data);
|
||||||
let dttm = 0;
|
|
||||||
const stopwatch = function () {
|
|
||||||
dttm += 10;
|
|
||||||
const num = dttm / 1000;
|
|
||||||
$('#timer').text(num.toFixed(2) + ' sec');
|
|
||||||
};
|
|
||||||
slice = {
|
slice = {
|
||||||
data,
|
data,
|
||||||
formData,
|
formData,
|
||||||
@ -114,8 +107,6 @@ const px = function () {
|
|||||||
},
|
},
|
||||||
/* eslint no-shadow: 0 */
|
/* eslint no-shadow: 0 */
|
||||||
always(data) {
|
always(data) {
|
||||||
clearInterval(timer);
|
|
||||||
$('#timer').removeClass('btn-warning');
|
|
||||||
if (data && data.query) {
|
if (data && data.query) {
|
||||||
slice.viewSqlQuery = data.query;
|
slice.viewSqlQuery = data.query;
|
||||||
}
|
}
|
||||||
@ -123,13 +114,10 @@ const px = function () {
|
|||||||
done(payload) {
|
done(payload) {
|
||||||
Object.assign(data, payload);
|
Object.assign(data, payload);
|
||||||
|
|
||||||
clearInterval(timer);
|
|
||||||
token.find('img.loading').hide();
|
token.find('img.loading').hide();
|
||||||
container.fadeTo(0.5, 1);
|
container.fadeTo(0.5, 1);
|
||||||
container.show();
|
container.show();
|
||||||
|
|
||||||
$('#timer').addClass('label-success');
|
|
||||||
$('#timer').removeClass('label-warning label-danger');
|
|
||||||
$('.query-and-save button').removeAttr('disabled');
|
$('.query-and-save button').removeAttr('disabled');
|
||||||
this.always(data);
|
this.always(data);
|
||||||
controller.done(this);
|
controller.done(this);
|
||||||
@ -173,7 +161,6 @@ const px = function () {
|
|||||||
container.html(errHtml);
|
container.html(errHtml);
|
||||||
container.show();
|
container.show();
|
||||||
$('span.query').removeClass('disabled');
|
$('span.query').removeClass('disabled');
|
||||||
$('#timer').addClass('btn-danger');
|
|
||||||
$('.query-and-save button').removeAttr('disabled');
|
$('.query-and-save button').removeAttr('disabled');
|
||||||
this.always(o);
|
this.always(o);
|
||||||
controller.error(this);
|
controller.error(this);
|
||||||
@ -213,10 +200,6 @@ const px = function () {
|
|||||||
token.find('img.loading').show();
|
token.find('img.loading').show();
|
||||||
container.fadeTo(0.5, 0.25);
|
container.fadeTo(0.5, 0.25);
|
||||||
container.css('height', this.height());
|
container.css('height', this.height());
|
||||||
dttm = 0;
|
|
||||||
timer = setInterval(stopwatch, 10);
|
|
||||||
$('#timer').removeClass('label-danger label-success');
|
|
||||||
$('#timer').addClass('label-warning');
|
|
||||||
$.getJSON(this.jsonEndpoint(), (queryResponse) => {
|
$.getJSON(this.jsonEndpoint(), (queryResponse) => {
|
||||||
try {
|
try {
|
||||||
vizMap[formData.viz_type](this, queryResponse);
|
vizMap[formData.viz_type](this, queryResponse);
|
||||||
|
@ -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(<CachedLabel {...defaultProps} />),
|
||||||
|
).to.equal(true);
|
||||||
|
});
|
||||||
|
it('renders', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<CachedLabel {...defaultProps} />,
|
||||||
|
);
|
||||||
|
expect(wrapper.find(Label)).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user