Skip to content

Commit

Permalink
Merge pull request #2109 from weaveworks/node-details-multicolumn-table
Browse files Browse the repository at this point in the history
Add support for generic multicolumn tables
  • Loading branch information
fbarl authored Jan 16, 2017
2 parents 92b3cbe + d3466b5 commit fa56c03
Show file tree
Hide file tree
Showing 20 changed files with 982 additions and 309 deletions.
38 changes: 32 additions & 6 deletions client/app/scripts/components/node-details.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import debug from 'debug';
import React from 'react';
import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';

import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions';
import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils';
import { isGenericTable, isPropertyList } from '../utils/node-details-utils';
import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils';

import MatchedText from './matched-text';
import NodeDetailsControls from './node-details/node-details-controls';
import NodeDetailsGenericTable from './node-details/node-details-generic-table';
import NodeDetailsPropertyList from './node-details/node-details-property-list';
import NodeDetailsHealth from './node-details/node-details-health';
import NodeDetailsInfo from './node-details/node-details-info';
import NodeDetailsLabels from './node-details/node-details-labels';
import NodeDetailsRelatives from './node-details/node-details-relatives';
import NodeDetailsTable from './node-details/node-details-table';
import Warning from './warning';


const log = debug('scope:node-details');

function getTruncationText(count) {
return 'This section was too long to be handled efficiently and has been truncated'
+ ` (${count} extra entries not included). We are working to remove this limitation.`;
}

class NodeDetails extends React.Component {

constructor(props, context) {
super(props, context);
this.handleClickClose = this.handleClickClose.bind(this);
Expand Down Expand Up @@ -208,15 +213,13 @@ class NodeDetails extends React.Component {
return (
<div className="node-details-content-section" key={table.id}>
<div className="node-details-content-section-header">
{table.label}
{table.label && table.label.length > 0 && table.label}
{table.truncationCount > 0 && <span
className="node-details-content-section-header-warning">
<Warning text={getTruncationText(table.truncationCount)} />
</span>}
</div>
<NodeDetailsLabels
rows={table.rows} controls={table.controls}
matches={nodeMatches.get('tables')} />
{this.renderTable(table)}
</div>
);
}
Expand All @@ -227,6 +230,29 @@ class NodeDetails extends React.Component {
);
}

renderTable(table) {
const { nodeMatches = makeMap() } = this.props;

if (isGenericTable(table)) {
return (
<NodeDetailsGenericTable
rows={table.rows} columns={table.columns}
matches={nodeMatches.get('tables')}
/>
);
} else if (isPropertyList(table)) {
return (
<NodeDetailsPropertyList
rows={table.rows} controls={table.controls}
matches={nodeMatches.get('property-lists')}
/>
);
}

log(`Undefined type '${table.type}' for table ${table.id}`);
return null;
}

componentDidUpdate() {
this.updateTitle();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import sortBy from 'lodash/sortBy';
import { Map as makeMap } from 'immutable';

import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';

import {
isNumber,
getTableColumnsStyles,
genericTableEntryKey
} from '../../utils/node-details-utils';
import NodeDetailsTableHeaders from './node-details-table-headers';
import MatchedText from '../matched-text';
import ShowMore from '../show-more';


function sortedRows(rows, columns, sortedBy, sortedDesc) {
const column = columns.find(c => c.id === sortedBy);
const sorted = sortBy(rows, (row) => {
let value = row.entries[sortedBy];
if (isNumber(column)) {
value = parseFloat(value);
}
return value;
});
if (sortedDesc) {
sorted.reverse();
}
return sorted;
}

export default class NodeDetailsGenericTable extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT,
sortedBy: props.columns && props.columns[0].id,
sortedDesc: true
};
this.handleLimitClick = this.handleLimitClick.bind(this);
this.updateSorted = this.updateSorted.bind(this);
}

updateSorted(sortedBy, sortedDesc) {
this.setState({ sortedBy, sortedDesc });
}

handleLimitClick() {
this.setState({
limit: this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT
});
}

render() {
const { sortedBy, sortedDesc } = this.state;
const { columns, matches = makeMap() } = this.props;
const expanded = this.state.limit === 0;

let rows = this.props.rows || [];
let notShown = 0;

// If there are rows that would be hidden behind 'show more', keep them
// expanded if any of them match the search query; otherwise hide them.
if (this.state.limit > 0 && rows.length > this.state.limit) {
const hasHiddenMatch = rows.slice(this.state.limit).some(row =>
columns.some(column => matches.has(genericTableEntryKey(row, column)))
);
if (!hasHiddenMatch) {
notShown = rows.length - NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT;
rows = rows.slice(0, this.state.limit);
}
}

const styles = getTableColumnsStyles(columns);
return (
<div className="node-details-generic-table">
<table>
<thead>
<NodeDetailsTableHeaders
headers={columns}
sortedBy={sortedBy}
sortedDesc={sortedDesc}
onClick={this.updateSorted}
/>
</thead>
<tbody>
{sortedRows(rows, columns, sortedBy, sortedDesc).map(row => (
<tr className="node-details-generic-table-row" key={row.id}>
{columns.map((column, index) => {
const match = matches.get(genericTableEntryKey(row, column));
const value = row.entries[column.id];
return (
<td
className="node-details-generic-table-value truncate"
title={value} key={column.id} style={styles[index]}>
<MatchedText text={value} match={match} />
</td>
);
})}
</tr>
))}
</tbody>
</table>
<ShowMore
handleClick={this.handleLimitClick} collection={this.props.rows}
expanded={expanded} notShown={notShown}
/>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,29 @@ import React from 'react';
import { Map as makeMap } from 'immutable';
import sortBy from 'lodash/sortBy';

import MatchedText from '../matched-text';
import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';
import NodeDetailsControlButton from './node-details-control-button';
import MatchedText from '../matched-text';
import ShowMore from '../show-more';


const Controls = controls => (
<div className="node-details-labels-controls">
<div className="node-details-property-list-controls">
{sortBy(controls, 'rank').map(control => <NodeDetailsControlButton
nodeId={control.nodeId} control={control} key={control.id} />)}
</div>
);

export default class NodeDetailsLabels extends React.Component {

export default class NodeDetailsPropertyList extends React.Component {
constructor(props, context) {
super(props, context);
this.DEFAULT_LIMIT = 5;
this.state = {
limit: this.DEFAULT_LIMIT,
limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT,
};
this.handleLimitClick = this.handleLimitClick.bind(this);
}

handleLimitClick() {
const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT;
this.setState({limit});
}

Expand All @@ -40,23 +38,25 @@ export default class NodeDetailsLabels extends React.Component {
const hasNotShownMatch = rows.filter((row, index) => index >= this.state.limit
&& matches.has(row.id)).length > 0;
if (!hasNotShownMatch) {
notShown = rows.length - this.DEFAULT_LIMIT;
notShown = rows.length - NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT;
rows = rows.slice(0, this.state.limit);
}
}

return (
<div className="node-details-labels">
<div className="node-details-property-list">
{controls && Controls(controls)}
{rows.map(field => (
<div className="node-details-labels-field" key={field.id}>
<div className="node-details-property-list-field" key={field.id}>
<div
className="node-details-labels-field-label truncate"
title={field.label} key={field.id}>
{field.label}
className="node-details-property-list-field-label truncate"
title={field.entries.label} key={field.id}>
{field.entries.label}
</div>
<div className="node-details-labels-field-value truncate" title={field.value}>
<MatchedText text={field.value} match={matches.get(field.id)} />
<div
className="node-details-property-list-field-value truncate"
title={field.entries.value}>
<MatchedText text={field.entries.value} match={matches.get(field.id)} />
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import React from 'react';
import { Map as makeMap } from 'immutable';

import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';
import NodeDetailsRelativesLink from './node-details-relatives-link';

export default class NodeDetailsRelatives extends React.Component {

constructor(props, context) {
super(props, context);
this.DEFAULT_LIMIT = 5;
this.state = {
limit: this.DEFAULT_LIMIT
limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT
};
this.handleLimitClick = this.handleLimitClick.bind(this);
}

handleLimitClick(ev) {
ev.preventDefault();
const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT;
this.setState({limit});
}

Expand All @@ -26,7 +26,7 @@ export default class NodeDetailsRelatives extends React.Component {

const limited = this.state.limit > 0 && relatives.length > this.state.limit;
const showLimitAction = limited || (this.state.limit === 0
&& relatives.length > this.DEFAULT_LIMIT);
&& relatives.length > NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT);
const limitActionText = limited ? 'Show more' : 'Show less';
if (limited) {
relatives = relatives.slice(0, this.state.limit);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import { defaultSortDesc, getTableColumnsStyles } from '../../utils/node-details-utils';
import { NODE_DETAILS_TABLE_CW, NODE_DETAILS_TABLE_XS_LABEL } from '../../constants/styles';


export default class NodeDetailsTableHeaders extends React.Component {
handleClick(ev, headerId, currentSortedBy, currentSortedDesc) {
ev.preventDefault();
const header = this.props.headers.find(h => h.id === headerId);
const sortedBy = header.id;
const sortedDesc = sortedBy === currentSortedBy
? !currentSortedDesc : defaultSortDesc(header);
this.props.onClick(sortedBy, sortedDesc);
}

render() {
const { headers, sortedBy, sortedDesc } = this.props;
const colStyles = getTableColumnsStyles(headers);
return (
<tr>
{headers.map((header, index) => {
const headerClasses = ['node-details-table-header', 'truncate'];
const onClick = (ev) => {
this.handleClick(ev, header.id, sortedBy, sortedDesc);
};
// sort by first metric by default
const isSorted = header.id === sortedBy;
const isSortedDesc = isSorted && sortedDesc;
const isSortedAsc = isSorted && !isSortedDesc;

if (isSorted) {
headerClasses.push('node-details-table-header-sorted');
}

const style = colStyles[index];
const label =
(style.width === NODE_DETAILS_TABLE_CW.XS && NODE_DETAILS_TABLE_XS_LABEL[header.id]) ?
NODE_DETAILS_TABLE_XS_LABEL[header.id] : header.label;

return (
<td
className={headerClasses.join(' ')} style={style} onClick={onClick}
title={header.label} key={header.id}>
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
{label}
</td>
);
})}
</tr>
);
}
}
Loading

0 comments on commit fa56c03

Please sign in to comment.