diff --git a/CHANGELOG.md b/CHANGELOG.md index ba20e3e5f..ef976082c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/server/dashboards.go b/server/dashboards.go index 8020f3cfb..07c1fb2e1 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -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 } diff --git a/server/dashboards_test.go b/server/dashboards_test.go index e792c2e38..55547f5fb 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -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) diff --git a/server/swagger.json b/server/swagger.json index 5c15a7a59..3ed4a9851 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3226,9 +3226,6 @@ "items": { "description": "cell visualization information", "type": "object", - "required": [ - "queries" - ], "properties": { "x": { "description": "X-coordinate of Cell in the Dashboard", diff --git a/ui/spec/dashboards/reducers/uiSpec.js b/ui/spec/dashboards/reducers/uiSpec.js index 318aeffa6..bb1521727 100644 --- a/ui/spec/dashboards/reducers/uiSpec.js +++ b/ui/spec/dashboards/reducers/uiSpec.js @@ -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)") }) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index c1ab8517f..b48541de3 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -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 + } +} diff --git a/ui/src/dashboards/apis/index.js b/ui/src/dashboards/apis/index.js index cc15d476d..7a4168f09 100644 --- a/ui/src/dashboards/apis/index.js +++ b/ui/src/dashboards/apis/index.js @@ -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 + } +} diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 9e8e3479e..3fbd5e7fa 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -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" /> + 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 diff --git a/ui/src/dashboards/constants/index.js b/ui/src/dashboards/constants/index.js index abc9aafb9..8ace203ba 100644 --- a/ui/src/dashboards/constants/index.js +++ b/ui/src/dashboards/constants/index.js @@ -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', +} diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 906126ac0..5610da0f6 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -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> diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index bff9e4375..abc1c73db 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -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 diff --git a/ui/src/shared/components/NameableGraph.js b/ui/src/shared/components/NameableGraph.js index f6881fc85..3ab2d991b 100644 --- a/ui/src/shared/components/NameableGraph.js +++ b/ui/src/shared/components/NameableGraph.js @@ -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> ))