diff --git a/CHANGELOG.md b/CHANGELOG.md index b176ab2f1..0f42054eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 1cf622cc7..41b31710d 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -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, diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index ff670787f..6566a713d 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -17,6 +17,7 @@ const DashboardHeader = ({ isHidden, handleChooseTimeRange, handleChooseAutoRefresh, + onManualRefresh, handleClickPresentationButton, onAddCell, onEditDashboard, @@ -76,6 +77,7 @@ const DashboardHeader = ({ : null} @@ -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, diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index fa518407b..4e48d5c6d 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -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) +) diff --git a/ui/src/data_explorer/components/VisView.js b/ui/src/data_explorer/components/VisView.js index 59865c021..0790774a2 100644 --- a/ui/src/data_explorer/components/VisView.js +++ b/ui/src/data_explorer/components/VisView.js @@ -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, } diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index 5d106e301..308084e84 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -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} /> @@ -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 diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index ab04cf0e3..3aa12dea2 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -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 {
@@ -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)) ) diff --git a/ui/src/data_explorer/containers/Header.js b/ui/src/data_explorer/containers/Header.js index 463907cea..df57ffb8f 100644 --- a/ui/src/data_explorer/containers/Header.js +++ b/ui/src/data_explorer/containers/Header.js @@ -8,64 +8,55 @@ 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, - timeRange, - } = this.props - - return ( -
-
-
-

Data Explorer

-
-
- - -
- - Write Data -
- - -
-
+const Header = ({ + timeRange, + autoRefresh, + showWriteForm, + onManualRefresh, + onChooseTimeRange, + onChooseAutoRefresh, +}) => +
+
+
+

Data Explorer

- ) - }, -}) +
+ + +
+ + Write Data +
+ + +
+
+
+ +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) diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 74dd2014b..e8276427a 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -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 ( ) - }, + } 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 (
{Object.keys(hosts).map((host, i) => { return ( @@ -232,8 +213,34 @@ export const HostPage = React.createClass({
) - }, -}) + } +} + +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) +) diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index 081364481..3bbf5f206 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -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 ( { isRefreshing={!isFirstFetch} /> ) - }, + } - renderNoResults() { + renderNoResults = () => { return (

No Results

) - }, + } - _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 } diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js index be71c7461..b7975d62b 100644 --- a/ui/src/shared/components/AutoRefreshDropdown.js +++ b/ui/src/shared/components/AutoRefreshDropdown.js @@ -28,37 +28,51 @@ 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 ( -
-
- 0 ? 'refresh' : 'pause' +
+
+
+ 0 ? 'refresh' : 'pause' + )} + /> + + {inputValue} + + +
+
    +
  • AutoRefresh Interval
  • + {autoRefreshItems.map(item => +
  • + + {item.menuOption} + +
  • )} - /> - - {inputValue} - - +
- + {+milliseconds === 0 + ?
+ +
+ : null}
) } @@ -69,6 +83,7 @@ const {number, func} = PropTypes AutoRefreshDropdown.propTypes = { selected: number.isRequired, onChoose: func.isRequired, + onManualRefresh: func, } export default OnClickOutside(AutoRefreshDropdown) diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index 42c086a33..ea69fe6c6 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -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, }), diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 7ab90c4dd..42bcc2230 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -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, }), diff --git a/ui/src/shared/components/ManualRefresh.js b/ui/src/shared/components/ManualRefresh.js new file mode 100644 index 000000000..d3edcd101 --- /dev/null +++ b/ui/src/shared/components/ManualRefresh.js @@ -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 ( + + ) + } + } + +export default ManualRefresh diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index dbc983d34..2320f1b65 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -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 ( ) @@ -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 diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss index 13cb3776c..f5307a51f 100644 --- a/ui/src/style/unsorted.scss +++ b/ui/src/style/unsorted.scss @@ -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; + } +}