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 a26bee551..cba5210c1 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 8c8d4dc4e..fb6eb92f4 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 49f0eb4bd..c24dcba00 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 c07891398..f20f39950 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);
+ }
+}
|