- {field.label}
+ className="node-details-property-list-field-label truncate"
+ title={field.entries.label} key={field.id}>
+ {field.entries.label}
-
))}
diff --git a/client/app/scripts/components/node-details/node-details-relatives.js b/client/app/scripts/components/node-details/node-details-relatives.js
index 26cef8a78..3004011fd 100644
--- a/client/app/scripts/components/node-details/node-details-relatives.js
+++ b/client/app/scripts/components/node-details/node-details-relatives.js
@@ -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);
diff --git a/client/app/scripts/components/node-details/node-details-table-headers.js b/client/app/scripts/components/node-details/node-details-table-headers.js
new file mode 100644
index 000000000..9d72a2ce4
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-table-headers.js
@@ -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 (
+
+ {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 (
+ |
+ {isSortedAsc
+ && }
+ {isSortedDesc
+ && }
+ {label}
+ |
+ );
+ })}
+
+ );
+ }
+}
diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js
index a7c820fc1..2cd8f9eba 100644
--- a/client/app/scripts/components/node-details/node-details-table.js
+++ b/client/app/scripts/components/node-details/node-details-table.js
@@ -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 (
-
- {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 (
- |
- {isSortedAsc
- && }
- {isSortedDesc
- && }
- {label}
- |
- );
- })}
-
- );
- }
-
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 (
- {this.renderHeaders(sortedBy, sortedDesc)}
+ {this.props.nodes && this.props.nodes.length > 0 && }
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: '#',
+};
diff --git a/client/app/scripts/utils/__tests__/search-utils-test.js b/client/app/scripts/utils/__tests__/search-utils-test.js
index 301b431a2..dde59e679 100644
--- a/client/app/scripts/utils/__tests__/search-utils-test.js
+++ b/client/app/scripts/utils/__tests__/search-utils-test.js
@@ -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');
});
});
diff --git a/client/app/scripts/utils/node-details-utils.js b/client/app/scripts/utils/node-details-utils.js
new file mode 100644
index 000000000..a7c84bde0
--- /dev/null
+++ b/client/app/scripts/utils/node-details-utils.js
@@ -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'
+ }));
+}
diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js
index b8f74d047..aec25f632 100644
--- a/client/app/scripts/utils/search-utils.js
+++ b/client/app/scripts/utils/search-utils.js
@@ -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) {
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index e3e8d75cb..b209661cd 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -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;
diff --git a/probe/docker/container.go b/probe/docker/container.go
index 50856ee0f..419fb8605 100644
--- a/probe/docker/container.go
+++ b/probe/docker/container.go
@@ -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
}
diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go
index b1e2eb75c..44efb69e4 100644
--- a/probe/docker/reporter.go
+++ b/probe/docker/reporter.go
@@ -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)
})
diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go
index c7db23d0c..fc8188ca3 100644
--- a/probe/kubernetes/meta.go
+++ b/probe/kubernetes/meta.go
@@ -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())
}
diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go
index 44c7c564a..dfebc1a39 100644
--- a/probe/kubernetes/reporter.go
+++ b/probe/kubernetes/reporter.go
@@ -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{
diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go
index 819d88eb5..b87cb88eb 100644
--- a/probe/overlay/weave.go
+++ b/probe/overlay/weave.go
@@ -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
}
diff --git a/render/detailed/tables_test.go b/render/detailed/tables_test.go
index 02f723391..ab847d276 100644
--- a/render/detailed/tables_test.go
+++ b/render/detailed/tables_test.go
@@ -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{},
},
},
},
diff --git a/report/table.go b/report/table.go
index 74cbc1a71..accdd6553 100644
--- a/report/table.go
+++ b/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
diff --git a/report/table_test.go b/report/table_test.go
index 72b6d94cd..fa94d5d8c 100644
--- a/report/table_test.go
+++ b/report/table_test.go
@@ -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))
+ }
+}