diff --git a/client/app/scripts/components/node-details/node-details-labels.js b/client/app/scripts/components/node-details/node-details-labels.js index 6823f1330..2fd01a482 100644 --- a/client/app/scripts/components/node-details/node-details-labels.js +++ b/client/app/scripts/components/node-details/node-details-labels.js @@ -6,7 +6,6 @@ import MatchedText from '../matched-text'; import NodeDetailsControlButton from './node-details-control-button'; import ShowMore from '../show-more'; - const Controls = controls => (
{sortBy(controls, 'rank').map(control =>
- {field.label} + title={field.entries.label} key={field.id}> + {field.entries.label}
-
- +
+
))} diff --git a/client/app/scripts/utils/__tests__/search-utils-test.js b/client/app/scripts/utils/__tests__/search-utils-test.js index 301b431a2..1596e3d62 100644 --- a/client/app/scripts/utils/__tests__/search-utils-test.js +++ b/client/app/scripts/utils/__tests__/search-utils-test.js @@ -30,9 +30,17 @@ describe('SearchUtils', () => { tables: [{ id: 'metric1', rows: [{ - id: 'row1', - label: 'Row 1', - value: 'Row Value 1' + entries: { + id: 'row1', + label: 'Row 1', + value: 'Row Value 1' + } + }, { + entries: { + id: 'row2', + label: 'Row 2', + value: 'Row Value 2' + } }] }], }, diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js index b8f74d047..c297ae96a 100644 --- a/client/app/scripts/utils/search-utils.js +++ b/client/app/scripts/utils/search-utils.js @@ -154,9 +154,10 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) { tables.forEach((table) => { if (table.get('rows')) { table.get('rows').forEach((field) => { + const entries = field.get('entries'); const keyPath = [nodeId, 'tables', field.get('id')]; - nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'), - query, prefix, field.get('label')); + nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'), + query, prefix, entries.get('label')); }); } }); diff --git a/probe/docker/container.go b/probe/docker/container.go index 50856ee0f..f23ed71fd 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.AddPrefixLabels(LabelPrefix, c.container.Config.Labels) + result = result.AddPrefixLabels(EnvPrefix, c.env()) return result } diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index b1e2eb75c..accd18301 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -258,7 +258,7 @@ func (r *Reporter) containerImageTopology() report.Topology { } nodeID := report.MakeContainerImageNodeID(imageID) node := report.MakeNodeWith(nodeID, latests) - node = node.AddPrefixTable(ImageLabelPrefix, image.Labels) + node = node.AddPrefixLabels(ImageLabelPrefix, image.Labels) result.AddNode(node) }) diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go index c7db23d0c..20fb213bb 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()) + }).AddPrefixLabels(LabelPrefix, m.Labels()) } diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go index 819d88eb5..13e34412c 100644 --- a/probe/overlay/weave.go +++ b/probe/overlay/weave.go @@ -19,34 +19,35 @@ 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" + WeaveConnectionsTablePrefix = "weave_connections_table_" + WeaveConnectionsMulticolumnTablePrefix = "weave_connections_multicolumn_table_" ) var ( @@ -73,14 +74,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,13 +95,19 @@ 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", @@ -103,8 +116,15 @@ var ( WeaveConnectionsTablePrefix: { ID: WeaveConnectionsTablePrefix, Label: "Connections", + Type: report.PropertyListType, Prefix: WeaveConnectionsTablePrefix, }, + WeaveConnectionsMulticolumnTablePrefix: { + ID: WeaveConnectionsMulticolumnTablePrefix, + Label: "Connections", + Type: report.MulticolumnTableType, + Prefix: WeaveConnectionsMulticolumnTablePrefix, + }, } ) @@ -434,28 +454,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.AddPrefixTable(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{ + "ip": fmt.Sprintf("%s %s", arrow, conn.Address), + "state": conn.State, + "info": conn.Info, + }, + }) } return table } diff --git a/render/detailed/tables_test.go b/render/detailed/tables_test.go index 02f723391..634755c54 100644 --- a/render/detailed/tables_test.go +++ b/render/detailed/tables_test.go @@ -35,23 +35,25 @@ func TestNodeTables(t *testing.T) { { ID: docker.EnvPrefix, Label: "Environment Variables", - Rows: []report.MetadataRow{}, + Rows: []report.Row{}, }, { ID: docker.LabelPrefix, Label: "Docker Labels", - Rows: []report.MetadataRow{ + Rows: []report.Row{ { - ID: "label_label1", - Label: "label1", - Value: "label1value", + Entries: map[string]string{ + "id": "label_label1", + "label": "label1", + "value": "label1value", + }, }, }, }, { ID: docker.ImageTableID, Label: "Image", - Rows: []report.MetadataRow{}, + Rows: []report.Row{}, }, }, }, diff --git a/report/table.go b/report/table.go index 74cbc1a71..791fe62ea 100644 --- a/report/table.go +++ b/report/table.go @@ -15,10 +15,33 @@ import ( const ( MaxTableRows = 20 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 { +// AddPrefixTable appends arbitrary rows to the Node, returning a new node. +func (node Node) AddPrefixTable(prefix string, rows []Row) Node { + count := 0 + for _, row := range rows { + if count >= MaxTableRows { + break + } + // TODO: Figure a more natural way of storing rows + for column, value := range row.Entries { + key := fmt.Sprintf("%s %s", row.ID, column) + node = node.WithLatest(prefix+key, mtime.Now(), value) + } + count++ + } + if len(rows) > MaxTableRows { + truncationCount := fmt.Sprintf("%d", len(rows)-MaxTableRows) + node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount) + } + return node +} + +// AddPrefixLabels appends arbitrary key-value pairs to the Node, returning a new node. +func (node Node) AddPrefixLabels(prefix string, labels map[string]string) Node { count := 0 for key, value := range labels { if count >= MaxTableRows { @@ -34,33 +57,86 @@ func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node { 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{} +// ExtractTable returns the rows to build a table from this node +func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCount int) { + rows = []Row{} + switch template.Type { + case MulticolumnTableType: + keyRows := map[string]Row{} + node.Latest.ForEach(func(key string, _ time.Time, value string) { + if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { + rowID, column := "", "" + fmt.Sscanf(key[len(template.Prefix):], "%s %s", &rowID, &column) + if _, ok := keyRows[rowID]; !ok { + keyRows[rowID] = Row{ + ID: rowID, + Entries: map[string]string{}, + } + } + keyRows[rowID].Entries[column] = value + } + }) + for _, row := range keyRows { + rows = append(rows, row) + } + // By default assume it's a property list (for backward compatibility) + default: + keyValues := map[string]string{} + node.Latest.ForEach(func(key string, _ time.Time, value string) { + if label, ok := template.FixedRows[key]; ok { + keyValues[label] = value + } + if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { + label := key[len(template.Prefix):] + keyValues[label] = value + } + }) + labels := make([]string, 0, len(rows)) + for label := range keyValues { + labels = append(labels, label) + } + sort.Strings(labels) + for _, label := range labels { + rows = append(rows, Row{ + ID: "label_" + label, + Entries: map[string]string{ + "label": label, + "value": keyValues[label], + }, + }) + } + } + truncationCount = 0 - node.Latest.ForEach(func(key string, _ time.Time, value string) { - if label, ok := template.FixedRows[key]; ok { - rows[label] = value - } - if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { - label := key[len(template.Prefix):] - rows[label] = value - } - }) 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 } +type Column struct { + ID string `json:"id"` + Label string `json:"label"` + DataType string `json:"dataType"` + Alignment string `json:"alignment"` +} + +type Row struct { + ID string `json:"id"` + Entries map[string]string `json:"entries"` +} + // 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 +148,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 +163,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 @@ -126,10 +202,13 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate { fixedRows = other.FixedRows } + // TODO: Refactor the merging logic, as mixing + // the types now might result in invalid tables. 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), FixedRows: fixedRows, } } @@ -142,25 +221,13 @@ func (t TableTemplates) Tables(node Node) []Table { var result []Table for _, template := range t { rows, truncationCount := node.ExtractTable(template) - table := Table{ + result = append(result, Table{ ID: template.ID, Label: template.Label, - Rows: []MetadataRow{}, + Type: template.Type, + 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..017762a7a 100644 --- a/report/table_test.go +++ b/report/table_test.go @@ -16,7 +16,7 @@ func TestPrefixTables(t *testing.T) { } nmd := report.MakeNode("foo1") - nmd = nmd.AddPrefixTable("foo_", want) + nmd = nmd.AddPrefixLabels("foo_", want) have, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) if truncationCount != 0 { @@ -62,7 +62,7 @@ func TestTruncation(t *testing.T) { nmd := report.MakeNode("foo1") - nmd = nmd.AddPrefixTable("foo_", want) + nmd = nmd.AddPrefixLabels("foo_", want) _, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) if truncationCount != wantTruncationCount {