mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 18:20:27 +00:00
Merge pull request #2109 from weaveworks/node-details-multicolumn-table
Add support for generic multicolumn tables
This commit is contained in:
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,59 +2,15 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { find, get, union, sortBy, groupBy, concat } 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';
|
||||
|
||||
|
||||
function isNumber(data) {
|
||||
return data.dataType && data.dataType === 'number';
|
||||
}
|
||||
|
||||
|
||||
function isIP(data) {
|
||||
return data.dataType && data.dataType === 'ip';
|
||||
}
|
||||
|
||||
|
||||
const CW = {
|
||||
XS: '32px',
|
||||
S: '50px',
|
||||
M: '70px',
|
||||
L: '120px',
|
||||
XL: '140px',
|
||||
XXL: '170px',
|
||||
};
|
||||
|
||||
|
||||
const XS_LABEL = {
|
||||
count: '#',
|
||||
// TODO: consider changing the name of this field on the BE
|
||||
container: '#',
|
||||
};
|
||||
|
||||
|
||||
const COLUMN_WIDTHS = {
|
||||
count: CW.XS,
|
||||
container: CW.XS,
|
||||
docker_container_created: CW.XXL,
|
||||
docker_container_restart_count: CW.M,
|
||||
docker_container_state_human: CW.XXL,
|
||||
docker_container_uptime: '85px',
|
||||
docker_cpu_total_usage: CW.M,
|
||||
docker_memory_usage: CW.M,
|
||||
open_files_count: CW.M,
|
||||
pid: CW.S,
|
||||
port: CW.S,
|
||||
ppid: CW.S,
|
||||
process_cpu_usage_percent: CW.M,
|
||||
process_memory_usage_bytes: CW.M,
|
||||
threads: CW.M,
|
||||
|
||||
// e.g. details panel > pods
|
||||
kubernetes_ip: CW.L,
|
||||
kubernetes_state: CW.M,
|
||||
};
|
||||
import {
|
||||
isIP, isNumber, defaultSortDesc, getTableColumnsStyles
|
||||
} from '../../utils/node-details-utils';
|
||||
|
||||
|
||||
function getDefaultSortedBy(columns, nodes) {
|
||||
@@ -158,54 +114,27 @@ function getSortedNodes(nodes, sortedByHeader, sortedDesc) {
|
||||
}
|
||||
|
||||
|
||||
function getColumnWidth(headers, h) {
|
||||
//
|
||||
// More beauty hacking, ports and counts can only get so big, free up WS for other longer
|
||||
// fields like IPs!
|
||||
//
|
||||
return COLUMN_WIDTHS[h.id];
|
||||
}
|
||||
|
||||
|
||||
function getColumnsStyles(headers) {
|
||||
return headers.map((h, i) => ({
|
||||
width: getColumnWidth(headers, h, i),
|
||||
textAlign: h.dataType === 'number' ? 'right' : 'left',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function defaultSortDesc(header) {
|
||||
return header && header.dataType === 'number';
|
||||
}
|
||||
|
||||
|
||||
export default class NodeDetailsTable extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.DEFAULT_LIMIT = 5;
|
||||
this.state = {
|
||||
limit: props.limit || this.DEFAULT_LIMIT,
|
||||
limit: props.limit || NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT,
|
||||
sortedDesc: this.props.sortedDesc,
|
||||
sortedBy: this.props.sortedBy
|
||||
};
|
||||
this.handleLimitClick = this.handleLimitClick.bind(this);
|
||||
this.updateSorted = this.updateSorted.bind(this);
|
||||
}
|
||||
|
||||
handleHeaderClick(ev, headerId, currentSortedBy, currentSortedDesc) {
|
||||
ev.preventDefault();
|
||||
const header = this.getColumnHeaders().find(h => h.id === headerId);
|
||||
const sortedBy = header.id;
|
||||
const sortedDesc = header.id === currentSortedBy
|
||||
? !currentSortedDesc : defaultSortDesc(header);
|
||||
this.setState({sortedBy, sortedDesc});
|
||||
updateSorted(sortedBy, sortedDesc) {
|
||||
this.setState({ sortedBy, sortedDesc });
|
||||
this.props.onSortChange(sortedBy, sortedDesc);
|
||||
}
|
||||
|
||||
handleLimitClick() {
|
||||
const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
|
||||
this.setState({limit});
|
||||
const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT;
|
||||
this.setState({ limit });
|
||||
}
|
||||
|
||||
getColumnHeaders() {
|
||||
@@ -213,60 +142,13 @@ export default class NodeDetailsTable extends React.Component {
|
||||
return [{id: 'label', label: this.props.label}].concat(columns);
|
||||
}
|
||||
|
||||
renderHeaders(sortedBy, sortedDesc) {
|
||||
if (!this.props.nodes || this.props.nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headers = this.getColumnHeaders();
|
||||
const colStyles = getColumnsStyles(headers);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{headers.map((header, i) => {
|
||||
const headerClasses = ['node-details-table-header', 'truncate'];
|
||||
const onHeaderClick = (ev) => {
|
||||
this.handleHeaderClick(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[i];
|
||||
const label = (style.width === CW.XS && XS_LABEL[header.id]) ?
|
||||
XS_LABEL[header.id] :
|
||||
header.label;
|
||||
|
||||
return (
|
||||
<td
|
||||
className={headerClasses.join(' ')} style={style} onClick={onHeaderClick}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodeIdKey, columns, topologyId, onClickRow, onMouseEnter, onMouseLeave,
|
||||
onMouseEnterRow, onMouseLeaveRow } = this.props;
|
||||
|
||||
const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes);
|
||||
const sortedByHeader = this.getColumnHeaders().find(h => h.id === sortedBy);
|
||||
const sortedDesc = this.state.sortedDesc !== null ?
|
||||
this.state.sortedDesc :
|
||||
defaultSortDesc(sortedByHeader);
|
||||
const sortedDesc = this.state.sortedDesc || defaultSortDesc(sortedByHeader);
|
||||
|
||||
let nodes = getSortedNodes(this.props.nodes, sortedByHeader, sortedDesc);
|
||||
const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit;
|
||||
@@ -277,13 +159,20 @@ export default class NodeDetailsTable extends React.Component {
|
||||
}
|
||||
|
||||
const className = classNames('node-details-table-wrapper-wrapper', this.props.className);
|
||||
const headers = this.getColumnHeaders();
|
||||
const styles = getTableColumnsStyles(headers);
|
||||
|
||||
return (
|
||||
<div className={className} style={this.props.style}>
|
||||
<div className="node-details-table-wrapper">
|
||||
<table className="node-details-table">
|
||||
<thead>
|
||||
{this.renderHeaders(sortedBy, sortedDesc)}
|
||||
{this.props.nodes && this.props.nodes.length > 0 && <NodeDetailsTableHeaders
|
||||
headers={headers}
|
||||
sortedBy={sortedBy}
|
||||
sortedDesc={sortedDesc}
|
||||
onClick={this.updateSorted}
|
||||
/>}
|
||||
</thead>
|
||||
<tbody
|
||||
style={this.props.tbodyStyle}
|
||||
@@ -296,7 +185,7 @@ export default class NodeDetailsTable extends React.Component {
|
||||
selected={this.props.selectedNodeId === node.id}
|
||||
node={node}
|
||||
nodeIdKey={nodeIdKey}
|
||||
colStyles={getColumnsStyles(this.getColumnHeaders())}
|
||||
colStyles={styles}
|
||||
columns={columns}
|
||||
onClick={onClickRow}
|
||||
onMouseLeaveRow={onMouseLeaveRow}
|
||||
|
||||
2
client/app/scripts/constants/limits.js
Normal file
2
client/app/scripts/constants/limits.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export const NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT = 5;
|
||||
@@ -26,3 +26,47 @@ export const MIN_NODE_SIZE = 24;
|
||||
export const MAX_NODE_SIZE = 96;
|
||||
export const BASE_NODE_LABEL_SIZE = 14;
|
||||
export const MIN_NODE_LABEL_SIZE = 12;
|
||||
|
||||
// Node details table constants
|
||||
export const NODE_DETAILS_TABLE_CW = {
|
||||
XS: '32px',
|
||||
S: '50px',
|
||||
M: '70px',
|
||||
L: '85px',
|
||||
XL: '120px',
|
||||
XXL: '140px',
|
||||
XXXL: '170px',
|
||||
};
|
||||
|
||||
export const NODE_DETAILS_TABLE_COLUMN_WIDTHS = {
|
||||
count: NODE_DETAILS_TABLE_CW.XS,
|
||||
container: NODE_DETAILS_TABLE_CW.XS,
|
||||
docker_container_created: NODE_DETAILS_TABLE_CW.XXXL,
|
||||
docker_container_restart_count: NODE_DETAILS_TABLE_CW.M,
|
||||
docker_container_state_human: NODE_DETAILS_TABLE_CW.XXXL,
|
||||
docker_container_uptime: NODE_DETAILS_TABLE_CW.L,
|
||||
docker_cpu_total_usage: NODE_DETAILS_TABLE_CW.M,
|
||||
docker_memory_usage: NODE_DETAILS_TABLE_CW.M,
|
||||
open_files_count: NODE_DETAILS_TABLE_CW.M,
|
||||
pid: NODE_DETAILS_TABLE_CW.S,
|
||||
port: NODE_DETAILS_TABLE_CW.S,
|
||||
ppid: NODE_DETAILS_TABLE_CW.S,
|
||||
process_cpu_usage_percent: NODE_DETAILS_TABLE_CW.M,
|
||||
process_memory_usage_bytes: NODE_DETAILS_TABLE_CW.M,
|
||||
threads: NODE_DETAILS_TABLE_CW.M,
|
||||
|
||||
// e.g. details panel > pods
|
||||
kubernetes_ip: NODE_DETAILS_TABLE_CW.XL,
|
||||
kubernetes_state: NODE_DETAILS_TABLE_CW.M,
|
||||
|
||||
// weave connections
|
||||
weave_connection_connection: NODE_DETAILS_TABLE_CW.XXL,
|
||||
weave_connection_state: NODE_DETAILS_TABLE_CW.L,
|
||||
weave_connection_info: NODE_DETAILS_TABLE_CW.XL,
|
||||
};
|
||||
|
||||
export const NODE_DETAILS_TABLE_XS_LABEL = {
|
||||
count: '#',
|
||||
// TODO: consider changing the name of this field on the BE
|
||||
container: '#',
|
||||
};
|
||||
|
||||
@@ -29,10 +29,51 @@ describe('SearchUtils', () => {
|
||||
}],
|
||||
tables: [{
|
||||
id: 'metric1',
|
||||
type: 'property-list',
|
||||
rows: [{
|
||||
id: 'label1',
|
||||
entries: {
|
||||
label: 'Label 1',
|
||||
value: 'Label Value 1'
|
||||
}
|
||||
}, {
|
||||
id: 'label2',
|
||||
entries: {
|
||||
label: 'Label 2',
|
||||
value: 'Label Value 2'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
id: 'metric2',
|
||||
type: 'multicolumn-table',
|
||||
columns: [{
|
||||
id: 'a',
|
||||
label: 'A'
|
||||
}, {
|
||||
id: 'c',
|
||||
label: 'C'
|
||||
}],
|
||||
rows: [{
|
||||
id: 'row1',
|
||||
label: 'Row 1',
|
||||
value: 'Row Value 1'
|
||||
entries: {
|
||||
a: 'xxxa',
|
||||
b: 'yyya',
|
||||
c: 'zzz1'
|
||||
}
|
||||
}, {
|
||||
id: 'row2',
|
||||
entries: {
|
||||
a: 'yyyb',
|
||||
b: 'xxxb',
|
||||
c: 'zzz2'
|
||||
}
|
||||
}, {
|
||||
id: 'row3',
|
||||
entries: {
|
||||
a: 'Value 1',
|
||||
b: 'Value 2',
|
||||
c: 'Value 3'
|
||||
}
|
||||
}]
|
||||
}],
|
||||
},
|
||||
@@ -72,7 +113,7 @@ describe('SearchUtils', () => {
|
||||
it('should filter nodes that do not match a pinned searches', () => {
|
||||
let nextState = fromJS({
|
||||
nodes: nodeSets.someNodes,
|
||||
pinnedSearches: ['row']
|
||||
pinnedSearches: ['Label Value 1']
|
||||
});
|
||||
nextState = fun(nextState);
|
||||
expect(nextState.get('nodes').filter(node => node.get('filtered')).size).toEqual(1);
|
||||
@@ -236,11 +277,31 @@ describe('SearchUtils', () => {
|
||||
expect(matches.getIn(['n1', 'metrics', 'metric1']).metric).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should match on a tables field', () => {
|
||||
it('should match on a property list value', () => {
|
||||
const nodes = nodeSets.someNodes;
|
||||
const matches = fun(nodes, {query: 'Row Value 1'});
|
||||
expect(matches.size).toEqual(1);
|
||||
expect(matches.getIn(['n2', 'tables', 'row1']).text).toBe('Row Value 1');
|
||||
const matches = fun(nodes, {query: 'Value 1'});
|
||||
expect(matches.size).toEqual(2);
|
||||
expect(matches.getIn(['n2', 'property-lists']).size).toEqual(1);
|
||||
expect(matches.getIn(['n2', 'property-lists', 'label1']).text).toBe('Label Value 1');
|
||||
});
|
||||
|
||||
it('should match on a generic table values', () => {
|
||||
const nodes = nodeSets.someNodes;
|
||||
const matches1 = fun(nodes, {query: 'xx'}).getIn(['n2', 'tables']);
|
||||
const matches2 = fun(nodes, {query: 'yy'}).getIn(['n2', 'tables']);
|
||||
const matches3 = fun(nodes, {query: 'zz'}).getIn(['n2', 'tables']);
|
||||
const matches4 = fun(nodes, {query: 'a'}).getIn(['n2', 'tables']);
|
||||
expect(matches1.size).toEqual(1);
|
||||
expect(matches2.size).toEqual(1);
|
||||
expect(matches3.size).toEqual(2);
|
||||
expect(matches4.size).toEqual(3);
|
||||
expect(matches1.get('row1_a').text).toBe('xxxa');
|
||||
expect(matches2.get('row2_a').text).toBe('yyyb');
|
||||
expect(matches3.get('row1_c').text).toBe('zzz1');
|
||||
expect(matches3.get('row2_c').text).toBe('zzz2');
|
||||
expect(matches4.get('row1_a').text).toBe('xxxa');
|
||||
expect(matches4.get('row3_a').text).toBe('Value 1');
|
||||
expect(matches4.get('row3_c').text).toBe('Value 3');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
36
client/app/scripts/utils/node-details-utils.js
Normal file
36
client/app/scripts/utils/node-details-utils.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NODE_DETAILS_TABLE_COLUMN_WIDTHS } from '../constants/styles';
|
||||
|
||||
export function isGenericTable(table) {
|
||||
return (table.type || (table.get && table.get('type'))) === 'multicolumn-table';
|
||||
}
|
||||
|
||||
export function isPropertyList(table) {
|
||||
return (table.type || (table.get && table.get('type'))) === 'property-list';
|
||||
}
|
||||
|
||||
export function isNumber(data) {
|
||||
return data && data.dataType && data.dataType === 'number';
|
||||
}
|
||||
|
||||
export function isIP(data) {
|
||||
return data && data.dataType && data.dataType === 'ip';
|
||||
}
|
||||
|
||||
export function genericTableEntryKey(row, column) {
|
||||
const columnId = column.id || column.get('id');
|
||||
const rowId = row.id || row.get('id');
|
||||
return `${rowId}_${columnId}`;
|
||||
}
|
||||
|
||||
export function defaultSortDesc(header) {
|
||||
return header && isNumber(header);
|
||||
}
|
||||
|
||||
export function getTableColumnsStyles(headers) {
|
||||
return headers.map(header => ({
|
||||
// More beauty hacking, ports and counts can only get
|
||||
// so big, free up WS for other longer fields like IPs!
|
||||
width: NODE_DETAILS_TABLE_COLUMN_WIDTHS[header.id],
|
||||
textAlign: isNumber(header) ? 'right' : 'left'
|
||||
}));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Map as makeMap, Set as makeSet, List as makeList } from 'immutable';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
|
||||
import { isGenericTable, isPropertyList, genericTableEntryKey } from './node-details-utils';
|
||||
import { slugify } from './string-utils';
|
||||
|
||||
// topolevel search fields
|
||||
@@ -148,19 +149,26 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) {
|
||||
});
|
||||
}
|
||||
|
||||
// tables (envvars and labels)
|
||||
const tables = node.get('tables');
|
||||
if (tables) {
|
||||
tables.forEach((table) => {
|
||||
if (table.get('rows')) {
|
||||
table.get('rows').forEach((field) => {
|
||||
const keyPath = [nodeId, 'tables', field.get('id')];
|
||||
nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
|
||||
query, prefix, field.get('label'));
|
||||
});
|
||||
}
|
||||
// property lists
|
||||
(node.get('tables') || []).filter(isPropertyList).forEach((propertyList) => {
|
||||
(propertyList.get('rows') || []).forEach((row) => {
|
||||
const entries = row.get('entries');
|
||||
const keyPath = [nodeId, 'property-lists', row.get('id')];
|
||||
nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'),
|
||||
query, prefix, entries.get('label'));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// generic tables
|
||||
(node.get('tables') || []).filter(isGenericTable).forEach((table) => {
|
||||
(table.get('rows') || []).forEach((row) => {
|
||||
table.get('columns').forEach((column) => {
|
||||
const val = row.get('entries').get(column.get('id'));
|
||||
const keyPath = [nodeId, 'tables', genericTableEntryKey(row, column)];
|
||||
nodeMatches = findNodeMatch(nodeMatches, keyPath, val, query);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (metric) {
|
||||
const metrics = node.get('metrics');
|
||||
if (metrics) {
|
||||
|
||||
@@ -864,7 +864,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
&-labels {
|
||||
&-property-list {
|
||||
&-controls {
|
||||
margin-left: -4px;
|
||||
}
|
||||
@@ -897,6 +897,17 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
&-generic-table {
|
||||
width: 100%;
|
||||
|
||||
tr {
|
||||
display: flex;
|
||||
th, td {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
|
||||
@@ -379,8 +379,8 @@ func (c *container) getBaseNode() report.Node {
|
||||
}).WithParents(report.EmptySets.
|
||||
Add(report.ContainerImage, report.MakeStringSet(report.MakeContainerImageNodeID(c.Image()))),
|
||||
)
|
||||
result = result.AddPrefixTable(LabelPrefix, c.container.Config.Labels)
|
||||
result = result.AddPrefixTable(EnvPrefix, c.env())
|
||||
result = result.AddPrefixPropertyList(LabelPrefix, c.container.Config.Labels)
|
||||
result = result.AddPrefixPropertyList(EnvPrefix, c.env())
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ var (
|
||||
}
|
||||
|
||||
ContainerTableTemplates = report.TableTemplates{
|
||||
ImageTableID: {ID: ImageTableID, Label: "Image",
|
||||
ImageTableID: {
|
||||
ID: ImageTableID,
|
||||
Label: "Image",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
ImageID: "ID",
|
||||
ImageName: "Name",
|
||||
@@ -56,12 +59,27 @@ var (
|
||||
ImageVirtualSize: "Virtual Size",
|
||||
},
|
||||
},
|
||||
LabelPrefix: {ID: LabelPrefix, Label: "Docker Labels", Prefix: LabelPrefix},
|
||||
EnvPrefix: {ID: EnvPrefix, Label: "Environment Variables", Prefix: EnvPrefix},
|
||||
LabelPrefix: {
|
||||
ID: LabelPrefix,
|
||||
Label: "Docker Labels",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: LabelPrefix,
|
||||
},
|
||||
EnvPrefix: {
|
||||
ID: EnvPrefix,
|
||||
Label: "Environment Variables",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: EnvPrefix,
|
||||
},
|
||||
}
|
||||
|
||||
ContainerImageTableTemplates = report.TableTemplates{
|
||||
ImageLabelPrefix: {ID: ImageLabelPrefix, Label: "Docker Labels", Prefix: ImageLabelPrefix},
|
||||
ImageLabelPrefix: {
|
||||
ID: ImageLabelPrefix,
|
||||
Label: "Docker Labels",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: ImageLabelPrefix,
|
||||
},
|
||||
}
|
||||
|
||||
ContainerControls = []report.Control{
|
||||
@@ -258,7 +276,7 @@ func (r *Reporter) containerImageTopology() report.Topology {
|
||||
}
|
||||
nodeID := report.MakeContainerImageNodeID(imageID)
|
||||
node := report.MakeNodeWith(nodeID, latests)
|
||||
node = node.AddPrefixTable(ImageLabelPrefix, image.Labels)
|
||||
node = node.AddPrefixPropertyList(ImageLabelPrefix, image.Labels)
|
||||
result.AddNode(node)
|
||||
})
|
||||
|
||||
|
||||
@@ -56,5 +56,5 @@ func (m meta) MetaNode(id string) report.Node {
|
||||
Name: m.Name(),
|
||||
Namespace: m.Namespace(),
|
||||
Created: m.Created(),
|
||||
}).AddPrefixTable(LabelPrefix, m.Labels())
|
||||
}).AddPrefixPropertyList(LabelPrefix, m.Labels())
|
||||
}
|
||||
|
||||
@@ -68,7 +68,12 @@ var (
|
||||
ReplicaSetMetricTemplates = PodMetricTemplates
|
||||
|
||||
TableTemplates = report.TableTemplates{
|
||||
LabelPrefix: {ID: LabelPrefix, Label: "Kubernetes Labels", Prefix: LabelPrefix},
|
||||
LabelPrefix: {
|
||||
ID: LabelPrefix,
|
||||
Label: "Kubernetes Labels",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: LabelPrefix,
|
||||
},
|
||||
}
|
||||
|
||||
ScalingControls = []report.Control{
|
||||
|
||||
@@ -19,34 +19,38 @@ import (
|
||||
|
||||
// Keys for use in Node
|
||||
const (
|
||||
WeavePeerName = "weave_peer_name"
|
||||
WeavePeerNickName = "weave_peer_nick_name"
|
||||
WeaveDNSHostname = "weave_dns_hostname"
|
||||
WeaveMACAddress = "weave_mac_address"
|
||||
WeaveVersion = "weave_version"
|
||||
WeaveEncryption = "weave_encryption"
|
||||
WeaveProtocol = "weave_protocol"
|
||||
WeavePeerDiscovery = "weave_peer_discovery"
|
||||
WeaveTargetCount = "weave_target_count"
|
||||
WeaveConnectionCount = "weave_connection_count"
|
||||
WeavePeerCount = "weave_peer_count"
|
||||
WeaveTrustedSubnets = "weave_trusted_subnet_count"
|
||||
WeaveIPAMTableID = "weave_ipam_table"
|
||||
WeaveIPAMStatus = "weave_ipam_status"
|
||||
WeaveIPAMRange = "weave_ipam_range"
|
||||
WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet"
|
||||
WeaveDNSTableID = "weave_dns_table"
|
||||
WeaveDNSDomain = "weave_dns_domain"
|
||||
WeaveDNSUpstream = "weave_dns_upstream"
|
||||
WeaveDNSTTL = "weave_dns_ttl"
|
||||
WeaveDNSEntryCount = "weave_dns_entry_count"
|
||||
WeaveProxyTableID = "weave_proxy_table"
|
||||
WeaveProxyStatus = "weave_proxy_status"
|
||||
WeaveProxyAddress = "weave_proxy_address"
|
||||
WeavePluginTableID = "weave_plugin_table"
|
||||
WeavePluginStatus = "weave_plugin_status"
|
||||
WeavePluginDriver = "weave_plugin_driver"
|
||||
WeaveConnectionsTablePrefix = "weave_connections_table_"
|
||||
WeavePeerName = "weave_peer_name"
|
||||
WeavePeerNickName = "weave_peer_nick_name"
|
||||
WeaveDNSHostname = "weave_dns_hostname"
|
||||
WeaveMACAddress = "weave_mac_address"
|
||||
WeaveVersion = "weave_version"
|
||||
WeaveEncryption = "weave_encryption"
|
||||
WeaveProtocol = "weave_protocol"
|
||||
WeavePeerDiscovery = "weave_peer_discovery"
|
||||
WeaveTargetCount = "weave_target_count"
|
||||
WeaveConnectionCount = "weave_connection_count"
|
||||
WeavePeerCount = "weave_peer_count"
|
||||
WeaveTrustedSubnets = "weave_trusted_subnet_count"
|
||||
WeaveIPAMTableID = "weave_ipam_table"
|
||||
WeaveIPAMStatus = "weave_ipam_status"
|
||||
WeaveIPAMRange = "weave_ipam_range"
|
||||
WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet"
|
||||
WeaveDNSTableID = "weave_dns_table"
|
||||
WeaveDNSDomain = "weave_dns_domain"
|
||||
WeaveDNSUpstream = "weave_dns_upstream"
|
||||
WeaveDNSTTL = "weave_dns_ttl"
|
||||
WeaveDNSEntryCount = "weave_dns_entry_count"
|
||||
WeaveProxyTableID = "weave_proxy_table"
|
||||
WeaveProxyStatus = "weave_proxy_status"
|
||||
WeaveProxyAddress = "weave_proxy_address"
|
||||
WeavePluginTableID = "weave_plugin_table"
|
||||
WeavePluginStatus = "weave_plugin_status"
|
||||
WeavePluginDriver = "weave_plugin_driver"
|
||||
WeaveConnectionsConnection = "weave_connection_connection"
|
||||
WeaveConnectionsState = "weave_connection_state"
|
||||
WeaveConnectionsInfo = "weave_connection_info"
|
||||
WeaveConnectionsTablePrefix = "weave_connections_table_"
|
||||
WeaveConnectionsMulticolumnTablePrefix = "weave_connections_multicolumn_table_"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,14 +77,20 @@ var (
|
||||
}
|
||||
|
||||
weaveTableTemplates = report.TableTemplates{
|
||||
WeaveIPAMTableID: {ID: WeaveIPAMTableID, Label: "IPAM",
|
||||
WeaveIPAMTableID: {
|
||||
ID: WeaveIPAMTableID,
|
||||
Label: "IPAM",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveIPAMStatus: "Status",
|
||||
WeaveIPAMRange: "Range",
|
||||
WeaveIPAMDefaultSubnet: "Default Subnet",
|
||||
},
|
||||
},
|
||||
WeaveDNSTableID: {ID: WeaveDNSTableID, Label: "DNS",
|
||||
WeaveDNSTableID: {
|
||||
ID: WeaveDNSTableID,
|
||||
Label: "DNS",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveDNSDomain: "Domain",
|
||||
WeaveDNSUpstream: "Upstream",
|
||||
@@ -88,21 +98,48 @@ var (
|
||||
WeaveDNSEntryCount: "Entries",
|
||||
},
|
||||
},
|
||||
WeaveProxyTableID: {ID: WeaveProxyTableID, Label: "Proxy",
|
||||
WeaveProxyTableID: {
|
||||
ID: WeaveProxyTableID,
|
||||
Label: "Proxy",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveProxyStatus: "Status",
|
||||
WeaveProxyAddress: "Address",
|
||||
},
|
||||
},
|
||||
WeavePluginTableID: {ID: WeavePluginTableID, Label: "Plugin",
|
||||
WeavePluginTableID: {
|
||||
ID: WeavePluginTableID,
|
||||
Label: "Plugin",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeavePluginStatus: "Status",
|
||||
WeavePluginDriver: "Driver Name",
|
||||
},
|
||||
},
|
||||
WeaveConnectionsMulticolumnTablePrefix: {
|
||||
ID: WeaveConnectionsMulticolumnTablePrefix,
|
||||
Type: report.MulticolumnTableType,
|
||||
Prefix: WeaveConnectionsMulticolumnTablePrefix,
|
||||
Columns: []report.Column{
|
||||
{
|
||||
ID: WeaveConnectionsConnection,
|
||||
Label: "Connections",
|
||||
},
|
||||
{
|
||||
ID: WeaveConnectionsState,
|
||||
Label: "State",
|
||||
},
|
||||
{
|
||||
ID: WeaveConnectionsInfo,
|
||||
Label: "Info",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Kept for backward-compatibility.
|
||||
WeaveConnectionsTablePrefix: {
|
||||
ID: WeaveConnectionsTablePrefix,
|
||||
Label: "Connections",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: WeaveConnectionsTablePrefix,
|
||||
},
|
||||
}
|
||||
@@ -434,28 +471,31 @@ func (w *Weave) addCurrentPeerInfo(latests map[string]string, node report.Node)
|
||||
latests[WeavePluginStatus] = "running"
|
||||
latests[WeavePluginDriver] = "weave"
|
||||
}
|
||||
node = node.AddPrefixTable(WeaveConnectionsTablePrefix, getConnectionsTable(w.statusCache.Router))
|
||||
node = node.AddPrefixMulticolumnTable(WeaveConnectionsMulticolumnTablePrefix, getConnectionsTable(w.statusCache.Router))
|
||||
node = node.WithParents(report.EmptySets.Add(report.Host, report.MakeStringSet(w.hostID)))
|
||||
|
||||
return latests, node
|
||||
}
|
||||
|
||||
func getConnectionsTable(router weave.Router) map[string]string {
|
||||
func getConnectionsTable(router weave.Router) []report.Row {
|
||||
const (
|
||||
outboundArrow = "->"
|
||||
inboundArrow = "<-"
|
||||
)
|
||||
table := make(map[string]string, len(router.Connections))
|
||||
table := make([]report.Row, len(router.Connections))
|
||||
for _, conn := range router.Connections {
|
||||
arrow := inboundArrow
|
||||
if conn.Outbound {
|
||||
arrow = outboundArrow
|
||||
}
|
||||
// TODO: we should probably use a multicolumn table for this
|
||||
// but there is no mechanism to support it yet.
|
||||
key := fmt.Sprintf("%s %s", arrow, conn.Address)
|
||||
value := fmt.Sprintf("%s, %s", conn.State, conn.Info)
|
||||
table[key] = value
|
||||
table = append(table, report.Row{
|
||||
ID: conn.Address,
|
||||
Entries: map[string]string{
|
||||
WeaveConnectionsConnection: fmt.Sprintf("%s %s", arrow, conn.Address),
|
||||
WeaveConnectionsState: conn.State,
|
||||
WeaveConnectionsInfo: conn.Info,
|
||||
},
|
||||
})
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
@@ -34,24 +34,29 @@ func TestNodeTables(t *testing.T) {
|
||||
want: []report.Table{
|
||||
{
|
||||
ID: docker.EnvPrefix,
|
||||
Type: report.PropertyListType,
|
||||
Label: "Environment Variables",
|
||||
Rows: []report.MetadataRow{},
|
||||
Rows: []report.Row{},
|
||||
},
|
||||
{
|
||||
ID: docker.LabelPrefix,
|
||||
Type: report.PropertyListType,
|
||||
Label: "Docker Labels",
|
||||
Rows: []report.MetadataRow{
|
||||
Rows: []report.Row{
|
||||
{
|
||||
ID: "label_label1",
|
||||
Label: "label1",
|
||||
Value: "label1value",
|
||||
ID: "label_label1",
|
||||
Entries: map[string]string{
|
||||
"label": "label1",
|
||||
"value": "label1value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: docker.ImageTableID,
|
||||
Type: report.PropertyListType,
|
||||
Label: "Image",
|
||||
Rows: []report.MetadataRow{},
|
||||
Rows: []report.Row{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
248
report/table.go
248
report/table.go
@@ -13,54 +13,184 @@ import (
|
||||
// MaxTableRows sets the limit on the table size to render
|
||||
// TODO: this won't be needed once we send reports incrementally
|
||||
const (
|
||||
MaxTableRows = 20
|
||||
TruncationCountPrefix = "table_truncation_count_"
|
||||
MaxTableRows = 20
|
||||
TableEntryKeySeparator = "___"
|
||||
TruncationCountPrefix = "table_truncation_count_"
|
||||
MulticolumnTableType = "multicolumn-table"
|
||||
PropertyListType = "property-list"
|
||||
)
|
||||
|
||||
// AddPrefixTable appends arbitrary key-value pairs to the Node, returning a new node.
|
||||
func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node {
|
||||
count := 0
|
||||
for key, value := range labels {
|
||||
if count >= MaxTableRows {
|
||||
break
|
||||
}
|
||||
node = node.WithLatest(prefix+key, mtime.Now(), value)
|
||||
count++
|
||||
}
|
||||
if len(labels) > MaxTableRows {
|
||||
truncationCount := fmt.Sprintf("%d", len(labels)-MaxTableRows)
|
||||
// WithTableTruncationInformation appends table truncation info to the node, returning the new node.
|
||||
func (node Node) WithTableTruncationInformation(prefix string, totalRowsCount int) Node {
|
||||
if totalRowsCount > MaxTableRows {
|
||||
truncationCount := fmt.Sprintf("%d", totalRowsCount-MaxTableRows)
|
||||
node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// ExtractTable returns the key-value pairs to build a table from this node
|
||||
func (node Node) ExtractTable(template TableTemplate) (rows map[string]string, truncationCount int) {
|
||||
rows = map[string]string{}
|
||||
truncationCount = 0
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if label, ok := template.FixedRows[key]; ok {
|
||||
rows[label] = value
|
||||
// AddPrefixMulticolumnTable appends arbitrary rows to the Node, returning a new node.
|
||||
func (node Node) AddPrefixMulticolumnTable(prefix string, rows []Row) Node {
|
||||
addedRowsCount := 0
|
||||
for _, row := range rows {
|
||||
if addedRowsCount >= MaxTableRows {
|
||||
break
|
||||
}
|
||||
if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) {
|
||||
label := key[len(template.Prefix):]
|
||||
rows[label] = value
|
||||
// Add all the row values as separate entries
|
||||
for columnID, value := range row.Entries {
|
||||
key := strings.Join([]string{row.ID, columnID}, TableEntryKeySeparator)
|
||||
node = node.WithLatest(prefix+key, mtime.Now(), value)
|
||||
}
|
||||
addedRowsCount++
|
||||
}
|
||||
return node.WithTableTruncationInformation(prefix, len(rows))
|
||||
}
|
||||
|
||||
// AddPrefixPropertyList appends arbitrary key-value pairs to the Node, returning a new node.
|
||||
func (node Node) AddPrefixPropertyList(prefix string, propertyList map[string]string) Node {
|
||||
addedPropertiesCount := 0
|
||||
for label, value := range propertyList {
|
||||
if addedPropertiesCount >= MaxTableRows {
|
||||
break
|
||||
}
|
||||
node = node.WithLatest(prefix+label, mtime.Now(), value)
|
||||
addedPropertiesCount++
|
||||
}
|
||||
return node.WithTableTruncationInformation(prefix, len(propertyList))
|
||||
}
|
||||
|
||||
// WithoutPrefix returns the string with trimmed prefix and a
|
||||
// boolean information of whether that prefix was really there.
|
||||
// NOTE: Consider moving this function to utilities.
|
||||
func WithoutPrefix(s string, prefix string) (string, bool) {
|
||||
return strings.TrimPrefix(s, prefix), len(prefix) > 0 && strings.HasPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// ExtractMulticolumnTable returns the rows to build a multicolumn table from this node
|
||||
func (node Node) ExtractMulticolumnTable(template TableTemplate) (rows []Row) {
|
||||
rowsMapByID := map[string]Row{}
|
||||
|
||||
// Itearate through the whole of our map to extract all the values with the key
|
||||
// with the given prefix. Since multicolumn tables don't support fixed rows (yet),
|
||||
// all the table values will be stored under the table prefix.
|
||||
// NOTE: It would be nice to optimize this part by only iterating through the keys
|
||||
// with the given prefix. If it is possible to traverse the keys in the Latest map
|
||||
// in a sorted order, then having LowerBoundEntry(key) and UpperBoundEntry(key)
|
||||
// methods should be enough to implement ForEachWithPrefix(prefix) straightforwardly.
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if keyWithoutPrefix, ok := WithoutPrefix(key, template.Prefix); ok {
|
||||
ids := strings.Split(keyWithoutPrefix, TableEntryKeySeparator)
|
||||
rowID, columnID := ids[0], ids[1]
|
||||
// If the row with the given ID doesn't yet exist, we create an empty one.
|
||||
if _, ok := rowsMapByID[rowID]; !ok {
|
||||
rowsMapByID[rowID] = Row{
|
||||
ID: rowID,
|
||||
Entries: map[string]string{},
|
||||
}
|
||||
}
|
||||
// At this point, the row with that ID always exists, so we just update the value.
|
||||
rowsMapByID[rowID].Entries[columnID] = value
|
||||
}
|
||||
})
|
||||
|
||||
// Gather a list of rows.
|
||||
rows = make([]Row, 0, len(rowsMapByID))
|
||||
for _, row := range rowsMapByID {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
// Return the rows sorted by ID.
|
||||
sort.Sort(rowsByID(rows))
|
||||
return rows
|
||||
}
|
||||
|
||||
// ExtractPropertyList returns the rows to build a property list from this node
|
||||
func (node Node) ExtractPropertyList(template TableTemplate) (rows []Row) {
|
||||
valuesMapByLabel := map[string]string{}
|
||||
|
||||
// Itearate through the whole of our map to extract all the values with the key
|
||||
// with the given prefix as well as the keys corresponding to the fixed table rows.
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if label, ok := template.FixedRows[key]; ok {
|
||||
valuesMapByLabel[label] = value
|
||||
} else if label, ok := WithoutPrefix(key, template.Prefix); ok {
|
||||
valuesMapByLabel[label] = value
|
||||
}
|
||||
})
|
||||
|
||||
// Gather a label-value formatted list of rows.
|
||||
rows = make([]Row, 0, len(valuesMapByLabel))
|
||||
for label, value := range valuesMapByLabel {
|
||||
rows = append(rows, Row{
|
||||
ID: "label_" + label,
|
||||
Entries: map[string]string{
|
||||
"label": label,
|
||||
"value": value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Return the rows sorted by ID.
|
||||
sort.Sort(rowsByID(rows))
|
||||
return rows
|
||||
}
|
||||
|
||||
// ExtractTable returns the rows to build either a property list or a generic table from this node
|
||||
func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCount int) {
|
||||
switch template.Type {
|
||||
case MulticolumnTableType:
|
||||
rows = node.ExtractMulticolumnTable(template)
|
||||
default: // By default assume it's a property list (for backward compatibility).
|
||||
rows = node.ExtractPropertyList(template)
|
||||
}
|
||||
|
||||
truncationCount = 0
|
||||
if str, ok := node.Latest.Lookup(TruncationCountPrefix + template.Prefix); ok {
|
||||
if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil {
|
||||
log.Warn("Unexpected truncation count format %q", str)
|
||||
}
|
||||
}
|
||||
|
||||
return rows, truncationCount
|
||||
}
|
||||
|
||||
// Column is the type for multi-column tables in the UI.
|
||||
type Column struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
DataType string `json:"dataType"`
|
||||
}
|
||||
|
||||
// Row is the type that holds the table data for the UI. Entries map from column ID to cell value.
|
||||
type Row struct {
|
||||
ID string `json:"id"`
|
||||
Entries map[string]string `json:"entries"`
|
||||
}
|
||||
|
||||
type rowsByID []Row
|
||||
|
||||
func (t rowsByID) Len() int { return len(t) }
|
||||
func (t rowsByID) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
func (t rowsByID) Less(i, j int) bool { return t[i].ID < t[j].ID }
|
||||
|
||||
// Copy returns a copy of the Row.
|
||||
func (r Row) Copy() Row {
|
||||
entriesCopy := make(map[string]string, len(r.Entries))
|
||||
for key, value := range r.Entries {
|
||||
entriesCopy[key] = value
|
||||
}
|
||||
r.Entries = entriesCopy
|
||||
return r
|
||||
}
|
||||
|
||||
// Table is the type for a table in the UI.
|
||||
type Table struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Rows []MetadataRow `json:"rows"`
|
||||
TruncationCount int `json:"truncationCount,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Columns []Column `json:"columns"`
|
||||
Rows []Row `json:"rows"`
|
||||
TruncationCount int `json:"truncationCount,omitempty"`
|
||||
}
|
||||
|
||||
type tablesByID []Table
|
||||
@@ -72,9 +202,14 @@ func (t tablesByID) Less(i, j int) bool { return t[i].ID < t[j].ID }
|
||||
// Copy returns a copy of the Table.
|
||||
func (t Table) Copy() Table {
|
||||
result := Table{
|
||||
ID: t.ID,
|
||||
Label: t.Label,
|
||||
Rows: make([]MetadataRow, 0, len(t.Rows)),
|
||||
ID: t.ID,
|
||||
Label: t.Label,
|
||||
Type: t.Type,
|
||||
Columns: make([]Column, 0, len(t.Columns)),
|
||||
Rows: make([]Row, 0, len(t.Rows)),
|
||||
}
|
||||
for _, column := range t.Columns {
|
||||
result.Columns = append(result.Columns, column)
|
||||
}
|
||||
for _, row := range t.Rows {
|
||||
result.Rows = append(result.Rows, row)
|
||||
@@ -82,18 +217,13 @@ func (t Table) Copy() Table {
|
||||
return result
|
||||
}
|
||||
|
||||
// FixedRow describes a row which is part of a TableTemplate and whose value is extracted
|
||||
// from a predetermined key
|
||||
type FixedRow struct {
|
||||
Label string `json:"label"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// TableTemplate describes how to render a table for the UI.
|
||||
type TableTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Prefix string `json:"prefix"`
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Prefix string `json:"prefix"`
|
||||
Type string `json:"type"`
|
||||
Columns []Column `json:"columns"`
|
||||
// FixedRows indicates what predetermined rows to render each entry is
|
||||
// indexed by the key to extract the row value is mapped to the row
|
||||
// label
|
||||
@@ -121,15 +251,24 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
|
||||
return s2
|
||||
}
|
||||
|
||||
// NOTE: Consider actually merging the columns and fixed rows.
|
||||
fixedRows := t.FixedRows
|
||||
if len(other.FixedRows) > len(fixedRows) {
|
||||
fixedRows = other.FixedRows
|
||||
}
|
||||
columns := t.Columns
|
||||
if len(other.Columns) > len(columns) {
|
||||
columns = other.Columns
|
||||
}
|
||||
|
||||
// TODO: Refactor the merging logic, as mixing the types now might result in
|
||||
// invalid tables. Maybe we should return an error if the types are different?
|
||||
return TableTemplate{
|
||||
ID: max(t.ID, other.ID),
|
||||
Label: max(t.Label, other.Label),
|
||||
Prefix: max(t.Prefix, other.Prefix),
|
||||
Type: max(t.Type, other.Type),
|
||||
Columns: columns,
|
||||
FixedRows: fixedRows,
|
||||
}
|
||||
}
|
||||
@@ -142,25 +281,20 @@ func (t TableTemplates) Tables(node Node) []Table {
|
||||
var result []Table
|
||||
for _, template := range t {
|
||||
rows, truncationCount := node.ExtractTable(template)
|
||||
table := Table{
|
||||
// Extract the type from the template; default to
|
||||
// property list for backwards-compatibility.
|
||||
tableType := template.Type
|
||||
if tableType == "" {
|
||||
tableType = PropertyListType
|
||||
}
|
||||
result = append(result, Table{
|
||||
ID: template.ID,
|
||||
Label: template.Label,
|
||||
Rows: []MetadataRow{},
|
||||
Columns: template.Columns,
|
||||
Type: tableType,
|
||||
Rows: rows,
|
||||
TruncationCount: truncationCount,
|
||||
}
|
||||
keys := make([]string, 0, len(rows))
|
||||
for k := range rows {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
table.Rows = append(table.Rows, MetadataRow{
|
||||
ID: "label_" + key,
|
||||
Label: key,
|
||||
Value: rows[key],
|
||||
})
|
||||
}
|
||||
result = append(result, table)
|
||||
})
|
||||
}
|
||||
sort.Sort(tablesByID(result))
|
||||
return result
|
||||
|
||||
@@ -9,15 +9,34 @@ import (
|
||||
"github.com/weaveworks/scope/report"
|
||||
)
|
||||
|
||||
func TestPrefixTables(t *testing.T) {
|
||||
want := map[string]string{
|
||||
"foo1": "bar1",
|
||||
"foo2": "bar2",
|
||||
func TestMulticolumnTables(t *testing.T) {
|
||||
want := []report.Row{
|
||||
{
|
||||
ID: "row1",
|
||||
Entries: map[string]string{
|
||||
"col1": "r1c1",
|
||||
"col2": "r1c2",
|
||||
"col3": "r1c3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "row2",
|
||||
Entries: map[string]string{
|
||||
"col1": "r2c1",
|
||||
"col3": "r2c3",
|
||||
},
|
||||
},
|
||||
}
|
||||
nmd := report.MakeNode("foo1")
|
||||
|
||||
nmd = nmd.AddPrefixTable("foo_", want)
|
||||
have, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"})
|
||||
nmd := report.MakeNode("foo1")
|
||||
nmd = nmd.AddPrefixMulticolumnTable("foo_", want)
|
||||
|
||||
template := report.TableTemplate{
|
||||
Type: report.MulticolumnTableType,
|
||||
Prefix: "foo_",
|
||||
}
|
||||
|
||||
have, truncationCount := nmd.ExtractTable(template)
|
||||
|
||||
if truncationCount != 0 {
|
||||
t.Error("Table shouldn't had been truncated")
|
||||
@@ -28,49 +47,258 @@ func TestPrefixTables(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixedTables(t *testing.T) {
|
||||
want := map[string]string{
|
||||
"foo1": "bar1",
|
||||
"foo2": "bar2",
|
||||
func TestPrefixPropertyLists(t *testing.T) {
|
||||
want := []report.Row{
|
||||
{
|
||||
ID: "label_foo1",
|
||||
Entries: map[string]string{
|
||||
"label": "foo1",
|
||||
"value": "bar1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "label_foo3",
|
||||
Entries: map[string]string{
|
||||
"label": "foo3",
|
||||
"value": "bar3",
|
||||
},
|
||||
},
|
||||
}
|
||||
nmd := report.MakeNodeWith("foo1", map[string]string{
|
||||
"foo1key": "bar1",
|
||||
"foo2key": "bar2",
|
||||
|
||||
nmd := report.MakeNode("foo1")
|
||||
nmd = nmd.AddPrefixPropertyList("foo_", map[string]string{
|
||||
"foo3": "bar3",
|
||||
"foo1": "bar1",
|
||||
})
|
||||
nmd = nmd.AddPrefixPropertyList("zzz_", map[string]string{
|
||||
"foo2": "bar2",
|
||||
})
|
||||
|
||||
template := report.TableTemplate{FixedRows: map[string]string{
|
||||
"foo1key": "foo1",
|
||||
"foo2key": "foo2",
|
||||
},
|
||||
template := report.TableTemplate{
|
||||
Type: report.PropertyListType,
|
||||
Prefix: "foo_",
|
||||
}
|
||||
|
||||
have, _ := nmd.ExtractTable(template)
|
||||
have, truncationCount := nmd.ExtractTable(template)
|
||||
|
||||
if truncationCount != 0 {
|
||||
t.Error("Table shouldn't had been truncated")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error(test.Diff(want, have))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncation(t *testing.T) {
|
||||
func TestFixedPropertyLists(t *testing.T) {
|
||||
want := []report.Row{
|
||||
{
|
||||
ID: "label_foo1",
|
||||
Entries: map[string]string{
|
||||
"label": "foo1",
|
||||
"value": "bar1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "label_foo2",
|
||||
Entries: map[string]string{
|
||||
"label": "foo2",
|
||||
"value": "bar2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
nmd := report.MakeNodeWith("foo1", map[string]string{
|
||||
"foo2key": "bar2",
|
||||
"foo1key": "bar1",
|
||||
})
|
||||
|
||||
template := report.TableTemplate{
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
"foo2key": "foo2",
|
||||
"foo1key": "foo1",
|
||||
},
|
||||
}
|
||||
|
||||
have, truncationCount := nmd.ExtractTable(template)
|
||||
|
||||
if truncationCount != 0 {
|
||||
t.Error("Table shouldn't had been truncated")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error(test.Diff(want, have))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertyListTruncation(t *testing.T) {
|
||||
wantTruncationCount := 1
|
||||
want := map[string]string{}
|
||||
propertyList := map[string]string{}
|
||||
for i := 0; i < report.MaxTableRows+wantTruncationCount; i++ {
|
||||
key := fmt.Sprintf("key%d", i)
|
||||
value := fmt.Sprintf("value%d", i)
|
||||
want[key] = value
|
||||
propertyList[key] = value
|
||||
}
|
||||
|
||||
nmd := report.MakeNode("foo1")
|
||||
nmd = nmd.AddPrefixPropertyList("foo_", propertyList)
|
||||
|
||||
nmd = nmd.AddPrefixTable("foo_", want)
|
||||
_, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"})
|
||||
template := report.TableTemplate{
|
||||
Type: report.PropertyListType,
|
||||
Prefix: "foo_",
|
||||
}
|
||||
|
||||
_, truncationCount := nmd.ExtractTable(template)
|
||||
|
||||
if truncationCount != wantTruncationCount {
|
||||
t.Error(
|
||||
"Table should had been truncated by",
|
||||
"Property list should had been truncated by",
|
||||
wantTruncationCount,
|
||||
"and not",
|
||||
truncationCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMulticolumnTableTruncation(t *testing.T) {
|
||||
wantTruncationCount := 1
|
||||
rows := []report.Row{}
|
||||
for i := 0; i < report.MaxTableRows+wantTruncationCount; i++ {
|
||||
rowID := fmt.Sprintf("row%d", i)
|
||||
colID := fmt.Sprintf("col%d", i)
|
||||
value := fmt.Sprintf("value%d", i)
|
||||
rows = append(rows, report.Row{
|
||||
ID: rowID,
|
||||
Entries: map[string]string{
|
||||
colID: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
nmd := report.MakeNode("foo1")
|
||||
nmd = nmd.AddPrefixMulticolumnTable("foo_", rows)
|
||||
|
||||
template := report.TableTemplate{
|
||||
Type: report.MulticolumnTableType,
|
||||
Prefix: "foo_",
|
||||
}
|
||||
|
||||
_, truncationCount := nmd.ExtractTable(template)
|
||||
|
||||
if truncationCount != wantTruncationCount {
|
||||
t.Error(
|
||||
"Property list should had been truncated by",
|
||||
wantTruncationCount,
|
||||
"and not",
|
||||
truncationCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTables(t *testing.T) {
|
||||
want := []report.Table{
|
||||
{
|
||||
ID: "AAA",
|
||||
Label: "Aaa",
|
||||
Type: report.PropertyListType,
|
||||
Columns: nil,
|
||||
Rows: []report.Row{
|
||||
{
|
||||
ID: "label_foo1",
|
||||
Entries: map[string]string{
|
||||
"label": "foo1",
|
||||
"value": "bar1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "label_foo3",
|
||||
Entries: map[string]string{
|
||||
"label": "foo3",
|
||||
"value": "bar3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "BBB",
|
||||
Label: "Bbb",
|
||||
Type: report.MulticolumnTableType,
|
||||
Columns: []report.Column{{ID: "col1", Label: "Column 1"}},
|
||||
Rows: []report.Row{
|
||||
{
|
||||
ID: "row1",
|
||||
Entries: map[string]string{
|
||||
"col1": "r1c1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "row2",
|
||||
Entries: map[string]string{
|
||||
"col3": "r2c3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "CCC",
|
||||
Label: "Ccc",
|
||||
Type: report.PropertyListType,
|
||||
Columns: nil,
|
||||
Rows: []report.Row{
|
||||
{
|
||||
ID: "label_foo3",
|
||||
Entries: map[string]string{
|
||||
"label": "foo3",
|
||||
"value": "bar3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
nmd := report.MakeNodeWith("foo1", map[string]string{
|
||||
"foo3key": "bar3",
|
||||
"foo1key": "bar1",
|
||||
})
|
||||
nmd = nmd.AddPrefixMulticolumnTable("bbb_", []report.Row{
|
||||
{ID: "row1", Entries: map[string]string{"col1": "r1c1"}},
|
||||
{ID: "row2", Entries: map[string]string{"col3": "r2c3"}},
|
||||
})
|
||||
nmd = nmd.AddPrefixPropertyList("aaa_", map[string]string{
|
||||
"foo3": "bar3",
|
||||
"foo1": "bar1",
|
||||
})
|
||||
|
||||
aaaTemplate := report.TableTemplate{
|
||||
ID: "AAA",
|
||||
Label: "Aaa",
|
||||
Prefix: "aaa_",
|
||||
Type: report.PropertyListType,
|
||||
}
|
||||
bbbTemplate := report.TableTemplate{
|
||||
ID: "BBB",
|
||||
Label: "Bbb",
|
||||
Prefix: "bbb_",
|
||||
Type: report.MulticolumnTableType,
|
||||
Columns: []report.Column{{ID: "col1", Label: "Column 1"}},
|
||||
}
|
||||
cccTemplate := report.TableTemplate{
|
||||
ID: "CCC",
|
||||
Label: "Ccc",
|
||||
Prefix: "ccc_",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{"foo3key": "foo3"},
|
||||
}
|
||||
templates := report.TableTemplates{
|
||||
aaaTemplate.ID: aaaTemplate,
|
||||
bbbTemplate.ID: bbbTemplate,
|
||||
cccTemplate.ID: cccTemplate,
|
||||
}
|
||||
|
||||
have := templates.Tables(nmd)
|
||||
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Error(test.Diff(want, have))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user