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
This commit is contained in:
Filip Barl
2016-12-23 17:00:24 +01:00
parent 31be525bd2
commit e475a09ee6
6 changed files with 224 additions and 14 deletions

View File

@@ -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 {
<Warning text={getTruncationText(table.truncationCount)} />
</span>}
</div>
<NodeDetailsLabels
rows={table.rows} controls={table.controls}
matches={nodeMatches.get('tables')} />
{this.renderTable(table)}
</div>
);
}
@@ -227,6 +232,29 @@ class NodeDetails extends React.Component {
);
}
renderTable(table) {
const { nodeMatches = makeMap() } = this.props;
switch (table.type) {
case TABLE_TYPE_GENERIC:
return (
<NodeDetailsGenericTable
rows={table.rows} columns={table.columns}
matches={nodeMatches.get('tables')}
/>
);
case TABLE_TYPE_PROPERTY_LIST:
return (
<NodeDetailsLabels
rows={table.rows} controls={table.controls}
matches={nodeMatches.get('tables')}
/>
);
default:
logError(`Undefined type '${table.type}' for table ${table.id}`);
return null;
}
}
componentDidUpdate() {
this.updateTitle();
}

View File

@@ -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 (
<div className="node-details-generic-table">
<table>
<thead>
<tr>
{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 (
<th
className="node-details-generic-table-header"
key={column.id} style={style} onClick={onHeaderClick}>
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
{column.label}
</th>
);
})}
</tr>
</thead>
<tbody>
{sortedRows(rows, sortedByColumn, sortedDesc).map(row => (
<tr className="node-details-generic-table-row" key={row.id}>
{columns.map((column) => {
const value = row.entries[column.id];
const match = matches.get(column.id);
return (
<td
className="node-details-generic-table-field-value truncate"
title={value} key={column.id} style={columnStyle(column)}>
<MatchedText text={value} match={match} />
</td>
);
})}
</tr>
))}
</tbody>
</table>
<ShowMore
handleClick={this.handleLimitClick} collection={this.props.rows}
expanded={expanded} notShown={notShown}
/>
</div>
);
}
}

View File

@@ -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{

View File

@@ -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{

View File

@@ -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",
},
},
},
}
)

View File

@@ -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,
})