diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js
index b8abffd1b..d37694729 100644
--- a/client/app/scripts/components/node-details.js
+++ b/client/app/scripts/components/node-details.js
@@ -1,3 +1,4 @@
+import debug from 'debug';
import React from 'react';
import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';
@@ -8,6 +9,7 @@ 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 NodeDetailsHealth from './node-details/node-details-health';
import NodeDetailsInfo from './node-details/node-details-info';
import NodeDetailsLabels from './node-details/node-details-labels';
@@ -15,13 +17,18 @@ import NodeDetailsRelatives from './node-details/node-details-relatives';
import NodeDetailsTable from './node-details/node-details-table';
import Warning from './warning';
+
+const logError = debug('scope:error');
+
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 {
+const TABLE_TYPE_PROPERTY_LIST = 'property-list';
+const TABLE_TYPE_GENERIC = 'multicolumn-table';
+class NodeDetails extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClickClose = this.handleClickClose.bind(this);
@@ -214,9 +221,7 @@ class NodeDetails extends React.Component {
}
-
+ {this.renderTable(table)}
);
}
@@ -227,6 +232,29 @@ class NodeDetails extends React.Component {
);
}
+ renderTable(table) {
+ const { nodeMatches = makeMap() } = this.props;
+ switch (table.type) {
+ case TABLE_TYPE_GENERIC:
+ return (
+
+ );
+ case TABLE_TYPE_PROPERTY_LIST:
+ return (
+
+ );
+ default:
+ logError(`Undefined type '${table.type}' for table ${table.id}`);
+ return null;
+ }
+ }
+
componentDidUpdate() {
this.updateTitle();
}
diff --git a/client/app/scripts/components/node-details/node-details-generic-table.js b/client/app/scripts/components/node-details/node-details-generic-table.js
new file mode 100644
index 000000000..ad37ee44a
--- /dev/null
+++ b/client/app/scripts/components/node-details/node-details-generic-table.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import { Map as makeMap } from 'immutable';
+import { sortBy } from 'lodash';
+
+import MatchedText from '../matched-text';
+import ShowMore from '../show-more';
+
+
+function columnStyle(column) {
+ return {
+ textAlign: column.dataType === 'number' ? 'right' : 'left',
+ paddingRight: '10px',
+ maxWidth: '140px'
+ };
+}
+
+function sortedRows(rows, sortedByColumn, sortedDesc) {
+ const orderedRows = sortBy(rows, row => row.id);
+ const sorted = sortBy(orderedRows, (row) => {
+ let value = row.entries[sortedByColumn.id];
+ if (sortedByColumn.dataType === 'number') {
+ 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.DEFAULT_LIMIT = 5;
+ this.state = {
+ limit: this.DEFAULT_LIMIT,
+ sortedByColumn: props.columns[0],
+ sortedDesc: true
+ };
+ this.handleLimitClick = this.handleLimitClick.bind(this);
+ }
+
+ handleHeaderClick(ev, column) {
+ ev.preventDefault();
+ this.setState({
+ sortedByColumn: column,
+ sortedDesc: this.state.sortedByColumn.id === column.id
+ ? !this.state.sortedDesc : true
+ });
+ }
+
+ handleLimitClick() {
+ const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT;
+ this.setState({limit});
+ }
+
+ render() {
+ const { sortedByColumn, sortedDesc } = this.state;
+ const { columns, matches = makeMap() } = this.props;
+ let rows = this.props.rows;
+ let notShown = 0;
+ const limited = rows && this.state.limit > 0 && rows.length > this.state.limit;
+ const expanded = this.state.limit === 0;
+ if (rows && limited) {
+ const hasNotShownMatch = rows.filter((row, index) => index >= this.state.limit
+ && matches.has(row.id)).length > 0;
+ if (!hasNotShownMatch) {
+ notShown = rows.length - this.DEFAULT_LIMIT;
+ rows = rows.slice(0, this.state.limit);
+ }
+ }
+
+ return (
+
+
+
+
+ {columns.map((column) => {
+ const onHeaderClick = (ev) => {
+ this.handleHeaderClick(ev, column);
+ };
+ const isSorted = column.id === this.state.sortedByColumn.id;
+ const isSortedDesc = isSorted && this.state.sortedDesc;
+ const isSortedAsc = isSorted && !isSortedDesc;
+ const style = Object.assign(columnStyle(column), {
+ cursor: 'pointer',
+ fontSize: '11px'
+ });
+ return (
+ |
+ {isSortedAsc
+ && }
+ {isSortedDesc
+ && }
+ {column.label}
+ |
+ );
+ })}
+
+
+
+ {sortedRows(rows, sortedByColumn, sortedDesc).map(row => (
+
+ {columns.map((column) => {
+ const value = row.entries[column.id];
+ const match = matches.get(column.id);
+ return (
+ |
+
+ |
+ );
+ })}
+
+ ))}
+
+
+
+
+ );
+ }
+}
diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go
index accd18301..4aacaca2d 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{
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 13e34412c..2fb3aea18 100644
--- a/probe/overlay/weave.go
+++ b/probe/overlay/weave.go
@@ -121,9 +121,23 @@ var (
},
WeaveConnectionsMulticolumnTablePrefix: {
ID: WeaveConnectionsMulticolumnTablePrefix,
- Label: "Connections",
+ Label: "Connections (new)",
Type: report.MulticolumnTableType,
Prefix: WeaveConnectionsMulticolumnTablePrefix,
+ Columns: []report.Column{
+ report.Column{
+ ID: "ip",
+ Label: "IP",
+ },
+ report.Column{
+ ID: "state",
+ Label: "State",
+ },
+ report.Column{
+ ID: "info",
+ Label: "Info",
+ },
+ },
},
}
)
diff --git a/report/table.go b/report/table.go
index 791fe62ea..e8e0f8297 100644
--- a/report/table.go
+++ b/report/table.go
@@ -118,10 +118,9 @@ func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCou
}
type Column struct {
- ID string `json:"id"`
- Label string `json:"label"`
- DataType string `json:"dataType"`
- Alignment string `json:"alignment"`
+ ID string `json:"id"`
+ Label string `json:"label"`
+ DataType string `json:"dataType"`
}
type Row struct {
@@ -129,6 +128,16 @@ type Row struct {
Entries map[string]string `json:"entries"`
}
+// 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"`
@@ -202,6 +211,11 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
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.
return TableTemplate{
@@ -209,6 +223,7 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
Label: max(t.Label, other.Label),
Prefix: max(t.Prefix, other.Prefix),
Type: max(t.Type, other.Type),
+ Columns: columns,
FixedRows: fixedRows,
}
}
@@ -225,6 +240,7 @@ func (t TableTemplates) Tables(node Node) []Table {
ID: template.ID,
Label: template.Label,
Type: template.Type,
+ Columns: template.Columns,
Rows: rows,
TruncationCount: truncationCount,
})