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 validationspull/1058/head
parent
959b387f61
commit
2d8d546368
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3226,9 +3226,6 @@
|
|||
"items": {
|
||||
"description": "cell visualization information",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"queries"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"description": "X-coordinate of Cell in the Dashboard",
|
||||
|
|
|
@ -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)")
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
|
|
Loading…
Reference in New Issue