Skip to content

Commit

Permalink
Maintain focus on hovered node table rows
Browse files Browse the repository at this point in the history
More sophisticated row focusing

Keeping deleted focused nodes in the table

Fixed focus debouncing
  • Loading branch information
fbarl committed Jan 19, 2017
1 parent 589171b commit 7a7968e
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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];
Expand All @@ -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"
Expand Down
67 changes: 60 additions & 7 deletions client/app/scripts/components/node-details/node-details-table.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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';

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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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}
Expand All @@ -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>
Expand Down
1 change: 1 addition & 0 deletions client/app/scripts/constants/timer.js
Original file line number Diff line number Diff line change
@@ -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;
36 changes: 36 additions & 0 deletions client/app/scripts/utils/__tests__/array-utils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
});
11 changes: 11 additions & 0 deletions client/app/scripts/utils/array-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 7a7968e

Please sign in to comment.