diff --git a/ui/spec/dashboards/reducers/uiSpec.js b/ui/spec/dashboards/reducers/uiSpec.js index 3fb00e392..4bf24f607 100644 --- a/ui/spec/dashboards/reducers/uiSpec.js +++ b/ui/spec/dashboards/reducers/uiSpec.js @@ -6,6 +6,8 @@ import { setDashboard, setTimeRange, setEditMode, + editCell, + renameCell, } from 'src/dashboards/actions' const noopAction = () => { @@ -49,4 +51,50 @@ describe('DataExplorer.Reducers.UI', () => { const actual = reducer(state, setEditMode(isEditMode)) expect(actual.isEditMode).to.equal(isEditMode) }) + + it('can edit cell', () => { + const dash = { + id: 1, + cells: [{ + x: 0, + y: 0, + w: 4, + h: 4, + id: 1, + isEditing: false, + name: "Gigawatts", + }], + } + + state = { + dashboard: dash, + dashboards: [dash], + } + + const actual = reducer(state, editCell(0, 0, true)) + expect(actual.dashboards[0].cells[0].isEditing).to.equal(true) + }) + + it('can rename cells', () => { + const dash = { + id: 1, + cells: [{ + x: 0, + y: 0, + w: 4, + h: 4, + id: 1, + isEditing: true, + name: "Gigawatts", + }], + } + + state = { + dashboard: dash, + dashboards: [dash], + } + + const actual = reducer(state, renameCell(0, 0, "Plutonium Consumption Rate (ug/sec)")) + expect(actual.dashboards[0].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 f28e0f355..90385fc7f 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -39,6 +39,27 @@ export const updateDashboard = (dashboard) => ({ }, }) +export const editCell = (x, y, isEditing) => ({ + type: 'EDIT_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 + // as a suitable id + payload: { + x, // x-coord of the cell to be edited + y, // y-coord of the cell to be edited + isEditing, + }, +}) + +export const renameCell = (x, y, name) => ({ + type: 'RENAME_CELL', + payload: { + x, // x-coord of the cell to be renamed + y, // y-coord of the cell to be renamed + name, + }, +}) // Async Action Creators diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index d2c48b2a4..101e21979 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -10,6 +10,8 @@ const Dashboard = ({ inPresentationMode, onPositionChange, onEditCell, + onRenameCell, + onUpdateCell, source, autoRefresh, timeRange, @@ -22,13 +24,13 @@ const Dashboard = ({
{isEditMode ? : null} - {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell)} + {Dashboard.renderDashboard(dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell)}
) } -Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell) => { +Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositionChange, onEditCell, onRenameCell, onUpdateCell) => { const cells = dashboard.cells.map((cell, i) => { i = `${i}` const dashboardCell = {...cell, i} @@ -47,6 +49,8 @@ Dashboard.renderDashboard = (dashboard, autoRefresh, timeRange, source, onPositi source={source.links.proxy} onPositionChange={onPositionChange} onEditCell={onEditCell} + onRenameCell={onRenameCell} + onUpdateCell={onUpdateCell} /> ) } @@ -65,6 +69,8 @@ Dashboard.propTypes = { inPresentationMode: bool, onPositionChange: func, onEditCell: func, + onRenameCell: func, + onUpdateCell: func, source: shape({ links: shape({ proxy: string, diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 41f1f2f5d..2aefc8729 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -42,6 +42,8 @@ const DashboardPage = React.createClass({ setDashboard: func.isRequired, setTimeRange: func.isRequired, setEditMode: func.isRequired, + editCell: func.isRequired, + renameCell: func.isRequired, }).isRequired, dashboards: arrayOf(shape({ id: number.isRequired, @@ -92,16 +94,32 @@ const DashboardPage = React.createClass({ this.props.dashboardActions.putDashboard({...this.props.dashboard, cells}) }, - handleEditCell(cell) { - const {cells} = this.props.dashboard - const targetIdx = cells.findIndex((c) => cell.x === c.x && cell.y === c.y && cell.h === c.h && cell.w === c.w) + // Places cell into editing mode. + handleEditCell(x, y, isEditing) { + return () => { + this.props.dashboardActions.editCell(x, y, !isEditing) /* eslint-disable no-negated-condition */ + } + }, - const newCells = [ - ...cells.slice(0, targetIdx), - cell, - ...cells.slice(targetIdx + 1), - ] - this.props.dashboardActions.putDashboard({...this.props.dashboard, cells: newCells}) + handleChangeCellName(x, y) { + return (evt) => { + this.props.dashboardActions.renameCell(x, y, evt.target.value) + } + }, + + handleUpdateCell(newCell) { + return () => { + const {cells} = this.props.dashboard + const cellIdx = cells.findIndex((c) => c.x === newCell.x && c.y === newCell.y) + + this.handleUpdatePosition([ + ...cells.slice(0, cellIdx), + newCell, + ...cells.slice(cellIdx + 1), + ]) + + this.props.dashboardActions.editCell(newCell.x, newCell.y, false) + } }, render() { @@ -153,6 +171,8 @@ const DashboardPage = React.createClass({ timeRange={timeRange} onPositionChange={this.handleUpdatePosition} onEditCell={this.handleEditCell} + onRenameCell={this.handleChangeCellName} + onUpdateCell={this.handleUpdateCell} /> ); diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 72c339294..2796b0b16 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -50,6 +50,62 @@ export default function ui(state = initialState, action) { return {...state, ...newState} } + + case 'EDIT_CELL': { + const {x, y, isEditing} = action.payload + const {dashboard} = state + + const cellIdx = dashboard.cells.findIndex((cell) => cell.x === x && cell.y === y) + + const newCell = { + ...dashboard.cells[cellIdx], + isEditing, + } + + const newDashboard = { + ...dashboard, + cells: [ + ...dashboard.cells.slice(0, cellIdx), + newCell, + ...dashboard.cells.slice(cellIdx + 1), + ], + } + + const newState = { + newDashboard, + dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), + } + + return {...state, ...newState} + } + + case 'RENAME_CELL': { + const {x, y, name} = action.payload + const {dashboard} = state + + const cellIdx = dashboard.cells.findIndex((cell) => cell.x === x && cell.y === y) + + const newCell = { + ...dashboard.cells[cellIdx], + name, + } + + const newDashboard = { + ...dashboard, + cells: [ + ...dashboard.cells.slice(0, cellIdx), + newCell, + ...dashboard.cells.slice(cellIdx + 1), + ], + } + + const newState = { + newDashboard, + dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), + } + + return {...state, ...newState} + } } return state; diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index dcda42b2a..f508617a6 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -52,6 +52,8 @@ export const LayoutRenderer = React.createClass({ source: string, onPositionChange: func, onEditCell: func, + onRenameCell: func, + onUpdateCell: func, }, buildQuery(q) { @@ -86,7 +88,7 @@ export const LayoutRenderer = React.createClass({ }, generateVisualizations() { - const {autoRefresh, source, cells} = this.props; + const {autoRefresh, source, cells, onEditCell, onRenameCell, onUpdateCell} = this.props; return cells.map((cell) => { const qs = cell.queries.map((q) => { @@ -99,7 +101,12 @@ export const LayoutRenderer = React.createClass({ if (cell.type === 'single-stat') { return (
- +
@@ -113,7 +120,12 @@ export const LayoutRenderer = React.createClass({ return (
- + { + let nameOrField - getInitialState() { - return { - editing: false, - name: this.props.cell.name, - } - }, + if (isEditing) { + nameOrField = ( + + ) + } else { + nameOrField = name + } - handleClick() { - this.setState({ - editing: !this.state.editing, /* eslint-disable no-negated-condition */ - }); - }, - - handleChangeName() { - this.props.onRename({ - ...this.props.cell, - name: this.state.name, - }) - }, - - handleChange(evt) { - this.setState({ - name: evt.target.value, - }) - }, - - render() { - let nameOrField - if (!this.state.editing) { - nameOrField = this.props.cell.name - } else { - nameOrField = - } - - return ( -
-

{nameOrField}

-
- {this.props.children} -
+ return ( +
+

{nameOrField}

+
+ {children}
- ); - }, -}); +
+ ) +} + +const { + func, + node, + shape, + string, +} = PropTypes + +NameableGraph.propTypes = { + cell: shape({ + name: string.isRequired, + x: string.isRequired, + y: string.isRequired, + }).isRequired, + children: node.isRequired, + onEditCell: func.isRequired, + onRenameCell: func.isRequired, + onUpdateCell: func.isRequired, +} export default NameableGraph;