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;