mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user