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
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)")
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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" />
|
||||||
|
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
|
||||||
|
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
))
|
))
|
||||||
|
|
Loading…
Reference in New Issue