Merge pull request #2002 from influxdata/ux-improvements

UX Improvements
pull/2042/head
Hunter Trujillo 2017-09-26 14:58:57 -06:00 committed by GitHub
commit 9208e76e2c
15 changed files with 270 additions and 324 deletions

View File

@ -12,6 +12,10 @@
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
### UI Improvements
1. [#2002](https://github.com/influxdata/chronograf/pull/2002): Require a second click when deleting a dashboard cell
1. [#2002](https://github.com/influxdata/chronograf/pull/2002): Sort database list in Schema Explorer alphabetically
1. [#2002](https://github.com/influxdata/chronograf/pull/2002): Improve usability of dashboard cell context menus
1. [#2002](https://github.com/influxdata/chronograf/pull/2002): Move dashboard cell renaming UI into Cell Editor Overlay
## v1.3.8.1 [unreleased]
### Bug Fixes

View File

@ -179,6 +179,10 @@ class CellEditorOverlay extends Component {
})
}
handleCellRename = newName => {
this.setState({cellWorkingName: newName})
}
handleSetScale = scale => () => {
const {axes} = this.state
@ -263,6 +267,7 @@ class CellEditorOverlay extends Component {
autoRefresh={autoRefresh}
queryConfigs={queriesWorkingDraft}
editQueryStatus={editQueryStatus}
onCellRename={this.handleCellRename}
/>
<CEOBottom>
<OverlayControls

View File

@ -10,11 +10,8 @@ const Dashboard = ({
onZoom,
dashboard,
onAddCell,
onEditCell,
timeRange,
autoRefresh,
onRenameCell,
onUpdateCell,
onDeleteCell,
synchronizer,
onPositionChange,
@ -24,7 +21,6 @@ const Dashboard = ({
onSummonOverlayTechnologies,
onSelectTemplate,
showTemplateControlBar,
onCancelEditCell,
}) => {
const cells = dashboard.cells.map(cell => {
const dashboardCell = {...cell}
@ -58,7 +54,6 @@ const Dashboard = ({
/>}
{cells.length
? <LayoutRenderer
onCancelEditCell={onCancelEditCell}
templates={templatesIncludingDashTime}
isEditable={true}
cells={cells}
@ -66,9 +61,6 @@ const Dashboard = ({
autoRefresh={autoRefresh}
source={source}
onPositionChange={onPositionChange}
onEditCell={onEditCell}
onRenameCell={onRenameCell}
onUpdateCell={onUpdateCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
synchronizer={synchronizer}
@ -112,9 +104,6 @@ Dashboard.propTypes = {
inPresentationMode: bool,
onAddCell: func,
onPositionChange: func,
onEditCell: func,
onRenameCell: func,
onUpdateCell: func,
onDeleteCell: func,
onSummonOverlayTechnologies: func,
synchronizer: func,
@ -128,7 +117,6 @@ Dashboard.propTypes = {
onOpenTemplateManager: func.isRequired,
onSelectTemplate: func.isRequired,
showTemplateControlBar: bool,
onCancelEditCell: func,
onZoom: func,
}

View File

@ -1,6 +1,7 @@
import React, {PropTypes} from 'react'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import buildQueries from 'utils/buildQueriesForGraphs'
import VisualizationName from 'src/dashboards/components/VisualizationName'
const DashVisualization = (
{
@ -10,17 +11,14 @@ const DashVisualization = (
templates,
timeRange,
autoRefresh,
onCellRename,
queryConfigs,
editQueryStatus,
},
{source: {links: {proxy}}}
) =>
<div className="graph">
<div className="graph-heading">
<div className="graph-title">
{name}
</div>
</div>
<VisualizationName defaultName={name} onCellRename={onCellRename} />
<div className="graph-container">
<RefreshingGraph
axes={axes}
@ -56,6 +54,7 @@ DashVisualization.propTypes = {
bounds: arrayOf(string),
}),
}),
onCellRename: func,
}
DashVisualization.contextTypes = {

View File

@ -0,0 +1,54 @@
import React, {Component, PropTypes} from 'react'
class VisualizationName extends Component {
constructor(props) {
super(props)
this.state = {
reset: false,
}
}
handleInputBlur = reset => e => {
this.props.onCellRename(reset ? this.props.defaultName : e.target.value)
this.setState({reset: false})
}
handleKeyDown = e => {
if (e.key === 'Enter') {
this.inputRef.blur()
}
if (e.key === 'Escape') {
this.inputRef.value = this.props.defaultName
this.setState({reset: true}, () => this.inputRef.blur())
}
}
render() {
const {defaultName} = this.props
const {reset} = this.state
return (
<div className="graph-heading">
<input
type="text"
className="form-control input-md"
defaultValue={defaultName}
onBlur={this.handleInputBlur(reset)}
onKeyDown={this.handleKeyDown}
placeholder="Name this Cell..."
ref={r => (this.inputRef = r)}
/>
</div>
)
}
}
const {string, func} = PropTypes
VisualizationName.propTypes = {
defaultName: string.isRequired,
onCellRename: func,
}
export default VisualizationName

View File

@ -17,7 +17,7 @@ export const NEW_DEFAULT_DASHBOARD_CELL = {
y: 0,
w: 4,
h: 4,
name: 'Name This Graph',
name: 'Untitled Cell',
type: 'line',
queries: [],
}

View File

@ -110,18 +110,6 @@ class DashboardPage extends Component {
this.props.dashboardActions.putDashboard(newDashboard)
}
// Places cell into editing mode.
handleEditDashboardCell = (x, y, isEditing) => {
return () => {
this.props.dashboardActions.editDashboardCell(
this.getActiveDashboard(),
x,
y,
!isEditing
) /* eslint-disable no-negated-condition */
}
}
handleUpdateDashboardCell = newCell => {
return () => {
this.props.dashboardActions.updateDashboardCell(
@ -187,13 +175,6 @@ class DashboardPage extends Component {
this.props.templateControlBarVisibilityToggled()
}
handleCancelEditCell = cellID => {
this.props.dashboardActions.cancelEditCell(
this.getActiveDashboard().id,
cellID
)
}
handleZoomedTimeRange = (zoomedLower, zoomedUpper) => {
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
}
@ -350,12 +331,9 @@ class DashboardPage extends Component {
onAddCell={this.handleAddCell}
synchronizer={this.synchronizer}
inPresentationMode={inPresentationMode}
onEditCell={this.handleEditDashboardCell}
onPositionChange={this.handleUpdatePosition}
onSelectTemplate={this.handleSelectTemplate}
onCancelEditCell={this.handleCancelEditCell}
onDeleteCell={this.handleDeleteDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
showTemplateControlBar={showTemplateControlBar}
onOpenTemplateManager={this.handleOpenTemplateManager}
templatesIncludingDashTime={templatesIncludingDashTime}

View File

@ -1,46 +0,0 @@
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

View File

@ -64,13 +64,17 @@ const DatabaseList = React.createClass({
render() {
const {query, onChooseNamespace} = this.props
const {namespaces} = this.state
const sortedNamespaces = namespaces.length
? _.sortBy(namespaces, n => n.database.toLowerCase())
: namespaces
return (
<div className="query-builder--column query-builder--column-db">
<div className="query-builder--heading">Databases</div>
<div className="query-builder--list">
<FancyScrollbar>
{this.state.namespaces.map(namespace => {
{sortedNamespaces.map(namespace => {
const {database, retentionPolicy} = namespace
const isActive =
database === query.database &&

View File

@ -1,41 +1,27 @@
import React, {Component, PropTypes} from 'react'
import _ from 'lodash'
import NameableGraphHeader from 'shared/components/NameableGraphHeader'
import ContextMenu from 'shared/components/ContextMenu'
import LayoutCellMenu from 'shared/components/LayoutCellMenu'
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
class NameableGraph extends Component {
class LayoutCell extends Component {
constructor(props) {
super(props)
this.state = {
isMenuOpen: false,
cellName: props.cell.name,
isDeleting: false,
}
}
toggleMenu = () => {
this.setState({
isMenuOpen: !this.state.isMenuOpen,
})
}
handleRenameCell = e => {
const cellName = e.target.value
this.setState({cellName})
}
handleCancelEdit = cellID => {
const {cell, onCancelEditCell} = this.props
this.setState({cellName: cell.name})
onCancelEditCell(cellID)
}
closeMenu = () => {
this.setState({
isMenuOpen: false,
isDeleting: false,
})
}
handleDeleteClick = () => {
this.setState({isDeleting: true})
}
handleDeleteCell = cell => () => {
this.props.onDeleteCell(cell)
}
@ -45,31 +31,27 @@ class NameableGraph extends Component {
}
render() {
const {cell, children, isEditable, onEditCell, onUpdateCell} = this.props
const {cell, children, isEditable} = this.props
const {cellName, isMenuOpen} = this.state
const {isDeleting} = this.state
const queries = _.get(cell, ['queries'], [])
return (
<div className="dash-graph">
<NameableGraphHeader
cell={cell}
cellName={cellName}
isEditable={isEditable}
onUpdateCell={onUpdateCell}
onRenameCell={this.handleRenameCell}
onCancelEditCell={this.handleCancelEdit}
/>
<ContextMenu
<LayoutCellMenu
cell={cell}
onDeleteClick={this.handleDeleteClick}
onDelete={this.handleDeleteCell}
onRename={!cell.isEditing && isEditable ? onEditCell : () => {}}
toggleMenu={this.toggleMenu}
isOpen={isMenuOpen}
isDeleting={isDeleting}
isEditable={isEditable}
handleClickOutside={this.closeMenu}
onEdit={this.handleSummonOverlay}
/>
<LayoutCellHeader
cellName={cell.name}
queries={queries}
isEditable={isEditable}
/>
<div className="dash-graph--container">
{queries.length
? children
@ -89,7 +71,7 @@ class NameableGraph extends Component {
const {array, bool, func, node, number, shape, string} = PropTypes
NameableGraph.propTypes = {
LayoutCell.propTypes = {
cell: shape({
name: string.isRequired,
isEditing: bool,
@ -98,13 +80,10 @@ NameableGraph.propTypes = {
queries: array,
}).isRequired,
children: node.isRequired,
onEditCell: func,
onRenameCell: func,
onUpdateCell: func,
onDeleteCell: func,
onSummonOverlayTechnologies: func,
isEditable: bool,
onCancelEditCell: func,
}
export default NameableGraph
export default LayoutCell

View File

@ -0,0 +1,41 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import CustomTimeIndicator from 'shared/components/CustomTimeIndicator'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants/index'
const LayoutCellHeader = ({queries, isEditable, cellName}) => {
const cellNameIsDefault = cellName === NEW_DEFAULT_DASHBOARD_CELL.name
return (
<div
className={classnames('dash-graph--heading', {
'dash-graph--heading-draggable': isEditable,
})}
>
<span
className={
cellNameIsDefault
? 'dash-graph--name dash-graph--name__default'
: 'dash-graph--name'
}
>
{cellName}
{queries && queries.length
? <CustomTimeIndicator queries={queries} />
: null}
</span>
</div>
)
}
const {array, bool, string} = PropTypes
LayoutCellHeader.propTypes = {
queries: array,
isEditable: bool,
cellName: string,
}
export default LayoutCellHeader

View File

@ -0,0 +1,53 @@
import React, {PropTypes} from 'react'
import OnClickOutside from 'react-onclickoutside'
const LayoutCellMenu = OnClickOutside(
({isDeleting, onEdit, onDeleteClick, onDelete, cell}) =>
<div
className={
isDeleting
? 'dash-graph-context dash-graph-context__deleting'
: 'dash-graph-context'
}
>
<div className="dash-graph-context--button" onClick={onEdit(cell)}>
<span className="icon pencil" />
</div>
{isDeleting
? <div className="dash-graph-context--button active">
<span className="icon trash" />
<div
className="dash-graph-context--confirm"
onClick={onDelete(cell)}
>
Confirm
</div>
</div>
: <div className="dash-graph-context--button" onClick={onDeleteClick}>
<span className="icon trash" />
</div>}
</div>
)
const LayoutCellMenuContainer = props => {
if (!props.isEditable) {
return null
}
return <LayoutCellMenu {...props} />
}
const {bool, func, shape} = PropTypes
LayoutCellMenuContainer.propTypes = {
isDeleting: bool,
onEdit: func,
onDelete: func,
onDeleteClick: func,
cell: shape(),
isEditable: bool,
}
LayoutCellMenu.propTypes = LayoutCellMenuContainer.propTypes
export default LayoutCellMenuContainer

View File

@ -2,7 +2,7 @@ import React, {Component, PropTypes} from 'react'
import ReactGridLayout, {WidthProvider} from 'react-grid-layout'
import NameableGraph from 'shared/components/NameableGraph'
import LayoutCell from 'shared/components/LayoutCell'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import AlertsApp from 'src/alerts/containers/AlertsApp'
import NewsFeed from 'src/status/components/NewsFeed'
@ -139,8 +139,6 @@ class LayoutRenderer extends Component {
cells,
onEditCell,
onCancelEditCell,
onRenameCell,
onUpdateCell,
onDeleteCell,
onSummonOverlayTechnologies,
timeRange,
@ -156,12 +154,10 @@ class LayoutRenderer extends Component {
return (
<div key={cell.i}>
<NameableGraph
<LayoutCell
onCancelEditCell={onCancelEditCell}
isEditable={isEditable}
onEditCell={onEditCell}
onRenameCell={onRenameCell}
onUpdateCell={onUpdateCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
cell={cell}
@ -179,7 +175,7 @@ class LayoutRenderer extends Component {
axes={axes}
onZoom={onZoom}
/>}
</NameableGraph>
</LayoutCell>
</div>
)
})
@ -297,8 +293,6 @@ LayoutRenderer.propTypes = {
}).isRequired,
onPositionChange: func,
onEditCell: func,
onRenameCell: func,
onUpdateCell: func,
onDeleteCell: func,
onSummonOverlayTechnologies: func,
synchronizer: func,

View File

@ -1,85 +0,0 @@
import React, {PropTypes} from 'react'
import classnames from 'classnames'
import CustomTimeIndicator from 'shared/components/CustomTimeIndicator'
const NameableGraphHeader = ({
onCancelEditCell,
isEditable,
onRenameCell,
onUpdateCell,
cell,
cellName,
cell: {i, 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, name: cellName})()
}
if (evt.key === 'Escape') {
onCancelEditCell(i)
}
}
return (
<div className={className}>
{isInputVisible
? <GraphNameInput
value={cellName}
onChange={onRenameCell}
onBlur={onUpdateCell({...cell, name: cellName})}
onKeyUp={onKeyUp}
/>
: <GraphName name={name} queries={queries} />}
</div>
)
}
const {arrayOf, bool, func, string, shape} = PropTypes
NameableGraphHeader.propTypes = {
cell: shape(),
cellName: string,
onRenameCell: func,
onUpdateCell: func,
isEditable: bool,
onCancelEditCell: func,
}
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

View File

@ -132,7 +132,6 @@ $dash-graph-options-arrow: 8px;
background-color: $g5-pepper;
}
}
.dash-graph--name-edit,
.dash-graph--name {
font-size: 13px;
font-weight: 600;
@ -142,25 +141,22 @@ $dash-graph-options-arrow: 8px;
white-space: nowrap;
}
.dash-graph--name {
position: relative;
height: $dash-graph-heading;
line-height: $dash-graph-heading;
width: calc(100% - 30px);
padding-left: 16px;
width: calc(100% - 53px);
padding-left: 10px;
transition:
color 0.25s ease,
background-color 0.25s ease,
border-color 0.25s ease;
}
input.form-control.dash-graph--name-edit {
margin-left: 8px;
padding: 0 6px;
width: calc(100% - 42px);
height: 26px !important;
line-height: (26px - 4px) !important;
position: relative;
top: -1px; // Fix for slight offset
.dash-graph--name.dash-graph--name__default {
font-style: italic;
}
.dash-graph--custom-time {
font-style: normal;
font-family: $code-font;
color: $c-pool;
background-color: $g2-kevlar;
@ -171,111 +167,93 @@ input.form-control.dash-graph--name-edit {
padding: 0 7px;
position: absolute;
top: 3px;
right: 30px;
}
.presentation-mode .dash-graph--custom-time {
right: 2px;
}
.dash-graph--options {
width: $dash-graph-heading;
.dash-graph-context {
z-index: 2;
position: absolute;
right: 0px;
top: 0px;
text-align: center;
top: 0;
right: 3px;
height: 30px;
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.dash-graph-context--button {
width: 24px;
height: 24px;
border-radius: 3px;
font-size: 12px;
position: relative;
color: $g11-sidewalk;
transition:
color 0.25s ease,
background-color 0.25s ease;
> .btn {
background-color: transparent !important;
padding: 0;
margin: 4px 0;
height: $dash-graph-heading-context;
width: $dash-graph-heading-context;
line-height: $dash-graph-heading-context;
transition:
background-color 0.25s ease,
color 0.25s ease !important;
&:hover,
&.active {
cursor: pointer;
color: $g20-white;
background-color: $g5-pepper;
}
&:first-child {margin-right: 2px;}
&:hover {
background-color: $g5-pepper !important;
color: $g20-white;
}
> .icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}
}
.presentation-mode .dash-graph--options {
display: none;
visibility: hidden;
}
.dash-graph--options-menu {
.dash-graph-context--confirm {
z-index: 3;
position: absolute;
top: ($dash-graph-heading + $dash-graph-options-arrow);
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
display: block;
z-index: 11;
list-style: none;
padding: 0;
margin: 0;
width: 90px;
visibility: hidden;
transition-property: all;
width: 58px;
height: 24px;
border-radius: 3px;
text-align: center;
font-size: 12px;
font-weight: 700;
line-height: 24px;
background-color: $c-curacao;
color: $g20-white;
transition: background-color 0.25s ease;
> li {
@include no-user-select;
position: relative;
&:before {
position: absolute;
content: '';
border: 6px solid transparent;
border-bottom-color: $c-curacao;
left: 50%;
top: 0;
transform: translate(-50%,-100%);
transition: border-color 0.25s ease;
}
&:hover {
background-color: $c-dreamsicle;
cursor: pointer;
}
&:hover:before {
border-bottom-color: $c-dreamsicle;
}
}
/* Presentation Mode */
.presentation-mode {
.dash-graph-context {
display: none;
}
.dash-graph--name {
width: 100%;
height: 28px;
line-height: 28px;
background-color: $g5-pepper;
padding: 0 11px;
margin: 0;
text-align: left;
color: $g15-platinum;
opacity: 0;
transition:
opacity 0.25s ease,
color 0.25s ease,
background-color 0.25s ease;
&:first-child {
border-radius: $radius $radius 0 0;
&:before {
content: '';
width: 0;
height: 0;
border-width: $dash-graph-options-arrow;
border-style: solid;
border-color: transparent transparent $g5-pepper transparent;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -($dash-graph-options-arrow * 2);
}
}
&:last-child {
border-radius: 0 0 $radius $radius;
}
&:hover {
cursor: pointer;
background-color: $g7-graphite;
color: $g20-white;
}
}
}
/* Menu Open State */
.dash-graph--options.dash-graph--options-show {
> .btn {
color: $g20-white;
background-color: $g5-pepper !important;
}
.dash-graph--options-menu { visibility: visible; }
.dash-graph--options-menu > li { opacity: 1; }
}
.graph-panel__refreshing {
position: absolute;
top: -18px !important;