Create a new dashboard cell; Fix remaining bugs with Overlay (#1056)

* Relax query validation for cell endpoint

* Dashboards can now add a cell; Rebase over 950-overlay_technologies-edit

* Server now returns empty queries array when creating a new dashboard cell

* Use async/await pattern for addDashboardCell, add basic error handling

* Update names of methods and actions for editing and updating cells to match those for adding

Factor out newDefaultCell to dashboard constants

* Update CHANGELOG

* Fix bug where Overlay wouldn’t display for query-less cells

* We removed these validations
pull/1058/head
lukevmorris 2017-03-23 17:50:21 -07:00 committed by Jared Scheib
parent 959b387f61
commit 2d8d546368
12 changed files with 99 additions and 70 deletions

View File

@ -6,6 +6,7 @@
1. [#1020](https://github.com/influxdata/chronograf/pull/1020): Users can now edit cell names on dashboards 1. [#1020](https://github.com/influxdata/chronograf/pull/1020): Users can now edit cell names on dashboards
2. [#1035](https://github.com/influxdata/chronograf/pull/1035): Convert many InfluxQL statements to query builder 2. [#1035](https://github.com/influxdata/chronograf/pull/1035): Convert many InfluxQL statements to query builder
3. [#1015](https://github.com/influxdata/chronograf/pull/1015): Introduce ability to edit a dashboard cell 3. [#1015](https://github.com/influxdata/chronograf/pull/1015): Introduce ability to edit a dashboard cell
4. [#1056](https://github.com/influxdata/chronograf/pull/1056): Introduce ability to add a dashboard cell
### UI Improvements ### UI Improvements

View File

@ -50,6 +50,9 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
AddQueryConfigs(&d) AddQueryConfigs(&d)
cells := make([]dashboardCellResponse, len(d.Cells)) cells := make([]dashboardCellResponse, len(d.Cells))
for i, cell := range d.Cells { for i, cell := range d.Cells {
if len(cell.Queries) == 0 {
cell.Queries = make([]chronograf.DashboardQuery, 0)
}
cells[i] = dashboardCellResponse{ cells[i] = dashboardCellResponse{
DashboardCell: cell, DashboardCell: cell,
Links: dashboardCellLinks{ Links: dashboardCellLinks{
@ -240,14 +243,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
// ValidDashboardRequest verifies that the dashboard cells have a query // ValidDashboardRequest verifies that the dashboard cells have a query
func ValidDashboardRequest(d *chronograf.Dashboard) error { func ValidDashboardRequest(d *chronograf.Dashboard) error {
if len(d.Cells) == 0 {
return fmt.Errorf("cells are required")
}
for i, c := range d.Cells { for i, c := range d.Cells {
if len(c.Queries) == 0 {
return fmt.Errorf("query required")
}
CorrectWidthHeight(&c) CorrectWidthHeight(&c)
d.Cells[i] = c d.Cells[i] = c
} }
@ -257,9 +253,6 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error {
// ValidDashboardCellRequest verifies that the dashboard cells have a query // ValidDashboardCellRequest verifies that the dashboard cells have a query
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
if len(c.Queries) == 0 {
return fmt.Errorf("query required")
}
CorrectWidthHeight(c) CorrectWidthHeight(c)
return nil return nil
} }

View File

@ -187,38 +187,6 @@ func TestValidDashboardRequest(t *testing.T) {
}, },
}, },
}, },
{
name: "No queries",
d: chronograf.Dashboard{
Cells: []chronograf.DashboardCell{
{
W: 2,
H: 2,
Queries: []chronograf.DashboardQuery{},
},
},
},
want: chronograf.Dashboard{
Cells: []chronograf.DashboardCell{
{
W: 2,
H: 2,
Queries: []chronograf.DashboardQuery{},
},
},
},
wantErr: true,
},
{
name: "Empty Cells",
d: chronograf.Dashboard{
Cells: []chronograf.DashboardCell{},
},
want: chronograf.Dashboard{
Cells: []chronograf.DashboardCell{},
},
wantErr: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
err := ValidDashboardRequest(&tt.d) err := ValidDashboardRequest(&tt.d)

View File

@ -3226,9 +3226,6 @@
"items": { "items": {
"description": "cell visualization information", "description": "cell visualization information",
"type": "object", "type": "object",
"required": [
"queries"
],
"properties": { "properties": {
"x": { "x": {
"description": "X-coordinate of Cell in the Dashboard", "description": "X-coordinate of Cell in the Dashboard",

View File

@ -7,8 +7,8 @@ import {
setTimeRange, setTimeRange,
setEditMode, setEditMode,
updateDashboardCells, updateDashboardCells,
editCell, editDashboardCell,
renameCell, renameDashboardCell,
syncDashboardCell, syncDashboardCell,
} from 'src/dashboards/actions' } from 'src/dashboards/actions'
@ -91,7 +91,7 @@ describe('DataExplorer.Reducers.UI', () => {
dashboards: [dash], dashboards: [dash],
} }
const actual = reducer(state, editCell(0, 0, true)) const actual = reducer(state, editDashboardCell(0, 0, true))
expect(actual.dashboards[0].cells[0].isEditing).to.equal(true) expect(actual.dashboards[0].cells[0].isEditing).to.equal(true)
expect(actual.dashboard.cells[0].isEditing).to.equal(true) expect(actual.dashboard.cells[0].isEditing).to.equal(true)
}) })
@ -122,7 +122,7 @@ describe('DataExplorer.Reducers.UI', () => {
dashboards: [dash], dashboards: [dash],
} }
const actual = reducer(state, renameCell(0, 0, "Plutonium Consumption Rate (ug/sec)")) const actual = reducer(state, renameDashboardCell(0, 0, "Plutonium Consumption Rate (ug/sec)"))
expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)") expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
expect(actual.dashboard.cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)") expect(actual.dashboard.cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
}) })

View File

@ -2,8 +2,11 @@ import {
getDashboards as getDashboardsAJAX, getDashboards as getDashboardsAJAX,
updateDashboard as updateDashboardAJAX, updateDashboard as updateDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX, updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
} from 'src/dashboards/apis' } from 'src/dashboards/apis'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
export const loadDashboards = (dashboards, dashboardID) => ({ export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS', type: 'LOAD_DASHBOARDS',
payload: { payload: {
@ -54,8 +57,15 @@ export const syncDashboardCell = (cell) => ({
}, },
}) })
export const editCell = (x, y, isEditing) => ({ export const addDashboardCell = (cell) => ({
type: 'EDIT_CELL', type: 'ADD_DASHBOARD_CELL',
payload: {
cell,
},
})
export const editDashboardCell = (x, y, isEditing) => ({
type: 'EDIT_DASHBOARD_CELL',
// x and y coords are used as a alternative to cell ids, which are not // x and y coords are used as a alternative to cell ids, which are not
// universally unique, and cannot be because React depends on a // universally unique, and cannot be because React depends on a
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act // quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
@ -67,8 +77,8 @@ export const editCell = (x, y, isEditing) => ({
}, },
}) })
export const renameCell = (x, y, name) => ({ export const renameDashboardCell = (x, y, name) => ({
type: 'RENAME_CELL', type: 'RENAME_DASHBOARD_CELL',
payload: { payload: {
x, // x-coord of the cell to be renamed x, // x-coord of the cell to be renamed
y, // y-coord of the cell to be renamed y, // y-coord of the cell to be renamed
@ -97,3 +107,13 @@ export const updateDashboardCell = (cell) => (dispatch) => {
dispatch(syncDashboardCell(data)) dispatch(syncDashboardCell(data))
}) })
} }
export const addDashboardCellAsync = (dashboard) => async (dispatch) => {
try {
const {data} = await addDashboardCellAJAX(dashboard, NEW_DEFAULT_DASHBOARD_CELL)
dispatch(addDashboardCell(data))
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -35,3 +35,16 @@ export const createDashboard = async (dashboard) => {
throw error throw error
} }
} }
export const addDashboardCell = async (dashboard, cell) => {
try {
return await AJAX({
method: 'POST',
url: dashboard.links.cells,
data: cell,
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -19,6 +19,7 @@ const DashboardHeader = ({
handleClickPresentationButton, handleClickPresentationButton,
sourceID, sourceID,
source, source,
onAddCell,
}) => isHidden ? null : ( }) => isHidden ? null : (
<div className="page-header full-width"> <div className="page-header full-width">
<div className="page-header__container"> <div className="page-header__container">
@ -39,6 +40,13 @@ const DashboardHeader = ({
} }
</div> </div>
<div className="page-header__right"> <div className="page-header__right">
{
dashboard ?
<button className="btn btn-info btn-sm" onClick={onAddCell}>
<span className="icon plus" />
&nbsp;Add Cell
</button> : null
}
{sourceID ? {sourceID ?
<Link className="btn btn-info btn-sm" to={`/sources/${sourceID}/dashboards/${dashboard && dashboard.id}/edit`} > <Link className="btn btn-info btn-sm" to={`/sources/${sourceID}/dashboards/${dashboard && dashboard.id}/edit`} >
<span className="icon pencil" /> <span className="icon pencil" />
@ -83,6 +91,7 @@ DashboardHeader.propTypes = {
handleChooseAutoRefresh: func.isRequired, handleChooseAutoRefresh: func.isRequired,
handleClickPresentationButton: func.isRequired, handleClickPresentationButton: func.isRequired,
source: shape({}), source: shape({}),
onAddCell: func.isRequired,
} }
export default DashboardHeader export default DashboardHeader

View File

@ -21,6 +21,7 @@ export const NEW_DASHBOARD = {
w: 4, w: 4,
h: 4, h: 4,
name: 'Name This Graph', name: 'Name This Graph',
type: 'line',
queries: [ queries: [
{ {
query: "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"", query: "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
@ -32,3 +33,8 @@ export const NEW_DASHBOARD = {
}, },
], ],
} }
export const NEW_DEFAULT_DASHBOARD_CELL = {
query: [],
type: 'line',
}

View File

@ -43,8 +43,9 @@ const DashboardPage = React.createClass({
setDashboard: func.isRequired, setDashboard: func.isRequired,
setTimeRange: func.isRequired, setTimeRange: func.isRequired,
setEditMode: func.isRequired, setEditMode: func.isRequired,
editCell: func.isRequired, addDashboardCellAsync: func.isRequired,
renameCell: func.isRequired, editDashboardCell: func.isRequired,
renameDashboardCell: func.isRequired,
}).isRequired, }).isRequired,
dashboards: arrayOf(shape({ dashboards: arrayOf(shape({
id: number.isRequired, id: number.isRequired,
@ -128,22 +129,27 @@ const DashboardPage = React.createClass({
this.props.dashboardActions.putDashboard() this.props.dashboardActions.putDashboard()
}, },
handleAddCell() {
const {dashboard} = this.props
this.props.dashboardActions.addDashboardCellAsync(dashboard)
},
// Places cell into editing mode. // Places cell into editing mode.
handleEditCell(x, y, isEditing) { handleEditDashboardCell(x, y, isEditing) {
return () => { return () => {
this.props.dashboardActions.editCell(x, y, !isEditing) /* eslint-disable no-negated-condition */ this.props.dashboardActions.editDashboardCell(x, y, !isEditing) /* eslint-disable no-negated-condition */
} }
}, },
handleChangeCellName(x, y) { handleRenameDashboardCell(x, y) {
return (evt) => { return (evt) => {
this.props.dashboardActions.renameCell(x, y, evt.target.value) this.props.dashboardActions.renameDashboardCell(x, y, evt.target.value)
} }
}, },
handleUpdateCell(newCell) { handleUpdateDashboardCell(newCell) {
return () => { return () => {
this.props.dashboardActions.editCell(newCell.x, newCell.y, false) this.props.dashboardActions.editDashboardCell(newCell.x, newCell.y, false)
this.props.dashboardActions.putDashboard() this.props.dashboardActions.putDashboard()
} }
}, },
@ -169,7 +175,7 @@ const DashboardPage = React.createClass({
return ( return (
<div className="page"> <div className="page">
{ {
selectedCell && selectedCell.queries.length ? selectedCell ?
<CellEditorOverlay <CellEditorOverlay
cell={selectedCell} cell={selectedCell}
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
@ -193,6 +199,7 @@ const DashboardPage = React.createClass({
dashboard={dashboard} dashboard={dashboard}
sourceID={sourceID} sourceID={sourceID}
source={source} source={source}
onAddCell={this.handleAddCell}
> >
{(dashboards).map((d, i) => { {(dashboards).map((d, i) => {
return ( return (
@ -213,9 +220,9 @@ const DashboardPage = React.createClass({
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
timeRange={timeRange} timeRange={timeRange}
onPositionChange={this.handleUpdatePosition} onPositionChange={this.handleUpdatePosition}
onEditCell={this.handleEditCell} onEditCell={this.handleEditDashboardCell}
onRenameCell={this.handleChangeCellName} onRenameCell={this.handleRenameDashboardCell}
onUpdateCell={this.handleUpdateCell} onUpdateCell={this.handleUpdateDashboardCell}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies} onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/> />
</div> </div>

View File

@ -70,7 +70,22 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} return {...state, ...newState}
} }
case 'EDIT_CELL': { case 'ADD_DASHBOARD_CELL': {
const {cell} = action.payload
const {dashboard, dashboards} = state
const newCells = [cell, ...dashboard.cells]
const newDashboard = {...dashboard, cells: newCells}
const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d)
const newState = {
dashboard: newDashboard,
dashboards: newDashboards,
}
return {...state, ...newState}
}
case 'EDIT_DASHBOARD_CELL': {
const {x, y, isEditing} = action.payload const {x, y, isEditing} = action.payload
const {dashboard} = state const {dashboard} = state
@ -111,7 +126,7 @@ export default function ui(state = initialState, action) {
return {...state, ...newState} return {...state, ...newState}
} }
case 'RENAME_CELL': { case 'RENAME_DASHBOARD_CELL': {
const {x, y, name} = action.payload const {x, y, name} = action.payload
const {dashboard} = state const {dashboard} = state

View File

@ -96,7 +96,7 @@ const NameableGraph = React.createClass({
<div className="dash-graph"> <div className="dash-graph">
<div className="dash-graph--heading"> <div className="dash-graph--heading">
<div onClick={onClickHandler(x, y, isEditing)}>{nameOrField}</div> <div onClick={onClickHandler(x, y, isEditing)}>{nameOrField}</div>
<ContextMenu isOpen={this.state.isMenuOpen} toggleMenu={this.toggleMenu} onSummonOverlayTechnologies={onSummonOverlayTechnologies} cell={cell} handleClickOutside={this.closeMenu}/> <ContextMenu isOpen={this.state.isMenuOpen} toggleMenu={this.toggleMenu} onEdit={onSummonOverlayTechnologies} cell={cell} handleClickOutside={this.closeMenu}/>
</div> </div>
<div className="dash-graph--container"> <div className="dash-graph--container">
{children} {children}
@ -106,13 +106,13 @@ const NameableGraph = React.createClass({
}, },
}) })
const ContextMenu = OnClickOutside(({isOpen, toggleMenu, onSummonOverlayTechnologies, cell}) => ( const ContextMenu = OnClickOutside(({isOpen, toggleMenu, onEdit, cell}) => (
<div className={classnames("dash-graph--options", {"dash-graph--options-show": isOpen})} onClick={toggleMenu}> <div className={classnames("dash-graph--options", {"dash-graph--options-show": isOpen})} onClick={toggleMenu}>
<button className="btn btn-info btn-xs"> <button className="btn btn-info btn-xs">
<span className="icon caret-down"></span> <span className="icon caret-down"></span>
</button> </button>
<ul className="dash-graph--options-menu"> <ul className="dash-graph--options-menu">
<li onClick={() => onSummonOverlayTechnologies(cell)}>Edit</li> <li onClick={() => onEdit(cell)}>Edit</li>
</ul> </ul>
</div> </div>
)) ))