From e475a09ee6e9a58beb15344198486cf8f7f228fb Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 23 Dec 2016 17:00:24 +0100 Subject: [PATCH] Rendering sortable generic tables in the UI Rendering generic table columns Made Type a required attribute for TableTemplate Made generic table sortable on the UI --- client/app/scripts/components/node-details.js | 36 ++++- .../node-details-generic-table.js | 129 ++++++++++++++++++ probe/docker/reporter.go | 26 +++- probe/kubernetes/reporter.go | 7 +- probe/overlay/weave.go | 16 ++- report/table.go | 24 +++- 6 files changed, 224 insertions(+), 14 deletions(-) create mode 100644 client/app/scripts/components/node-details/node-details-generic-table.js 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 ( + + ); + })} + + + + {sortedRows(rows, sortedByColumn, sortedDesc).map(row => ( + + {columns.map((column) => { + const value = row.entries[column.id]; + const match = matches.get(column.id); + return ( + + ); + })} + + ))} + +
+ {isSortedAsc + && } + {isSortedDesc + && } + {column.label} +
+ +
+ +
+ ); + } +} 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, })