diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index 6c566c3722..6bd6017e32 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -2,15 +2,22 @@ import _ from 'lodash'; import React from 'react'; import { connect } from 'react-redux'; +import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions'; +import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils'; +import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils'; + import NodeDetailsControls from './node-details/node-details-controls'; 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 { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions'; -import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils'; -import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils'; +import Warning from './warning'; + +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.`; +} export class NodeDetails extends React.Component { @@ -196,7 +203,13 @@ export class NodeDetails extends React.Component { if (table.rows.length > 0) { return (
-
{table.label}
+
+ {table.label} + {table.truncationCount > 0 && + + } +
); diff --git a/client/app/scripts/components/warning.js b/client/app/scripts/components/warning.js new file mode 100644 index 0000000000..161c68d5ab --- /dev/null +++ b/client/app/scripts/components/warning.js @@ -0,0 +1,39 @@ +import React from 'react'; +import classnames from 'classnames'; + + +class Warning extends React.Component { + + constructor(props, context) { + super(props, context); + this.handleClick = this.handleClick.bind(this); + this.state = { + expanded: false + }; + } + + handleClick() { + const expanded = !this.state.expanded; + this.setState({ expanded }); + } + + render() { + const { text } = this.props; + const { expanded } = this.state; + + const className = classnames('warning', { + 'warning-expanded': expanded + }); + + return ( +
+
+ + {expanded && {text}} +
+
+ ); + } +} + +export default Warning; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 321468f475..43132ba893 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1098,6 +1098,42 @@ h2 { } } +.warning { + display: inline-block; + cursor: pointer; + border: 1px dashed transparent; + text-transform: none; + border-radius: @border-radius; + margin-left: 4px; + + &-wrapper { + display: flex; + } + + &-text { + display: inline-block; + color: @text-secondary-color; + padding-left: 0.5em; + } + + &-icon { + .btn-opacity; + } + + &-expanded { + margin-left: 0; + padding: 2px 4px; + border-color: @text-tertiary-color; + } + + &-expanded &-icon { + position: relative; + top: 4px; + left: 2px; + } + +} + .sidebar { position: fixed; bottom: 16px; diff --git a/report/table.go b/report/table.go index 25052640b8..6ab4f5a20e 100644 --- a/report/table.go +++ b/report/table.go @@ -1,37 +1,62 @@ package report import ( + "fmt" "sort" "strings" + log "github.com/Sirupsen/logrus" "github.com/weaveworks/scope/common/mtime" ) +// 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_" +) + // AddTable appends arbirary key-value pairs to the Node, returning a new node. func (node Node) AddTable(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) + node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount) } return node } // ExtractTable returns the key-value pairs with the given prefix from this Node, -func (node Node) ExtractTable(prefix string) map[string]string { - result := map[string]string{} +func (node Node) ExtractTable(prefix string) (rows map[string]string, truncationCount int) { + rows = map[string]string{} + truncationCount = 0 node.Latest.ForEach(func(key, value string) { if strings.HasPrefix(key, prefix) { label := key[len(prefix):] - result[label] = value + rows[label] = value } }) - return result + if str, ok := node.Latest.Lookup(TruncationCountPrefix + 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 } // 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"` + ID string `json:"id"` + Label string `json:"label"` + Rows []MetadataRow `json:"rows"` + TruncationCount int `json:"truncationCount,omitempty"` } type tablesByID []Table @@ -90,12 +115,13 @@ type TableTemplates map[string]TableTemplate func (t TableTemplates) Tables(node Node) []Table { var result []Table for _, template := range t { + rows, truncationCount := node.ExtractTable(template.Prefix) table := Table{ - ID: template.ID, - Label: template.Label, - Rows: []MetadataRow{}, + ID: template.ID, + Label: template.Label, + Rows: []MetadataRow{}, + TruncationCount: truncationCount, } - rows := node.ExtractTable(template.Prefix) keys := make([]string, 0, len(rows)) for k := range rows { keys = append(keys, k) diff --git a/report/table_test.go b/report/table_test.go index b1866b072a..cf276ba75c 100644 --- a/report/table_test.go +++ b/report/table_test.go @@ -1,6 +1,7 @@ package report_test import ( + "fmt" "reflect" "testing" @@ -16,9 +17,37 @@ func TestTables(t *testing.T) { nmd := report.MakeNode("foo1") nmd = nmd.AddTable("foo_", want) - have := nmd.ExtractTable("foo_") + have, truncationCount := nmd.ExtractTable("foo_") + + 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) { + wantTruncationCount := 1 + want := 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 + } + + nmd := report.MakeNode("foo1") + + nmd = nmd.AddTable("foo_", want) + _, truncationCount := nmd.ExtractTable("foo_") + + if truncationCount != wantTruncationCount { + t.Error( + "Table should had been truncated by", + wantTruncationCount, + "and not", + truncationCount, + ) + } +}