Merge pull request #2103 from influxdata/dashboard-manual-refresh

[WIP] Manual Refresh Button
pull/2144/head
Andrew Watkins 2017-10-20 12:02:00 -07:00 committed by GitHub
commit 1b3ad86e3f
16 changed files with 353 additions and 245 deletions

View File

@ -13,6 +13,7 @@
1. [#2045](https://github.com/influxdata/chronograf/pull/2045): Add CSV download option in dashboard cells
1. [#2133](https://github.com/influxdata/chronograf/pull/2133): Implicitly prepend source urls with http://
1. [#2127](https://github.com/influxdata/chronograf/pull/2127): Add support for graph zooming and point display on the millisecond-level
1. [#2103](https://github.com/influxdata/chronograf/pull/2103): Add manual refresh button for Dashboard, Data Explorer, and Host Pages
### UI Improvements
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings

View File

@ -13,6 +13,7 @@ const Dashboard = ({
onAddCell,
timeRange,
autoRefresh,
manualRefresh,
onDeleteCell,
synchronizer,
onPositionChange,
@ -57,6 +58,7 @@ const Dashboard = ({
isEditable={true}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
synchronizer={synchronizer}
onDeleteCell={onDeleteCell}
onPositionChange={onPositionChange}
@ -111,6 +113,7 @@ Dashboard.propTypes = {
}).isRequired,
sources: arrayOf(shape({})).isRequired,
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({}).isRequired,
onOpenTemplateManager: func.isRequired,
onSelectTemplate: func.isRequired,

View File

@ -17,6 +17,7 @@ const DashboardHeader = ({
isHidden,
handleChooseTimeRange,
handleChooseAutoRefresh,
onManualRefresh,
handleClickPresentationButton,
onAddCell,
onEditDashboard,
@ -76,6 +77,7 @@ const DashboardHeader = ({
: null}
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
onManualRefresh={onManualRefresh}
selected={autoRefresh}
iconName="refresh"
/>
@ -118,6 +120,7 @@ DashboardHeader.propTypes = {
isHidden: bool.isRequired,
handleChooseTimeRange: func.isRequired,
handleChooseAutoRefresh: func.isRequired,
onManualRefresh: func.isRequired,
handleClickPresentationButton: func.isRequired,
onAddCell: func,
onEditDashboard: func,

View File

@ -11,6 +11,7 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
import Dashboard from 'src/dashboards/components/Dashboard'
import TemplateVariableManager from 'src/dashboards/components/template_variables/Manager'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
@ -171,7 +172,7 @@ class DashboardPage extends Component {
}
synchronizer = dygraph => {
const dygraphs = [...this.state.dygraphs, dygraph]
const dygraphs = [...this.state.dygraphs, dygraph].filter(d => d.graphDiv)
const {dashboards, params: {dashboardID}} = this.props
const dashboard = dashboards.find(
@ -189,6 +190,7 @@ class DashboardPage extends Component {
range: false,
})
}
this.setState({dygraphs})
}
@ -213,6 +215,8 @@ class DashboardPage extends Component {
dashboard,
dashboards,
autoRefresh,
manualRefresh,
onManualRefresh,
cellQueryStatus,
dashboardActions,
inPresentationMode,
@ -324,6 +328,7 @@ class DashboardPage extends Component {
buttonText={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
handleChooseAutoRefresh={handleChooseAutoRefresh}
onManualRefresh={onManualRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
onToggleTempVarControls={this.handleToggleTempVarControls}
handleClickPresentationButton={handleClickPresentationButton}
@ -345,6 +350,7 @@ class DashboardPage extends Component {
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
onZoom={this.handleZoomedTimeRange}
onAddCell={this.handleAddCell}
synchronizer={this.synchronizer}
@ -429,6 +435,8 @@ DashboardPage.propTypes = {
status: shape(),
}).isRequired,
errorThrown: func,
manualRefresh: number.isRequired,
onManualRefresh: func.isRequired,
}
const mapStateToProps = (state, {params: {dashboardID}}) => {
@ -474,4 +482,6 @@ const mapDispatchToProps = dispatch => ({
errorThrown: bindActionCreators(errorThrownAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage)
export default connect(mapStateToProps, mapDispatchToProps)(
ManualRefresh(DashboardPage)
)

View File

@ -12,6 +12,7 @@ const VisView = ({
templates,
autoRefresh,
heightPixels,
manualRefresh,
editQueryStatus,
resizerBottomHeight,
}) => {
@ -41,6 +42,7 @@ const VisView = ({
templates={templates}
cellHeight={heightPixels}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
/>
)
@ -58,6 +60,7 @@ VisView.propTypes = {
autoRefresh: number.isRequired,
heightPixels: number,
editQueryStatus: func.isRequired,
manualRefresh: number,
activeQueryIndex: number,
resizerBottomHeight: number,
}

View File

@ -55,9 +55,9 @@ class Visualization extends Component {
autoRefresh,
heightPixels,
queryConfigs,
manualRefresh,
editQueryStatus,
activeQueryIndex,
isInDataExplorer,
resizerBottomHeight,
errorThrown,
} = this.props
@ -99,12 +99,12 @@ class Visualization extends Component {
axes={axes}
query={query}
queries={queries}
templates={templates}
cellType={cellType}
templates={templates}
autoRefresh={autoRefresh}
heightPixels={heightPixels}
manualRefresh={manualRefresh}
editQueryStatus={editQueryStatus}
isInDataExplorer={isInDataExplorer}
resizerBottomHeight={resizerBottomHeight}
/>
</div>
@ -123,7 +123,7 @@ Visualization.defaultProps = {
cellType: '',
}
const {arrayOf, bool, func, number, shape, string} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
Visualization.contextTypes = {
source: shape({
@ -138,7 +138,6 @@ Visualization.propTypes = {
cellType: string,
autoRefresh: number.isRequired,
templates: arrayOf(shape()),
isInDataExplorer: bool,
timeRange: shape({
upper: string,
lower: string,
@ -156,6 +155,7 @@ Visualization.propTypes = {
}),
resizerBottomHeight: number,
errorThrown: func.isRequired,
manualRefresh: number,
}
export default Visualization

View File

@ -12,6 +12,7 @@ import WriteDataForm from 'src/data_explorer/components/WriteDataForm'
import Header from '../containers/Header'
import ResizeContainer from 'shared/components/ResizeContainer'
import OverlayTechnologies from 'shared/components/OverlayTechnologies'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {VIS_VIEWS} from 'shared/constants'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from '../constants'
@ -67,17 +68,22 @@ class DataExplorer extends Component {
this.setState({showWriteForm: true})
}
handleChooseTimeRange = bounds => {
this.props.setTimeRange(bounds)
}
render() {
const {
autoRefresh,
errorThrownAction,
handleChooseAutoRefresh,
timeRange,
setTimeRange,
queryConfigs,
queryConfigActions,
source,
timeRange,
autoRefresh,
queryConfigs,
manualRefresh,
onManualRefresh,
errorThrownAction,
writeLineProtocol,
queryConfigActions,
handleChooseAutoRefresh,
} = this.props
const {showWriteForm} = this.state
@ -99,8 +105,10 @@ class DataExplorer extends Component {
<Header
timeRange={timeRange}
autoRefresh={autoRefresh}
actions={{handleChooseAutoRefresh, setTimeRange}}
showWriteForm={this.handleOpenWriteData}
onChooseTimeRange={this.handleChooseTimeRange}
onChooseAutoRefresh={handleChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
<ResizeContainer
containerClass="page-contents"
@ -116,14 +124,14 @@ class DataExplorer extends Component {
activeQuery={this.getActiveQuery()}
/>
<Visualization
isInDataExplorer={true}
autoRefresh={autoRefresh}
timeRange={timeRange}
queryConfigs={queryConfigs}
errorThrown={errorThrownAction}
activeQueryIndex={0}
editQueryStatus={queryConfigActions.editQueryStatus}
views={VIS_VIEWS}
activeQueryIndex={0}
timeRange={timeRange}
autoRefresh={autoRefresh}
queryConfigs={queryConfigs}
manualRefresh={manualRefresh}
errorThrown={errorThrownAction}
editQueryStatus={queryConfigActions.editQueryStatus}
/>
</ResizeContainer>
</div>
@ -163,6 +171,8 @@ DataExplorer.propTypes = {
}).isRequired,
writeLineProtocol: func.isRequired,
errorThrownAction: func.isRequired,
onManualRefresh: func.isRequired,
manualRefresh: number.isRequired,
}
DataExplorer.childContextTypes = {
@ -208,5 +218,5 @@ const mapDispatchToProps = dispatch => {
}
export default connect(mapStateToProps, mapDispatchToProps)(
withRouter(DataExplorer)
withRouter(ManualRefresh(DataExplorer))
)

View File

@ -8,33 +8,14 @@ import GraphTips from 'shared/components/GraphTips'
const {func, number, shape, string} = PropTypes
const Header = React.createClass({
propTypes: {
actions: shape({
handleChooseAutoRefresh: func.isRequired,
setTimeRange: func.isRequired,
}),
autoRefresh: number.isRequired,
showWriteForm: func.isRequired,
timeRange: shape({
lower: string,
upper: string,
}).isRequired,
},
handleChooseTimeRange(bounds) {
this.props.actions.setTimeRange(bounds)
},
render() {
const {
autoRefresh,
actions: {handleChooseAutoRefresh},
showWriteForm,
const Header = ({
timeRange,
} = this.props
return (
autoRefresh,
showWriteForm,
onManualRefresh,
onChooseTimeRange,
onChooseAutoRefresh,
}) =>
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
@ -52,20 +33,30 @@ const Header = React.createClass({
Write Data
</div>
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
selected={autoRefresh}
iconName="refresh"
selected={autoRefresh}
onChoose={onChooseAutoRefresh}
onManualRefresh={onManualRefresh}
/>
<TimeRangeDropdown
onChooseTimeRange={this.handleChooseTimeRange}
selected={timeRange}
page="DataExplorer"
onChooseTimeRange={onChooseTimeRange}
/>
</div>
</div>
</div>
)
},
})
Header.propTypes = {
onChooseAutoRefresh: func.isRequired,
onChooseTimeRange: func.isRequired,
onManualRefresh: func.isRequired,
autoRefresh: number.isRequired,
showWriteForm: func.isRequired,
timeRange: shape({
lower: string,
upper: string,
}).isRequired,
}
export default withRouter(Header)

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import {Link} from 'react-router'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
@ -10,6 +10,7 @@ import Dygraph from 'src/external/dygraph'
import LayoutRenderer from 'shared/components/LayoutRenderer'
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import timeRanges from 'hson!shared/data/timeRanges.hson'
import {
@ -23,39 +24,16 @@ import {fetchLayouts} from 'shared/apis'
import {setAutoRefresh} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
const {shape, string, bool, func, number} = PropTypes
export const HostPage = React.createClass({
propTypes: {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
telegraf: string.isRequired,
id: string.isRequired,
}),
params: shape({
hostID: string.isRequired,
}).isRequired,
location: shape({
query: shape({
app: string,
}),
}),
autoRefresh: number.isRequired,
handleChooseAutoRefresh: func.isRequired,
inPresentationMode: bool,
handleClickPresentationButton: func,
},
getInitialState() {
return {
class HostPage extends Component {
constructor(props) {
super(props)
this.state = {
layouts: [],
hosts: [],
timeRange: timeRanges.find(tr => tr.lower === 'now() - 1h'),
dygraphs: [],
}
},
}
async componentDidMount() {
const {source, params, location} = this.props
@ -96,19 +74,19 @@ export const HostPage = React.createClass({
}
this.setState({layouts: filteredLayouts, hosts: filteredHosts}) // eslint-disable-line react/no-did-mount-set-state
},
}
handleChooseTimeRange({lower, upper}) {
handleChooseTimeRange = ({lower, upper}) => {
if (upper) {
this.setState({timeRange: {lower, upper}})
} else {
const timeRange = timeRanges.find(range => range.lower === lower)
this.setState({timeRange})
}
},
}
synchronizer(dygraph) {
const dygraphs = [...this.state.dygraphs, dygraph]
synchronizer = dygraph => {
const dygraphs = [...this.state.dygraphs, dygraph].filter(d => d.graphDiv)
const numGraphs = this.state.layouts.reduce((acc, {cells}) => {
return acc + cells.length
}, 0)
@ -121,11 +99,11 @@ export const HostPage = React.createClass({
})
}
this.setState({dygraphs})
},
}
renderLayouts(layouts) {
renderLayouts = layouts => {
const {timeRange} = this.state
const {source, autoRefresh} = this.props
const {source, autoRefresh, manualRefresh} = this.props
const autoflowLayouts = layouts.filter(layout => !!layout.autoflow)
@ -173,27 +151,29 @@ export const HostPage = React.createClass({
return (
<LayoutRenderer
timeRange={timeRange}
cells={layoutCells}
autoRefresh={autoRefresh}
source={source}
host={this.props.params.hostID}
isEditable={false}
cells={layoutCells}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
host={this.props.params.hostID}
synchronizer={this.synchronizer}
/>
)
},
}
render() {
const {
params: {hostID},
location: {query: {app}},
source: {id},
autoRefresh,
handleChooseAutoRefresh,
inPresentationMode,
handleClickPresentationButton,
source,
autoRefresh,
source: {id},
onManualRefresh,
params: {hostID},
inPresentationMode,
handleChooseAutoRefresh,
location: {query: {app}},
handleClickPresentationButton,
} = this.props
const {layouts, timeRange, hosts} = this.state
const appParam = app ? `?app=${app}` : ''
@ -201,14 +181,15 @@ export const HostPage = React.createClass({
return (
<div className="page">
<DashboardHeader
source={source}
buttonText={hostID}
autoRefresh={autoRefresh}
timeRange={timeRange}
autoRefresh={autoRefresh}
isHidden={inPresentationMode}
onManualRefresh={onManualRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
handleChooseAutoRefresh={handleChooseAutoRefresh}
handleClickPresentationButton={handleClickPresentationButton}
source={source}
>
{Object.keys(hosts).map((host, i) => {
return (
@ -232,8 +213,34 @@ export const HostPage = React.createClass({
</FancyScrollbar>
</div>
)
},
})
}
}
const {shape, string, bool, func, number} = PropTypes
HostPage.propTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
telegraf: string.isRequired,
id: string.isRequired,
}),
params: shape({
hostID: string.isRequired,
}).isRequired,
location: shape({
query: shape({
app: string,
}),
}),
inPresentationMode: bool,
autoRefresh: number.isRequired,
manualRefresh: number.isRequired,
onManualRefresh: func.isRequired,
handleChooseAutoRefresh: func.isRequired,
handleClickPresentationButton: func,
}
const mapStateToProps = ({
app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}},
@ -247,4 +254,6 @@ const mapDispatchToProps = dispatch => ({
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(HostPage)
export default connect(mapStateToProps, mapDispatchToProps)(
ManualRefresh(HostPage)
)

View File

@ -1,66 +1,19 @@
import React, {PropTypes} from 'react'
import React, {PropTypes, Component} from 'react'
import _ from 'lodash'
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
const {
array,
arrayOf,
bool,
element,
func,
number,
oneOfType,
shape,
string,
} = PropTypes
const AutoRefresh = ComposedComponent => {
const wrapper = React.createClass({
propTypes: {
children: element,
autoRefresh: number.isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
rp: string,
influxql: string,
}),
values: arrayOf(
shape({
type: string.isRequired,
value: string.isRequired,
selected: bool,
})
).isRequired,
})
),
queries: arrayOf(
shape({
host: oneOfType([string, arrayOf(string)]),
text: string,
}).isRequired
).isRequired,
axes: shape({
bounds: shape({
y: array,
y2: array,
}),
}),
editQueryStatus: func,
grabDataForDownload: func,
},
getInitialState() {
return {
class wrapper extends Component {
constructor() {
super()
this.state = {
lastQuerySuccessful: false,
timeSeries: [],
resolution: null,
}
},
}
componentDidMount() {
const {queries, templates, autoRefresh} = this.props
@ -71,7 +24,7 @@ const AutoRefresh = ComposedComponent => {
autoRefresh
)
}
},
}
componentWillReceiveProps(nextProps) {
const queriesDidUpdate = this.queryDifference(
@ -100,18 +53,18 @@ const AutoRefresh = ComposedComponent => {
)
}
}
},
}
queryDifference(left, right) {
queryDifference = (left, right) => {
const leftStrs = left.map(q => `${q.host}${q.text}`)
const rightStrs = right.map(q => `${q.host}${q.text}`)
return _.difference(
_.union(leftStrs, rightStrs),
_.intersection(leftStrs, rightStrs)
)
},
}
executeQueries(queries, templates = []) {
executeQueries = async (queries, templates = []) => {
const {editQueryStatus, grabDataForDownload} = this.props
const {resolution} = this.state
@ -148,28 +101,33 @@ const AutoRefresh = ComposedComponent => {
)
})
Promise.all(timeSeriesPromises).then(timeSeries => {
try {
const timeSeries = await Promise.all(timeSeriesPromises)
const newSeries = timeSeries.map(response => ({response}))
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
const lastQuerySuccessful = this._resultsForQuery(newSeries)
this.setState({
timeSeries: newSeries,
lastQuerySuccessful,
isFetching: false,
})
if (grabDataForDownload) {
grabDataForDownload(timeSeries)
}
})
},
} catch (err) {
console.error(err)
}
}
componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = false
},
}
setResolution(resolution) {
setResolution = resolution => {
this.setState({resolution})
},
}
render() {
const {timeSeries} = this.state
@ -179,7 +137,7 @@ const AutoRefresh = ComposedComponent => {
}
if (
this._noResultsForQuery(timeSeries) ||
!this._resultsForQuery(timeSeries) ||
!this.state.lastQuerySuccessful
) {
return this.renderNoResults()
@ -192,13 +150,13 @@ const AutoRefresh = ComposedComponent => {
setResolution={this.setResolution}
/>
)
},
}
/**
* Graphs can potentially show mulitple kinds of spinners based on whether
* a graph is being fetched for the first time, or is being refreshed.
*/
renderFetching(data) {
renderFetching = data => {
const isFirstFetch = !Object.keys(this.state.timeSeries).length
return (
<ComposedComponent
@ -209,30 +167,76 @@ const AutoRefresh = ComposedComponent => {
isRefreshing={!isFirstFetch}
/>
)
},
}
renderNoResults() {
renderNoResults = () => {
return (
<div className="graph-empty">
<p data-test="data-explorer-no-results">No Results</p>
</div>
)
},
_noResultsForQuery(data) {
if (!data.length) {
return true
}
return data.every(({response}) => {
return _.get(response, 'results', []).every(result => {
return (
Object.keys(result).filter(k => k !== 'statement_id').length === 0
_resultsForQuery = data =>
data.length
? data.every(({response}) =>
_.get(response, 'results', []).every(
result =>
Object.keys(result).filter(k => k !== 'statement_id').length !==
0
)
)
: false
}
const {
array,
arrayOf,
bool,
element,
func,
number,
oneOfType,
shape,
string,
} = PropTypes
wrapper.propTypes = {
children: element,
autoRefresh: number.isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
rp: string,
influxql: string,
}),
values: arrayOf(
shape({
type: string.isRequired,
value: string.isRequired,
selected: bool,
})
).isRequired,
})
},
})
),
queries: arrayOf(
shape({
host: oneOfType([string, arrayOf(string)]),
text: string,
}).isRequired
).isRequired,
axes: shape({
bounds: shape({
y: array,
y2: array,
}),
}),
editQueryStatus: func,
grabDataForDownload: func,
}
return wrapper
}

View File

@ -28,11 +28,16 @@ class AutoRefreshDropdown extends Component {
toggleMenu = () => this.setState({isOpen: !this.state.isOpen})
render() {
const {selected} = this.props
const {selected, onManualRefresh} = this.props
const {isOpen} = this.state
const {milliseconds, inputValue} = this.findAutoRefreshItem(selected)
return (
<div
className={classnames('autorefresh-dropdown', {
paused: +milliseconds === 0,
})}
>
<div className={classnames('dropdown dropdown-160', {open: isOpen})}>
<div
className="btn btn-sm btn-default dropdown-toggle"
@ -60,6 +65,15 @@ class AutoRefreshDropdown extends Component {
)}
</ul>
</div>
{+milliseconds === 0
? <div
className="btn btn-sm btn-default btn-square"
onClick={onManualRefresh}
>
<span className="icon refresh" />
</div>
: null}
</div>
)
}
}
@ -69,6 +83,7 @@ const {number, func} = PropTypes
AutoRefreshDropdown.propTypes = {
selected: number.isRequired,
onChoose: func.isRequired,
onManualRefresh: func,
}
export default OnClickOutside(AutoRefreshDropdown)

View File

@ -52,6 +52,7 @@ const Layout = (
isEditable,
onEditCell,
autoRefresh,
manualRefresh,
onDeleteCell,
synchronizer,
resizeCoords,
@ -82,6 +83,7 @@ const Layout = (
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
synchronizer={synchronizer}
grabDataForDownload={grabDataForDownload}
resizeCoords={resizeCoords}
@ -102,6 +104,7 @@ Layout.contextTypes = {
const propTypes = {
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({
lower: string.isRequired,
}),

View File

@ -75,6 +75,7 @@ class LayoutRenderer extends Component {
isEditable,
onEditCell,
autoRefresh,
manualRefresh,
onDeleteCell,
synchronizer,
onCancelEditCell,
@ -114,6 +115,7 @@ class LayoutRenderer extends Component {
onEditCell={onEditCell}
resizeCoords={resizeCoords}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
onDeleteCell={onDeleteCell}
synchronizer={synchronizer}
onCancelEditCell={onCancelEditCell}
@ -131,6 +133,7 @@ const {arrayOf, bool, func, number, shape, string} = PropTypes
LayoutRenderer.propTypes = {
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({
lower: string.isRequired,
}),

View File

@ -0,0 +1,29 @@
import React, {Component} from 'react'
const ManualRefresh = WrappedComponent =>
class extends Component {
constructor(props) {
super(props)
this.state = {
manualRefresh: Date.now(),
}
}
handleManualRefresh = () => {
this.setState({
manualRefresh: Date.now(),
})
}
render() {
return (
<WrappedComponent
{...this.props}
manualRefresh={this.state.manualRefresh}
onManualRefresh={this.handleManualRefresh}
/>
)
}
}
export default ManualRefresh

View File

@ -18,6 +18,7 @@ const RefreshingGraph = ({
timeRange,
cellHeight,
autoRefresh,
manualRefresh, // when changed, re-mounts the component
synchronizer,
resizeCoords,
editQueryStatus,
@ -36,6 +37,7 @@ const RefreshingGraph = ({
if (type === 'single-stat') {
return (
<RefreshingSingleStat
key={manualRefresh}
queries={[queries[0]]}
templates={templates}
autoRefresh={autoRefresh}
@ -54,7 +56,7 @@ const RefreshingGraph = ({
axes={axes}
onZoom={onZoom}
queries={queries}
grabDataForDownload={grabDataForDownload}
key={manualRefresh}
templates={templates}
timeRange={timeRange}
autoRefresh={autoRefresh}
@ -63,6 +65,7 @@ const RefreshingGraph = ({
resizeCoords={resizeCoords}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
grabDataForDownload={grabDataForDownload}
showSingleStat={type === 'line-plus-single-stat'}
/>
)
@ -75,6 +78,7 @@ RefreshingGraph.propTypes = {
lower: string.isRequired,
}),
autoRefresh: number.isRequired,
manualRefresh: number,
templates: arrayOf(shape()),
synchronizer: func,
type: string.isRequired,
@ -87,4 +91,8 @@ RefreshingGraph.propTypes = {
grabDataForDownload: func,
}
RefreshingGraph.defaultProps = {
manualRefresh: 0,
}
export default RefreshingGraph

View File

@ -318,3 +318,19 @@ $tick-script-overlay-margin: 30px;
> a {display: block;}
}
/*
Auto Refresh Dropdown
-----------------------------------------------------------------------------
*/
.autorefresh-dropdown {
display: flex;
flex-wrap: nowrap;
&.paused .dropdown {
margin-right: 4px;
}
&.paused .dropdown > .btn.dropdown-toggle {
width: 126px;
}
}