diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 3eff674a23..2c64b71ebb 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -26,16 +26,19 @@ const Dashboard = ({ }) => { const cells = dashboard.cells.map(cell => { const dashboardCell = {...cell} - dashboardCell.queries = dashboardCell.queries.map( - ({label, query, queryConfig, db}) => ({ - label, - query, - queryConfig, - db, - database: db, - text: query, - }) - ) + dashboardCell.queries = dashboardCell.queries.map(({ + label, + query, + queryConfig, + db, + }) => ({ + label, + query, + queryConfig, + db, + database: db, + text: query, + })) return dashboardCell }) @@ -57,6 +60,7 @@ const Dashboard = ({ {cells.length ? <LayoutRenderer templates={templatesIncludingDashTime} + isEditable={true} cells={cells} timeRange={timeRange} autoRefresh={autoRefresh} diff --git a/ui/src/hosts/containers/HostPage.js b/ui/src/hosts/containers/HostPage.js index 3d474bd94d..b55a15dc37 100644 --- a/ui/src/hosts/containers/HostPage.js +++ b/ui/src/hosts/containers/HostPage.js @@ -174,7 +174,7 @@ export const HostPage = React.createClass({ autoRefresh={autoRefresh} source={source} host={this.props.params.hostID} - shouldNotBeEditable={true} + isEditable={false} synchronizer={this.synchronizer} /> ) diff --git a/ui/src/shared/components/ContextMenu.js b/ui/src/shared/components/ContextMenu.js new file mode 100644 index 0000000000..e94beb87bc --- /dev/null +++ b/ui/src/shared/components/ContextMenu.js @@ -0,0 +1,52 @@ +import React, {PropTypes} from 'react' +import classnames from 'classnames' +import OnClickOutside from 'react-onclickoutside' + +const ContextMenu = OnClickOutside(({ + isOpen, + toggleMenu, + onEdit, + onRename, + onDelete, + cell, +}) => ( + <div + className={classnames('dash-graph--options', { + 'dash-graph--options-show': isOpen, + })} + onClick={toggleMenu} + > + <button className="btn btn-info btn-xs"> + <span className="icon caret-down" /> + </button> + <ul className="dash-graph--options-menu"> + <li onClick={() => onEdit(cell)}>Edit</li> + <li onClick={onRename(cell.x, cell.y, cell.isEditing)}>Rename</li> + <li onClick={() => onDelete(cell)}>Delete</li> + </ul> + </div> +)) + +const ContextMenuContainer = props => { + if (!props.isEditable) { + return null + } + + return <ContextMenu {...props} /> +} + +const {bool, func, shape} = PropTypes + +ContextMenuContainer.propTypes = { + isOpen: bool, + toggleMenu: func, + onEdit: func, + onRename: func, + onDelete: func, + cell: shape(), + isEditable: bool, +} + +ContextMenu.propTypes = ContextMenuContainer.propTypes + +export default ContextMenuContainer diff --git a/ui/src/shared/components/CustomTimeIndicator.js b/ui/src/shared/components/CustomTimeIndicator.js new file mode 100644 index 0000000000..4dedc42417 --- /dev/null +++ b/ui/src/shared/components/CustomTimeIndicator.js @@ -0,0 +1,26 @@ +import React, {PropTypes} from 'react' +import _ from 'lodash' + +const CustomTimeIndicator = ({queries}) => { + const q = queries.find(({query}) => !query.includes(':dashboardTime:')) + const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null) + const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null) + + if (!customLower) { + return null + } + + const customTimeRange = customUpper + ? `${customLower} AND ${customUpper}` + : customLower + + return <span className="dash-graph--custom-time">{customTimeRange}</span> +} + +const {arrayOf, shape} = PropTypes + +CustomTimeIndicator.propTypes = { + queries: arrayOf(shape()), +} + +export default CustomTimeIndicator diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index c3af31d29d..7bd9f3124b 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -138,6 +138,7 @@ class LayoutRenderer extends Component { autoRefresh, templates, synchronizer, + isEditable, } = this.props return cells.map(cell => { @@ -146,6 +147,7 @@ class LayoutRenderer extends Component { return ( <div key={cell.i}> <NameableGraph + isEditable={isEditable} onEditCell={onEditCell} onRenameCell={onRenameCell} onUpdateCell={onUpdateCell} @@ -290,6 +292,7 @@ LayoutRenderer.propTypes = { shouldNotBeEditable: bool, synchronizer: func, isStatusPage: bool, + isEditable: bool, } export default LayoutRenderer diff --git a/ui/src/shared/components/NameableGraph.js b/ui/src/shared/components/NameableGraph.js index d9420bf90e..d07e8db05a 100644 --- a/ui/src/shared/components/NameableGraph.js +++ b/ui/src/shared/components/NameableGraph.js @@ -1,157 +1,88 @@ -import React, {PropTypes} from 'react' -import classnames from 'classnames' -import OnClickOutside from 'react-onclickoutside' +import React, {Component, PropTypes} from 'react' -const {array, bool, func, node, number, shape, string} = PropTypes +import NameableGraphHeader from 'shared/components/NameableGraphHeader' +import ContextMenu from 'shared/components/ContextMenu' -const NameableGraph = React.createClass({ - propTypes: { - cell: shape({ - name: string.isRequired, - isEditing: bool, - x: number.isRequired, - y: number.isRequired, - queries: array.isRequired, - }).isRequired, - children: node.isRequired, - onEditCell: func, - onRenameCell: func, - onUpdateCell: func, - onDeleteCell: func, - onSummonOverlayTechnologies: func, - shouldNotBeEditable: bool, - }, - - getInitialState() { - return { +class NameableGraph extends Component { + constructor(props) { + super(props) + this.state = { isMenuOpen: false, } - }, + + this.toggleMenu = ::this.toggleMenu + this.closeMenu = ::this.closeMenu + } toggleMenu() { this.setState({ isMenuOpen: !this.state.isMenuOpen, }) - }, + } closeMenu() { this.setState({ isMenuOpen: false, }) - }, - - renderCustomTimeIndicator() { - const {cell} = this.props - let customTimeRange = null - - cell.queries.map(q => { - if (!q.query.includes(':dashboardTime:')) { - customTimeRange = q.queryConfig.range.lower.split(' ').reverse()[0] - } - }) - - return customTimeRange - ? <span className="dash-graph--custom-time">{customTimeRange}</span> - : null - }, + } render() { const { cell, - cell: {x, y, name, isEditing}, onEditCell, onRenameCell, onUpdateCell, onDeleteCell, onSummonOverlayTechnologies, - shouldNotBeEditable, + isEditable, children, } = this.props - const isEditable = !!(onEditCell || onRenameCell || onUpdateCell) - - let nameOrField - if (isEditing && isEditable) { - nameOrField = ( - <input - className="form-control input-sm dash-graph--name-edit" - type="text" - value={name} - autoFocus={true} - onChange={onRenameCell(x, y)} - onBlur={onUpdateCell(cell)} - onKeyUp={evt => { - if (evt.key === 'Enter') { - onUpdateCell(cell)() - } - if (evt.key === 'Escape') { - onEditCell(x, y, true)() - } - }} - /> - ) - } else { - nameOrField = ( - <span className="dash-graph--name"> - {name} - {this.renderCustomTimeIndicator()} - </span> - ) - } - - let onStartRenaming - if (!isEditing && isEditable) { - onStartRenaming = onEditCell - } else { - onStartRenaming = () => { - // no-op - } - } - return ( <div className="dash-graph"> - <div - className={classnames('dash-graph--heading', { - 'dash-graph--heading-draggable': !shouldNotBeEditable, - })} - > - {nameOrField} - </div> - {shouldNotBeEditable - ? null - : <ContextMenu - isOpen={this.state.isMenuOpen} - toggleMenu={this.toggleMenu} - onEdit={onSummonOverlayTechnologies} - onRename={onStartRenaming} - onDelete={onDeleteCell} - cell={cell} - handleClickOutside={this.closeMenu} - />} + <NameableGraphHeader + cell={cell} + isEditable={isEditable} + onEditCell={onEditCell} + onRenameCell={onRenameCell} + onUpdateCell={onUpdateCell} + /> + <ContextMenu + cell={cell} + onDelete={onDeleteCell} + onRename={!cell.isEditing && isEditable ? onEditCell : () => {}} + toggleMenu={this.toggleMenu} + isOpen={this.state.isMenuOpen} + isEditable={isEditable} + handleClickOutside={this.closeMenu} + onEdit={onSummonOverlayTechnologies} + /> <div className="dash-graph--container"> {children} </div> </div> ) - }, -}) + } +} + +const {array, bool, func, node, number, shape, string} = PropTypes + +NameableGraph.propTypes = { + cell: shape({ + name: string.isRequired, + isEditing: bool, + x: number.isRequired, + y: number.isRequired, + queries: array, + }).isRequired, + children: node.isRequired, + onEditCell: func, + onRenameCell: func, + onUpdateCell: func, + onDeleteCell: func, + onSummonOverlayTechnologies: func, + shouldNotBeEditable: bool, + isEditable: bool, +} -const ContextMenu = OnClickOutside( - ({isOpen, toggleMenu, onEdit, onRename, onDelete, cell}) => - <div - className={classnames('dash-graph--options', { - 'dash-graph--options-show': isOpen, - })} - onClick={toggleMenu} - > - <button className="btn btn-info btn-xs"> - <span className="icon caret-down" /> - </button> - <ul className="dash-graph--options-menu"> - <li onClick={() => onEdit(cell)}>Edit</li> - <li onClick={onRename(cell.x, cell.y, cell.isEditing)}>Rename</li> - <li onClick={() => onDelete(cell)}>Delete</li> - </ul> - </div> -) export default NameableGraph diff --git a/ui/src/shared/components/NameableGraphHeader.js b/ui/src/shared/components/NameableGraphHeader.js new file mode 100644 index 0000000000..2613cc579d --- /dev/null +++ b/ui/src/shared/components/NameableGraphHeader.js @@ -0,0 +1,82 @@ +import React, {PropTypes} from 'react' +import classnames from 'classnames' + +import CustomTimeIndicator from 'shared/components/CustomTimeIndicator' + +const NameableGraphHeader = ({ + isEditable, + onEditCell, + onRenameCell, + onUpdateCell, + cell, + cell: {x, y, name, queries}, +}) => { + const isInputVisible = isEditable && cell.isEditing + const className = classnames('dash-graph--heading', { + 'dash-graph--heading-draggable': isEditable, + }) + const onKeyUp = evt => { + if (evt.key === 'Enter') { + onUpdateCell(cell)() + } + if (evt.key === 'Escape') { + onEditCell(x, y, true)() + } + } + + return ( + <div className={className}> + {isInputVisible + ? <GraphNameInput + value={name} + onChange={onRenameCell(x, y)} + onBlur={onUpdateCell(cell)} + onKeyUp={onKeyUp} + /> + : <GraphName name={name} queries={queries} />} + </div> + ) +} + +const {arrayOf, bool, func, string, shape} = PropTypes + +NameableGraphHeader.propTypes = { + cell: shape(), + onEditCell: func, + onRenameCell: func, + onUpdateCell: func, + isEditable: bool, +} + +const GraphName = ({name, queries}) => + <span className="dash-graph--name"> + {name} + {queries && queries.length + ? <CustomTimeIndicator queries={queries} /> + : null} + </span> + +GraphName.propTypes = { + name: string, + queries: arrayOf(shape()), +} + +const GraphNameInput = ({value, onKeyUp, onChange, onBlur}) => + <input + className="form-control input-sm dash-graph--name-edit" + type="text" + value={value} + autoFocus={true} + onChange={onChange} + onBlur={onBlur} + onKeyUp={onKeyUp} + /> + +GraphNameInput.propTypes = { + value: string, + onKeyUp: func, + onChange: func, + onBlur: func, +} + +export default NameableGraphHeader diff --git a/ui/src/status/containers/StatusPage.js b/ui/src/status/containers/StatusPage.js index 99a0a971bd..c575549062 100644 --- a/ui/src/status/containers/StatusPage.js +++ b/ui/src/status/containers/StatusPage.js @@ -59,6 +59,7 @@ class StatusPage extends Component { source={source} shouldNotBeEditable={true} isStatusPage={true} + isEditable={false} /> : <span>Loading Status Page...</span>} </div>