Maintain focus on hovered node table rows

More sophisticated row focusing

Keeping deleted focused nodes in the table

Fixed focus debouncing
This commit is contained in:
Filip Barl
2017-01-06 15:26:05 +01:00
parent 38734addeb
commit 687fb3a201
5 changed files with 111 additions and 23 deletions

View File

@@ -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 {
<tr
onMouseDown={onClick && this.onMouseDown}
onMouseUp={onClick && this.onMouseUp}
onMouseEnter={onMouseEnterRow && this.onMouseEnter}
onMouseLeave={onMouseLeaveRow && this.onMouseLeave}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
className={className}>
<td
className="node-details-table-node-label truncate"

View File

@@ -1,6 +1,7 @@
import debug from 'debug';
import React from 'react';
import classNames from 'classnames';
import { find, get, union, sortBy, groupBy, concat } from 'lodash';
import { find, get, union, sortBy, groupBy, concat, debounce, findIndex } from 'lodash';
import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';
@@ -8,6 +9,8 @@ import ShowMore from '../show-more';
import NodeDetailsTableRow from './node-details-table-row';
import NodeDetailsTableHeaders from './node-details-table-headers';
import { ipToPaddedString } from '../../utils/string-utils';
import { moveElement, insertElement } from '../../utils/array-utils';
import { TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL } from '../../constants/timer';
import {
isIP, isNumber, defaultSortDesc, getTableColumnsStyles
} from '../../utils/node-details-utils';
@@ -123,8 +126,14 @@ export default class NodeDetailsTable extends React.Component {
sortedDesc: this.props.sortedDesc,
sortedBy: this.props.sortedBy
};
this.handleLimitClick = this.handleLimitClick.bind(this);
this.updateSorted = this.updateSorted.bind(this);
this.handleLimitClick = this.handleLimitClick.bind(this);
this.onMouseLeaveRow = this.onMouseLeaveRow.bind(this);
this.onMouseEnterRow = this.onMouseEnterRow.bind(this);
// Use debouncing to prevent event flooding when e.g. crossing fast with mouse cursor
// over the whole table. That would be expensive as each focus causes table to rerender.
this.debouncedFocusRow = debounce(this.focusRow, TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL);
this.debouncedUnfocusRow = debounce(this.unfocusRow, TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL);
}
updateSorted(sortedBy, sortedDesc) {
@@ -137,20 +146,64 @@ export default class NodeDetailsTable extends React.Component {
this.setState({ limit });
}
focusRow(rowIndex, node) {
this.setState({
focusedRowIndex: rowIndex,
focusedNode: node
});
log(`Focused row ${rowIndex}`);
}
unfocusRow() {
if (this.state.focusedRowIndex) {
this.setState({
focusedRowIndex: null,
focusedNode: null
});
log('Unfocused row');
}
}
onMouseEnterRow(rowIndex, node) {
this.debouncedUnfocusRow.cancel();
this.debouncedFocusRow(rowIndex, node);
}
onMouseLeaveRow() {
this.debouncedFocusRow.cancel();
this.debouncedUnfocusRow();
}
getColumnHeaders() {
const columns = this.props.columns || [];
return [{id: 'label', label: this.props.label}].concat(columns);
}
render() {
const { nodeIdKey, columns, topologyId, onClickRow, onMouseEnter, onMouseLeave,
onMouseEnterRow, onMouseLeaveRow } = this.props;
const { nodeIdKey, columns, topologyId, onClickRow, onMouseEnter, onMouseLeave } = this.props;
const { focusedRowIndex, focusedNode } = this.state;
const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes);
const sortedByHeader = this.getColumnHeaders().find(h => 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) => (
<NodeDetailsTableRow
key={node.id}
renderIdCell={this.props.renderIdCell}
@@ -188,8 +241,8 @@ export default class NodeDetailsTable extends React.Component {
colStyles={styles}
columns={columns}
onClick={onClickRow}
onMouseLeaveRow={onMouseLeaveRow}
onMouseEnterRow={onMouseEnterRow}
onMouseEnter={() => { this.onMouseEnterRow(index, node); }}
onMouseLeave={this.onMouseLeaveRow}
topologyId={topologyId} />
))}
</tbody>

View File

@@ -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;

View File

@@ -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']);
});
});
});

View File

@@ -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);
}
}