From 028dd5f8223176e8b2ebe4882f33d2e4bb3a1c6f Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 24 Feb 2017 17:07:14 -0800 Subject: [PATCH 01/47] Add appConfig reducer for autoRefresh --- ui/src/shared/reducers/appConfig.js | 16 ++++++++++++++++ ui/src/shared/reducers/index.js | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 ui/src/shared/reducers/appConfig.js diff --git a/ui/src/shared/reducers/appConfig.js b/ui/src/shared/reducers/appConfig.js new file mode 100644 index 000000000..05c200604 --- /dev/null +++ b/ui/src/shared/reducers/appConfig.js @@ -0,0 +1,16 @@ +const initialState = { + autoRefreshMs: 15000, +}; + +export default function appConfig(state = initialState, action) { + switch (action.type) { + case 'SET_AUTOREFRESH': { + return { + ...state, + autoRefreshMs: action.payload, + } + } + } + + return state +} diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index 47ed0c62f..233c4ff97 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -1,4 +1,5 @@ import appUI from './ui'; +import appConfig from './appConfig'; import me from './me'; import auth from './auth'; import notifications from './notifications'; @@ -6,6 +7,7 @@ import sources from './sources'; export { appUI, + appConfig, me, auth, notifications, From 5ac7a678cbf45e0961cb277d1381a7d6a51fb917 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 24 Feb 2017 18:10:27 -0800 Subject: [PATCH 02/47] WIP: partially implement autoRefresh dropdown and app config --- .../dashboards/components/DashboardHeader.js | 10 +++ ui/src/hosts/containers/HostPage.js | 11 +++- ui/src/index.js | 1 + ui/src/shared/actions/config.js | 16 +++++ .../shared/components/AutoRefreshDropdown.js | 64 +++++++++++++++++++ ui/src/shared/components/Dropdown.js | 22 +++++-- ui/src/shared/components/TimeRangeDropdown.js | 4 +- ui/src/shared/data/autoRefreshValues.hson | 7 ++ ui/src/shared/reducers/appConfig.js | 2 + 9 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 ui/src/shared/actions/config.js create mode 100644 ui/src/shared/components/AutoRefreshDropdown.js create mode 100644 ui/src/shared/data/autoRefreshValues.hson diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index be6fd69a0..7fb7757ef 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -2,6 +2,7 @@ import React, {PropTypes} from 'react' import ReactTooltip from 'react-tooltip' import {Link} from 'react-router'; +import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' const DashboardHeader = ({ @@ -10,8 +11,10 @@ const DashboardHeader = ({ dashboard, headerText, timeRange, + selectedAutoRefresh, isHidden, handleChooseTimeRange, + handleChooseAutoRefresh, handleClickPresentationButton, sourceID, }) => isHidden ? null : ( @@ -45,6 +48,11 @@ const DashboardHeader = ({ Graph Tips +
@@ -69,8 +77,10 @@ DashboardHeader.propTypes = { dashboard: shape({}), headerText: string, timeRange: shape({}).isRequired, + selectedAutoRefresh: shape({}).isRequired, isHidden: bool.isRequired, handleChooseTimeRange: func.isRequired, + handleChooseAutoRefresh: func.isRequired, handleClickPresentationButton: func.isRequired, } diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 38ff3ae16..282332360 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -3,6 +3,7 @@ import {Link} from 'react-router' import {connect} from 'react-redux' import _ from 'lodash' import classnames from 'classnames'; +import {loadLocalStorage} from 'src/localStorage' import LayoutRenderer from 'shared/components/LayoutRenderer'; import DashboardHeader from 'src/dashboards/components/DashboardHeader'; @@ -86,10 +87,13 @@ export const HostPage = React.createClass({ this.setState({timeRange}); }, + // TODO implement + handleChooseAutoRefresh({}) + renderLayouts(layouts) { - const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source} = this.props; + const {source, autoRefreshMs} = this.props; + console.log(autoRefreshMs) const autoflowLayouts = layouts.filter((layout) => !!layout.autoflow); @@ -156,6 +160,7 @@ export const HostPage = React.createClass({ timeRange={timeRange} isHidden={inPresentationMode} handleChooseTimeRange={this.handleChooseTimeRange} + handleChooseAutoRefresh={this.handleChooseAutoRefresh} handleClickPresentationButton={handleClickPresentationButton} > {Object.keys(hosts).map((host, i) => { @@ -183,9 +188,11 @@ export const HostPage = React.createClass({ const mapStateToProps = (state) => ({ inPresentationMode: state.appUI.presentationMode, + autoRefreshMs: state.appConfig.autoRefreshMs, }) const mapDispatchToProps = (dispatch) => ({ + handleChooseAutoRefresh: dispatch(setAutoRefresh()), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/index.js b/ui/src/index.js index c2a8f4cd3..1bf13489a 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -24,6 +24,7 @@ import {loadLocalStorage} from './localStorage'; import 'src/style/chronograf.scss'; +// TODO ensure appConfig data in localstorage const store = configureStore(loadLocalStorage()); const rootNode = document.getElementById('react-root'); diff --git a/ui/src/shared/actions/config.js b/ui/src/shared/actions/config.js new file mode 100644 index 000000000..97653b5e2 --- /dev/null +++ b/ui/src/shared/actions/config.js @@ -0,0 +1,16 @@ +// TODO implement both fns below + +export function setAutoRefresh(seconds) { + // write to local storage + // on callback & confirmation, return state + return { + type: 'SET_AUTOREFRESH', + payload: { + seconds, + }, + }; +} + +// getAutoRefresh (used by app when loads) + // load from localstorage + // return state diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js new file mode 100644 index 000000000..5448410c6 --- /dev/null +++ b/ui/src/shared/components/AutoRefreshDropdown.js @@ -0,0 +1,64 @@ +import React from 'react'; +import classnames from 'classnames'; +import OnClickOutside from 'shared/components/OnClickOutside'; + +import autoRefreshValues from 'hson!../data/autoRefreshValues.hson'; + +const AutoRefreshDropdown = React.createClass({ + autobind: false, + + propTypes: { + selected: React.PropTypes.string.isRequired, + onChoose: React.PropTypes.func.isRequired, + }, + + getInitialState() { + return { + isOpen: false, + }; + }, + + handleClickOutside() { + this.setState({isOpen: false}); + }, + + handleSelection(params) { + const {seconds, menuOption} = params; + this.props.onChoose({seconds}); + this.setState({isOpen: false}); + }, + + toggleMenu() { + this.setState({isOpen: !this.state.isOpen}); + }, + + render() { + const self = this; + const {selected} = self.props; + const {isOpen} = self.state; + + return ( +
+
self.toggleMenu()}> + + {selected} + +
+ +
+ ); + }, +}); + +export default OnClickOutside(AutoRefreshDropdown); diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 178b842d5..2c749449a 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -1,14 +1,23 @@ import React, {PropTypes} from 'react'; +import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; +const { + arrayOf, + shape, + string, + func, +} = PropTypes + const Dropdown = React.createClass({ propTypes: { - items: PropTypes.arrayOf(PropTypes.shape({ - text: PropTypes.string.isRequired, + items: arrayOf(shape({ + text: string.isRequired, })).isRequired, - onChoose: PropTypes.func.isRequired, - selected: PropTypes.string.isRequired, - className: PropTypes.string, + onChoose: func.isRequired, + selected: string.isRequired, + iconName: string, + className: string, }, getInitialState() { return { @@ -39,11 +48,12 @@ const Dropdown = React.createClass({ }, render() { const self = this; - const {items, selected, className, actions} = self.props; + const {items, selected, className, iconName, actions} = self.props; return (
+ {iconName ? : null} {selected}
diff --git a/ui/src/shared/components/TimeRangeDropdown.js b/ui/src/shared/components/TimeRangeDropdown.js index dbdf5155c..076226896 100644 --- a/ui/src/shared/components/TimeRangeDropdown.js +++ b/ui/src/shared/components/TimeRangeDropdown.js @@ -1,5 +1,5 @@ import React from 'react'; -import cN from 'classnames'; +import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; import timeRanges from 'hson!../data/timeRanges.hson'; @@ -48,7 +48,7 @@ const TimeRangeDropdown = React.createClass({ {selected}
-
    +
    • Time Range
    • {timeRanges.map((item) => { return ( diff --git a/ui/src/shared/data/autoRefreshValues.hson b/ui/src/shared/data/autoRefreshValues.hson new file mode 100644 index 000000000..391bebaa7 --- /dev/null +++ b/ui/src/shared/data/autoRefreshValues.hson @@ -0,0 +1,7 @@ +[ + {ms: 5000, inputValue: 'Every 5 seconds', menuOption: 'Every 5 seconds'}, + {ms: 10000, inputValue: 'Every 10 seconds', menuOption: 'Every 15 seconds'}, + {ms: 15000, inputValue: 'Every 15 seconds', menuOption: 'Every 15 seconds'}, + {ms: 30000, inputValue: 'Every 30 seconds', menuOption: 'Every 30 seconds'}, + {ms: 60000, inputValue: 'Every 60 seconds', menuOption: 'Every 60 seconds'} +] diff --git a/ui/src/shared/reducers/appConfig.js b/ui/src/shared/reducers/appConfig.js index 05c200604..e9093381a 100644 --- a/ui/src/shared/reducers/appConfig.js +++ b/ui/src/shared/reducers/appConfig.js @@ -10,6 +10,8 @@ export default function appConfig(state = initialState, action) { autoRefreshMs: action.payload, } } + + // TODO implement 'GET_AUTOREFRESH' } return state From bbe9c511044d314d3107c2f53a05f82fbcd89ec8 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Mon, 27 Feb 2017 18:34:58 -0800 Subject: [PATCH 03/47] Implement chooseable Host Page autoRefresh persisted to localStorage, WIP for Dashboards and Kubernetes --- ui/src/dashboards/components/Dashboard.js | 10 ++++--- .../dashboards/components/DashboardHeader.js | 11 +++---- ui/src/dashboards/containers/DashboardPage.js | 8 ++++- .../data_explorer/components/Visualization.js | 6 ++-- ui/src/hosts/containers/HostPage.js | 24 ++++++++------- ui/src/localStorage.js | 3 +- ui/src/shared/actions/appConfig.js | 6 ++++ ui/src/shared/actions/config.js | 16 ---------- ui/src/shared/components/AutoRefresh.js | 25 +++++++++++----- .../shared/components/AutoRefreshDropdown.js | 30 ++++++++++++------- ui/src/shared/components/LayoutRenderer.js | 8 ++--- ui/src/shared/constants/index.js | 2 ++ ui/src/shared/data/autoRefreshValues.hson | 7 ----- ui/src/shared/data/autoRefreshes.hson | 7 +++++ ui/src/shared/reducers/appConfig.js | 16 +++++----- 15 files changed, 99 insertions(+), 80 deletions(-) create mode 100644 ui/src/shared/actions/appConfig.js delete mode 100644 ui/src/shared/actions/config.js delete mode 100644 ui/src/shared/data/autoRefreshValues.hson create mode 100644 ui/src/shared/data/autoRefreshes.hson diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 46eb173b0..3ff1c1ca4 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -10,6 +10,7 @@ const Dashboard = ({ inPresentationMode, onPositionChange, source, + autoRefresh, timeRange, }) => { if (dashboard.id === 0) { @@ -20,14 +21,13 @@ const Dashboard = ({
      {isEditMode ? : null} - {Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)} + {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange)}
      ) } -Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => { - const autoRefreshMs = 15000 +Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange) => { const cells = dashboard.cells.map((cell, i) => { i = `${i}` const dashboardCell = {...cell, i} @@ -42,7 +42,7 @@ Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => @@ -54,6 +54,7 @@ const { func, shape, string, + number, } = PropTypes Dashboard.propTypes = { @@ -66,6 +67,7 @@ Dashboard.propTypes = { proxy: string, }).isRequired, }).isRequired, + autoRefresh: number.isRequired, timeRange: shape({}).isRequired, } diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 7fb7757ef..16afa1a2f 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -11,7 +11,7 @@ const DashboardHeader = ({ dashboard, headerText, timeRange, - selectedAutoRefresh, + autoRefresh, isHidden, handleChooseTimeRange, handleChooseAutoRefresh, @@ -48,11 +48,7 @@ const DashboardHeader = ({ Graph Tips
- +
@@ -67,6 +63,7 @@ const { array, string, func, + number, bool, } = PropTypes @@ -77,7 +74,7 @@ DashboardHeader.propTypes = { dashboard: shape({}), headerText: string, timeRange: shape({}).isRequired, - selectedAutoRefresh: shape({}).isRequired, + autoRefresh: number.isRequired, isHidden: bool.isRequired, handleChooseTimeRange: func.isRequired, handleChooseAutoRefresh: func.isRequired, diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index c83c92020..0766fea0d 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -51,6 +51,7 @@ const DashboardPage = React.createClass({ id: number.isRequired, cells: arrayOf(shape({})).isRequired, }).isRequired, + autoRefresh: number.isRequired, timeRange: shape({}).isRequired, inPresentationMode: bool.isRequired, isEditMode: bool.isRequired, @@ -100,6 +101,7 @@ const DashboardPage = React.createClass({ isEditMode, handleClickPresentationButton, source, + autoRefresh, timeRange, } = this.props @@ -110,6 +112,7 @@ const DashboardPage = React.createClass({ {}} /> :
@@ -144,6 +148,7 @@ const DashboardPage = React.createClass({ const mapStateToProps = (state) => { const { appUI, + appConfig, dashboardUI: { dashboards, dashboard, @@ -153,11 +158,12 @@ const mapStateToProps = (state) => { } = state return { - inPresentationMode: appUI.presentationMode, dashboards, dashboard, + autoRefresh: appConfig.autoRefresh, timeRange, isEditMode, + inPresentationMode: appUI.presentationMode, } } diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index b88972a9e..b58355da6 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -15,6 +15,7 @@ const { const Visualization = React.createClass({ propTypes: { + autoRefresh: number.isRequired, timeRange: shape({ upper: string, lower: string, @@ -45,7 +46,7 @@ const Visualization = React.createClass({ }, render() { - const {queryConfigs, timeRange, activeQueryIndex, height, heightPixels} = this.props; + const {queryConfigs, autoRefresh, timeRange, activeQueryIndex, height, heightPixels} = this.props; const {source} = this.context; const proxyLink = source.links.proxy; @@ -57,7 +58,6 @@ const Visualization = React.createClass({ const queries = statements.filter((s) => s.text !== null).map((s) => { return {host: [proxyLink], text: s.text, id: s.id}; }); - const autoRefreshMs = 10000; const isInDataExplorer = true; return ( @@ -77,7 +77,7 @@ const Visualization = React.createClass({ {isGraphInView ? ( diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 282332360..ea427ea1f 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -3,20 +3,23 @@ import {Link} from 'react-router' import {connect} from 'react-redux' import _ from 'lodash' import classnames from 'classnames'; -import {loadLocalStorage} from 'src/localStorage' import LayoutRenderer from 'shared/components/LayoutRenderer'; import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis'; import {fetchLayouts} from 'shared/apis'; + +import {setAutoRefresh} from 'shared/actions/appConfig' import {presentationButtonDispatcher} from 'shared/dispatchers' +import {bindActionCreators} from 'redux' const { shape, string, bool, func, + number, } = PropTypes export const HostPage = React.createClass({ @@ -36,6 +39,8 @@ export const HostPage = React.createClass({ app: string, }), }), + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool, handleClickPresentationButton: func, }, @@ -87,13 +92,9 @@ export const HostPage = React.createClass({ this.setState({timeRange}); }, - // TODO implement - handleChooseAutoRefresh({}) - renderLayouts(layouts) { const {timeRange} = this.state; - const {source, autoRefreshMs} = this.props; - console.log(autoRefreshMs) + const {source, autoRefresh} = this.props; const autoflowLayouts = layouts.filter((layout) => !!layout.autoflow); @@ -141,7 +142,7 @@ export const HostPage = React.createClass({ @@ -149,7 +150,7 @@ export const HostPage = React.createClass({ }, render() { - const {params: {hostID}, location: {query: {app}}, source: {id}, inPresentationMode, handleClickPresentationButton} = this.props + const {params: {hostID}, location: {query: {app}}, source: {id}, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props const {layouts, timeRange, hosts} = this.state const appParam = app ? `?app=${app}` : '' @@ -157,10 +158,11 @@ export const HostPage = React.createClass({
{Object.keys(hosts).map((host, i) => { @@ -188,11 +190,11 @@ export const HostPage = React.createClass({ const mapStateToProps = (state) => ({ inPresentationMode: state.appUI.presentationMode, - autoRefreshMs: state.appConfig.autoRefreshMs, + autoRefresh: state.appConfig.autoRefresh, }) const mapDispatchToProps = (dispatch) => ({ - handleChooseAutoRefresh: dispatch(setAutoRefresh()), + handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 5cd29aa07..7f0856068 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -9,9 +9,10 @@ export const loadLocalStorage = () => { } }; -export const saveToLocalStorage = ({queryConfigs, timeRange, dataExplorer}) => { +export const saveToLocalStorage = ({appConfig, queryConfigs, timeRange, dataExplorer}) => { try { window.localStorage.setItem('state', JSON.stringify({ + appConfig, queryConfigs, timeRange, dataExplorer, diff --git a/ui/src/shared/actions/appConfig.js b/ui/src/shared/actions/appConfig.js new file mode 100644 index 000000000..1fd509e9e --- /dev/null +++ b/ui/src/shared/actions/appConfig.js @@ -0,0 +1,6 @@ +export const setAutoRefresh = (milliseconds) => ({ + type: 'SET_AUTOREFRESH', + payload: { + milliseconds, + }, +}) diff --git a/ui/src/shared/actions/config.js b/ui/src/shared/actions/config.js deleted file mode 100644 index 97653b5e2..000000000 --- a/ui/src/shared/actions/config.js +++ /dev/null @@ -1,16 +0,0 @@ -// TODO implement both fns below - -export function setAutoRefresh(seconds) { - // write to local storage - // on callback & confirmation, return state - return { - type: 'SET_AUTOREFRESH', - payload: { - seconds, - }, - }; -} - -// getAutoRefresh (used by app when loads) - // load from localstorage - // return state diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index fd79cd620..ecf560bef 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -6,25 +6,34 @@ function _fetchTimeSeries(source, db, rp, query) { return proxy({source, db, rp, query}); } +const { + element, + number, + arrayOf, + shape, + oneOfType, + string, +} = PropTypes + export default function AutoRefresh(ComposedComponent) { const wrapper = React.createClass({ displayName: `AutoRefresh_${ComposedComponent.displayName}`, propTypes: { - children: PropTypes.element, - autoRefresh: PropTypes.number, - queries: PropTypes.arrayOf(PropTypes.shape({ - host: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), - text: PropTypes.string, + children: element, + autoRefresh: number.isRequired, + queries: arrayOf(shape({ + host: oneOfType([string, arrayOf(string)]), + text: string, }).isRequired).isRequired, }, getInitialState() { return {timeSeries: []}; }, componentDidMount() { - const {queries} = this.props; + const {queries, autoRefresh} = this.props; this.executeQueries(queries); - if (this.props.autoRefresh) { - this.intervalID = setInterval(() => this.executeQueries(queries), this.props.autoRefresh); + if (autoRefresh) { + this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh); } }, componentWillReceiveProps(nextProps) { diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js index 5448410c6..d8bafe2fd 100644 --- a/ui/src/shared/components/AutoRefreshDropdown.js +++ b/ui/src/shared/components/AutoRefreshDropdown.js @@ -1,15 +1,20 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; -import autoRefreshValues from 'hson!../data/autoRefreshValues.hson'; +import autoRefreshItems from 'hson!../data/autoRefreshes.hson'; + +const { + number, + func, +} = PropTypes const AutoRefreshDropdown = React.createClass({ autobind: false, propTypes: { - selected: React.PropTypes.string.isRequired, - onChoose: React.PropTypes.func.isRequired, + selected: number.isRequired, + onChoose: func.isRequired, }, getInitialState() { @@ -18,13 +23,16 @@ const AutoRefreshDropdown = React.createClass({ }; }, + findAutoRefreshItem(milliseconds) { + return autoRefreshItems.find((values) => values.milliseconds === milliseconds) + }, + handleClickOutside() { this.setState({isOpen: false}); }, - handleSelection(params) { - const {seconds, menuOption} = params; - this.props.onChoose({seconds}); + handleSelection(milliseconds) { + this.props.onChoose(milliseconds); this.setState({isOpen: false}); }, @@ -41,15 +49,15 @@ const AutoRefreshDropdown = React.createClass({
self.toggleMenu()}> - {selected} + {this.findAutoRefreshItem(selected).inputValue}
    -
  • Time Range
  • - {autoRefreshValues.map((item) => { +
  • AutoRefresh Interval
  • + {autoRefreshItems.map((item) => { return (
  • - self.handleSelection(item)}> + self.handleSelection(item.milliseconds)}> {item.menuOption}
  • diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index e1c2df5e7..60eace146 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -18,6 +18,7 @@ const { export const LayoutRenderer = React.createClass({ propTypes: { + autoRefresh: number.isRequired, timeRange: shape({ defaultGroupBy: string.isRequired, queryValue: string.isRequired, @@ -46,7 +47,6 @@ export const LayoutRenderer = React.createClass({ name: string.isRequired, }).isRequired ), - autoRefreshMs: number.isRequired, host: string, source: string, onPositionChange: func, @@ -84,7 +84,7 @@ export const LayoutRenderer = React.createClass({ }, generateVisualizations() { - const {autoRefreshMs, source, cells} = this.props; + const {autoRefresh, source, cells} = this.props; return cells.map((cell) => { const qs = cell.queries.map((q) => { @@ -100,7 +100,7 @@ export const LayoutRenderer = React.createClass({

    {cell.name || `Graph`}

    - +
    ); @@ -117,7 +117,7 @@ export const LayoutRenderer = React.createClass({
    diff --git a/ui/src/shared/constants/index.js b/ui/src/shared/constants/index.js index 6b0d7cb60..f6f5bb7b6 100644 --- a/ui/src/shared/constants/index.js +++ b/ui/src/shared/constants/index.js @@ -470,3 +470,5 @@ export const STROKE_WIDTH = { export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds. export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds. + +export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds diff --git a/ui/src/shared/data/autoRefreshValues.hson b/ui/src/shared/data/autoRefreshValues.hson deleted file mode 100644 index 391bebaa7..000000000 --- a/ui/src/shared/data/autoRefreshValues.hson +++ /dev/null @@ -1,7 +0,0 @@ -[ - {ms: 5000, inputValue: 'Every 5 seconds', menuOption: 'Every 5 seconds'}, - {ms: 10000, inputValue: 'Every 10 seconds', menuOption: 'Every 15 seconds'}, - {ms: 15000, inputValue: 'Every 15 seconds', menuOption: 'Every 15 seconds'}, - {ms: 30000, inputValue: 'Every 30 seconds', menuOption: 'Every 30 seconds'}, - {ms: 60000, inputValue: 'Every 60 seconds', menuOption: 'Every 60 seconds'} -] diff --git a/ui/src/shared/data/autoRefreshes.hson b/ui/src/shared/data/autoRefreshes.hson new file mode 100644 index 000000000..09b4237ff --- /dev/null +++ b/ui/src/shared/data/autoRefreshes.hson @@ -0,0 +1,7 @@ +[ + {milliseconds: 5000, inputValue: 'Every 5 seconds', menuOption: 'Every 5 seconds'}, + {milliseconds: 10000, inputValue: 'Every 10 seconds', menuOption: 'Every 10 seconds'}, + {milliseconds: 15000, inputValue: 'Every 15 seconds', menuOption: 'Every 15 seconds'}, + {milliseconds: 30000, inputValue: 'Every 30 seconds', menuOption: 'Every 30 seconds'}, + {milliseconds: 60000, inputValue: 'Every 60 seconds', menuOption: 'Every 60 seconds'} +] diff --git a/ui/src/shared/reducers/appConfig.js b/ui/src/shared/reducers/appConfig.js index e9093381a..e4264f5ee 100644 --- a/ui/src/shared/reducers/appConfig.js +++ b/ui/src/shared/reducers/appConfig.js @@ -1,18 +1,20 @@ -const initialState = { - autoRefreshMs: 15000, -}; +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' -export default function appConfig(state = initialState, action) { +const initialState = { + autoRefresh: AUTOREFRESH_DEFAULT, +} + +const appConfig = (state = initialState, action) => { switch (action.type) { case 'SET_AUTOREFRESH': { return { ...state, - autoRefreshMs: action.payload, + autoRefresh: action.payload.milliseconds, } } - - // TODO implement 'GET_AUTOREFRESH' } return state } + +export default appConfig From 134f12be234be88c96594eafe69230a8d87ff3ed Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Tue, 28 Feb 2017 17:17:18 -0800 Subject: [PATCH 04/47] Add autoRefresh interval choice to DataExplorer --- .../data_explorer/containers/DataExplorer.js | 30 +++++++++++++------ ui/src/data_explorer/containers/Header.js | 29 +++++++++++++----- ui/src/hosts/containers/HostPage.js | 2 +- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index 68a7c7b2e..d89a8dcd9 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -1,17 +1,18 @@ import React, {PropTypes} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import QueryBuilder from '../components/QueryBuilder'; import Visualization from '../components/Visualization'; import Header from '../containers/Header'; import ResizeContainer from 'src/shared/components/ResizeContainer'; -import { - setTimeRange as setTimeRangeAction, -} from '../actions/view'; +import {setAutoRefresh} from 'shared/actions/appConfig' +import {setTimeRange as setTimeRangeAction} from '../actions/view'; const { arrayOf, func, + number, shape, string, } = PropTypes; @@ -25,6 +26,8 @@ const DataExplorer = React.createClass({ }).isRequired, }).isRequired, queryConfigs: PropTypes.shape({}), + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, timeRange: shape({ upper: string, lower: string, @@ -59,18 +62,20 @@ const DataExplorer = React.createClass({ }, render() { - const {timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; + const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; const {activeQueryID} = this.state; const queries = dataExplorer.queryIDs.map((qid) => queryConfigs[qid]); return (
    @@ -50,6 +62,7 @@ const Header = React.createClass({ {this.context.source.name}
    +
diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index ea427ea1f..30255b1fb 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -1,6 +1,7 @@ import React, {PropTypes} from 'react' import {Link} from 'react-router' import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import _ from 'lodash' import classnames from 'classnames'; @@ -12,7 +13,6 @@ import {fetchLayouts} from 'shared/apis'; import {setAutoRefresh} from 'shared/actions/appConfig' import {presentationButtonDispatcher} from 'shared/dispatchers' -import {bindActionCreators} from 'redux' const { shape, From db9a34c5367824cd643cba99d17d9f5bb2ed0f0d Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Tue, 28 Feb 2017 17:18:59 -0800 Subject: [PATCH 05/47] Put PropTypes in conventional order --- ui/src/dashboards/components/DashboardHeader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 16afa1a2f..55dd1b7b0 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -59,12 +59,12 @@ const DashboardHeader = ({ ) const { - shape, array, - string, + bool, func, number, - bool, + shape, + string, } = PropTypes DashboardHeader.propTypes = { From e455f1ba15557fd79c857052f43d7004d6c93356 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Tue, 28 Feb 2017 17:26:26 -0800 Subject: [PATCH 06/47] Add autoRefresh interval choice to Kubernetes dashboard --- .../kubernetes/components/KubernetesDashboard.js | 16 ++++++++++------ ui/src/kubernetes/containers/KubernetesPage.js | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ui/src/kubernetes/components/KubernetesDashboard.js b/ui/src/kubernetes/components/KubernetesDashboard.js index 2c4afc5b4..6092b03e8 100644 --- a/ui/src/kubernetes/components/KubernetesDashboard.js +++ b/ui/src/kubernetes/components/KubernetesDashboard.js @@ -6,11 +6,12 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; const { - shape, - string, arrayOf, bool, func, + number, + shape, + string, } = PropTypes export const KubernetesDashboard = React.createClass({ @@ -22,6 +23,8 @@ export const KubernetesDashboard = React.createClass({ telegraf: string.isRequired, }), layouts: arrayOf(shape().isRequired).isRequired, + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool.isRequired, handleClickPresentationButton: func, }, @@ -34,9 +37,8 @@ export const KubernetesDashboard = React.createClass({ }, renderLayouts(layouts) { - const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source} = this.props; + const {source, autoRefresh} = this.props; let layoutCells = []; layouts.forEach((layout) => { @@ -56,7 +58,7 @@ export const KubernetesDashboard = React.createClass({ ); @@ -68,7 +70,7 @@ export const KubernetesDashboard = React.createClass({ }, render() { - const {layouts, inPresentationMode, handleClickPresentationButton} = this.props; + const {layouts, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props; const {timeRange} = this.state; const emptyState = (
@@ -81,6 +83,8 @@ export const KubernetesDashboard = React.createClass({
@@ -52,10 +60,12 @@ export const KubernetesPage = React.createClass({ }); const mapStateToProps = (state) => ({ + autoRefresh: state.appConfig.autoRefresh, inPresentationMode: state.appUI.presentationMode, }) const mapDispatchToProps = (dispatch) => ({ + handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) From d4abbbdbfb788f498f63662f157666cc0fe55257 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Tue, 28 Feb 2017 17:28:51 -0800 Subject: [PATCH 07/47] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99cbd06ce..dccf3492c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ 4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable 5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position 6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for [Heroku](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#heroku) and [Google](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#google) + 7. [#781](https://github.com/influxdata/chronograf/issues/781): Add global auto-refresh dropdown to all graph dashboards ### UI Improvements 1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger From c6a0d18aac9a2fc40664394d84375f5ce3cb095f Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 1 Mar 2017 19:49:38 -0800 Subject: [PATCH 08/47] Fix rule disable bug --- ui/src/kapacitor/actions/view/index.js | 3 +- ui/src/kapacitor/components/KapacitorRules.js | 160 ++++++++++++++++++ .../containers/KapacitorRulesPage.js | 133 +++------------ 3 files changed, 186 insertions(+), 110 deletions(-) create mode 100644 ui/src/kapacitor/components/KapacitorRules.js diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js index 2c89fe293..3d9f9e6d9 100644 --- a/ui/src/kapacitor/actions/view/index.js +++ b/ui/src/kapacitor/actions/view/index.js @@ -163,12 +163,11 @@ export function deleteRule(rule) { }; } -export function updateRuleStatus(rule, {status}) { +export function updateRuleStatus(rule, status) { return (dispatch) => { updateRuleStatusAPI(rule, status).then(() => { dispatch(publishNotification('success', `${rule.name} ${status} successfully`)); }).catch(() => { - dispatch(updateRuleStatusSuccess(rule.id, status)); dispatch(publishNotification('error', `${rule.name} could not be ${status}`)); }); }; diff --git a/ui/src/kapacitor/components/KapacitorRules.js b/ui/src/kapacitor/components/KapacitorRules.js new file mode 100644 index 000000000..fdfc37298 --- /dev/null +++ b/ui/src/kapacitor/components/KapacitorRules.js @@ -0,0 +1,160 @@ +import React, {PropTypes} from 'react'; +import NoKapacitorError from '../../shared/components/NoKapacitorError'; +import {Link} from 'react-router'; + +const KapacitorRules = ({ + source, + rules, + hasKapacitor, + loading, + onDelete, + onChangeRuleStatus, +}) => { + if (loading) { + return + } + + if (!hasKapacitor) { + return + } + + return ( + +
+
+

Alert Rules

+ Create New Rule +
+ +
+
+ ) +} + +const KapacitorRuleTable = ({source, rules, onDelete, onChangeRuleStatus}) => { + return ( +
+ + + + + + + + + + + + + { + rules.map((rule) => { + return + }) + } + +
NameTriggerMessageAlertsEnabled
+
+ ) +} + +const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => { + return ( + + {rule.name} + {rule.trigger} + {rule.message} + {rule.alerts.join(', ')} + +
+ onChangeRuleStatus(rule)} + /> + +
+ + + + ) +} + +const Header = () => ( +
+
+
+

Kapacitor Rules

+
+
+
+) + +const Loading = () => ( + +

Loading...

+
+) + +const KapacitorError = ({source}) => ( + + ; + +) + +const PageContents = ({children}) => ( +
+
+
+
+
+
+ {children} +
+
+
+
+
+) + +const { + arrayOf, + bool, + func, + shape, +} = PropTypes + +KapacitorRules.propTypes = { + source: shape(), + rules: arrayOf(shape()), + hasKapacitor: bool, + loading: bool, + onChangeRuleStatus: func, + onDelete: func, +} + +KapacitorRuleTable.propTypes = { + source: shape(), + rules: arrayOf(shape()), + onChangeRuleStatus: func, + onDelete: func, +} + +RuleRow.propTypes = { + rule: shape(), + source: shape(), + onChangeRuleStatus: func, + onDelete: func, +} + +KapacitorError.propTypes = { + source: shape(), +} + +export default KapacitorRules diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index 5ab8d1e75..3bd506cb6 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -1,17 +1,16 @@ -import React, {PropTypes} from 'react'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {Link} from 'react-router'; -import {getKapacitor} from 'src/shared/apis'; -import * as kapacitorActionCreators from '../actions/view'; -import NoKapacitorError from '../../shared/components/NoKapacitorError'; +import React, {PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' +import {getKapacitor} from 'src/shared/apis' +import * as kapacitorActionCreators from '../actions/view' +import KapacitorRules from 'src/kapacitor/components/KapacitorRules' const { arrayOf, func, shape, string, -} = PropTypes; +} = PropTypes export const KapacitorRulesPage = React.createClass({ propTypes: { @@ -41,6 +40,7 @@ export const KapacitorRulesPage = React.createClass({ return { hasKapacitor: false, loading: true, + fooRule: 'HAI', }; }, @@ -58,111 +58,28 @@ export const KapacitorRulesPage = React.createClass({ actions.deleteRule(rule); }, - handleRuleStatus(e, rule) { - const {actions} = this.props; - const status = e.target.checked ? 'enabled' : 'disabled'; + handleRuleStatus(rule) { + const {actions} = this.props + const status = rule.status === 'enabled' ? 'disabled' : 'enabled' - actions.updateRuleStatusSuccess(rule.id, status); - actions.updateRuleStatus(rule, {status}); - }, - - renderSubComponent() { - const {source} = this.props; - const {hasKapacitor, loading} = this.state; - - let component; - if (loading) { - component = (

Loading...

); - } else if (hasKapacitor) { - component = ( -
-
-

Alert Rules

- Create New Rule -
-
- - - - - - - - - - - - - {this.renderAlertsTableRows()} - -
NameTriggerMessageAlertsEnabled
-
-
- ); - } else { - component = ; - } - return component; + actions.updateRuleStatus(rule, status) + actions.updateRuleStatusSuccess(rule.id, status) }, render() { + const {source, rules} = this.props + const {hasKapacitor, loading} = this.state + return ( -
-
-
-
-

Kapacitor Rules

-
-
-
-
-
- {this.renderSubComponent()} -
-
-
- ); - }, - - renderAlertsTableRows() { - const {rules, source} = this.props; - const numRules = rules.length; - - if (numRules === 0) { - return ( - - -

You don't have any Kapacitor
Rules, why not create one?

- Create New Rule - - - ); - } - - return rules.map((rule) => { - return ( - - {rule.name} - {rule.trigger} - {rule.message} - {rule.alerts.join(', ')} - -
- this.enabled = r} - defaultChecked={rule.status === "enabled"} - onClick={(e) => this.handleRuleStatus(e, rule)} - /> - -
- - - - ); - }); + + ) }, }); From f22b81ced545c2155ba1214be76fc52693c15732 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Mar 2017 09:51:17 -0800 Subject: [PATCH 09/47] Update eslintrc --- ui/.eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/.eslintrc b/ui/.eslintrc index c86aa2eea..a0605ff3a 100644 --- a/ui/.eslintrc +++ b/ui/.eslintrc @@ -230,7 +230,7 @@ 'react/no-string-refs': 0, // TODO: 2 'react/no-unknown-property': 2, 'react/prop-types': 2, - 'react/prefer-es6-class': [2, 'never'], + 'react/prefer-es6-class': [0, 'never'], 'react/react-in-jsx-scope': 2, 'react/require-extension': 0, 'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons) From 465d1aaa23209d6014b3819853a5eae90865c607 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Mar 2017 09:51:41 -0800 Subject: [PATCH 10/47] Update container to extend Component --- .../containers/KapacitorRulesPage.js | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index 3bd506cb6..6ee082c55 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -1,48 +1,21 @@ -import React, {PropTypes} from 'react' +import React, {PropTypes, Component} from 'react' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import {getKapacitor} from 'src/shared/apis' import * as kapacitorActionCreators from '../actions/view' import KapacitorRules from 'src/kapacitor/components/KapacitorRules' -const { - arrayOf, - func, - shape, - string, -} = PropTypes - -export const KapacitorRulesPage = React.createClass({ - propTypes: { - source: shape({ - id: string.isRequired, - links: shape({ - proxy: string.isRequired, - self: string.isRequired, - kapacitors: string.isRequired, - }), - }), - rules: arrayOf(shape({ - name: string.isRequired, - trigger: string.isRequired, - message: string.isRequired, - alerts: arrayOf(string.isRequired).isRequired, - })).isRequired, - actions: shape({ - fetchRules: func.isRequired, - deleteRule: func.isRequired, - updateRuleStatus: func.isRequired, - }).isRequired, - addFlashMessage: func, - }, - - getInitialState() { - return { +class KapacitorRulesPage extends Component { + constructor(props) { + super(props); + this.state = { hasKapacitor: false, loading: true, - fooRule: 'HAI', - }; - }, + } + + this.handleDeleteRule = this.handleDeleteRule.bind(this) + this.handleRuleStatus = this.handleRuleStatus.bind(this) + } componentDidMount() { getKapacitor(this.props.source).then((kapacitor) => { @@ -51,12 +24,12 @@ export const KapacitorRulesPage = React.createClass({ } this.setState({loading: false, hasKapacitor: !!kapacitor}); }); - }, + } handleDeleteRule(rule) { - const {actions} = this.props; - actions.deleteRule(rule); - }, + const {actions} = this.props + actions.deleteRule(rule) + } handleRuleStatus(rule) { const {actions} = this.props @@ -64,7 +37,7 @@ export const KapacitorRulesPage = React.createClass({ actions.updateRuleStatus(rule, status) actions.updateRuleStatusSuccess(rule.id, status) - }, + } render() { const {source, rules} = this.props @@ -80,19 +53,49 @@ export const KapacitorRulesPage = React.createClass({ onChangeRuleStatus={this.handleRuleStatus} /> ) - }, -}); + } +} -function mapStateToProps(state) { +const { + arrayOf, + func, + shape, + string, +} = PropTypes + +KapacitorRulesPage.propTypes = { + source: shape({ + id: string.isRequired, + links: shape({ + proxy: string.isRequired, + self: string.isRequired, + kapacitors: string.isRequired, + }), + }), + rules: arrayOf(shape({ + name: string.isRequired, + trigger: string.isRequired, + message: string.isRequired, + alerts: arrayOf(string.isRequired).isRequired, + })).isRequired, + actions: shape({ + fetchRules: func.isRequired, + deleteRule: func.isRequired, + updateRuleStatus: func.isRequired, + }).isRequired, + addFlashMessage: func, +} + +const mapStateToProps = (state) => { return { rules: Object.values(state.rules), - }; + } } -function mapDispatchToProps(dispatch) { +const mapDispatchToProps = (dispatch) => { return { actions: bindActionCreators(kapacitorActionCreators, dispatch), - }; + } } -export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulesPage); +export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulesPage) From e998cf8c6030b14de396fae28dad688a6b5c2c00 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Mar 2017 10:13:54 -0800 Subject: [PATCH 11/47] Move KapRulesTable into separate file --- ui/src/kapacitor/components/KapacitorRules.js | 143 +++++------------- .../components/KapacitorRulesTable.js | 74 +++++++++ ui/src/shared/components/NoKapacitorError.js | 12 +- 3 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 ui/src/kapacitor/components/KapacitorRulesTable.js diff --git a/ui/src/kapacitor/components/KapacitorRules.js b/ui/src/kapacitor/components/KapacitorRules.js index fdfc37298..da7835b6c 100644 --- a/ui/src/kapacitor/components/KapacitorRules.js +++ b/ui/src/kapacitor/components/KapacitorRules.js @@ -1,7 +1,9 @@ import React, {PropTypes} from 'react'; -import NoKapacitorError from '../../shared/components/NoKapacitorError'; import {Link} from 'react-router'; +import NoKapacitorError from '../../shared/components/NoKapacitorError'; +import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable' + const KapacitorRules = ({ source, rules, @@ -10,112 +12,54 @@ const KapacitorRules = ({ onDelete, onChangeRuleStatus, }) => { - if (loading) { - return + if (!hasKapacitor) { + return ( + + + + ) } - if (!hasKapacitor) { - return + if (loading) { + return ( + +

Loading...

+
+ ) } return ( -
-
-

Alert Rules

- Create New Rule -
- +
+

Alert Rules

+ Create New Rule
+ ) } -const KapacitorRuleTable = ({source, rules, onDelete, onChangeRuleStatus}) => { - return ( -
- - - - - - - - - - - - - { - rules.map((rule) => { - return - }) - } - -
NameTriggerMessageAlertsEnabled
-
- ) -} - -const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => { - return ( - - {rule.name} - {rule.trigger} - {rule.message} - {rule.alerts.join(', ')} - -
- onChangeRuleStatus(rule)} - /> - -
- - - - ) -} - -const Header = () => ( -
-
-
-

Kapacitor Rules

-
-
-
-) - -const Loading = () => ( - -

Loading...

-
-) - -const KapacitorError = ({source}) => ( - - ; - -) - const PageContents = ({children}) => (
-
+
+
+
+

Kapacitor Rules

+
+
+
- {children} +
+ {children} +
@@ -128,6 +72,7 @@ const { bool, func, shape, + node, } = PropTypes KapacitorRules.propTypes = { @@ -139,22 +84,8 @@ KapacitorRules.propTypes = { onDelete: func, } -KapacitorRuleTable.propTypes = { - source: shape(), - rules: arrayOf(shape()), - onChangeRuleStatus: func, - onDelete: func, -} - -RuleRow.propTypes = { - rule: shape(), - source: shape(), - onChangeRuleStatus: func, - onDelete: func, -} - -KapacitorError.propTypes = { - source: shape(), +PageContents.propTypes = { + children: node, } export default KapacitorRules diff --git a/ui/src/kapacitor/components/KapacitorRulesTable.js b/ui/src/kapacitor/components/KapacitorRulesTable.js new file mode 100644 index 000000000..e861acbf9 --- /dev/null +++ b/ui/src/kapacitor/components/KapacitorRulesTable.js @@ -0,0 +1,74 @@ +import React, {PropTypes} from 'react'; +import {Link} from 'react-router'; + +const KapacitorRulesTable = ({source, rules, onDelete, onChangeRuleStatus}) => { + return ( +
+ + + + + + + + + + + + + { + rules.map((rule) => { + return + }) + } + +
NameTriggerMessageAlertsEnabled
+
+ ) +} + +const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => { + return ( + + {rule.name} + {rule.trigger} + {rule.message} + {rule.alerts.join(', ')} + +
+ onChangeRuleStatus(rule)} + /> + +
+ + + + ) +} + +const { + arrayOf, + func, + shape, +} = PropTypes + +KapacitorRulesTable.propTypes = { + source: shape(), + rules: arrayOf(shape()), + onChangeRuleStatus: func, + onDelete: func, +} + +RuleRow.propTypes = { + rule: shape(), + source: shape(), + onChangeRuleStatus: func, + onDelete: func, +} + +export default KapacitorRulesTable diff --git a/ui/src/shared/components/NoKapacitorError.js b/ui/src/shared/components/NoKapacitorError.js index a82451dcc..fda9ce61f 100644 --- a/ui/src/shared/components/NoKapacitorError.js +++ b/ui/src/shared/components/NoKapacitorError.js @@ -1,5 +1,5 @@ -import React, {PropTypes} from 'react'; -import {Link} from 'react-router'; +import React, {PropTypes} from 'react' +import {Link} from 'react-router' const NoKapacitorError = React.createClass({ propTypes: { @@ -9,14 +9,14 @@ const NoKapacitorError = React.createClass({ }, render() { - const path = `/sources/${this.props.source.id}/kapacitor-config`; + const path = `/sources/${this.props.source.id}/kapacitor-config` return (

The current source does not have an associated Kapacitor instance, please configure one.

Add Kapacitor
- ); + ) }, -}); +}) -export default NoKapacitorError; +export default NoKapacitorError From 3ab0e6e038dc01ed90c501be19fd67652b6a8883 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Mar 2017 10:59:09 -0800 Subject: [PATCH 12/47] Use bind operator --- ui/src/kapacitor/containers/KapacitorRulesPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index 6ee082c55..e095fa00b 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -13,8 +13,8 @@ class KapacitorRulesPage extends Component { loading: true, } - this.handleDeleteRule = this.handleDeleteRule.bind(this) - this.handleRuleStatus = this.handleRuleStatus.bind(this) + this.handleDeleteRule = ::this.handleDeleteRule + this.handleRuleStatus = ::this.handleRuleStatus } componentDidMount() { From 3ad6ec894a2cee3e1b397d69147309c23a1054ac Mon Sep 17 00:00:00 2001 From: Jade McGough Date: Thu, 2 Mar 2017 11:14:42 -0800 Subject: [PATCH 13/47] #855 - don't show "loading hosts" after hosts have been loaded --- ui/src/hosts/components/HostsTable.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index e4a98f4d2..0801871d0 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -87,12 +87,12 @@ const HostsTable = React.createClass({ const hostCount = sortedHosts.length; let hostsTitle; - if (hostCount === 1) { - hostsTitle = `${hostCount} Host`; - } else if (hostCount > 1) { - hostsTitle = `${hostCount} Hosts`; - } else { + if (hosts.length === 0) { hostsTitle = `Loading Hosts...`; + } else if (hostCount === 1) { + hostsTitle = `${hostCount} Host`; + } else { + hostsTitle = `${hostCount} Hosts`; } return ( From a16f8f80c1c707981383692a14e074b3968201a3 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 3 Mar 2017 11:16:49 -0800 Subject: [PATCH 14/47] Add pause icon to icon font --- ui/src/style/fonts/icomoon.eot | Bin 21968 -> 22068 bytes ui/src/style/fonts/icomoon.svg | 1 + ui/src/style/fonts/icomoon.ttf | Bin 21804 -> 21904 bytes ui/src/style/fonts/icomoon.woff | Bin 21880 -> 21980 bytes ui/src/style/fonts/icomoon.woff2 | Bin 10236 -> 10256 bytes ui/src/style/theme/bootstrap-theme.scss | 3 +++ 6 files changed, 4 insertions(+) diff --git a/ui/src/style/fonts/icomoon.eot b/ui/src/style/fonts/icomoon.eot index c525fa69a31ea33ac54868d28236a47e6af1ebab..689d779b4eebee865a9d216ed8997f36515d8526 100755 GIT binary patch delta 417 zcmcbxnsLh-MplzB28Ia}SSgG!heKMCWf7=byzd4kj2I5bRM1e!CZZeA1U%*c3l z@|&Q~`e91bl-?;@DaR;JQ2wD(q_RtuL)AsKLUoGj4%Ihm4r)ni=hPY0ebk#67=ebQ KZsrR)#0UTv^KRz= delta 333 zcmdn8hVjB`M%D|V3=BFGS + diff --git a/ui/src/style/fonts/icomoon.ttf b/ui/src/style/fonts/icomoon.ttf index 1cfe84f25325cd65247ebc03e5ba84c9111e3f7f..8ac3dfcb34d2604138f777405823a023212832f2 100755 GIT binary patch delta 381 zcmZ3pigChf#tDk`!WpL-7#L+37#N(lh7lJE*{0yL(Q*!c?6VG0L*vG&S z^am(^CO5I7fT5U?nSmkr1dy+gmzbL>G1HIpKTzNWP(xcmesKu{P?~`uv|!?n>5N{J z0~q7wPl}!pJtBHQbh+qK(Fvm60!9Lc0s;aY0;~c|{CD`TZa&A@?jpd%aQ1&1voX^w z27U&41_sv2>%HU+7&nXa&*B#c6O0WAcCmoCxB$~L5cyXM!DiST;+DWAT5y*c6Wd5V+025C~MyI3XuLIq{tFZV3j4 zpcz20)_$x0{jB(0xSZI{I~h9Y(B@>?y`BSTRfMH;G357 z@%%Pl8Ms+MhA>>YRyzeoPc{fNXH40g5$Mb~d413a{WPV0%3R7i$_dIRRGd`0RBot> ksCuZTsCKCCQ4>%zQk$oCL0v{Y2&kPAh*LJ73qHgM0EC@a!T9IKy|?$Ks9GT`0V9}eYuGh1v(52AtFEl1u!mVWX?;>O$CaX z05!IO@Jv6>{{{KQB|!J@nfPZuqu1m)jPc4RMNfzx5j`NfTy&}E1kr8*BLPDJ0RavH zRskmdJN#D}m>C#>9@N?F#T4!$z{GI&e;Ttf(<}yl26+Ys*2%xU&q*u20kflJ1Nzd1Rc-{va=H!xIy;L82vmoR#=OrSYq>gJF@ zXGX@elcxlIwhU96ru0tPN;yV(g7OcQB9&dL9I7s=6{=HIcc{Kmb5KiCJEzW|?xWrW Xj-XUVMh0H6sZ2n(3va$0yp$0Duy1i5 delta 367 zcmcb!n(@afMu~ENH#Y`G1|SHnVBiMRIzT?lWJM;Ci8`wF+!=?`6N?KN7?^y3k{nPh zke*YS1{4cnVDMW4!V4zP=*~z@OkrRM_yJU72Esh&mvmSaiSWGSMZX{i0n0h5`lx`~vI( zECP)DxB0IyFf%X$J*czUiz(b?GoyPvmyF<>mhP=IH`20+)x!!^-xVw?NHsLCZJ}dHc#z>x{P`d PP&+7KQ#MP59AX3j?+Rdk diff --git a/ui/src/style/fonts/icomoon.woff2 b/ui/src/style/fonts/icomoon.woff2 index 77a391b1890ab764c191072497f3d7180c55bd67..fae3be996648529c8503bc70d8f4d687fd5230b7 100755 GIT binary patch literal 10256 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 CVB>QD diff --git a/ui/src/style/theme/bootstrap-theme.scss b/ui/src/style/theme/bootstrap-theme.scss index 9b600c65e..ed76d4444 100755 --- a/ui/src/style/theme/bootstrap-theme.scss +++ b/ui/src/style/theme/bootstrap-theme.scss @@ -269,6 +269,9 @@ .icon.refresh:before { content: "\e949"; } +.icon.pause:before { + content: "\e94a"; +} .icon.clock:before { content: "\e91b"; } From 3d1aa9eac7e980dafe827f00bf44cfa9fe4108bd Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 3 Mar 2017 13:34:04 -0800 Subject: [PATCH 15/47] Refactor app ui and config reducers, clean up --- ui/src/dashboards/containers/DashboardPage.js | 10 ++-- .../data_explorer/containers/DataExplorer.js | 4 +- ui/src/data_explorer/reducers/index.js | 4 +- ui/src/hosts/containers/HostPage.js | 8 +-- ui/src/index.js | 3 +- .../kubernetes/containers/KubernetesPage.js | 8 +-- ui/src/localStorage.js | 6 +- ui/src/shared/actions/app.js | 22 +++++++ ui/src/shared/actions/appConfig.js | 6 -- ui/src/shared/actions/ui.js | 19 ------- ui/src/shared/dispatchers/index.js | 2 +- ui/src/shared/reducers/app.js | 57 +++++++++++++++++++ ui/src/shared/reducers/appConfig.js | 20 ------- ui/src/shared/reducers/index.js | 10 ++-- ui/src/shared/reducers/ui.js | 23 -------- ui/src/side_nav/containers/SideNavApp.js | 10 ++-- ui/src/store/configureStore.js | 4 +- 17 files changed, 113 insertions(+), 103 deletions(-) create mode 100644 ui/src/shared/actions/app.js delete mode 100644 ui/src/shared/actions/appConfig.js delete mode 100644 ui/src/shared/actions/ui.js create mode 100644 ui/src/shared/reducers/app.js delete mode 100644 ui/src/shared/reducers/appConfig.js delete mode 100644 ui/src/shared/reducers/ui.js diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 0766fea0d..0b6efe1b4 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -147,8 +147,10 @@ const DashboardPage = React.createClass({ const mapStateToProps = (state) => { const { - appUI, - appConfig, + app: { + ephemeral: {inPresentationMode}, + persisted: {autoRefresh}, + }, dashboardUI: { dashboards, dashboard, @@ -160,10 +162,10 @@ const mapStateToProps = (state) => { return { dashboards, dashboard, - autoRefresh: appConfig.autoRefresh, + autoRefresh, timeRange, isEditMode, - inPresentationMode: appUI.presentationMode, + inPresentationMode, } } diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index d89a8dcd9..9468fca75 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -6,7 +6,7 @@ import Visualization from '../components/Visualization'; import Header from '../containers/Header'; import ResizeContainer from 'src/shared/components/ResizeContainer'; -import {setAutoRefresh} from 'shared/actions/appConfig' +import {setAutoRefresh} from 'shared/actions/app' import {setTimeRange as setTimeRangeAction} from '../actions/view'; const { @@ -95,7 +95,7 @@ const DataExplorer = React.createClass({ }); function mapStateToProps(state) { - const {appConfig: {autoRefresh}, timeRange, queryConfigs, dataExplorer} = state; + const {app: {persisted: {autoRefresh}}, timeRange, queryConfigs, dataExplorer} = state; return { autoRefresh, diff --git a/ui/src/data_explorer/reducers/index.js b/ui/src/data_explorer/reducers/index.js index cccca5d79..6db959b19 100644 --- a/ui/src/data_explorer/reducers/index.js +++ b/ui/src/data_explorer/reducers/index.js @@ -2,8 +2,8 @@ import queryConfigs from './queryConfigs'; import timeRange from './timeRange'; import dataExplorer from './ui'; -export { +export default { queryConfigs, timeRange, dataExplorer, -}; +} diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 30255b1fb..641aa63f1 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -11,7 +11,7 @@ import timeRanges from 'hson!../../shared/data/timeRanges.hson'; import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis'; import {fetchLayouts} from 'shared/apis'; -import {setAutoRefresh} from 'shared/actions/appConfig' +import {setAutoRefresh} from 'shared/actions/app' import {presentationButtonDispatcher} from 'shared/dispatchers' const { @@ -188,9 +188,9 @@ export const HostPage = React.createClass({ }, }); -const mapStateToProps = (state) => ({ - inPresentationMode: state.appUI.presentationMode, - autoRefresh: state.appConfig.autoRefresh, +const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ + inPresentationMode, + autoRefresh, }) const mapDispatchToProps = (dispatch) => ({ diff --git a/ui/src/index.js b/ui/src/index.js index 1bf13489a..e45201ad6 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -19,12 +19,11 @@ import configureStore from 'src/store/configureStore'; import {getMe, getSources} from 'shared/apis'; import {receiveMe} from 'shared/actions/me'; import {receiveAuth} from 'shared/actions/auth'; -import {disablePresentationMode} from 'shared/actions/ui'; +import {disablePresentationMode} from 'shared/actions/app'; import {loadLocalStorage} from './localStorage'; import 'src/style/chronograf.scss'; -// TODO ensure appConfig data in localstorage const store = configureStore(loadLocalStorage()); const rootNode = document.getElementById('react-root'); diff --git a/ui/src/kubernetes/containers/KubernetesPage.js b/ui/src/kubernetes/containers/KubernetesPage.js index 657f436b4..f8cc8808e 100644 --- a/ui/src/kubernetes/containers/KubernetesPage.js +++ b/ui/src/kubernetes/containers/KubernetesPage.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux' import {fetchLayouts} from 'shared/apis'; import KubernetesDashboard from 'src/kubernetes/components/KubernetesDashboard'; -import {setAutoRefresh} from 'shared/actions/appConfig' +import {setAutoRefresh} from 'shared/actions/app' import {presentationButtonDispatcher} from 'shared/dispatchers' const { @@ -59,9 +59,9 @@ export const KubernetesPage = React.createClass({ }, }); -const mapStateToProps = (state) => ({ - autoRefresh: state.appConfig.autoRefresh, - inPresentationMode: state.appUI.presentationMode, +const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ + inPresentationMode, + autoRefresh, }) const mapDispatchToProps = (dispatch) => ({ diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 7f0856068..4b8381503 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -9,10 +9,12 @@ export const loadLocalStorage = () => { } }; -export const saveToLocalStorage = ({appConfig, queryConfigs, timeRange, dataExplorer}) => { +export const saveToLocalStorage = ({app: {persisted}, queryConfigs, timeRange, dataExplorer}) => { try { + const appPersisted = Object.assign({}, {app: {persisted}}) + window.localStorage.setItem('state', JSON.stringify({ - appConfig, + ...appPersisted, queryConfigs, timeRange, dataExplorer, diff --git a/ui/src/shared/actions/app.js b/ui/src/shared/actions/app.js new file mode 100644 index 000000000..f8939f408 --- /dev/null +++ b/ui/src/shared/actions/app.js @@ -0,0 +1,22 @@ +import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' + +// ephemeral state reducers +export const enablePresentationMode = () => ({ + type: 'ENABLE_PRESENTATION_MODE', +}) + +export const disablePresentationMode = () => ({ + type: 'DISABLE_PRESENTATION_MODE', +}) + +export const delayEnablePresentationMode = () => (dispatch) => { + setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) +} + +// persistent state reducers +export const setAutoRefresh = (milliseconds) => ({ + type: 'SET_AUTOREFRESH', + payload: { + milliseconds, + }, +}) diff --git a/ui/src/shared/actions/appConfig.js b/ui/src/shared/actions/appConfig.js deleted file mode 100644 index 1fd509e9e..000000000 --- a/ui/src/shared/actions/appConfig.js +++ /dev/null @@ -1,6 +0,0 @@ -export const setAutoRefresh = (milliseconds) => ({ - type: 'SET_AUTOREFRESH', - payload: { - milliseconds, - }, -}) diff --git a/ui/src/shared/actions/ui.js b/ui/src/shared/actions/ui.js deleted file mode 100644 index 740566beb..000000000 --- a/ui/src/shared/actions/ui.js +++ /dev/null @@ -1,19 +0,0 @@ -import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' - -export function enablePresentationMode() { - return { - type: 'ENABLE_PRESENTATION_MODE', - } -} - -export function disablePresentationMode() { - return { - type: 'DISABLE_PRESENTATION_MODE', - } -} - -export function delayEnablePresentationMode() { - return (dispatch) => { - setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) - } -} diff --git a/ui/src/shared/dispatchers/index.js b/ui/src/shared/dispatchers/index.js index fcf02767f..a7771a9ff 100644 --- a/ui/src/shared/dispatchers/index.js +++ b/ui/src/shared/dispatchers/index.js @@ -1,4 +1,4 @@ -import {delayEnablePresentationMode} from 'shared/actions/ui' +import {delayEnablePresentationMode} from 'shared/actions/app' import {publishNotification, delayDismissNotification} from 'shared/actions/notifications' import {PRESENTATION_MODE_NOTIFICATION_DELAY} from 'shared/constants' diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js new file mode 100644 index 000000000..adcb4c242 --- /dev/null +++ b/ui/src/shared/reducers/app.js @@ -0,0 +1,57 @@ +import {combineReducers} from 'redux'; + +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' + +const initialState = { + ephemeral: { + inPresentationMode: false, + }, + persisted: { + autoRefresh: AUTOREFRESH_DEFAULT, + }, +} + +const { + ephemeral: initialEphemeralState, + persisted: initialPersistedState, +} = initialState + +const ephemeralReducer = (state = initialEphemeralState, action) => { + switch (action.type) { + case 'ENABLE_PRESENTATION_MODE': { + return { + ...state, + inPresentationMode: true, + } + } + + case 'DISABLE_PRESENTATION_MODE': { + return { + ...state, + inPresentationMode: false, + } + } + + default: + return state + } +} + +const persistedReducer = (state = initialPersistedState, action) => { + switch (action.type) { + case 'SET_AUTOREFRESH': { + return { + ...state, + autoRefresh: action.payload.milliseconds, + } + } + + default: + return state + } +} + +export default combineReducers({ + ephemeral: ephemeralReducer, + persisted: persistedReducer, +}) diff --git a/ui/src/shared/reducers/appConfig.js b/ui/src/shared/reducers/appConfig.js deleted file mode 100644 index e4264f5ee..000000000 --- a/ui/src/shared/reducers/appConfig.js +++ /dev/null @@ -1,20 +0,0 @@ -import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' - -const initialState = { - autoRefresh: AUTOREFRESH_DEFAULT, -} - -const appConfig = (state = initialState, action) => { - switch (action.type) { - case 'SET_AUTOREFRESH': { - return { - ...state, - autoRefresh: action.payload.milliseconds, - } - } - } - - return state -} - -export default appConfig diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index 233c4ff97..bf191ecd7 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -1,15 +1,13 @@ -import appUI from './ui'; -import appConfig from './appConfig'; import me from './me'; +import app from './app'; import auth from './auth'; import notifications from './notifications'; import sources from './sources'; -export { - appUI, - appConfig, +export default { me, + app, auth, notifications, sources, -}; +} diff --git a/ui/src/shared/reducers/ui.js b/ui/src/shared/reducers/ui.js deleted file mode 100644 index 77a2f77a8..000000000 --- a/ui/src/shared/reducers/ui.js +++ /dev/null @@ -1,23 +0,0 @@ -const initialState = { - presentationMode: false, -}; - -export default function ui(state = initialState, action) { - switch (action.type) { - case 'ENABLE_PRESENTATION_MODE': { - return { - ...state, - presentationMode: true, - } - } - - case 'DISABLE_PRESENTATION_MODE': { - return { - ...state, - presentationMode: false, - } - } - } - - return state -} diff --git a/ui/src/side_nav/containers/SideNavApp.js b/ui/src/side_nav/containers/SideNavApp.js index a93bffdf7..5c2541571 100644 --- a/ui/src/side_nav/containers/SideNavApp.js +++ b/ui/src/side_nav/containers/SideNavApp.js @@ -34,11 +34,9 @@ const SideNavApp = React.createClass({ }, }); -function mapStateToProps(state) { - return { - me: state.me, - inPresentationMode: state.appUI.presentationMode, - }; -} +const mapStateToProps = ({me, app: {ephemeral: {inPresentationMode}}}) => ({ + me, + inPresentationMode, +}) export default connect(mapStateToProps)(SideNavApp); diff --git a/ui/src/store/configureStore.js b/ui/src/store/configureStore.js index 44b03fa66..5aa6b77ee 100644 --- a/ui/src/store/configureStore.js +++ b/ui/src/store/configureStore.js @@ -3,8 +3,8 @@ import {combineReducers} from 'redux'; import thunkMiddleware from 'redux-thunk'; import makeQueryExecuter from 'src/shared/middleware/queryExecuter'; import resizeLayout from 'src/shared/middleware/resizeLayout'; -import * as dataExplorerReducers from 'src/data_explorer/reducers'; -import * as sharedReducers from 'src/shared/reducers'; +import sharedReducers from 'src/shared/reducers'; +import dataExplorerReducers from 'src/data_explorer/reducers'; import rulesReducer from 'src/kapacitor/reducers/rules'; import dashboardUI from 'src/dashboards/reducers/ui'; import persistStateEnhancer from './persistStateEnhancer'; From b15cac73c88a5d862d5ffd3078aa5e2488f56cb9 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 3 Mar 2017 14:20:07 -0800 Subject: [PATCH 16/47] Add AutoRefresh Pause option --- ui/src/shared/components/AutoRefreshDropdown.js | 5 +++-- ui/src/shared/data/autoRefreshes.hson | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js index d8bafe2fd..ba1e4a0a6 100644 --- a/ui/src/shared/components/AutoRefreshDropdown.js +++ b/ui/src/shared/components/AutoRefreshDropdown.js @@ -44,12 +44,13 @@ const AutoRefreshDropdown = React.createClass({ const self = this; const {selected} = self.props; const {isOpen} = self.state; + const {milliseconds, inputValue} = this.findAutoRefreshItem(selected) return (

self.toggleMenu()}> - - {this.findAutoRefreshItem(selected).inputValue} + 0 ? "refresh" : "pause")}> + {inputValue}
    diff --git a/ui/src/shared/data/autoRefreshes.hson b/ui/src/shared/data/autoRefreshes.hson index 09b4237ff..56453c0b8 100644 --- a/ui/src/shared/data/autoRefreshes.hson +++ b/ui/src/shared/data/autoRefreshes.hson @@ -1,4 +1,5 @@ [ + {milliseconds: 0, inputValue: 'Paused', menuOption: 'Paused'}, {milliseconds: 5000, inputValue: 'Every 5 seconds', menuOption: 'Every 5 seconds'}, {milliseconds: 10000, inputValue: 'Every 10 seconds', menuOption: 'Every 10 seconds'}, {milliseconds: 15000, inputValue: 'Every 15 seconds', menuOption: 'Every 15 seconds'}, From 874e41075adc6dcd72bcd7bd54904c9e9b075b5e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 3 Mar 2017 14:22:07 -0800 Subject: [PATCH 17/47] Revert "Allow user to set auto-refresh interval" --- CHANGELOG.md | 1 - ui/src/dashboards/components/Dashboard.js | 10 ++- .../dashboards/components/DashboardHeader.js | 13 +--- ui/src/dashboards/containers/DashboardPage.js | 12 +--- .../data_explorer/components/Visualization.js | 6 +- .../data_explorer/containers/DataExplorer.js | 30 +++----- ui/src/data_explorer/containers/Header.js | 29 +++----- ui/src/data_explorer/reducers/index.js | 4 +- ui/src/hosts/containers/HostPage.js | 21 ++---- ui/src/index.js | 2 +- .../components/KubernetesDashboard.js | 16 ++--- .../kubernetes/containers/KubernetesPage.js | 20 ++---- ui/src/localStorage.js | 5 +- ui/src/shared/actions/app.js | 22 ------ ui/src/shared/actions/ui.js | 19 +++++ ui/src/shared/components/AutoRefresh.js | 25 +++---- .../shared/components/AutoRefreshDropdown.js | 72 ------------------- ui/src/shared/components/Dropdown.js | 22 ++---- ui/src/shared/components/LayoutRenderer.js | 8 +-- ui/src/shared/components/TimeRangeDropdown.js | 4 +- ui/src/shared/constants/index.js | 2 - ui/src/shared/data/autoRefreshes.hson | 7 -- ui/src/shared/dispatchers/index.js | 2 +- ui/src/shared/reducers/app.js | 57 --------------- ui/src/shared/reducers/index.js | 8 +-- ui/src/shared/reducers/ui.js | 23 ++++++ ui/src/side_nav/containers/SideNavApp.js | 10 +-- ui/src/store/configureStore.js | 4 +- 28 files changed, 125 insertions(+), 329 deletions(-) delete mode 100644 ui/src/shared/actions/app.js create mode 100644 ui/src/shared/actions/ui.js delete mode 100644 ui/src/shared/components/AutoRefreshDropdown.js delete mode 100644 ui/src/shared/data/autoRefreshes.hson delete mode 100644 ui/src/shared/reducers/app.js create mode 100644 ui/src/shared/reducers/ui.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6da3508..d157bc66b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ 4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable 5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position 6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for [Heroku](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#heroku) and [Google](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#google) - 7. [#781](https://github.com/influxdata/chronograf/issues/781): Add global auto-refresh dropdown to all graph dashboards ### UI Improvements 1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 3ff1c1ca4..46eb173b0 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -10,7 +10,6 @@ const Dashboard = ({ inPresentationMode, onPositionChange, source, - autoRefresh, timeRange, }) => { if (dashboard.id === 0) { @@ -21,13 +20,14 @@ const Dashboard = ({
    {isEditMode ? : null} - {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange)} + {Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)}
    ) } -Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange) => { +Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => { + const autoRefreshMs = 15000 const cells = dashboard.cells.map((cell, i) => { i = `${i}` const dashboardCell = {...cell, i} @@ -42,7 +42,7 @@ Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositi @@ -54,7 +54,6 @@ const { func, shape, string, - number, } = PropTypes Dashboard.propTypes = { @@ -67,7 +66,6 @@ Dashboard.propTypes = { proxy: string, }).isRequired, }).isRequired, - autoRefresh: number.isRequired, timeRange: shape({}).isRequired, } diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 55dd1b7b0..be6fd69a0 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -2,7 +2,6 @@ import React, {PropTypes} from 'react' import ReactTooltip from 'react-tooltip' import {Link} from 'react-router'; -import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' const DashboardHeader = ({ @@ -11,10 +10,8 @@ const DashboardHeader = ({ dashboard, headerText, timeRange, - autoRefresh, isHidden, handleChooseTimeRange, - handleChooseAutoRefresh, handleClickPresentationButton, sourceID, }) => isHidden ? null : ( @@ -48,7 +45,6 @@ const DashboardHeader = ({ Graph Tips
-
@@ -59,12 +55,11 @@ const DashboardHeader = ({ ) const { - array, - bool, - func, - number, shape, + array, string, + func, + bool, } = PropTypes DashboardHeader.propTypes = { @@ -74,10 +69,8 @@ DashboardHeader.propTypes = { dashboard: shape({}), headerText: string, timeRange: shape({}).isRequired, - autoRefresh: number.isRequired, isHidden: bool.isRequired, handleChooseTimeRange: func.isRequired, - handleChooseAutoRefresh: func.isRequired, handleClickPresentationButton: func.isRequired, } diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 0b6efe1b4..c83c92020 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -51,7 +51,6 @@ const DashboardPage = React.createClass({ id: number.isRequired, cells: arrayOf(shape({})).isRequired, }).isRequired, - autoRefresh: number.isRequired, timeRange: shape({}).isRequired, inPresentationMode: bool.isRequired, isEditMode: bool.isRequired, @@ -101,7 +100,6 @@ const DashboardPage = React.createClass({ isEditMode, handleClickPresentationButton, source, - autoRefresh, timeRange, } = this.props @@ -112,7 +110,6 @@ const DashboardPage = React.createClass({ {}} /> :
@@ -147,10 +143,7 @@ const DashboardPage = React.createClass({ const mapStateToProps = (state) => { const { - app: { - ephemeral: {inPresentationMode}, - persisted: {autoRefresh}, - }, + appUI, dashboardUI: { dashboards, dashboard, @@ -160,12 +153,11 @@ const mapStateToProps = (state) => { } = state return { + inPresentationMode: appUI.presentationMode, dashboards, dashboard, - autoRefresh, timeRange, isEditMode, - inPresentationMode, } } diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index b58355da6..b88972a9e 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -15,7 +15,6 @@ const { const Visualization = React.createClass({ propTypes: { - autoRefresh: number.isRequired, timeRange: shape({ upper: string, lower: string, @@ -46,7 +45,7 @@ const Visualization = React.createClass({ }, render() { - const {queryConfigs, autoRefresh, timeRange, activeQueryIndex, height, heightPixels} = this.props; + const {queryConfigs, timeRange, activeQueryIndex, height, heightPixels} = this.props; const {source} = this.context; const proxyLink = source.links.proxy; @@ -58,6 +57,7 @@ const Visualization = React.createClass({ const queries = statements.filter((s) => s.text !== null).map((s) => { return {host: [proxyLink], text: s.text, id: s.id}; }); + const autoRefreshMs = 10000; const isInDataExplorer = true; return ( @@ -77,7 +77,7 @@ const Visualization = React.createClass({ {isGraphInView ? ( diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index 9468fca75..68a7c7b2e 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -1,18 +1,17 @@ import React, {PropTypes} from 'react'; import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; import QueryBuilder from '../components/QueryBuilder'; import Visualization from '../components/Visualization'; import Header from '../containers/Header'; import ResizeContainer from 'src/shared/components/ResizeContainer'; -import {setAutoRefresh} from 'shared/actions/app' -import {setTimeRange as setTimeRangeAction} from '../actions/view'; +import { + setTimeRange as setTimeRangeAction, +} from '../actions/view'; const { arrayOf, func, - number, shape, string, } = PropTypes; @@ -26,8 +25,6 @@ const DataExplorer = React.createClass({ }).isRequired, }).isRequired, queryConfigs: PropTypes.shape({}), - autoRefresh: number.isRequired, - handleChooseAutoRefresh: func.isRequired, timeRange: shape({ upper: string, lower: string, @@ -62,20 +59,18 @@ const DataExplorer = React.createClass({ }, render() { - const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; + const {timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; const {activeQueryID} = this.state; const queries = dataExplorer.queryIDs.map((qid) => queryConfigs[qid]); return (
@@ -62,7 +50,6 @@ const Header = React.createClass({ {this.context.source.name}
-
diff --git a/ui/src/data_explorer/reducers/index.js b/ui/src/data_explorer/reducers/index.js index 6db959b19..cccca5d79 100644 --- a/ui/src/data_explorer/reducers/index.js +++ b/ui/src/data_explorer/reducers/index.js @@ -2,8 +2,8 @@ import queryConfigs from './queryConfigs'; import timeRange from './timeRange'; import dataExplorer from './ui'; -export default { +export { queryConfigs, timeRange, dataExplorer, -} +}; diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 641aa63f1..38ff3ae16 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -1,7 +1,6 @@ import React, {PropTypes} from 'react' import {Link} from 'react-router' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' import _ from 'lodash' import classnames from 'classnames'; @@ -10,8 +9,6 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis'; import {fetchLayouts} from 'shared/apis'; - -import {setAutoRefresh} from 'shared/actions/app' import {presentationButtonDispatcher} from 'shared/dispatchers' const { @@ -19,7 +16,6 @@ const { string, bool, func, - number, } = PropTypes export const HostPage = React.createClass({ @@ -39,8 +35,6 @@ export const HostPage = React.createClass({ app: string, }), }), - autoRefresh: number.isRequired, - handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool, handleClickPresentationButton: func, }, @@ -93,8 +87,9 @@ export const HostPage = React.createClass({ }, renderLayouts(layouts) { + const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source, autoRefresh} = this.props; + const {source} = this.props; const autoflowLayouts = layouts.filter((layout) => !!layout.autoflow); @@ -142,7 +137,7 @@ export const HostPage = React.createClass({ @@ -150,7 +145,7 @@ export const HostPage = React.createClass({ }, render() { - const {params: {hostID}, location: {query: {app}}, source: {id}, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props + const {params: {hostID}, location: {query: {app}}, source: {id}, inPresentationMode, handleClickPresentationButton} = this.props const {layouts, timeRange, hosts} = this.state const appParam = app ? `?app=${app}` : '' @@ -158,11 +153,9 @@ export const HostPage = React.createClass({
{Object.keys(hosts).map((host, i) => { @@ -188,13 +181,11 @@ export const HostPage = React.createClass({ }, }); -const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ - inPresentationMode, - autoRefresh, +const mapStateToProps = (state) => ({ + inPresentationMode: state.appUI.presentationMode, }) const mapDispatchToProps = (dispatch) => ({ - handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/index.js b/ui/src/index.js index e45201ad6..c2a8f4cd3 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -19,7 +19,7 @@ import configureStore from 'src/store/configureStore'; import {getMe, getSources} from 'shared/apis'; import {receiveMe} from 'shared/actions/me'; import {receiveAuth} from 'shared/actions/auth'; -import {disablePresentationMode} from 'shared/actions/app'; +import {disablePresentationMode} from 'shared/actions/ui'; import {loadLocalStorage} from './localStorage'; import 'src/style/chronograf.scss'; diff --git a/ui/src/kubernetes/components/KubernetesDashboard.js b/ui/src/kubernetes/components/KubernetesDashboard.js index 6092b03e8..2c4afc5b4 100644 --- a/ui/src/kubernetes/components/KubernetesDashboard.js +++ b/ui/src/kubernetes/components/KubernetesDashboard.js @@ -6,12 +6,11 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; const { + shape, + string, arrayOf, bool, func, - number, - shape, - string, } = PropTypes export const KubernetesDashboard = React.createClass({ @@ -23,8 +22,6 @@ export const KubernetesDashboard = React.createClass({ telegraf: string.isRequired, }), layouts: arrayOf(shape().isRequired).isRequired, - autoRefresh: number.isRequired, - handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool.isRequired, handleClickPresentationButton: func, }, @@ -37,8 +34,9 @@ export const KubernetesDashboard = React.createClass({ }, renderLayouts(layouts) { + const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source, autoRefresh} = this.props; + const {source} = this.props; let layoutCells = []; layouts.forEach((layout) => { @@ -58,7 +56,7 @@ export const KubernetesDashboard = React.createClass({ ); @@ -70,7 +68,7 @@ export const KubernetesDashboard = React.createClass({ }, render() { - const {layouts, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props; + const {layouts, inPresentationMode, handleClickPresentationButton} = this.props; const {timeRange} = this.state; const emptyState = (
@@ -83,8 +81,6 @@ export const KubernetesDashboard = React.createClass({
@@ -59,13 +51,11 @@ export const KubernetesPage = React.createClass({ }, }); -const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ - inPresentationMode, - autoRefresh, +const mapStateToProps = (state) => ({ + inPresentationMode: state.appUI.presentationMode, }) const mapDispatchToProps = (dispatch) => ({ - handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 4b8381503..5cd29aa07 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -9,12 +9,9 @@ export const loadLocalStorage = () => { } }; -export const saveToLocalStorage = ({app: {persisted}, queryConfigs, timeRange, dataExplorer}) => { +export const saveToLocalStorage = ({queryConfigs, timeRange, dataExplorer}) => { try { - const appPersisted = Object.assign({}, {app: {persisted}}) - window.localStorage.setItem('state', JSON.stringify({ - ...appPersisted, queryConfigs, timeRange, dataExplorer, diff --git a/ui/src/shared/actions/app.js b/ui/src/shared/actions/app.js deleted file mode 100644 index f8939f408..000000000 --- a/ui/src/shared/actions/app.js +++ /dev/null @@ -1,22 +0,0 @@ -import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' - -// ephemeral state reducers -export const enablePresentationMode = () => ({ - type: 'ENABLE_PRESENTATION_MODE', -}) - -export const disablePresentationMode = () => ({ - type: 'DISABLE_PRESENTATION_MODE', -}) - -export const delayEnablePresentationMode = () => (dispatch) => { - setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) -} - -// persistent state reducers -export const setAutoRefresh = (milliseconds) => ({ - type: 'SET_AUTOREFRESH', - payload: { - milliseconds, - }, -}) diff --git a/ui/src/shared/actions/ui.js b/ui/src/shared/actions/ui.js new file mode 100644 index 000000000..740566beb --- /dev/null +++ b/ui/src/shared/actions/ui.js @@ -0,0 +1,19 @@ +import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' + +export function enablePresentationMode() { + return { + type: 'ENABLE_PRESENTATION_MODE', + } +} + +export function disablePresentationMode() { + return { + type: 'DISABLE_PRESENTATION_MODE', + } +} + +export function delayEnablePresentationMode() { + return (dispatch) => { + setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) + } +} diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index ecf560bef..fd79cd620 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -6,34 +6,25 @@ function _fetchTimeSeries(source, db, rp, query) { return proxy({source, db, rp, query}); } -const { - element, - number, - arrayOf, - shape, - oneOfType, - string, -} = PropTypes - export default function AutoRefresh(ComposedComponent) { const wrapper = React.createClass({ displayName: `AutoRefresh_${ComposedComponent.displayName}`, propTypes: { - children: element, - autoRefresh: number.isRequired, - queries: arrayOf(shape({ - host: oneOfType([string, arrayOf(string)]), - text: string, + children: PropTypes.element, + autoRefresh: PropTypes.number, + queries: PropTypes.arrayOf(PropTypes.shape({ + host: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + text: PropTypes.string, }).isRequired).isRequired, }, getInitialState() { return {timeSeries: []}; }, componentDidMount() { - const {queries, autoRefresh} = this.props; + const {queries} = this.props; this.executeQueries(queries); - if (autoRefresh) { - this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh); + if (this.props.autoRefresh) { + this.intervalID = setInterval(() => this.executeQueries(queries), this.props.autoRefresh); } }, componentWillReceiveProps(nextProps) { diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js deleted file mode 100644 index d8bafe2fd..000000000 --- a/ui/src/shared/components/AutoRefreshDropdown.js +++ /dev/null @@ -1,72 +0,0 @@ -import React, {PropTypes} from 'react'; -import classnames from 'classnames'; -import OnClickOutside from 'shared/components/OnClickOutside'; - -import autoRefreshItems from 'hson!../data/autoRefreshes.hson'; - -const { - number, - func, -} = PropTypes - -const AutoRefreshDropdown = React.createClass({ - autobind: false, - - propTypes: { - selected: number.isRequired, - onChoose: func.isRequired, - }, - - getInitialState() { - return { - isOpen: false, - }; - }, - - findAutoRefreshItem(milliseconds) { - return autoRefreshItems.find((values) => values.milliseconds === milliseconds) - }, - - handleClickOutside() { - this.setState({isOpen: false}); - }, - - handleSelection(milliseconds) { - this.props.onChoose(milliseconds); - this.setState({isOpen: false}); - }, - - toggleMenu() { - this.setState({isOpen: !this.state.isOpen}); - }, - - render() { - const self = this; - const {selected} = self.props; - const {isOpen} = self.state; - - return ( -
-
self.toggleMenu()}> - - {this.findAutoRefreshItem(selected).inputValue} - -
- -
- ); - }, -}); - -export default OnClickOutside(AutoRefreshDropdown); diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 2c749449a..178b842d5 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -1,23 +1,14 @@ import React, {PropTypes} from 'react'; -import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; -const { - arrayOf, - shape, - string, - func, -} = PropTypes - const Dropdown = React.createClass({ propTypes: { - items: arrayOf(shape({ - text: string.isRequired, + items: PropTypes.arrayOf(PropTypes.shape({ + text: PropTypes.string.isRequired, })).isRequired, - onChoose: func.isRequired, - selected: string.isRequired, - iconName: string, - className: string, + onChoose: PropTypes.func.isRequired, + selected: PropTypes.string.isRequired, + className: PropTypes.string, }, getInitialState() { return { @@ -48,12 +39,11 @@ const Dropdown = React.createClass({ }, render() { const self = this; - const {items, selected, className, iconName, actions} = self.props; + const {items, selected, className, actions} = self.props; return (
- {iconName ? : null} {selected}
diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 60eace146..e1c2df5e7 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -18,7 +18,6 @@ const { export const LayoutRenderer = React.createClass({ propTypes: { - autoRefresh: number.isRequired, timeRange: shape({ defaultGroupBy: string.isRequired, queryValue: string.isRequired, @@ -47,6 +46,7 @@ export const LayoutRenderer = React.createClass({ name: string.isRequired, }).isRequired ), + autoRefreshMs: number.isRequired, host: string, source: string, onPositionChange: func, @@ -84,7 +84,7 @@ export const LayoutRenderer = React.createClass({ }, generateVisualizations() { - const {autoRefresh, source, cells} = this.props; + const {autoRefreshMs, source, cells} = this.props; return cells.map((cell) => { const qs = cell.queries.map((q) => { @@ -100,7 +100,7 @@ export const LayoutRenderer = React.createClass({

{cell.name || `Graph`}

- +
); @@ -117,7 +117,7 @@ export const LayoutRenderer = React.createClass({
diff --git a/ui/src/shared/components/TimeRangeDropdown.js b/ui/src/shared/components/TimeRangeDropdown.js index 076226896..dbdf5155c 100644 --- a/ui/src/shared/components/TimeRangeDropdown.js +++ b/ui/src/shared/components/TimeRangeDropdown.js @@ -1,5 +1,5 @@ import React from 'react'; -import classnames from 'classnames'; +import cN from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; import timeRanges from 'hson!../data/timeRanges.hson'; @@ -48,7 +48,7 @@ const TimeRangeDropdown = React.createClass({ {selected}
-
    +
    • Time Range
    • {timeRanges.map((item) => { return ( diff --git a/ui/src/shared/constants/index.js b/ui/src/shared/constants/index.js index f6f5bb7b6..6b0d7cb60 100644 --- a/ui/src/shared/constants/index.js +++ b/ui/src/shared/constants/index.js @@ -470,5 +470,3 @@ export const STROKE_WIDTH = { export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds. export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds. - -export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds diff --git a/ui/src/shared/data/autoRefreshes.hson b/ui/src/shared/data/autoRefreshes.hson deleted file mode 100644 index 09b4237ff..000000000 --- a/ui/src/shared/data/autoRefreshes.hson +++ /dev/null @@ -1,7 +0,0 @@ -[ - {milliseconds: 5000, inputValue: 'Every 5 seconds', menuOption: 'Every 5 seconds'}, - {milliseconds: 10000, inputValue: 'Every 10 seconds', menuOption: 'Every 10 seconds'}, - {milliseconds: 15000, inputValue: 'Every 15 seconds', menuOption: 'Every 15 seconds'}, - {milliseconds: 30000, inputValue: 'Every 30 seconds', menuOption: 'Every 30 seconds'}, - {milliseconds: 60000, inputValue: 'Every 60 seconds', menuOption: 'Every 60 seconds'} -] diff --git a/ui/src/shared/dispatchers/index.js b/ui/src/shared/dispatchers/index.js index a7771a9ff..fcf02767f 100644 --- a/ui/src/shared/dispatchers/index.js +++ b/ui/src/shared/dispatchers/index.js @@ -1,4 +1,4 @@ -import {delayEnablePresentationMode} from 'shared/actions/app' +import {delayEnablePresentationMode} from 'shared/actions/ui' import {publishNotification, delayDismissNotification} from 'shared/actions/notifications' import {PRESENTATION_MODE_NOTIFICATION_DELAY} from 'shared/constants' diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js deleted file mode 100644 index adcb4c242..000000000 --- a/ui/src/shared/reducers/app.js +++ /dev/null @@ -1,57 +0,0 @@ -import {combineReducers} from 'redux'; - -import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' - -const initialState = { - ephemeral: { - inPresentationMode: false, - }, - persisted: { - autoRefresh: AUTOREFRESH_DEFAULT, - }, -} - -const { - ephemeral: initialEphemeralState, - persisted: initialPersistedState, -} = initialState - -const ephemeralReducer = (state = initialEphemeralState, action) => { - switch (action.type) { - case 'ENABLE_PRESENTATION_MODE': { - return { - ...state, - inPresentationMode: true, - } - } - - case 'DISABLE_PRESENTATION_MODE': { - return { - ...state, - inPresentationMode: false, - } - } - - default: - return state - } -} - -const persistedReducer = (state = initialPersistedState, action) => { - switch (action.type) { - case 'SET_AUTOREFRESH': { - return { - ...state, - autoRefresh: action.payload.milliseconds, - } - } - - default: - return state - } -} - -export default combineReducers({ - ephemeral: ephemeralReducer, - persisted: persistedReducer, -}) diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index bf191ecd7..47ed0c62f 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -1,13 +1,13 @@ +import appUI from './ui'; import me from './me'; -import app from './app'; import auth from './auth'; import notifications from './notifications'; import sources from './sources'; -export default { +export { + appUI, me, - app, auth, notifications, sources, -} +}; diff --git a/ui/src/shared/reducers/ui.js b/ui/src/shared/reducers/ui.js new file mode 100644 index 000000000..77a2f77a8 --- /dev/null +++ b/ui/src/shared/reducers/ui.js @@ -0,0 +1,23 @@ +const initialState = { + presentationMode: false, +}; + +export default function ui(state = initialState, action) { + switch (action.type) { + case 'ENABLE_PRESENTATION_MODE': { + return { + ...state, + presentationMode: true, + } + } + + case 'DISABLE_PRESENTATION_MODE': { + return { + ...state, + presentationMode: false, + } + } + } + + return state +} diff --git a/ui/src/side_nav/containers/SideNavApp.js b/ui/src/side_nav/containers/SideNavApp.js index 5c2541571..a93bffdf7 100644 --- a/ui/src/side_nav/containers/SideNavApp.js +++ b/ui/src/side_nav/containers/SideNavApp.js @@ -34,9 +34,11 @@ const SideNavApp = React.createClass({ }, }); -const mapStateToProps = ({me, app: {ephemeral: {inPresentationMode}}}) => ({ - me, - inPresentationMode, -}) +function mapStateToProps(state) { + return { + me: state.me, + inPresentationMode: state.appUI.presentationMode, + }; +} export default connect(mapStateToProps)(SideNavApp); diff --git a/ui/src/store/configureStore.js b/ui/src/store/configureStore.js index 5aa6b77ee..44b03fa66 100644 --- a/ui/src/store/configureStore.js +++ b/ui/src/store/configureStore.js @@ -3,8 +3,8 @@ import {combineReducers} from 'redux'; import thunkMiddleware from 'redux-thunk'; import makeQueryExecuter from 'src/shared/middleware/queryExecuter'; import resizeLayout from 'src/shared/middleware/resizeLayout'; -import sharedReducers from 'src/shared/reducers'; -import dataExplorerReducers from 'src/data_explorer/reducers'; +import * as dataExplorerReducers from 'src/data_explorer/reducers'; +import * as sharedReducers from 'src/shared/reducers'; import rulesReducer from 'src/kapacitor/reducers/rules'; import dashboardUI from 'src/dashboards/reducers/ui'; import persistStateEnhancer from './persistStateEnhancer'; From 2751a0081373c1e5061f7140d4ac327e0584e0a4 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 3 Mar 2017 15:30:33 -0800 Subject: [PATCH 18/47] Fix revert failure chain and thus: add autoRefresh with pause --- CHANGELOG.md | 1 + ui/src/dashboards/components/Dashboard.js | 10 ++-- .../dashboards/components/DashboardHeader.js | 13 ++++- ui/src/dashboards/containers/DashboardPage.js | 12 +++- .../data_explorer/components/Visualization.js | 6 +- .../data_explorer/containers/DataExplorer.js | 30 +++++++--- ui/src/data_explorer/containers/Header.js | 29 +++++++--- ui/src/data_explorer/reducers/index.js | 4 +- ui/src/hosts/containers/HostPage.js | 21 +++++-- ui/src/index.js | 2 +- .../components/KubernetesDashboard.js | 16 ++++-- .../kubernetes/containers/KubernetesPage.js | 20 +++++-- ui/src/localStorage.js | 5 +- ui/src/shared/actions/app.js | 22 +++++++ ui/src/shared/actions/ui.js | 19 ------- ui/src/shared/components/AutoRefresh.js | 25 +++++--- ui/src/shared/components/Dropdown.js | 22 +++++-- ui/src/shared/components/LayoutRenderer.js | 8 +-- ui/src/shared/components/TimeRangeDropdown.js | 4 +- ui/src/shared/constants/index.js | 2 + ui/src/shared/dispatchers/index.js | 2 +- ui/src/shared/reducers/app.js | 57 +++++++++++++++++++ ui/src/shared/reducers/index.js | 8 +-- ui/src/shared/reducers/ui.js | 23 -------- ui/src/side_nav/containers/SideNavApp.js | 10 ++-- ui/src/store/configureStore.js | 4 +- 26 files changed, 250 insertions(+), 125 deletions(-) create mode 100644 ui/src/shared/actions/app.js delete mode 100644 ui/src/shared/actions/ui.js create mode 100644 ui/src/shared/reducers/app.js delete mode 100644 ui/src/shared/reducers/ui.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d157bc66b..9a6da3508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ 4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable 5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position 6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for [Heroku](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#heroku) and [Google](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#google) + 7. [#781](https://github.com/influxdata/chronograf/issues/781): Add global auto-refresh dropdown to all graph dashboards ### UI Improvements 1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 46eb173b0..3ff1c1ca4 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -10,6 +10,7 @@ const Dashboard = ({ inPresentationMode, onPositionChange, source, + autoRefresh, timeRange, }) => { if (dashboard.id === 0) { @@ -20,14 +21,13 @@ const Dashboard = ({
      {isEditMode ? : null} - {Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)} + {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange)}
      ) } -Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => { - const autoRefreshMs = 15000 +Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange) => { const cells = dashboard.cells.map((cell, i) => { i = `${i}` const dashboardCell = {...cell, i} @@ -42,7 +42,7 @@ Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => @@ -54,6 +54,7 @@ const { func, shape, string, + number, } = PropTypes Dashboard.propTypes = { @@ -66,6 +67,7 @@ Dashboard.propTypes = { proxy: string, }).isRequired, }).isRequired, + autoRefresh: number.isRequired, timeRange: shape({}).isRequired, } diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index be6fd69a0..55dd1b7b0 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -2,6 +2,7 @@ import React, {PropTypes} from 'react' import ReactTooltip from 'react-tooltip' import {Link} from 'react-router'; +import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' const DashboardHeader = ({ @@ -10,8 +11,10 @@ const DashboardHeader = ({ dashboard, headerText, timeRange, + autoRefresh, isHidden, handleChooseTimeRange, + handleChooseAutoRefresh, handleClickPresentationButton, sourceID, }) => isHidden ? null : ( @@ -45,6 +48,7 @@ const DashboardHeader = ({ Graph Tips
+
@@ -55,11 +59,12 @@ const DashboardHeader = ({ ) const { - shape, array, - string, - func, bool, + func, + number, + shape, + string, } = PropTypes DashboardHeader.propTypes = { @@ -69,8 +74,10 @@ DashboardHeader.propTypes = { dashboard: shape({}), headerText: string, timeRange: shape({}).isRequired, + autoRefresh: number.isRequired, isHidden: bool.isRequired, handleChooseTimeRange: func.isRequired, + handleChooseAutoRefresh: func.isRequired, handleClickPresentationButton: func.isRequired, } diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index c83c92020..0b6efe1b4 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -51,6 +51,7 @@ const DashboardPage = React.createClass({ id: number.isRequired, cells: arrayOf(shape({})).isRequired, }).isRequired, + autoRefresh: number.isRequired, timeRange: shape({}).isRequired, inPresentationMode: bool.isRequired, isEditMode: bool.isRequired, @@ -100,6 +101,7 @@ const DashboardPage = React.createClass({ isEditMode, handleClickPresentationButton, source, + autoRefresh, timeRange, } = this.props @@ -110,6 +112,7 @@ const DashboardPage = React.createClass({ {}} /> :
@@ -143,7 +147,10 @@ const DashboardPage = React.createClass({ const mapStateToProps = (state) => { const { - appUI, + app: { + ephemeral: {inPresentationMode}, + persisted: {autoRefresh}, + }, dashboardUI: { dashboards, dashboard, @@ -153,11 +160,12 @@ const mapStateToProps = (state) => { } = state return { - inPresentationMode: appUI.presentationMode, dashboards, dashboard, + autoRefresh, timeRange, isEditMode, + inPresentationMode, } } diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index b88972a9e..b58355da6 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -15,6 +15,7 @@ const { const Visualization = React.createClass({ propTypes: { + autoRefresh: number.isRequired, timeRange: shape({ upper: string, lower: string, @@ -45,7 +46,7 @@ const Visualization = React.createClass({ }, render() { - const {queryConfigs, timeRange, activeQueryIndex, height, heightPixels} = this.props; + const {queryConfigs, autoRefresh, timeRange, activeQueryIndex, height, heightPixels} = this.props; const {source} = this.context; const proxyLink = source.links.proxy; @@ -57,7 +58,6 @@ const Visualization = React.createClass({ const queries = statements.filter((s) => s.text !== null).map((s) => { return {host: [proxyLink], text: s.text, id: s.id}; }); - const autoRefreshMs = 10000; const isInDataExplorer = true; return ( @@ -77,7 +77,7 @@ const Visualization = React.createClass({ {isGraphInView ? ( diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index 68a7c7b2e..9468fca75 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -1,17 +1,18 @@ import React, {PropTypes} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import QueryBuilder from '../components/QueryBuilder'; import Visualization from '../components/Visualization'; import Header from '../containers/Header'; import ResizeContainer from 'src/shared/components/ResizeContainer'; -import { - setTimeRange as setTimeRangeAction, -} from '../actions/view'; +import {setAutoRefresh} from 'shared/actions/app' +import {setTimeRange as setTimeRangeAction} from '../actions/view'; const { arrayOf, func, + number, shape, string, } = PropTypes; @@ -25,6 +26,8 @@ const DataExplorer = React.createClass({ }).isRequired, }).isRequired, queryConfigs: PropTypes.shape({}), + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, timeRange: shape({ upper: string, lower: string, @@ -59,18 +62,20 @@ const DataExplorer = React.createClass({ }, render() { - const {timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; + const {autoRefresh, handleChooseAutoRefresh, timeRange, setTimeRange, queryConfigs, dataExplorer} = this.props; const {activeQueryID} = this.state; const queries = dataExplorer.queryIDs.map((qid) => queryConfigs[qid]); return (
@@ -50,6 +62,7 @@ const Header = React.createClass({ {this.context.source.name}
+
diff --git a/ui/src/data_explorer/reducers/index.js b/ui/src/data_explorer/reducers/index.js index cccca5d79..6db959b19 100644 --- a/ui/src/data_explorer/reducers/index.js +++ b/ui/src/data_explorer/reducers/index.js @@ -2,8 +2,8 @@ import queryConfigs from './queryConfigs'; import timeRange from './timeRange'; import dataExplorer from './ui'; -export { +export default { queryConfigs, timeRange, dataExplorer, -}; +} diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 38ff3ae16..641aa63f1 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -1,6 +1,7 @@ import React, {PropTypes} from 'react' import {Link} from 'react-router' import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import _ from 'lodash' import classnames from 'classnames'; @@ -9,6 +10,8 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis'; import {fetchLayouts} from 'shared/apis'; + +import {setAutoRefresh} from 'shared/actions/app' import {presentationButtonDispatcher} from 'shared/dispatchers' const { @@ -16,6 +19,7 @@ const { string, bool, func, + number, } = PropTypes export const HostPage = React.createClass({ @@ -35,6 +39,8 @@ export const HostPage = React.createClass({ app: string, }), }), + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool, handleClickPresentationButton: func, }, @@ -87,9 +93,8 @@ export const HostPage = React.createClass({ }, renderLayouts(layouts) { - const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source} = this.props; + const {source, autoRefresh} = this.props; const autoflowLayouts = layouts.filter((layout) => !!layout.autoflow); @@ -137,7 +142,7 @@ export const HostPage = React.createClass({ @@ -145,7 +150,7 @@ export const HostPage = React.createClass({ }, render() { - const {params: {hostID}, location: {query: {app}}, source: {id}, inPresentationMode, handleClickPresentationButton} = this.props + const {params: {hostID}, location: {query: {app}}, source: {id}, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props const {layouts, timeRange, hosts} = this.state const appParam = app ? `?app=${app}` : '' @@ -153,9 +158,11 @@ export const HostPage = React.createClass({
{Object.keys(hosts).map((host, i) => { @@ -181,11 +188,13 @@ export const HostPage = React.createClass({ }, }); -const mapStateToProps = (state) => ({ - inPresentationMode: state.appUI.presentationMode, +const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ + inPresentationMode, + autoRefresh, }) const mapDispatchToProps = (dispatch) => ({ + handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/index.js b/ui/src/index.js index c2a8f4cd3..e45201ad6 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -19,7 +19,7 @@ import configureStore from 'src/store/configureStore'; import {getMe, getSources} from 'shared/apis'; import {receiveMe} from 'shared/actions/me'; import {receiveAuth} from 'shared/actions/auth'; -import {disablePresentationMode} from 'shared/actions/ui'; +import {disablePresentationMode} from 'shared/actions/app'; import {loadLocalStorage} from './localStorage'; import 'src/style/chronograf.scss'; diff --git a/ui/src/kubernetes/components/KubernetesDashboard.js b/ui/src/kubernetes/components/KubernetesDashboard.js index 2c4afc5b4..6092b03e8 100644 --- a/ui/src/kubernetes/components/KubernetesDashboard.js +++ b/ui/src/kubernetes/components/KubernetesDashboard.js @@ -6,11 +6,12 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; const { - shape, - string, arrayOf, bool, func, + number, + shape, + string, } = PropTypes export const KubernetesDashboard = React.createClass({ @@ -22,6 +23,8 @@ export const KubernetesDashboard = React.createClass({ telegraf: string.isRequired, }), layouts: arrayOf(shape().isRequired).isRequired, + autoRefresh: number.isRequired, + handleChooseAutoRefresh: func.isRequired, inPresentationMode: bool.isRequired, handleClickPresentationButton: func, }, @@ -34,9 +37,8 @@ export const KubernetesDashboard = React.createClass({ }, renderLayouts(layouts) { - const autoRefreshMs = 15000; const {timeRange} = this.state; - const {source} = this.props; + const {source, autoRefresh} = this.props; let layoutCells = []; layouts.forEach((layout) => { @@ -56,7 +58,7 @@ export const KubernetesDashboard = React.createClass({ ); @@ -68,7 +70,7 @@ export const KubernetesDashboard = React.createClass({ }, render() { - const {layouts, inPresentationMode, handleClickPresentationButton} = this.props; + const {layouts, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props; const {timeRange} = this.state; const emptyState = (
@@ -81,6 +83,8 @@ export const KubernetesDashboard = React.createClass({
@@ -51,11 +59,13 @@ export const KubernetesPage = React.createClass({ }, }); -const mapStateToProps = (state) => ({ - inPresentationMode: state.appUI.presentationMode, +const mapStateToProps = ({app: {ephemeral: {inPresentationMode}, persisted: {autoRefresh}}}) => ({ + inPresentationMode, + autoRefresh, }) const mapDispatchToProps = (dispatch) => ({ + handleChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch), handleClickPresentationButton: presentationButtonDispatcher(dispatch), }) diff --git a/ui/src/localStorage.js b/ui/src/localStorage.js index 5cd29aa07..4b8381503 100644 --- a/ui/src/localStorage.js +++ b/ui/src/localStorage.js @@ -9,9 +9,12 @@ export const loadLocalStorage = () => { } }; -export const saveToLocalStorage = ({queryConfigs, timeRange, dataExplorer}) => { +export const saveToLocalStorage = ({app: {persisted}, queryConfigs, timeRange, dataExplorer}) => { try { + const appPersisted = Object.assign({}, {app: {persisted}}) + window.localStorage.setItem('state', JSON.stringify({ + ...appPersisted, queryConfigs, timeRange, dataExplorer, diff --git a/ui/src/shared/actions/app.js b/ui/src/shared/actions/app.js new file mode 100644 index 000000000..f8939f408 --- /dev/null +++ b/ui/src/shared/actions/app.js @@ -0,0 +1,22 @@ +import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' + +// ephemeral state reducers +export const enablePresentationMode = () => ({ + type: 'ENABLE_PRESENTATION_MODE', +}) + +export const disablePresentationMode = () => ({ + type: 'DISABLE_PRESENTATION_MODE', +}) + +export const delayEnablePresentationMode = () => (dispatch) => { + setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) +} + +// persistent state reducers +export const setAutoRefresh = (milliseconds) => ({ + type: 'SET_AUTOREFRESH', + payload: { + milliseconds, + }, +}) diff --git a/ui/src/shared/actions/ui.js b/ui/src/shared/actions/ui.js deleted file mode 100644 index 740566beb..000000000 --- a/ui/src/shared/actions/ui.js +++ /dev/null @@ -1,19 +0,0 @@ -import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' - -export function enablePresentationMode() { - return { - type: 'ENABLE_PRESENTATION_MODE', - } -} - -export function disablePresentationMode() { - return { - type: 'DISABLE_PRESENTATION_MODE', - } -} - -export function delayEnablePresentationMode() { - return (dispatch) => { - setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) - } -} diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index fd79cd620..ecf560bef 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -6,25 +6,34 @@ function _fetchTimeSeries(source, db, rp, query) { return proxy({source, db, rp, query}); } +const { + element, + number, + arrayOf, + shape, + oneOfType, + string, +} = PropTypes + export default function AutoRefresh(ComposedComponent) { const wrapper = React.createClass({ displayName: `AutoRefresh_${ComposedComponent.displayName}`, propTypes: { - children: PropTypes.element, - autoRefresh: PropTypes.number, - queries: PropTypes.arrayOf(PropTypes.shape({ - host: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), - text: PropTypes.string, + children: element, + autoRefresh: number.isRequired, + queries: arrayOf(shape({ + host: oneOfType([string, arrayOf(string)]), + text: string, }).isRequired).isRequired, }, getInitialState() { return {timeSeries: []}; }, componentDidMount() { - const {queries} = this.props; + const {queries, autoRefresh} = this.props; this.executeQueries(queries); - if (this.props.autoRefresh) { - this.intervalID = setInterval(() => this.executeQueries(queries), this.props.autoRefresh); + if (autoRefresh) { + this.intervalID = setInterval(() => this.executeQueries(queries), autoRefresh); } }, componentWillReceiveProps(nextProps) { diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 178b842d5..2c749449a 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -1,14 +1,23 @@ import React, {PropTypes} from 'react'; +import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; +const { + arrayOf, + shape, + string, + func, +} = PropTypes + const Dropdown = React.createClass({ propTypes: { - items: PropTypes.arrayOf(PropTypes.shape({ - text: PropTypes.string.isRequired, + items: arrayOf(shape({ + text: string.isRequired, })).isRequired, - onChoose: PropTypes.func.isRequired, - selected: PropTypes.string.isRequired, - className: PropTypes.string, + onChoose: func.isRequired, + selected: string.isRequired, + iconName: string, + className: string, }, getInitialState() { return { @@ -39,11 +48,12 @@ const Dropdown = React.createClass({ }, render() { const self = this; - const {items, selected, className, actions} = self.props; + const {items, selected, className, iconName, actions} = self.props; return (
+ {iconName ? : null} {selected}
diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index e1c2df5e7..60eace146 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -18,6 +18,7 @@ const { export const LayoutRenderer = React.createClass({ propTypes: { + autoRefresh: number.isRequired, timeRange: shape({ defaultGroupBy: string.isRequired, queryValue: string.isRequired, @@ -46,7 +47,6 @@ export const LayoutRenderer = React.createClass({ name: string.isRequired, }).isRequired ), - autoRefreshMs: number.isRequired, host: string, source: string, onPositionChange: func, @@ -84,7 +84,7 @@ export const LayoutRenderer = React.createClass({ }, generateVisualizations() { - const {autoRefreshMs, source, cells} = this.props; + const {autoRefresh, source, cells} = this.props; return cells.map((cell) => { const qs = cell.queries.map((q) => { @@ -100,7 +100,7 @@ export const LayoutRenderer = React.createClass({

{cell.name || `Graph`}

- +
); @@ -117,7 +117,7 @@ export const LayoutRenderer = React.createClass({
diff --git a/ui/src/shared/components/TimeRangeDropdown.js b/ui/src/shared/components/TimeRangeDropdown.js index dbdf5155c..076226896 100644 --- a/ui/src/shared/components/TimeRangeDropdown.js +++ b/ui/src/shared/components/TimeRangeDropdown.js @@ -1,5 +1,5 @@ import React from 'react'; -import cN from 'classnames'; +import classnames from 'classnames'; import OnClickOutside from 'shared/components/OnClickOutside'; import timeRanges from 'hson!../data/timeRanges.hson'; @@ -48,7 +48,7 @@ const TimeRangeDropdown = React.createClass({ {selected}
-
    +
    • Time Range
    • {timeRanges.map((item) => { return ( diff --git a/ui/src/shared/constants/index.js b/ui/src/shared/constants/index.js index 6b0d7cb60..f6f5bb7b6 100644 --- a/ui/src/shared/constants/index.js +++ b/ui/src/shared/constants/index.js @@ -470,3 +470,5 @@ export const STROKE_WIDTH = { export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds. export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds. + +export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds diff --git a/ui/src/shared/dispatchers/index.js b/ui/src/shared/dispatchers/index.js index fcf02767f..a7771a9ff 100644 --- a/ui/src/shared/dispatchers/index.js +++ b/ui/src/shared/dispatchers/index.js @@ -1,4 +1,4 @@ -import {delayEnablePresentationMode} from 'shared/actions/ui' +import {delayEnablePresentationMode} from 'shared/actions/app' import {publishNotification, delayDismissNotification} from 'shared/actions/notifications' import {PRESENTATION_MODE_NOTIFICATION_DELAY} from 'shared/constants' diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js new file mode 100644 index 000000000..adcb4c242 --- /dev/null +++ b/ui/src/shared/reducers/app.js @@ -0,0 +1,57 @@ +import {combineReducers} from 'redux'; + +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' + +const initialState = { + ephemeral: { + inPresentationMode: false, + }, + persisted: { + autoRefresh: AUTOREFRESH_DEFAULT, + }, +} + +const { + ephemeral: initialEphemeralState, + persisted: initialPersistedState, +} = initialState + +const ephemeralReducer = (state = initialEphemeralState, action) => { + switch (action.type) { + case 'ENABLE_PRESENTATION_MODE': { + return { + ...state, + inPresentationMode: true, + } + } + + case 'DISABLE_PRESENTATION_MODE': { + return { + ...state, + inPresentationMode: false, + } + } + + default: + return state + } +} + +const persistedReducer = (state = initialPersistedState, action) => { + switch (action.type) { + case 'SET_AUTOREFRESH': { + return { + ...state, + autoRefresh: action.payload.milliseconds, + } + } + + default: + return state + } +} + +export default combineReducers({ + ephemeral: ephemeralReducer, + persisted: persistedReducer, +}) diff --git a/ui/src/shared/reducers/index.js b/ui/src/shared/reducers/index.js index 47ed0c62f..bf191ecd7 100644 --- a/ui/src/shared/reducers/index.js +++ b/ui/src/shared/reducers/index.js @@ -1,13 +1,13 @@ -import appUI from './ui'; import me from './me'; +import app from './app'; import auth from './auth'; import notifications from './notifications'; import sources from './sources'; -export { - appUI, +export default { me, + app, auth, notifications, sources, -}; +} diff --git a/ui/src/shared/reducers/ui.js b/ui/src/shared/reducers/ui.js deleted file mode 100644 index 77a2f77a8..000000000 --- a/ui/src/shared/reducers/ui.js +++ /dev/null @@ -1,23 +0,0 @@ -const initialState = { - presentationMode: false, -}; - -export default function ui(state = initialState, action) { - switch (action.type) { - case 'ENABLE_PRESENTATION_MODE': { - return { - ...state, - presentationMode: true, - } - } - - case 'DISABLE_PRESENTATION_MODE': { - return { - ...state, - presentationMode: false, - } - } - } - - return state -} diff --git a/ui/src/side_nav/containers/SideNavApp.js b/ui/src/side_nav/containers/SideNavApp.js index a93bffdf7..5c2541571 100644 --- a/ui/src/side_nav/containers/SideNavApp.js +++ b/ui/src/side_nav/containers/SideNavApp.js @@ -34,11 +34,9 @@ const SideNavApp = React.createClass({ }, }); -function mapStateToProps(state) { - return { - me: state.me, - inPresentationMode: state.appUI.presentationMode, - }; -} +const mapStateToProps = ({me, app: {ephemeral: {inPresentationMode}}}) => ({ + me, + inPresentationMode, +}) export default connect(mapStateToProps)(SideNavApp); diff --git a/ui/src/store/configureStore.js b/ui/src/store/configureStore.js index 44b03fa66..5aa6b77ee 100644 --- a/ui/src/store/configureStore.js +++ b/ui/src/store/configureStore.js @@ -3,8 +3,8 @@ import {combineReducers} from 'redux'; import thunkMiddleware from 'redux-thunk'; import makeQueryExecuter from 'src/shared/middleware/queryExecuter'; import resizeLayout from 'src/shared/middleware/resizeLayout'; -import * as dataExplorerReducers from 'src/data_explorer/reducers'; -import * as sharedReducers from 'src/shared/reducers'; +import sharedReducers from 'src/shared/reducers'; +import dataExplorerReducers from 'src/data_explorer/reducers'; import rulesReducer from 'src/kapacitor/reducers/rules'; import dashboardUI from 'src/dashboards/reducers/ui'; import persistStateEnhancer from './persistStateEnhancer'; From 80e6e14b0db61d5e7335a3ea650f7f073c139343 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 3 Mar 2017 16:57:05 -0800 Subject: [PATCH 19/47] Add test for autoRefresh reducer --- ui/spec/shared/reducers/appSpec.js | 12 ++++++++++++ ui/src/shared/actions/app.js | 4 ++-- ui/src/shared/reducers/app.js | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 ui/spec/shared/reducers/appSpec.js diff --git a/ui/spec/shared/reducers/appSpec.js b/ui/spec/shared/reducers/appSpec.js new file mode 100644 index 000000000..b238279bf --- /dev/null +++ b/ui/spec/shared/reducers/appSpec.js @@ -0,0 +1,12 @@ +import {ephemeralReducer, persistedReducer} from 'src/shared/reducers/app' +import {setAutoRefresh} from 'src/shared/actions/app' + +describe('Shared.Reducers.app.persisted', () => { + it('should handle SET_AUTOREFRESH', () => { + const initialState = {autoRefresh: 15000} + const milliseconds = 0 + + const reducedState = persistedReducer(initialState, setAutoRefresh(milliseconds)); + expect(reducedState.autoRefresh).to.equal(0); + }) +}) diff --git a/ui/src/shared/actions/app.js b/ui/src/shared/actions/app.js index f8939f408..87c78706a 100644 --- a/ui/src/shared/actions/app.js +++ b/ui/src/shared/actions/app.js @@ -1,6 +1,6 @@ import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants' -// ephemeral state reducers +// ephemeral state action creators export const enablePresentationMode = () => ({ type: 'ENABLE_PRESENTATION_MODE', }) @@ -13,7 +13,7 @@ export const delayEnablePresentationMode = () => (dispatch) => { setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY) } -// persistent state reducers +// persistent state action creators export const setAutoRefresh = (milliseconds) => ({ type: 'SET_AUTOREFRESH', payload: { diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js index adcb4c242..c8bc89ec9 100644 --- a/ui/src/shared/reducers/app.js +++ b/ui/src/shared/reducers/app.js @@ -16,7 +16,7 @@ const { persisted: initialPersistedState, } = initialState -const ephemeralReducer = (state = initialEphemeralState, action) => { +export const ephemeralReducer = (state = initialEphemeralState, action) => { switch (action.type) { case 'ENABLE_PRESENTATION_MODE': { return { @@ -37,7 +37,7 @@ const ephemeralReducer = (state = initialEphemeralState, action) => { } } -const persistedReducer = (state = initialPersistedState, action) => { +export const persistedReducer = (state = initialPersistedState, action) => { switch (action.type) { case 'SET_AUTOREFRESH': { return { From b577515b06f2130be35a831dfac418a7bb7f1c50 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 3 Mar 2017 17:06:47 -0800 Subject: [PATCH 20/47] Refactor to test combined appReducer, add tests for ephemeral app state reducer --- ui/spec/shared/reducers/appSpec.js | 42 +++++++++++++++++++++++++----- ui/src/shared/reducers/app.js | 4 +-- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/ui/spec/shared/reducers/appSpec.js b/ui/spec/shared/reducers/appSpec.js index b238279bf..e43ee650a 100644 --- a/ui/spec/shared/reducers/appSpec.js +++ b/ui/spec/shared/reducers/appSpec.js @@ -1,12 +1,40 @@ -import {ephemeralReducer, persistedReducer} from 'src/shared/reducers/app' -import {setAutoRefresh} from 'src/shared/actions/app' +import appReducer from 'src/shared/reducers/app' +import { + enablePresentationMode, + disablePresentationMode, + // delayEnablePresentationMode, + setAutoRefresh, +} from 'src/shared/actions/app' + +describe('Shared.Reducers.appReducer', () => { + const initialState = { + ephemeral: { + inPresentationMode: false, + }, + persisted: { + autoRefresh: 0 + }, + } + + it('should handle ENABLE_PRESENTATION_MODE', () => { + const reducedState = appReducer(initialState, enablePresentationMode()); + + expect(reducedState.ephemeral.inPresentationMode).to.equal(true); + }) + + it('should handle DISABLE_PRESENTATION_MODE', () => { + Object.assign(initialState, {ephemeral: {inPresentationMode: true}}) + + const reducedState = appReducer(initialState, disablePresentationMode()); + + expect(reducedState.ephemeral.inPresentationMode).to.equal(false); + }) -describe('Shared.Reducers.app.persisted', () => { it('should handle SET_AUTOREFRESH', () => { - const initialState = {autoRefresh: 15000} - const milliseconds = 0 + const expectedMs = 15000 - const reducedState = persistedReducer(initialState, setAutoRefresh(milliseconds)); - expect(reducedState.autoRefresh).to.equal(0); + const reducedState = appReducer(initialState, setAutoRefresh(expectedMs)); + + expect(reducedState.persisted.autoRefresh).to.equal(expectedMs); }) }) diff --git a/ui/src/shared/reducers/app.js b/ui/src/shared/reducers/app.js index c8bc89ec9..adcb4c242 100644 --- a/ui/src/shared/reducers/app.js +++ b/ui/src/shared/reducers/app.js @@ -16,7 +16,7 @@ const { persisted: initialPersistedState, } = initialState -export const ephemeralReducer = (state = initialEphemeralState, action) => { +const ephemeralReducer = (state = initialEphemeralState, action) => { switch (action.type) { case 'ENABLE_PRESENTATION_MODE': { return { @@ -37,7 +37,7 @@ export const ephemeralReducer = (state = initialEphemeralState, action) => { } } -export const persistedReducer = (state = initialPersistedState, action) => { +const persistedReducer = (state = initialPersistedState, action) => { switch (action.type) { case 'SET_AUTOREFRESH': { return { From 8dc012aeac2e1c52d26eab6bc90e3561250f4417 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Mon, 6 Mar 2017 10:11:52 -0600 Subject: [PATCH 21/47] Update to fix go linter issues --- bolt/alerts.go | 2 ++ bolt/client.go | 2 ++ bolt/dashboards.go | 16 +++++++++------- bolt/layouts.go | 2 ++ bolt/servers.go | 3 +++ bolt/sources.go | 2 ++ canned/apps.go | 14 ++++++++++---- canned/bin.go | 1 + dist/dir.go | 1 + enterprise/enterprise.go | 2 +- enterprise/meta_test.go | 4 ++-- kapacitor/http_out.go | 2 ++ kapacitor/operators.go | 8 ++++---- kapacitor/validate.go | 3 +++ kapacitor/vars.go | 13 ++++++------- log/log.go | 1 + oauth2/doc.go | 2 +- oauth2/google.go | 3 ++- oauth2/heroku.go | 10 +++++----- oauth2/mux.go | 3 ++- oauth2/oauth2_test.go | 4 ++-- server/server.go | 3 +++ server/url_prefixer.go | 5 +++-- server/version.go | 1 + 24 files changed, 70 insertions(+), 37 deletions(-) diff --git a/bolt/alerts.go b/bolt/alerts.go index d67264840..d49f4d3a1 100644 --- a/bolt/alerts.go +++ b/bolt/alerts.go @@ -11,8 +11,10 @@ import ( // Ensure AlertsStore implements chronograf.AlertsStore. var _ chronograf.AlertRulesStore = &AlertsStore{} +// AlertsBucket is the name of the bucket alert configuration is stored in var AlertsBucket = []byte("Alerts") +// AlertsStore represents the bolt implementation of a store for alerts type AlertsStore struct { client *Client } diff --git a/bolt/client.go b/bolt/client.go index e34a37828..ae57cc558 100644 --- a/bolt/client.go +++ b/bolt/client.go @@ -23,6 +23,7 @@ type Client struct { DashboardsStore *DashboardsStore } +// NewClient initializes all stores func NewClient() *Client { c := &Client{Now: time.Now} c.SourcesStore = &SourcesStore{client: c} @@ -79,6 +80,7 @@ func (c *Client) Open() error { return nil } +// Close the connection to the bolt database func (c *Client) Close() error { if c.db != nil { return c.db.Close() diff --git a/bolt/dashboards.go b/bolt/dashboards.go index 4df9ebf8e..28cf83f41 100644 --- a/bolt/dashboards.go +++ b/bolt/dashboards.go @@ -12,8 +12,10 @@ import ( // Ensure DashboardsStore implements chronograf.DashboardsStore. var _ chronograf.DashboardsStore = &DashboardsStore{} +// DashboardBucket is the bolt bucket dashboards are stored in var DashboardBucket = []byte("Dashoard") +// DashboardsStore is the bolt implementation of storing dashboards type DashboardsStore struct { client *Client IDs chronograf.DashboardID @@ -81,9 +83,9 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c } // Delete the dashboard from DashboardsStore -func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) error { - if err := s.client.db.Update(func(tx *bolt.Tx) error { - if err := tx.Bucket(DashboardBucket).Delete(itob(int(d.ID))); err != nil { +func (d *DashboardsStore) Delete(ctx context.Context, dash chronograf.Dashboard) error { + if err := d.client.db.Update(func(tx *bolt.Tx) error { + if err := tx.Bucket(DashboardBucket).Delete(itob(int(dash.ID))); err != nil { return err } return nil @@ -95,16 +97,16 @@ func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) er } // Update the dashboard in DashboardsStore -func (s *DashboardsStore) Update(ctx context.Context, d chronograf.Dashboard) error { - if err := s.client.db.Update(func(tx *bolt.Tx) error { +func (d *DashboardsStore) Update(ctx context.Context, dash chronograf.Dashboard) error { + if err := d.client.db.Update(func(tx *bolt.Tx) error { // Get an existing dashboard with the same ID. b := tx.Bucket(DashboardBucket) - strID := strconv.Itoa(int(d.ID)) + strID := strconv.Itoa(int(dash.ID)) if v := b.Get([]byte(strID)); v == nil { return chronograf.ErrDashboardNotFound } - if v, err := internal.MarshalDashboard(d); err != nil { + if v, err := internal.MarshalDashboard(dash); err != nil { return err } else if err := b.Put([]byte(strID), v); err != nil { return err diff --git a/bolt/layouts.go b/bolt/layouts.go index bd6ce4a8c..e443f80b1 100644 --- a/bolt/layouts.go +++ b/bolt/layouts.go @@ -11,8 +11,10 @@ import ( // Ensure LayoutStore implements chronograf.LayoutStore. var _ chronograf.LayoutStore = &LayoutStore{} +// LayoutBucket is the bolt bucket layouts are stored in var LayoutBucket = []byte("Layout") +// LayoutStore is the bolt implementation to store layouts type LayoutStore struct { client *Client IDs chronograf.ID diff --git a/bolt/servers.go b/bolt/servers.go index d9a4ebccb..63bd7c801 100644 --- a/bolt/servers.go +++ b/bolt/servers.go @@ -11,8 +11,11 @@ import ( // Ensure ServersStore implements chronograf.ServersStore. var _ chronograf.ServersStore = &ServersStore{} +// ServersBucket is the bolt bucket to store lists of servers var ServersBucket = []byte("Servers") +// ServersStore is the bolt implementation to store servers in a store. +// Used store servers that are associated in some way with a source type ServersStore struct { client *Client } diff --git a/bolt/sources.go b/bolt/sources.go index 46ced92b8..a2809ff23 100644 --- a/bolt/sources.go +++ b/bolt/sources.go @@ -11,8 +11,10 @@ import ( // Ensure SourcesStore implements chronograf.SourcesStore. var _ chronograf.SourcesStore = &SourcesStore{} +// SourcesBucket is the bolt bucket used to store source information var SourcesBucket = []byte("Sources") +// SourcesStore is a bolt implementation to store time-series source information. type SourcesStore struct { client *Client } diff --git a/canned/apps.go b/canned/apps.go index 1fd8440d3..8ad626e8d 100644 --- a/canned/apps.go +++ b/canned/apps.go @@ -11,6 +11,7 @@ import ( "github.com/influxdata/chronograf" ) +// AppExt is the the file extension searched for in the directory for layout files const AppExt = ".json" // Apps are canned JSON layouts. Implements LayoutStore. @@ -25,6 +26,7 @@ type Apps struct { Logger chronograf.Logger } +// NewApps constructs a layout store wrapping a file system directory func NewApps(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.LayoutStore { return &Apps{ Dir: dir, @@ -63,14 +65,14 @@ func createLayout(file string, layout chronograf.Layout) error { defer h.Close() if octets, err := json.MarshalIndent(layout, " ", " "); err != nil { return chronograf.ErrLayoutInvalid - } else { - if _, err := h.Write(octets); err != nil { - return err - } + } else if _, err := h.Write(octets); err != nil { + return err } + return nil } +// All returns all layouts from the directory func (a *Apps) All(ctx context.Context) ([]chronograf.Layout, error) { files, err := a.ReadDir(a.Dir) if err != nil { @@ -91,6 +93,7 @@ func (a *Apps) All(ctx context.Context) ([]chronograf.Layout, error) { return layouts, nil } +// Add creates a new layout within the directory func (a *Apps) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) { var err error layout.ID, err = a.IDs.Generate() @@ -118,6 +121,7 @@ func (a *Apps) Add(ctx context.Context, layout chronograf.Layout) (chronograf.La return layout, nil } +// Delete removes a layout file from the directory func (a *Apps) Delete(ctx context.Context, layout chronograf.Layout) error { _, file, err := a.idToFile(layout.ID) if err != nil { @@ -134,6 +138,7 @@ func (a *Apps) Delete(ctx context.Context, layout chronograf.Layout) error { return nil } +// Get returns an app file from the layout directory func (a *Apps) Get(ctx context.Context, ID string) (chronograf.Layout, error) { l, file, err := a.idToFile(ID) if err != nil { @@ -157,6 +162,7 @@ func (a *Apps) Get(ctx context.Context, ID string) (chronograf.Layout, error) { return l, nil } +// Update replaces a layout from the file system directory func (a *Apps) Update(ctx context.Context, layout chronograf.Layout) error { l, _, err := a.idToFile(layout.ID) if err != nil { diff --git a/canned/bin.go b/canned/bin.go index 2c32e9e04..1b5fd4399 100644 --- a/canned/bin.go +++ b/canned/bin.go @@ -10,6 +10,7 @@ import ( //go:generate go-bindata -o bin_gen.go -ignore README|apps|.sh|go -pkg canned . +// BinLayoutStore represents a layout store using data generated by go-bindata type BinLayoutStore struct { Logger chronograf.Logger } diff --git a/dist/dir.go b/dist/dir.go index 828ef231d..1f4ac90b9 100644 --- a/dist/dir.go +++ b/dist/dir.go @@ -11,6 +11,7 @@ type Dir struct { dir http.Dir } +// NewDir constructs a Dir with a default file func NewDir(dir, def string) Dir { return Dir{ Default: def, diff --git a/enterprise/enterprise.go b/enterprise/enterprise.go index 3a3999349..bc7e3af73 100644 --- a/enterprise/enterprise.go +++ b/enterprise/enterprise.go @@ -148,7 +148,7 @@ func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) { return c.RolesStore, nil } -// Allowances returns all Influx Enterprise permission strings +// Permissions returns all Influx Enterprise permission strings func (c *Client) Permissions(context.Context) chronograf.Permissions { all := chronograf.Allowances{ "NoPermissions", diff --git a/enterprise/meta_test.go b/enterprise/meta_test.go index 2823c1256..32d05bdea 100644 --- a/enterprise/meta_test.go +++ b/enterprise/meta_test.go @@ -70,7 +70,7 @@ func TestMetaClient_ShowCluster(t *testing.T) { http.StatusBadGateway, nil, nil, - fmt.Errorf("Time circuits on. Flux Capacitor... fluxxing."), + fmt.Errorf("time circuits on. Flux Capacitor... fluxxing"), ), }, wantErr: true, @@ -214,7 +214,7 @@ func TestMetaClient_Users(t *testing.T) { http.StatusOK, []byte(`{"users":[{"name":"admin","hash":"1234","permissions":{"":["ViewAdmin","ViewChronograf"]}}]}`), nil, - fmt.Errorf("Time circuits on. Flux Capacitor... fluxxing."), + fmt.Errorf("time circuits on. Flux Capacitor... fluxxing"), ), }, args: args{ diff --git a/kapacitor/http_out.go b/kapacitor/http_out.go index 848e44a97..45bb0b36e 100644 --- a/kapacitor/http_out.go +++ b/kapacitor/http_out.go @@ -6,8 +6,10 @@ import ( "github.com/influxdata/chronograf" ) +// HTTPEndpoint is the default location of the tickscript output const HTTPEndpoint = "output" +// HTTPOut adds a kapacitor httpOutput to a tickscript func HTTPOut(rule chronograf.AlertRule) (string, error) { return fmt.Sprintf(`trigger|httpOut('%s')`, HTTPEndpoint), nil } diff --git a/kapacitor/operators.go b/kapacitor/operators.go index 745789fa6..869c6ec23 100644 --- a/kapacitor/operators.go +++ b/kapacitor/operators.go @@ -3,8 +3,8 @@ package kapacitor import "fmt" const ( - GreaterThan = "greater than" - LessThan = "less than" + greaterThan = "greater than" + lessThan = "less than" LessThanEqual = "equal to or less than" GreaterThanEqual = "equal to or greater" Equal = "equal to" @@ -16,9 +16,9 @@ const ( // kapaOperator converts UI strings to kapacitor operators func kapaOperator(operator string) (string, error) { switch operator { - case GreaterThan: + case greaterThan: return ">", nil - case LessThan: + case lessThan: return "<", nil case LessThanEqual: return "<=", nil diff --git a/kapacitor/validate.go b/kapacitor/validate.go index ed8dca32a..4a81a9d0e 100644 --- a/kapacitor/validate.go +++ b/kapacitor/validate.go @@ -41,6 +41,9 @@ func validateTick(script chronograf.TICKScript) error { return err } +// deadman is an empty implementation of a kapacitor DeadmanService to allow CreatePipeline +var _ pipeline.DeadmanService = &deadman{} + type deadman struct { interval time.Duration threshold float64 diff --git a/kapacitor/vars.go b/kapacitor/vars.go index 081f349fb..038834b46 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -41,17 +41,16 @@ func Vars(rule chronograf.AlertRule) (string, error) { var crit = %s ` return fmt.Sprintf(vars, common, formatValue(rule.TriggerValues.Value)), nil - } else { - vars := ` + } + vars := ` %s var lower = %s var upper = %s ` - return fmt.Sprintf(vars, - common, - rule.TriggerValues.Value, - rule.TriggerValues.RangeValue), nil - } + return fmt.Sprintf(vars, + common, + rule.TriggerValues.Value, + rule.TriggerValues.RangeValue), nil case Relative: vars := ` %s diff --git a/log/log.go b/log/log.go index 2cfbb2513..2eb6b3ae9 100644 --- a/log/log.go +++ b/log/log.go @@ -81,6 +81,7 @@ func (ll *logrusLogger) WithField(key string, value interface{}) chronograf.Logg return &logrusLogger{ll.l.WithField(key, value)} } +// New wraps a logrus Logger func New(l Level) chronograf.Logger { logger := &logrus.Logger{ Out: os.Stderr, diff --git a/oauth2/doc.go b/oauth2/doc.go index db132a6ca..7fed8f3b6 100644 --- a/oauth2/doc.go +++ b/oauth2/doc.go @@ -1,4 +1,4 @@ -// The oauth2 package provides http.Handlers necessary for implementing Oauth2 +// Package oauth2 provides http.Handlers necessary for implementing Oauth2 // authentication with multiple Providers. // // This is how the pieces of this package fit together: diff --git a/oauth2/google.go b/oauth2/google.go index fb082ace6..ee5ff3bb1 100644 --- a/oauth2/google.go +++ b/oauth2/google.go @@ -10,7 +10,7 @@ import ( goauth2 "google.golang.org/api/oauth2/v2" ) -// Endpoint is Google's OAuth 2.0 endpoint. +// GoogleEndpoint is Google's OAuth 2.0 endpoint. // Copied here to remove tons of package dependencies var GoogleEndpoint = oauth2.Endpoint{ AuthURL: "https://accounts.google.com/o/oauth2/auth", @@ -18,6 +18,7 @@ var GoogleEndpoint = oauth2.Endpoint{ } var _ Provider = &Google{} +// Google is an oauth2 provider supporting google. type Google struct { ClientID string ClientSecret string diff --git a/oauth2/heroku.go b/oauth2/heroku.go index 637c57adf..831b095df 100644 --- a/oauth2/heroku.go +++ b/oauth2/heroku.go @@ -14,8 +14,8 @@ import ( var _ Provider = &Heroku{} const ( - // Routes required for interacting with Heroku API - HEROKU_ACCOUNT_ROUTE string = "https://api.heroku.com/account" + // HerokuAccountRoute is required for interacting with Heroku API + HerokuAccountRoute string = "https://api.heroku.com/account" ) // Heroku is an OAuth2 Provider allowing users to authenticate with Heroku to @@ -61,13 +61,14 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) { DefaultOrganization DefaultOrg `json:"default_organization"` } - resp, err := provider.Get(HEROKU_ACCOUNT_ROUTE) + resp, err := provider.Get(HerokuAccountRoute) if err != nil { h.Logger.Error("Unable to communicate with Heroku. err:", err) return "", err } defer resp.Body.Close() d := json.NewDecoder(resp.Body) + var account Account if err := d.Decode(&account); err != nil { h.Logger.Error("Unable to decode response from Heroku. err:", err) @@ -83,9 +84,8 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) { } h.Logger.Error(ErrOrgMembership) return "", ErrOrgMembership - } else { - return account.Email, nil } + return account.Email, nil } // Scopes for heroku is "identity" which grants access to user account diff --git a/oauth2/mux.go b/oauth2/mux.go index dc45a1525..9ecc11dba 100644 --- a/oauth2/mux.go +++ b/oauth2/mux.go @@ -24,6 +24,7 @@ type cookie struct { // Check to ensure CookieMux is an oauth2.Mux var _ Mux = &CookieMux{} +// NewCookieMux constructs a Mux handler that checks a cookie against the authenticator func NewCookieMux(p Provider, a Authenticator, l chronograf.Logger) *CookieMux { return &CookieMux{ Provider: p, @@ -55,7 +56,7 @@ type CookieMux struct { Now func() time.Time // Now returns the current time } -// Uses a Cookie with a random string as the state validation method. JWTs are +// Login uses a Cookie with a random string as the state validation method. JWTs are // a good choice here for encoding because they can be validated without // storing state. func (j *CookieMux) Login() http.Handler { diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go index 60c34e1eb..ed6379414 100644 --- a/oauth2/oauth2_test.go +++ b/oauth2/oauth2_test.go @@ -27,8 +27,8 @@ func (mp *MockProvider) Config() *goauth.Config { ClientID: "4815162342", ClientSecret: "8675309", Endpoint: goauth.Endpoint{ - mp.ProviderURL + "/oauth/auth", - mp.ProviderURL + "/oauth/token", + AuthURL: mp.ProviderURL + "/oauth/auth", + TokenURL: mp.ProviderURL + "/oauth/token", }, } } diff --git a/server/server.go b/server/server.go index f9a12cd97..738f0d906 100644 --- a/server/server.go +++ b/server/server.go @@ -73,14 +73,17 @@ func provide(p oauth2.Provider, m oauth2.Mux, ok func() bool) func(func(oauth2.P } } +// UseGithub validates the CLI parameters to enable github oauth support func (s *Server) UseGithub() bool { return s.TokenSecret != "" && s.GithubClientID != "" && s.GithubClientSecret != "" } +// UseGoogle validates the CLI parameters to enable google oauth support func (s *Server) UseGoogle() bool { return s.TokenSecret != "" && s.GoogleClientID != "" && s.GoogleClientSecret != "" && s.PublicURL != "" } +// UseHeroku validates the CLI parameters to enable heroku oauth support func (s *Server) UseHeroku() bool { return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != "" } diff --git a/server/url_prefixer.go b/server/url_prefixer.go index d20563fc1..6cbed57cb 100644 --- a/server/url_prefixer.go +++ b/server/url_prefixer.go @@ -62,7 +62,8 @@ func (wrw *wrapResponseWriter) Header() http.Header { return *wrw.dupHeader } -const CHUNK_SIZE int = 512 +// ChunkSize is the number of bytes per chunked transfer-encoding +const ChunkSize int = 512 // ServeHTTP implements an http.Handler that prefixes relative URLs from the // Next handler with the configured prefix. It does this by examining the @@ -109,7 +110,7 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { writtenCount++ buf.Write(src.Bytes()) - if writtenCount >= CHUNK_SIZE { + if writtenCount >= ChunkSize { flusher.Flush() writtenCount = 0 } diff --git a/server/version.go b/server/version.go index 6bf7dbe9d..e7fc4c901 100644 --- a/server/version.go +++ b/server/version.go @@ -4,6 +4,7 @@ import ( "net/http" ) +// Version handler adds X-Chronograf-Version header to responses func Version(version string, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { w.Header().Add("X-Chronograf-Version", version) From 18553deeff50e00fd73b359318b5132f6e963277 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Mon, 6 Mar 2017 13:29:26 -0600 Subject: [PATCH 22/47] Fix dashboards to use specified database --- ui/src/dashboards/components/Dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 3ff1c1ca4..2f03f4079 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -33,7 +33,7 @@ Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositi const dashboardCell = {...cell, i} dashboardCell.queries.forEach((q) => { q.text = q.query; - q.database = source.telegraf; + q.database = q.db; }); return dashboardCell; }) From 833d20c29c087b58372e047cccd7db2739af0d02 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Mon, 6 Mar 2017 13:42:07 -0600 Subject: [PATCH 23/47] Update CHANGELOG to mention fixing dashboards #968 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6da3508..943eecb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Bug Fixes 1. [#936](https://github.com/influxdata/chronograf/pull/936): Fix leaking sockets for InfluxQL queries + 1. [#968](https://github.com/influxdata/chronograf/issue/968): Fix wrong database used in dashboards ### Features From b60901e766cc6a8a8720f1f07117a0c13f3fd47e Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Mon, 6 Mar 2017 15:16:45 -0600 Subject: [PATCH 24/47] Add structured logging to underlying http server --- chronograf.go | 8 +++++--- kapacitor/validate.go | 2 -- log/log.go | 5 +++++ server/server.go | 24 +++++++++++++++++++----- server/url_prefixer.go | 6 +++--- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/chronograf.go b/chronograf.go index 1a8b7730d..fc624bea4 100644 --- a/chronograf.go +++ b/chronograf.go @@ -2,6 +2,7 @@ package chronograf import ( "context" + "io" "net/http" ) @@ -32,12 +33,13 @@ func (e Error) Error() string { type Logger interface { Debug(...interface{}) Info(...interface{}) - Warn(...interface{}) Error(...interface{}) - Fatal(...interface{}) - Panic(...interface{}) WithField(string, interface{}) Logger + + // Logger can be transformed into an io.Writer. + // That writer is the end of an io.Pipe and it is your responsibility to close it. + Writer() *io.PipeWriter } // Assets returns a handler to serve the website. diff --git a/kapacitor/validate.go b/kapacitor/validate.go index 4a81a9d0e..b7984fc16 100644 --- a/kapacitor/validate.go +++ b/kapacitor/validate.go @@ -3,7 +3,6 @@ package kapacitor import ( "bytes" "fmt" - "log" "time" "github.com/influxdata/chronograf" @@ -25,7 +24,6 @@ func ValidateAlert(service string) error { func formatTick(tickscript string) (chronograf.TICKScript, error) { node, err := ast.Parse(tickscript) if err != nil { - log.Fatalf("parse execution: %s", err) return "", err } diff --git a/log/log.go b/log/log.go index 2eb6b3ae9..1b00e3332 100644 --- a/log/log.go +++ b/log/log.go @@ -1,6 +1,7 @@ package log import ( + "io" "os" "github.com/Sirupsen/logrus" @@ -81,6 +82,10 @@ func (ll *logrusLogger) WithField(key string, value interface{}) chronograf.Logg return &logrusLogger{ll.l.WithField(key, value)} } +func (ll *logrusLogger) Writer() *io.PipeWriter { + return ll.l.Logger.WriterLevel(logrus.ErrorLevel) +} + // New wraps a logrus Logger func New(l Level) chronograf.Logger { logger := &logrus.Logger{ diff --git a/server/server.go b/server/server.go index 738f0d906..3a494b9f8 100644 --- a/server/server.go +++ b/server/server.go @@ -2,9 +2,11 @@ package server import ( "crypto/tls" + "log" "math/rand" "net" "net/http" + "os" "runtime" "strconv" "time" @@ -57,7 +59,7 @@ type Server struct { HerokuOrganizations []string `long:"heroku-organization" description:"Heroku Organization Memberships a user is required to have for access to Chronograf (comma separated)" env:"HEROKU_ORGS" env-delim:","` ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` - LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` + LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"` ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` BuildInfo BuildInfo @@ -211,10 +213,21 @@ func (s *Server) Serve() error { } s.Listener = listener - httpServer := &graceful.Server{Server: new(http.Server)} + // Using a log writer for http server logging + w := logger.Writer() + defer w.Close() + stdLog := log.New(w, "", 0) + + // TODO: Remove graceful when changing to go 1.8 + httpServer := &graceful.Server{ + Server: &http.Server{ + ErrorLog: stdLog, + Handler: s.handler, + }, + Logger: stdLog, + TCPKeepAlive: 5 * time.Second, + } httpServer.SetKeepAlivesEnabled(true) - httpServer.TCPKeepAlive = 5 * time.Second - httpServer.Handler = s.handler if !s.ReportingDisabled { go reportUsageStats(s.BuildInfo, logger) @@ -247,7 +260,8 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth if err := db.Open(); err != nil { logger. WithField("component", "boltstore"). - Fatal("Unable to open boltdb; is there a chronograf already running? ", err) + Error("Unable to open boltdb; is there a chronograf already running? ", err) + os.Exit(1) } // These apps are those handled from a directory diff --git a/server/url_prefixer.go b/server/url_prefixer.go index 6cbed57cb..10387c08e 100644 --- a/server/url_prefixer.go +++ b/server/url_prefixer.go @@ -80,9 +80,9 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { // extract the flusher for flushing chunks flusher, ok := rw.(http.Flusher) if !ok { - up.Logger. - WithField("component", "prefixer"). - Fatal("Expected http.ResponseWriter to be an http.Flusher, but wasn't") + msg := "Expected http.ResponseWriter to be an http.Flusher, but wasn't" + Error(rw, http.StatusInternalServerError, msg, up.Logger) + return } nextRead, nextWrite := io.Pipe() From aabc1011e28f0bb8a3ff66ed76d3e81921e8e0cb Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 6 Mar 2017 14:26:35 -0800 Subject: [PATCH 25/47] Query + button has proper margins --- ui/src/style/pages/data-explorer/query-builder.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/style/pages/data-explorer/query-builder.scss b/ui/src/style/pages/data-explorer/query-builder.scss index b04151d5e..110f58554 100644 --- a/ui/src/style/pages/data-explorer/query-builder.scss +++ b/ui/src/style/pages/data-explorer/query-builder.scss @@ -105,7 +105,7 @@ padding: 0; > .icon { - margin: 0; + margin: 0 !important; font-size: 12px; position: relative; top: -1px; From c9d2d489442a183799a0342615cbcd87a5488d6f Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 6 Mar 2017 14:26:44 -0800 Subject: [PATCH 26/47] Toggles don't wrap anymore --- ui/src/style/theme/theme-dark.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/style/theme/theme-dark.scss b/ui/src/style/theme/theme-dark.scss index 49b4b775b..6a7391b45 100644 --- a/ui/src/style/theme/theme-dark.scss +++ b/ui/src/style/theme/theme-dark.scss @@ -433,6 +433,7 @@ $toggle-border: 2px; border-radius: 3px; background-color: $g5-pepper; font-size: 0; + white-space: nowrap; } .toggle-btn { border: 0; From a82b98598d2f27fedb614d464aa53363e1a5938a Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 6 Mar 2017 14:42:48 -0800 Subject: [PATCH 27/47] Re-style disabled buttons --- ui/src/style/theme/theme-dark.scss | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ui/src/style/theme/theme-dark.scss b/ui/src/style/theme/theme-dark.scss index 6a7391b45..c408dc396 100644 --- a/ui/src/style/theme/theme-dark.scss +++ b/ui/src/style/theme/theme-dark.scss @@ -86,6 +86,72 @@ button.btn.btn-sm > span.icon { top: -1px; } +.btn.disabled, +.btn[disabled="true"] { + &.btn-success, + &.btn-success:hover, + &.btn-success:focus, + &.btn-success:active, + &.btn-success:hover:active, + &.btn-success:hover:focus, + &.btn-success:focus:active, + &.btn-success:focus:active:hover { + border-color: $c-emerald; + background-color: $c-emerald; + color: $g2-kevlar !important; + } + &.btn-primary, + &.btn-primary:hover, + &.btn-primary:focus, + &.btn-primary:active, + &.btn-primary:hover:active, + &.btn-primary:hover:focus, + &.btn-primary:focus:active, + &.btn-primary:focus:active:hover { + border-color: $c-sapphire; + background-color: $c-sapphire; + color: $g2-kevlar !important; + } + &.btn-info, + &.btn-info:hover, + &.btn-info:focus, + &.btn-info:active, + &.btn-info:hover:active, + &.btn-info:hover:focus, + &.btn-info:focus:active, + &.btn-info:focus:active:hover { + border-color: $g5-pepper; + background-color: $g5-pepper; + color: $g8-storm !important; + } + &.btn-danger, + &.btn-danger:hover, + &.btn-danger:focus, + &.btn-danger:active, + &.btn-danger:hover:active, + &.btn-danger:hover:focus, + &.btn-danger:focus:active, + &.btn-danger:focus:active:hover { + border-color: $c-ruby; + background-color: $c-ruby; + color: $g2-kevlar !important; + } + &.btn-warning, + &.btn-warning:hover, + &.btn-warning:focus, + &.btn-warning:active, + &.btn-warning:hover:active, + &.btn-warning:hover:focus, + &.btn-warning:focus:active, + &.btn-warning:focus:active:hover { + border-color: $c-star; + background-color: $c-star; + color: $g2-kevlar !important; + } +} + + + /* Dark Inputs ---------------------------------------------- From 2a632d9c3654a99ef42b3d1ba4ca66174388938e Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 6 Mar 2017 14:43:05 -0800 Subject: [PATCH 28/47] Make rule page save button always "success" style --- ui/src/kapacitor/components/RuleHeader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/kapacitor/components/RuleHeader.js b/ui/src/kapacitor/components/RuleHeader.js index bfddb764f..d5a0889ed 100644 --- a/ui/src/kapacitor/components/RuleHeader.js +++ b/ui/src/kapacitor/components/RuleHeader.js @@ -58,7 +58,7 @@ export const RuleHeader = React.createClass({ renderSave() { const {validationError, onSave, timeRange, onChooseTimeRange} = this.props; const saveButton = validationError ? - : ; From b445e30843e6e0a4ec04d84341062619e8abde17 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 7 Mar 2017 13:19:47 -0800 Subject: [PATCH 29/47] Remove right margin on source indicator --- ui/src/style/pages/data-explorer/page-header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/style/pages/data-explorer/page-header.scss b/ui/src/style/pages/data-explorer/page-header.scss index f150832f3..2745948b7 100644 --- a/ui/src/style/pages/data-explorer/page-header.scss +++ b/ui/src/style/pages/data-explorer/page-header.scss @@ -35,7 +35,7 @@ $sessions-dropdown-width: 227px; font-weight: 700; font-size: 12px; border-radius: 3px; - margin-right: 16px; + margin-right: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; From 19eabe5d7adc67017eddeb22cb8792d0a7c689e1 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 8 Mar 2017 12:17:47 -0800 Subject: [PATCH 30/47] Disable text selection on a ton of UI elements much cleaner now --- ui/src/kapacitor/components/AlertOutputs.js | 2 +- ui/src/kapacitor/components/AlertaConfig.js | 4 +- ui/src/kapacitor/components/HipChatConfig.js | 4 +- ui/src/kapacitor/components/KapacitorForm.js | 4 +- ui/src/kapacitor/components/OpsGenieConfig.js | 4 +- .../kapacitor/components/PagerDutyConfig.js | 4 +- ui/src/kapacitor/components/SMTPConfig.js | 4 +- ui/src/kapacitor/components/SensuConfig.js | 4 +- ui/src/kapacitor/components/SlackConfig.js | 4 +- ui/src/kapacitor/components/TalkConfig.js | 4 +- ui/src/kapacitor/components/TelegramConfig.js | 4 +- .../kapacitor/components/VictorOpsConfig.js | 4 +- .../components/page-header-dropdown.scss | 2 + ui/src/style/layout/page-header.scss | 1 + ui/src/style/layout/sidebar.scss | 1 + ui/src/style/mixins/mixins.scss | 121 ++++++++++-------- .../pages/data-explorer/page-header.scss | 1 + .../pages/data-explorer/query-builder.scss | 3 + .../pages/data-explorer/query-editor.scss | 2 + .../pages/data-explorer/visualization.scss | 2 + ui/src/style/pages/kapacitor.scss | 7 + ui/src/style/theme/theme-dark.scss | 18 ++- 22 files changed, 128 insertions(+), 76 deletions(-) diff --git a/ui/src/kapacitor/components/AlertOutputs.js b/ui/src/kapacitor/components/AlertOutputs.js index 303fc1c6b..04b154f5e 100644 --- a/ui/src/kapacitor/components/AlertOutputs.js +++ b/ui/src/kapacitor/components/AlertOutputs.js @@ -105,7 +105,7 @@ const AlertOutputs = React.createClass({ return (
      -

      Configure Alert Endpoints

      +

      Configure Alert Endpoints


      diff --git a/ui/src/kapacitor/components/AlertaConfig.js b/ui/src/kapacitor/components/AlertaConfig.js index edc394f18..cb0f9ff62 100644 --- a/ui/src/kapacitor/components/AlertaConfig.js +++ b/ui/src/kapacitor/components/AlertaConfig.js @@ -31,10 +31,10 @@ const AlertaConfig = React.createClass({ return (
      -

      Alerta Alert

      +

      Alerta Alert


      -

      +

      Have alerts sent to Alerta

      diff --git a/ui/src/kapacitor/components/HipChatConfig.js b/ui/src/kapacitor/components/HipChatConfig.js index f53596f0d..d6878c093 100644 --- a/ui/src/kapacitor/components/HipChatConfig.js +++ b/ui/src/kapacitor/components/HipChatConfig.js @@ -30,9 +30,9 @@ const HipchatConfig = React.createClass({ return (
      -

      HipChat Alert

      +

      HipChat Alert


      -

      Have alerts sent to HipChat.

      +

      Have alerts sent to HipChat.

      diff --git a/ui/src/kapacitor/components/KapacitorForm.js b/ui/src/kapacitor/components/KapacitorForm.js index f99e7a920..345fa6d32 100644 --- a/ui/src/kapacitor/components/KapacitorForm.js +++ b/ui/src/kapacitor/components/KapacitorForm.js @@ -45,13 +45,13 @@ const KapacitorForm = React.createClass({
      -

      +

      Kapacitor is used as the monitoring and alerting agent. This page will let you configure which Kapacitor to use and set up alert end points like email, Slack, and others.


      -

      Connect Kapacitor to Source

      +

      Connect Kapacitor to Source

      {source.url}


      diff --git a/ui/src/kapacitor/components/OpsGenieConfig.js b/ui/src/kapacitor/components/OpsGenieConfig.js index 1b3f38bf5..01cbef3a3 100644 --- a/ui/src/kapacitor/components/OpsGenieConfig.js +++ b/ui/src/kapacitor/components/OpsGenieConfig.js @@ -65,9 +65,9 @@ const OpsGenieConfig = React.createClass({ return (
      -

      OpsGenie Alert

      +

      OpsGenie Alert


      -

      Have alerts sent to OpsGenie.

      +

      Have alerts sent to OpsGenie.

      diff --git a/ui/src/kapacitor/components/PagerDutyConfig.js b/ui/src/kapacitor/components/PagerDutyConfig.js index 3c5119fef..d29e8720a 100644 --- a/ui/src/kapacitor/components/PagerDutyConfig.js +++ b/ui/src/kapacitor/components/PagerDutyConfig.js @@ -29,9 +29,9 @@ const PagerDutyConfig = React.createClass({ return (
      -

      PagerDuty Alert

      +

      PagerDuty Alert


      -

      You can have alerts sent to PagerDuty by entering info below.

      +

      You can have alerts sent to PagerDuty by entering info below.

      diff --git a/ui/src/kapacitor/components/SMTPConfig.js b/ui/src/kapacitor/components/SMTPConfig.js index f68a8b89b..581cd060b 100644 --- a/ui/src/kapacitor/components/SMTPConfig.js +++ b/ui/src/kapacitor/components/SMTPConfig.js @@ -33,9 +33,9 @@ const SMTPConfig = React.createClass({ return (
      -

      SMTP Alert

      +

      SMTP Alert


      -

      You can have alerts sent to an email address by setting up an SMTP endpoint.

      +

      You can have alerts sent to an email address by setting up an SMTP endpoint.

      diff --git a/ui/src/kapacitor/components/SensuConfig.js b/ui/src/kapacitor/components/SensuConfig.js index c23743649..826ab347d 100644 --- a/ui/src/kapacitor/components/SensuConfig.js +++ b/ui/src/kapacitor/components/SensuConfig.js @@ -27,9 +27,9 @@ const SensuConfig = React.createClass({ return (
      -

      Sensu Alert

      +

      Sensu Alert


      -

      Have alerts sent to Sensu.

      +

      Have alerts sent to Sensu.

      diff --git a/ui/src/kapacitor/components/SlackConfig.js b/ui/src/kapacitor/components/SlackConfig.js index 88e91e1ca..1526d0c58 100644 --- a/ui/src/kapacitor/components/SlackConfig.js +++ b/ui/src/kapacitor/components/SlackConfig.js @@ -48,9 +48,9 @@ const SlackConfig = React.createClass({ return (
      -

      Slack Alert

      +

      Slack Alert


      -

      Post alerts to a Slack channel.

      +

      Post alerts to a Slack channel.

      diff --git a/ui/src/kapacitor/components/TalkConfig.js b/ui/src/kapacitor/components/TalkConfig.js index 595adfac6..93ce4a6ee 100644 --- a/ui/src/kapacitor/components/TalkConfig.js +++ b/ui/src/kapacitor/components/TalkConfig.js @@ -34,9 +34,9 @@ const TalkConfig = React.createClass({ return (
      -

      Talk Alert

      +

      Talk Alert


      -

      Have alerts sent to Talk.

      +

      Have alerts sent to Talk.

      diff --git a/ui/src/kapacitor/components/TelegramConfig.js b/ui/src/kapacitor/components/TelegramConfig.js index 11e3147a1..30f45625b 100644 --- a/ui/src/kapacitor/components/TelegramConfig.js +++ b/ui/src/kapacitor/components/TelegramConfig.js @@ -48,9 +48,9 @@ const TelegramConfig = React.createClass({ return (
      -

      Telegram Alert

      +

      Telegram Alert


      -

      You can have alerts sent to Telegram by entering info below.

      +

      You can have alerts sent to Telegram by entering info below.

      diff --git a/ui/src/kapacitor/components/VictorOpsConfig.js b/ui/src/kapacitor/components/VictorOpsConfig.js index 5dfd3201a..98a5ba9a1 100644 --- a/ui/src/kapacitor/components/VictorOpsConfig.js +++ b/ui/src/kapacitor/components/VictorOpsConfig.js @@ -32,9 +32,9 @@ const VictorOpsConfig = React.createClass({ return (
      -

      VictorOps Alert

      +

      VictorOps Alert


      -

      Have alerts sent to VictorOps.

      +

      Have alerts sent to VictorOps.

      diff --git a/ui/src/style/components/page-header-dropdown.scss b/ui/src/style/components/page-header-dropdown.scss index 065241d68..84ad4deb0 100644 --- a/ui/src/style/components/page-header-dropdown.scss +++ b/ui/src/style/components/page-header-dropdown.scss @@ -11,6 +11,7 @@ font-size: 17px; font-weight: 400; transition: color 0.25s ease; + @include no-user-select(); } .dropdown-toggle > .caret { right: 0; @@ -35,6 +36,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + @include no-user-select(); } } } \ No newline at end of file diff --git a/ui/src/style/layout/page-header.scss b/ui/src/style/layout/page-header.scss index 2efe54044..8f5977e64 100644 --- a/ui/src/style/layout/page-header.scss +++ b/ui/src/style/layout/page-header.scss @@ -31,6 +31,7 @@ margin: 0; display: inline-block; vertical-align: middle; + @include no-user-select(); } &__left, &__right { diff --git a/ui/src/style/layout/sidebar.scss b/ui/src/style/layout/sidebar.scss index a17712290..e93b9d85a 100644 --- a/ui/src/style/layout/sidebar.scss +++ b/ui/src/style/layout/sidebar.scss @@ -214,6 +214,7 @@ $sidebar-logo-color: $g8-storm; white-space: nowrap; margin: 0; display: block; + @include no-user-select(); } &__menu-item { diff --git a/ui/src/style/mixins/mixins.scss b/ui/src/style/mixins/mixins.scss index 9d03a2567..66f4332e7 100644 --- a/ui/src/style/mixins/mixins.scss +++ b/ui/src/style/mixins/mixins.scss @@ -1,70 +1,87 @@ // Gradients @mixin gradient-v($startColor, $endColor) { - background: $startColor; - background: -moz-linear-gradient(top, $startColor 0%, $endColor 100%); - background: -webkit-linear-gradient(top, $startColor 0%,$endColor 100%); - background: linear-gradient(to bottom, $startColor 0%,$endColor 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=0 ); + background: $startColor; + background: -moz-linear-gradient(top, $startColor 0%, $endColor 100%); + background: -webkit-linear-gradient(top, $startColor 0%,$endColor 100%); + background: linear-gradient(to bottom, $startColor 0%,$endColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=0 ); } @mixin gradient-h($startColor, $endColor) { - background: $startColor; - background: -moz-linear-gradient(left, $startColor 0%, $endColor 100%); - background: -webkit-linear-gradient(left, $startColor 0%,$endColor 100%); - background: linear-gradient(to right, $startColor 0%,$endColor 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); + background: $startColor; + background: -moz-linear-gradient(left, $startColor 0%, $endColor 100%); + background: -webkit-linear-gradient(left, $startColor 0%,$endColor 100%); + background: linear-gradient(to right, $startColor 0%,$endColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); } @mixin gradient-diag-up($startColor, $endColor) { - background: $startColor; - background: -moz-linear-gradient(45deg, $startColor 0%, $endColor 100%); - background: -webkit-linear-gradient(45deg, $startColor 0%,$endColor 100%); - background: linear-gradient(45deg, $startColor 0%,$endColor 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); + background: $startColor; + background: -moz-linear-gradient(45deg, $startColor 0%, $endColor 100%); + background: -webkit-linear-gradient(45deg, $startColor 0%,$endColor 100%); + background: linear-gradient(45deg, $startColor 0%,$endColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); } @mixin gradient-diag-down($startColor, $endColor) { - background: $startColor; - background: -moz-linear-gradient(135deg, $startColor 0%, $endColor 100%); - background: -webkit-linear-gradient(135deg, $startColor 0%,$endColor 100%); - background: linear-gradient(135deg, $startColor 0%,$endColor 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); + background: $startColor; + background: -moz-linear-gradient(135deg, $startColor 0%, $endColor 100%); + background: -webkit-linear-gradient(135deg, $startColor 0%,$endColor 100%); + background: linear-gradient(135deg, $startColor 0%,$endColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); } @mixin gradient-r($startColor, $endColor) { - background: $startColor; - background: -moz-radial-gradient(center, ellipse cover, $startColor 0%, $endColor 100%); - background: -webkit-radial-gradient(center, ellipse cover, $startColor 0%,$endColor 100%); - background: radial-gradient(ellipse at center, $startColor 0%,$endColor 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); + background: $startColor; + background: -moz-radial-gradient(center, ellipse cover, $startColor 0%, $endColor 100%); + background: -webkit-radial-gradient(center, ellipse cover, $startColor 0%,$endColor 100%); + background: radial-gradient(ellipse at center, $startColor 0%,$endColor 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 ); } // Custom Scrollbars (Chrome Only) $scrollbar-width: 16px; $scrollbar-offset: 3px; @mixin custom-scrollbar($trackColor, $handleColor) { - &::-webkit-scrollbar { - width: $scrollbar-width; - border-bottom-right-radius: $radius; + &::-webkit-scrollbar { + width: $scrollbar-width; + border-bottom-right-radius: $radius; - &-button { - background-color: $trackColor; - } - &-track { - background-color: $trackColor; - border-bottom-right-radius: $radius; - } - &-track-piece { - background-color: $trackColor; - border: $scrollbar-offset solid $trackColor; - border-radius: ($scrollbar-width / 2); - } - &-thumb { - background-color: $handleColor; - border: $scrollbar-offset solid $trackColor; - border-radius: ($scrollbar-width / 2); - } - &-corner { - background-color: $trackColor; - } - } - &::-webkit-resizer { - background-color: $trackColor; - } + &-button { + background-color: $trackColor; + } + &-track { + background-color: $trackColor; + border-bottom-right-radius: $radius; + } + &-track-piece { + background-color: $trackColor; + border: $scrollbar-offset solid $trackColor; + border-radius: ($scrollbar-width / 2); + } + &-thumb { + background-color: $handleColor; + border: $scrollbar-offset solid $trackColor; + border-radius: ($scrollbar-width / 2); + } + &-corner { + background-color: $trackColor; + } + } + &::-webkit-resizer { + background-color: $trackColor; + } +} + + +// Block user select +@mixin no-user-select() { + user-select: none !important; + -moz-user-select: none !important; + -webkit-user-select: none !important; + -ms-user-select: none !important; + -o-user-select: none !important; +} +.no-user-select { + user-select: none !important; + -moz-user-select: none !important; + -webkit-user-select: none !important; + -ms-user-select: none !important; + -o-user-select: none !important; } diff --git a/ui/src/style/pages/data-explorer/page-header.scss b/ui/src/style/pages/data-explorer/page-header.scss index 2745948b7..cd2a959ac 100644 --- a/ui/src/style/pages/data-explorer/page-header.scss +++ b/ui/src/style/pages/data-explorer/page-header.scss @@ -26,6 +26,7 @@ $sessions-dropdown-width: 227px; border-radius: 0 3px 3px 0; } .source-indicator { + @include no-user-select(); padding: 0 9px; border: 0; background-color: $g5-pepper; diff --git a/ui/src/style/pages/data-explorer/query-builder.scss b/ui/src/style/pages/data-explorer/query-builder.scss index 110f58554..5b97d8aa1 100644 --- a/ui/src/style/pages/data-explorer/query-builder.scss +++ b/ui/src/style/pages/data-explorer/query-builder.scss @@ -29,6 +29,7 @@ justify-content: space-between; h1 { + @include no-user-select(); font-size: 17px; font-weight: 400; text-transform: uppercase; @@ -135,6 +136,7 @@ overflow: hidden; max-width: 177px; text-overflow: ellipsis; + @include no-user-select(); } /* @@ -165,6 +167,7 @@ $query-builder--column-heading-height: 60px; top: 60px; } .query-builder--column-heading { + @include no-user-select(); width: 100%; height: $query-builder--column-heading-height; display: flex; diff --git a/ui/src/style/pages/data-explorer/query-editor.scss b/ui/src/style/pages/data-explorer/query-editor.scss index 16d045ea1..990a2936c 100644 --- a/ui/src/style/pages/data-explorer/query-editor.scss +++ b/ui/src/style/pages/data-explorer/query-editor.scss @@ -60,6 +60,7 @@ font-weight: 600; border-radius: $radius-small $radius-small 0 0; margin-right: 2px; + @include no-user-select(); transition: color 0.25s ease, background-color 0.25s ease; @@ -82,6 +83,7 @@ border-radius: 0 0 $radius-small $radius-small; &-item { + @include no-user-select(); color: $g11-sidewalk; list-style-type: none; margin: 0; diff --git a/ui/src/style/pages/data-explorer/visualization.scss b/ui/src/style/pages/data-explorer/visualization.scss index a6bb70e95..b9e76b3c0 100644 --- a/ui/src/style/pages/data-explorer/visualization.scss +++ b/ui/src/style/pages/data-explorer/visualization.scss @@ -27,6 +27,7 @@ color: $g13-mist; font-weight: 600; margin-right: 16px; + @include no-user-select(); transition: color 0.25s ease; } @@ -70,6 +71,7 @@ } } .graph-container { + @include no-user-select(); background-color: $graph-bg-color; border-radius: 0 0 $graph-radius $graph-radius; padding: 8px 16px; diff --git a/ui/src/style/pages/kapacitor.scss b/ui/src/style/pages/kapacitor.scss index e87897b66..b52915555 100644 --- a/ui/src/style/pages/kapacitor.scss +++ b/ui/src/style/pages/kapacitor.scss @@ -80,6 +80,7 @@ $kapacitor-font-sm: 13px; display: flex; align-items: center; justify-content: center; + @include no-user-select(); p { margin: 0; @@ -101,6 +102,7 @@ $kapacitor-font-sm: 13px; font-weight: 400; color: $g13-mist; position: relative; + @include no-user-select(); &:before, &:after { @@ -451,6 +453,7 @@ div.qeditor.kapacitor-metric-selector { font-weight: 600; display: inline-block; color: $g15-platinum; + @include no-user-select(); } > code { background-color: $g2-kevlar; @@ -465,6 +468,7 @@ div.qeditor.kapacitor-metric-selector { font-size: ($kapacitor-font-sm - 2px); font-weight: 600; transition: color 0.25s ease; + @include no-user-select(); &:hover { color: $c-rainforest; @@ -483,6 +487,7 @@ div.qeditor.kapacitor-metric-selector { font-weight: 600; display: inline-block; color: $g15-platinum; + @include no-user-select(); } &.top { @@ -514,6 +519,7 @@ div.qeditor.kapacitor-metric-selector { } > p { + @include no-user-select(); white-space: nowrap; font-weight: 600; color: $g15-platinum; @@ -521,6 +527,7 @@ div.qeditor.kapacitor-metric-selector { margin-right: ($kap-padding-sm / 2); } > span { + @include no-user-select(); color: $kapacitor-accent; height: $kap-input-height; line-height: $kap-input-height; diff --git a/ui/src/style/theme/theme-dark.scss b/ui/src/style/theme/theme-dark.scss index c408dc396..c832ff20b 100644 --- a/ui/src/style/theme/theme-dark.scss +++ b/ui/src/style/theme/theme-dark.scss @@ -19,6 +19,10 @@ .panel hr { background-color: $g5-pepper; } +.panel-title, +.panel-title a { + @include no-user-select(); +} .panel-minimal { border: 0; @@ -55,7 +59,9 @@ } } } - +table thead th { + @include no-user-select(); +} /* @@ -165,6 +171,7 @@ button.btn.btn-sm > span.icon { font-weight: 600; margin-bottom: 4px; padding: 0 13px; + @include no-user-select(); } .form-control { padding: 0 13px; @@ -500,6 +507,7 @@ $toggle-border: 2px; background-color: $g5-pepper; font-size: 0; white-space: nowrap; + @include no-user-select(); } .toggle-btn { border: 0; @@ -740,4 +748,12 @@ $form-static-checkbox-size: 16px; opacity: 1; transform: translate(-50%,-50%) scale(1,1); } +} + + + + + +br { + @include no-user-select(); } \ No newline at end of file From 0c2a5787c64697037e0ed5cb4370c1ef74a475aa Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 8 Mar 2017 13:20:50 -0800 Subject: [PATCH 31/47] Create SourceIndicator component Re-using this thing a lot --- ui/src/shared/components/SourceIndicator.js | 19 +++++++++++++ ui/src/style/chronograf.scss | 1 + ui/src/style/components/source-indicator.scss | 27 +++++++++++++++++++ .../pages/data-explorer/page-header.scss | 22 --------------- 4 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 ui/src/shared/components/SourceIndicator.js create mode 100644 ui/src/style/components/source-indicator.scss diff --git a/ui/src/shared/components/SourceIndicator.js b/ui/src/shared/components/SourceIndicator.js new file mode 100644 index 000000000..4d681a70b --- /dev/null +++ b/ui/src/shared/components/SourceIndicator.js @@ -0,0 +1,19 @@ +import React, {PropTypes} from 'react'; + +const SourceIndicator = React.createClass({ + propTypes: { + source: PropTypes.shape({}).isRequired, + }, + + render() { + const {source} = this.props; + return ( +
      + + {source && source.name} +
      + ); + }, +}); + +export default SourceIndicator; diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 83e08cd7c..e45e32ba4 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -37,6 +37,7 @@ @import 'components/search-widget'; @import 'components/tables'; @import 'components/resizer'; +@import 'components/source-indicator'; // Pages @import 'pages/alerts'; diff --git a/ui/src/style/components/source-indicator.scss b/ui/src/style/components/source-indicator.scss new file mode 100644 index 000000000..64e9bc410 --- /dev/null +++ b/ui/src/style/components/source-indicator.scss @@ -0,0 +1,27 @@ +/* + Source Indicator component styles + ---------------------------------------------------------------- +*/ +.source-indicator { + @include no-user-select(); + display: inline-block; + padding: 0 9px; + border: 0; + background-color: $g5-pepper; + height: 30px; + line-height: 30px; + color: $g13-mist; + font-weight: 700; + font-size: 12px; + border-radius: 3px; + margin-right: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > .icon { + display: inline-block; + font-size: 16px; + margin: 0 4px 0 -2px; + } +} \ No newline at end of file diff --git a/ui/src/style/pages/data-explorer/page-header.scss b/ui/src/style/pages/data-explorer/page-header.scss index cd2a959ac..9cfb34060 100644 --- a/ui/src/style/pages/data-explorer/page-header.scss +++ b/ui/src/style/pages/data-explorer/page-header.scss @@ -24,26 +24,4 @@ $sessions-dropdown-width: 227px; .sessions-dropdown__btn { margin: 0 16px 0 0; border-radius: 0 3px 3px 0; -} -.source-indicator { - @include no-user-select(); - padding: 0 9px; - border: 0; - background-color: $g5-pepper; - height: 30px; - line-height: 30px; - color: $g13-mist; - font-weight: 700; - font-size: 12px; - border-radius: 3px; - margin-right: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - > .icon { - display: inline-block; - font-size: 16px; - margin: 0 4px 0 -2px; - } } \ No newline at end of file From 398b29a758e161b62330b5a43427fb4d1ab0c283 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 8 Mar 2017 16:00:50 -0800 Subject: [PATCH 32/47] Fix for #941 --- ui/src/style/components/dygraphs.scss | 25 +++++++++++++++++++++++++ ui/src/style/pages/hosts.scss | 16 ---------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ui/src/style/components/dygraphs.scss b/ui/src/style/components/dygraphs.scss index b6653fff9..748755928 100644 --- a/ui/src/style/components/dygraphs.scss +++ b/ui/src/style/components/dygraphs.scss @@ -124,3 +124,28 @@ padding: 0 10px 0 1px !important; } } + + + +.single-stat { + position: absolute; + width: 100%; + height: 100%; + font-size: 60px; + font-weight: 300; + color: $c-pool; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + text-align: center; + font-size: 54px; + font-weight: 300; + color: $c-pool; + + &.graph-single-stat { + position: absolute; + width: 100%; + top: 0; + } +} diff --git a/ui/src/style/pages/hosts.scss b/ui/src/style/pages/hosts.scss index 79d8a4b80..01f805418 100644 --- a/ui/src/style/pages/hosts.scss +++ b/ui/src/style/pages/hosts.scss @@ -5,22 +5,6 @@ .graph-container.hosts-graph { padding: 8px 16px; - - .single-stat { - font-size: 60px; - font-weight: 300; - color: $c-pool; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - - &.graph-single-stat { - position: absolute; - width: 100%; - top: 0; - } - } } .host-list--active-source { From 9eec21e99d7b8e33c31ac490fb1b7f9931118fff Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 8 Mar 2017 16:20:17 -0800 Subject: [PATCH 33/47] Change sourceIndicator to receive a string instead of an object --- ui/src/shared/components/SourceIndicator.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/src/shared/components/SourceIndicator.js b/ui/src/shared/components/SourceIndicator.js index 4d681a70b..83bcbeeed 100644 --- a/ui/src/shared/components/SourceIndicator.js +++ b/ui/src/shared/components/SourceIndicator.js @@ -2,15 +2,18 @@ import React, {PropTypes} from 'react'; const SourceIndicator = React.createClass({ propTypes: { - source: PropTypes.shape({}).isRequired, + sourceName: PropTypes.string, }, render() { - const {source} = this.props; + const {sourceName} = this.props; + if (!sourceName) { + return null; + } return (
      - {source && source.name} + {sourceName}
      ); }, From a2c8ab4867ad6716a7753766d839810e68d0cd60 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 8 Mar 2017 16:20:30 -0800 Subject: [PATCH 34/47] Add Source Indicator to most pages --- ui/src/alerts/containers/AlertsApp.js | 5 +++++ ui/src/dashboards/components/DashboardHeader.js | 4 ++++ ui/src/dashboards/containers/DashboardPage.js | 1 + ui/src/dashboards/containers/DashboardsPage.js | 4 ++++ ui/src/data_explorer/containers/Header.js | 7 ++----- ui/src/hosts/containers/HostPage.js | 5 ++++- ui/src/hosts/containers/HostsPage.js | 7 ++++++- ui/src/kapacitor/components/KapacitorRule.js | 1 + ui/src/kapacitor/components/KapacitorRules.js | 15 ++++++++++----- ui/src/kapacitor/components/RuleHeader.js | 5 ++++- .../kubernetes/components/KubernetesDashboard.js | 3 ++- 11 files changed, 43 insertions(+), 14 deletions(-) diff --git a/ui/src/alerts/containers/AlertsApp.js b/ui/src/alerts/containers/AlertsApp.js index 7a3f3e9cb..3e27cd5c6 100644 --- a/ui/src/alerts/containers/AlertsApp.js +++ b/ui/src/alerts/containers/AlertsApp.js @@ -1,5 +1,6 @@ import React, {PropTypes} from 'react'; import AlertsTable from '../components/AlertsTable'; +import SourceIndicator from '../../shared/components/SourceIndicator'; import {getAlerts} from '../apis'; import AJAX from 'utils/ajax'; import _ from 'lodash'; @@ -93,6 +94,7 @@ const AlertsApp = React.createClass({ }, render() { + const {source} = this.props; return ( // I stole this from the Hosts page. // Perhaps we should create an abstraction? @@ -104,6 +106,9 @@ const AlertsApp = React.createClass({ Alert History
      +
      + +
      diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 55dd1b7b0..3c9c657d2 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -4,6 +4,7 @@ import {Link} from 'react-router'; import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' +import SourceIndicator from '../../shared/components/SourceIndicator' const DashboardHeader = ({ children, @@ -17,6 +18,7 @@ const DashboardHeader = ({ handleChooseAutoRefresh, handleClickPresentationButton, sourceID, + source, }) => isHidden ? null : (
      @@ -48,6 +50,7 @@ const DashboardHeader = ({ Graph Tips
      +
      @@ -79,6 +82,7 @@ DashboardHeader.propTypes = { handleChooseTimeRange: func.isRequired, handleChooseAutoRefresh: func.isRequired, handleClickPresentationButton: func.isRequired, + source: shape({}), } export default DashboardHeader diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 0b6efe1b4..38aa33c5b 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -119,6 +119,7 @@ const DashboardPage = React.createClass({ handleClickPresentationButton={handleClickPresentationButton} dashboard={dashboard} sourceID={sourceID} + source={source} > {(dashboards).map((d, i) => { return ( diff --git a/ui/src/dashboards/containers/DashboardsPage.js b/ui/src/dashboards/containers/DashboardsPage.js index 6ffc66d09..cedbcae9d 100644 --- a/ui/src/dashboards/containers/DashboardsPage.js +++ b/ui/src/dashboards/containers/DashboardsPage.js @@ -1,5 +1,6 @@ import React, {PropTypes} from 'react'; import {Link} from 'react-router'; +import SourceIndicator from '../../shared/components/SourceIndicator'; import {getDashboards} from '../apis'; @@ -53,6 +54,9 @@ const DashboardsPage = React.createClass({ Dashboards
      +
      + +
      diff --git a/ui/src/data_explorer/containers/Header.js b/ui/src/data_explorer/containers/Header.js index 7f8e71f98..01497635b 100644 --- a/ui/src/data_explorer/containers/Header.js +++ b/ui/src/data_explorer/containers/Header.js @@ -4,6 +4,7 @@ import {withRouter} from 'react-router'; import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown'; +import SourceIndicator from '../../shared/components/SourceIndicator'; import timeRanges from 'hson!../../shared/data/timeRanges.hson'; @@ -57,11 +58,7 @@ const Header = React.createClass({

      Explorer

      -

      Source:

      -
      - - {this.context.source.name} -
      +
      diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 641aa63f1..90b236ea9 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -150,7 +150,9 @@ export const HostPage = React.createClass({ }, render() { - const {params: {hostID}, location: {query: {app}}, source: {id}, autoRefresh, handleChooseAutoRefresh, inPresentationMode, handleClickPresentationButton} = this.props + const {params: {hostID}, location: {query: {app}}, source: {id}, + autoRefresh, handleChooseAutoRefresh, inPresentationMode, + handleClickPresentationButton, source} = this.props const {layouts, timeRange, hosts} = this.state const appParam = app ? `?app=${app}` : '' @@ -164,6 +166,7 @@ export const HostPage = React.createClass({ handleChooseTimeRange={this.handleChooseTimeRange} handleChooseAutoRefresh={handleChooseAutoRefresh} handleClickPresentationButton={handleClickPresentationButton} + source={source} > {Object.keys(hosts).map((host, i) => { return ( diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js index dd3a7b846..127d0d582 100644 --- a/ui/src/hosts/containers/HostsPage.js +++ b/ui/src/hosts/containers/HostsPage.js @@ -1,6 +1,7 @@ import React, {PropTypes} from 'react'; import _ from 'lodash'; import HostsTable from '../components/HostsTable'; +import SourceIndicator from '../../shared/components/SourceIndicator' import {getCpuAndLoadForHosts, getMappings, getAppsForHosts} from '../apis'; export const HostsPage = React.createClass({ @@ -44,6 +45,7 @@ export const HostsPage = React.createClass({ }, render() { + const {source} = this.props; return (
      @@ -53,13 +55,16 @@ export const HostsPage = React.createClass({ Host List
      +
      + +
      - +
      diff --git a/ui/src/kapacitor/components/KapacitorRule.js b/ui/src/kapacitor/components/KapacitorRule.js index dd1b03833..963dcbbbd 100644 --- a/ui/src/kapacitor/components/KapacitorRule.js +++ b/ui/src/kapacitor/components/KapacitorRule.js @@ -47,6 +47,7 @@ export const KapacitorRule = React.createClass({ onChooseTimeRange={this.handleChooseTimeRange} validationError={this.validationError()} timeRange={timeRange} + source={source} />
      diff --git a/ui/src/kapacitor/components/KapacitorRules.js b/ui/src/kapacitor/components/KapacitorRules.js index da7835b6c..0ece0aa64 100644 --- a/ui/src/kapacitor/components/KapacitorRules.js +++ b/ui/src/kapacitor/components/KapacitorRules.js @@ -1,7 +1,8 @@ -import React, {PropTypes} from 'react'; -import {Link} from 'react-router'; +import React, {PropTypes} from 'react' +import {Link} from 'react-router' -import NoKapacitorError from '../../shared/components/NoKapacitorError'; +import NoKapacitorError from '../../shared/components/NoKapacitorError' +import SourceIndicator from '../../shared/components/SourceIndicator' import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable' const KapacitorRules = ({ @@ -29,7 +30,7 @@ const KapacitorRules = ({ } return ( - +

      Alert Rules

      Create New Rule @@ -44,13 +45,16 @@ const KapacitorRules = ({ ) } -const PageContents = ({children}) => ( +const PageContents = ({children, source}) => (

      Kapacitor Rules

      +
      + +
      @@ -86,6 +90,7 @@ KapacitorRules.propTypes = { PageContents.propTypes = { children: node, + source: shape(), } export default KapacitorRules diff --git a/ui/src/kapacitor/components/RuleHeader.js b/ui/src/kapacitor/components/RuleHeader.js index d5a0889ed..4ed6e62ee 100644 --- a/ui/src/kapacitor/components/RuleHeader.js +++ b/ui/src/kapacitor/components/RuleHeader.js @@ -1,9 +1,11 @@ import React, {PropTypes} from 'react'; import ReactTooltip from 'react-tooltip'; import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown'; +import SourceIndicator from '../../shared/components/SourceIndicator'; export const RuleHeader = React.createClass({ propTypes: { + source: PropTypes.shape({}).isRequired, onSave: PropTypes.func.isRequired, rule: PropTypes.shape({}).isRequired, actions: PropTypes.shape({ @@ -56,7 +58,7 @@ export const RuleHeader = React.createClass({ }, renderSave() { - const {validationError, onSave, timeRange, onChooseTimeRange} = this.props; + const {validationError, onSave, timeRange, onChooseTimeRange, source} = this.props; const saveButton = validationError ?