From 7a7968e05deb7857d100d470ea956ebded5c445f Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 6 Jan 2017 15:26:05 +0100 Subject: [PATCH] Maintain focus on hovered node table rows More sophisticated row focusing Keeping deleted focused nodes in the table Fixed focus debouncing --- .../node-details/node-details-table-row.js | 19 +----- .../node-details/node-details-table.js | 67 +++++++++++++++++-- client/app/scripts/constants/timer.js | 1 + .../utils/__tests__/array-utils-test.js | 36 ++++++++++ client/app/scripts/utils/array-utils.js | 11 +++ 5 files changed, 111 insertions(+), 23 deletions(-) diff --git a/client/app/scripts/components/node-details/node-details-table-row.js b/client/app/scripts/components/node-details/node-details-table-row.js index a26bee5518..cba5210c1b 100644 --- a/client/app/scripts/components/node-details/node-details-table-row.js +++ b/client/app/scripts/components/node-details/node-details-table-row.js @@ -81,24 +81,12 @@ export default class NodeDetailsTableRow extends React.Component { this.saveLabelElementRef = this.saveLabelElementRef.bind(this); this.onMouseDown = this.onMouseDown.bind(this); this.onMouseUp = this.onMouseUp.bind(this); - this.onMouseEnter = this.onMouseEnter.bind(this); - this.onMouseLeave = this.onMouseLeave.bind(this); } saveLabelElementRef(ref) { this.labelElement = ref; } - onMouseEnter() { - const { node, onMouseEnterRow } = this.props; - onMouseEnterRow(node); - } - - onMouseLeave() { - const { node, onMouseLeaveRow } = this.props; - onMouseLeaveRow(node); - } - onMouseDown(ev) { const { pageX, pageY } = ev; this.mouseDragOrigin = [pageX, pageY]; @@ -121,8 +109,7 @@ export default class NodeDetailsTableRow extends React.Component { } render() { - const { node, nodeIdKey, topologyId, columns, onClick, onMouseEnterRow, onMouseLeaveRow, - selected, colStyles } = this.props; + const { node, nodeIdKey, topologyId, columns, onClick, selected, colStyles } = this.props; const [firstColumnStyle, ...columnStyles] = colStyles; const values = renderValues(node, columns, columnStyles); const nodeId = node[nodeIdKey]; @@ -132,8 +119,8 @@ export default class NodeDetailsTableRow extends React.Component { h.id === sortedBy); const sortedDesc = this.state.sortedDesc || defaultSortDesc(sortedByHeader); let nodes = getSortedNodes(this.props.nodes, sortedByHeader, sortedDesc); + if (focusedRowIndex && focusedRowIndex < nodes.length) { + const nodeRowIndex = findIndex(nodes, node => node.id === focusedNode.id); + if (nodeRowIndex >= 0) { + // If the focused node still exists in the table, we move it + // to the hovered row, keeping the rest of the table sorted. + moveElement(nodes, nodeRowIndex, focusedRowIndex); + } else { + // Otherwise we insert the dead focused node there, pretending + // it's still alive. That enables the users to read off all the + // info they want and perhaps even open the details panel. Also, + // only if we do this, we can guarantee that mouse hover will + // always freeze the table row until we focus out. + insertElement(nodes, focusedRowIndex, focusedNode); + } + } + const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit; const expanded = this.state.limit === 0; const notShown = nodes.length - this.state.limit; @@ -178,7 +231,7 @@ export default class NodeDetailsTable extends React.Component { style={this.props.tbodyStyle} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> - {nodes && nodes.map(node => ( + {nodes && nodes.map((node, index) => ( { this.onMouseEnterRow(index, node); }} + onMouseLeave={this.onMouseLeaveRow} topologyId={topologyId} /> ))} diff --git a/client/app/scripts/constants/timer.js b/client/app/scripts/constants/timer.js index 8c8d4dc4e0..fb6eb92f4e 100644 --- a/client/app/scripts/constants/timer.js +++ b/client/app/scripts/constants/timer.js @@ -1,3 +1,4 @@ /* Intervals in ms */ export const API_INTERVAL = 30000; export const TOPOLOGY_INTERVAL = 5000; +export const TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL = 200; diff --git a/client/app/scripts/utils/__tests__/array-utils-test.js b/client/app/scripts/utils/__tests__/array-utils-test.js index 49f0eb4bdf..c24dcba00a 100644 --- a/client/app/scripts/utils/__tests__/array-utils-test.js +++ b/client/app/scripts/utils/__tests__/array-utils-test.js @@ -45,4 +45,40 @@ describe('ArrayUtils', () => { } }); }); + + describe('insertElement', () => { + const f = (array, index, element) => { + ArrayUtils.insertElement(array, index, element); + return array; + }; + + it('it should insert an element into the array at the specified index', () => { + expect(f(['x', 'y', 'z'], 0, 'a')).toEqual(['a', 'x', 'y', 'z']); + expect(f(['x', 'y', 'z'], 1, 'a')).toEqual(['x', 'a', 'y', 'z']); + expect(f(['x', 'y', 'z'], 2, 'a')).toEqual(['x', 'y', 'a', 'z']); + expect(f(['x', 'y', 'z'], 3, 'a')).toEqual(['x', 'y', 'z', 'a']); + }); + }); + + describe('moveElement', () => { + const f = (array, from, to) => { + ArrayUtils.moveElement(array, from, to); + return array; + }; + + it('it should move an array element, modifying the array', () => { + expect(f(['x', 'y', 'z'], 0, 1)).toEqual(['y', 'x', 'z']); + expect(f(['x', 'y', 'z'], 1, 0)).toEqual(['y', 'x', 'z']); + expect(f(['x', 'y', 'z'], 0, 2)).toEqual(['y', 'z', 'x']); + expect(f(['x', 'y', 'z'], 2, 0)).toEqual(['z', 'x', 'y']); + expect(f(['x', 'y', 'z'], 1, 2)).toEqual(['x', 'z', 'y']); + expect(f(['x', 'y', 'z'], 2, 1)).toEqual(['x', 'z', 'y']); + expect(f(['x', 'y', 'z'], 0, 0)).toEqual(['x', 'y', 'z']); + expect(f(['x', 'y', 'z'], 1, 1)).toEqual(['x', 'y', 'z']); + expect(f(['x', 'y', 'z'], 2, 2)).toEqual(['x', 'y', 'z']); + expect(f(['a', 'b', 'c', 'd', 'e'], 4, 1)).toEqual(['a', 'e', 'b', 'c', 'd']); + expect(f(['a', 'b', 'c', 'd', 'e'], 1, 4)).toEqual(['a', 'c', 'd', 'e', 'b']); + expect(f(['a', 'b', 'c', 'd', 'e'], 1, 3)).toEqual(['a', 'c', 'd', 'b', 'e']); + }); + }); }); diff --git a/client/app/scripts/utils/array-utils.js b/client/app/scripts/utils/array-utils.js index c078913984..f20f399500 100644 --- a/client/app/scripts/utils/array-utils.js +++ b/client/app/scripts/utils/array-utils.js @@ -9,3 +9,14 @@ export function uniformSelect(array, size) { array[parseInt(index * (array.length / (size - (1 - 1e-9))), 10)] ); } + +export function insertElement(array, index, element) { + array.splice(index, 0, element); +} + +export function moveElement(array, from, to) { + if (from !== to) { + const removedElement = array.splice(from, 1)[0]; + insertElement(array, to, removedElement); + } +}