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
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
4. [#1056](https://github.com/influxdata/chronograf/pull/1056): Introduce ability to add a dashboard cell
### UI Improvements

View File

@ -50,6 +50,9 @@ func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
AddQueryConfigs(&d)
cells := make([]dashboardCellResponse, len(d.Cells))
for i, cell := range d.Cells {
if len(cell.Queries) == 0 {
cell.Queries = make([]chronograf.DashboardQuery, 0)
}
cells[i] = dashboardCellResponse{
DashboardCell: cell,
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
func ValidDashboardRequest(d *chronograf.Dashboard) error {
if len(d.Cells) == 0 {
return fmt.Errorf("cells are required")
}
for i, c := range d.Cells {
if len(c.Queries) == 0 {
return fmt.Errorf("query required")
}
CorrectWidthHeight(&c)
d.Cells[i] = c
}
@ -257,9 +253,6 @@ func ValidDashboardRequest(d *chronograf.Dashboard) error {
// ValidDashboardCellRequest verifies that the dashboard cells have a query
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
if len(c.Queries) == 0 {
return fmt.Errorf("query required")
}
CorrectWidthHeight(c)
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 {
err := ValidDashboardRequest(&tt.d)

View File

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

View File

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

View File

@ -2,8 +2,11 @@ import {
getDashboards as getDashboardsAJAX,
updateDashboard as updateDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
} from 'src/dashboards/apis'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS',
payload: {
@ -54,8 +57,15 @@ export const syncDashboardCell = (cell) => ({
},
})
export const editCell = (x, y, isEditing) => ({
type: 'EDIT_CELL',
export const addDashboardCell = (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
// universally unique, and cannot be because React depends on a
// 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) => ({
type: 'RENAME_CELL',
export const renameDashboardCell = (x, y, name) => ({
type: 'RENAME_DASHBOARD_CELL',
payload: {
x, // x-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))
})
}
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
}
}
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,
sourceID,
source,
onAddCell,
}) => isHidden ? null : (
<div className="page-header full-width">
<div className="page-header__container">
@ -39,6 +40,13 @@ const DashboardHeader = ({
}
</div>
<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 ?
<Link className="btn btn-info btn-sm" to={`/sources/${sourceID}/dashboards/${dashboard && dashboard.id}/edit`} >
<span className="icon pencil" />
@ -83,6 +91,7 @@ DashboardHeader.propTypes = {
handleChooseAutoRefresh: func.isRequired,
handleClickPresentationButton: func.isRequired,
source: shape({}),
onAddCell: func.isRequired,
}
export default DashboardHeader

View File

@ -21,6 +21,7 @@ export const NEW_DASHBOARD = {
w: 4,
h: 4,
name: 'Name This Graph',
type: 'line',
queries: [
{
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,
setTimeRange: func.isRequired,
setEditMode: func.isRequired,
editCell: func.isRequired,
renameCell: func.isRequired,
addDashboardCellAsync: func.isRequired,
editDashboardCell: func.isRequired,
renameDashboardCell: func.isRequired,
}).isRequired,
dashboards: arrayOf(shape({
id: number.isRequired,
@ -128,22 +129,27 @@ const DashboardPage = React.createClass({
this.props.dashboardActions.putDashboard()
},
handleAddCell() {
const {dashboard} = this.props
this.props.dashboardActions.addDashboardCellAsync(dashboard)
},
// Places cell into editing mode.
handleEditCell(x, y, isEditing) {
handleEditDashboardCell(x, y, isEditing) {
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) => {
this.props.dashboardActions.renameCell(x, y, evt.target.value)
this.props.dashboardActions.renameDashboardCell(x, y, evt.target.value)
}
},
handleUpdateCell(newCell) {
handleUpdateDashboardCell(newCell) {
return () => {
this.props.dashboardActions.editCell(newCell.x, newCell.y, false)
this.props.dashboardActions.editDashboardCell(newCell.x, newCell.y, false)
this.props.dashboardActions.putDashboard()
}
},
@ -169,7 +175,7 @@ const DashboardPage = React.createClass({
return (
<div className="page">
{
selectedCell && selectedCell.queries.length ?
selectedCell ?
<CellEditorOverlay
cell={selectedCell}
autoRefresh={autoRefresh}
@ -193,6 +199,7 @@ const DashboardPage = React.createClass({
dashboard={dashboard}
sourceID={sourceID}
source={source}
onAddCell={this.handleAddCell}
>
{(dashboards).map((d, i) => {
return (
@ -213,9 +220,9 @@ const DashboardPage = React.createClass({
autoRefresh={autoRefresh}
timeRange={timeRange}
onPositionChange={this.handleUpdatePosition}
onEditCell={this.handleEditCell}
onRenameCell={this.handleChangeCellName}
onUpdateCell={this.handleUpdateCell}
onEditCell={this.handleEditDashboardCell}
onRenameCell={this.handleRenameDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/>
</div>

View File

@ -70,7 +70,22 @@ export default function ui(state = initialState, action) {
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 {dashboard} = state
@ -111,7 +126,7 @@ export default function ui(state = initialState, action) {
return {...state, ...newState}
}
case 'RENAME_CELL': {
case 'RENAME_DASHBOARD_CELL': {
const {x, y, name} = action.payload
const {dashboard} = state

View File

@ -96,7 +96,7 @@ const NameableGraph = React.createClass({
<div className="dash-graph">
<div className="dash-graph--heading">
<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 className="dash-graph--container">
{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}>
<button className="btn btn-info btn-xs">
<span className="icon caret-down"></span>
</button>
<ul className="dash-graph--options-menu">
<li onClick={() => onSummonOverlayTechnologies(cell)}>Edit</li>
<li onClick={() => onEdit(cell)}>Edit</li>
</ul>
</div>
))