diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index d37694729..387565ea6 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -5,6 +5,7 @@ import { Map as makeMap } from 'immutable'; import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions'; import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils'; +import { isGenericTable, isPropertyList } from '../utils/node-details-utils'; import { resetDocumentTitle, setDocumentTitle } from '../utils/title-utils'; import MatchedText from './matched-text'; @@ -25,9 +26,6 @@ function getTruncationText(count) { + ` (${count} extra entries not included). We are working to remove this limitation.`; } -const TABLE_TYPE_PROPERTY_LIST = 'property-list'; -const TABLE_TYPE_GENERIC = 'multicolumn-table'; - class NodeDetails extends React.Component { constructor(props, context) { super(props, context); @@ -215,7 +213,7 @@ class NodeDetails extends React.Component { return (
- {table.label} + {table.label.length > 0 && table.label} {table.truncationCount > 0 && @@ -234,25 +232,25 @@ 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; + + if (isGenericTable(table)) { + return ( + + ); + } else if (isPropertyList(table)) { + return ( + + ); } + + logError(`Undefined type '${table.type}' for table ${table.id}`); + return null; } componentDidUpdate() { 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 index ad37ee44a..3478078be 100644 --- a/client/app/scripts/components/node-details/node-details-generic-table.js +++ b/client/app/scripts/components/node-details/node-details-generic-table.js @@ -2,28 +2,25 @@ import React from 'react'; import { Map as makeMap } from 'immutable'; import { sortBy } from 'lodash'; +import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits'; +import { + isNumber, getTableColumnsStyles, genericTableEntryKey +} from '../../utils/node-details-utils'; +import NodeDetailsTableHeaders from './node-details-table-headers'; 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') { +function sortedRows(rows, columns, sortedBy, sortedDesc) { + const column = columns.find(c => c.id === sortedBy); + const sorted = sortBy(rows, (row) => { + let value = row.entries[sortedBy]; + if (isNumber(column)) { value = parseFloat(value); } return value; }); - if (!sortedDesc) { + if (sortedDesc) { sorted.reverse(); } return sorted; @@ -32,85 +29,68 @@ function sortedRows(rows, sortedByColumn, sortedDesc) { 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], + limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT, + sortedBy: props.columns[0].id, sortedDesc: true }; this.handleLimitClick = this.handleLimitClick.bind(this); + this.updateSorted = this.updateSorted.bind(this); } - handleHeaderClick(ev, column) { - ev.preventDefault(); - this.setState({ - sortedByColumn: column, - sortedDesc: this.state.sortedByColumn.id === column.id - ? !this.state.sortedDesc : true - }); + updateSorted(sortedBy, sortedDesc) { + this.setState({ sortedBy, sortedDesc }); } handleLimitClick() { - const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT; - this.setState({limit}); + this.setState({ + limit: this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT + }); } render() { - const { sortedByColumn, sortedDesc } = this.state; + const { sortedBy, 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; + + // Stabilize the order of rows + let rows = sortBy(this.props.rows || [], row => row.id); + let notShown = 0; + + // If there are rows that would be hidden behind 'show more', keep them + // expanded if any of them match the search query; otherwise hide them. + if (this.state.limit > 0 && rows.length > this.state.limit) { + const hasHiddenMatch = rows.slice(this.state.limit).some(row => + columns.some(column => matches.has(genericTableEntryKey(row, column))) + ); + if (!hasHiddenMatch) { + notShown = rows.length - NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; rows = rows.slice(0, this.state.limit); } } + const styles = getTableColumnsStyles(columns); 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 => ( + {sortedRows(rows, columns, sortedBy, sortedDesc).map(row => ( - {columns.map((column) => { + {columns.map((column, index) => { + const match = matches.get(genericTableEntryKey(row, column)); const value = row.entries[column.id]; - const match = matches.get(column.id); return ( ); 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 2fd01a482..e25ec4ccd 100644 --- a/client/app/scripts/components/node-details/node-details-labels.js +++ b/client/app/scripts/components/node-details/node-details-labels.js @@ -2,8 +2,9 @@ import React from 'react'; import { Map as makeMap } from 'immutable'; import sortBy from 'lodash/sortBy'; -import MatchedText from '../matched-text'; +import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits'; import NodeDetailsControlButton from './node-details-control-button'; +import MatchedText from '../matched-text'; import ShowMore from '../show-more'; const Controls = controls => ( @@ -17,15 +18,14 @@ export default class NodeDetailsLabels extends React.Component { constructor(props, context) { super(props, context); - this.DEFAULT_LIMIT = 5; this.state = { - limit: this.DEFAULT_LIMIT, + limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT, }; this.handleLimitClick = this.handleLimitClick.bind(this); } handleLimitClick() { - const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT; + const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; this.setState({limit}); } @@ -39,7 +39,7 @@ export default class NodeDetailsLabels extends React.Component { const hasNotShownMatch = rows.filter((row, index) => index >= this.state.limit && matches.has(row.id)).length > 0; if (!hasNotShownMatch) { - notShown = rows.length - this.DEFAULT_LIMIT; + notShown = rows.length - NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; rows = rows.slice(0, this.state.limit); } } diff --git a/client/app/scripts/components/node-details/node-details-relatives.js b/client/app/scripts/components/node-details/node-details-relatives.js index 26cef8a78..3004011fd 100644 --- a/client/app/scripts/components/node-details/node-details-relatives.js +++ b/client/app/scripts/components/node-details/node-details-relatives.js @@ -1,22 +1,22 @@ import React from 'react'; import { Map as makeMap } from 'immutable'; +import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits'; import NodeDetailsRelativesLink from './node-details-relatives-link'; export default class NodeDetailsRelatives extends React.Component { constructor(props, context) { super(props, context); - this.DEFAULT_LIMIT = 5; this.state = { - limit: this.DEFAULT_LIMIT + limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT }; this.handleLimitClick = this.handleLimitClick.bind(this); } handleLimitClick(ev) { ev.preventDefault(); - const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT; + const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; this.setState({limit}); } @@ -26,7 +26,7 @@ export default class NodeDetailsRelatives extends React.Component { const limited = this.state.limit > 0 && relatives.length > this.state.limit; const showLimitAction = limited || (this.state.limit === 0 - && relatives.length > this.DEFAULT_LIMIT); + && relatives.length > NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT); const limitActionText = limited ? 'Show more' : 'Show less'; if (limited) { relatives = relatives.slice(0, this.state.limit); diff --git a/client/app/scripts/components/node-details/node-details-table-headers.js b/client/app/scripts/components/node-details/node-details-table-headers.js new file mode 100644 index 000000000..9d72a2ce4 --- /dev/null +++ b/client/app/scripts/components/node-details/node-details-table-headers.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { defaultSortDesc, getTableColumnsStyles } from '../../utils/node-details-utils'; +import { NODE_DETAILS_TABLE_CW, NODE_DETAILS_TABLE_XS_LABEL } from '../../constants/styles'; + + +export default class NodeDetailsTableHeaders extends React.Component { + handleClick(ev, headerId, currentSortedBy, currentSortedDesc) { + ev.preventDefault(); + const header = this.props.headers.find(h => h.id === headerId); + const sortedBy = header.id; + const sortedDesc = sortedBy === currentSortedBy + ? !currentSortedDesc : defaultSortDesc(header); + this.props.onClick(sortedBy, sortedDesc); + } + + render() { + const { headers, sortedBy, sortedDesc } = this.props; + const colStyles = getTableColumnsStyles(headers); + return ( + + {headers.map((header, index) => { + const headerClasses = ['node-details-table-header', 'truncate']; + const onClick = (ev) => { + this.handleClick(ev, header.id, sortedBy, sortedDesc); + }; + // sort by first metric by default + const isSorted = header.id === sortedBy; + const isSortedDesc = isSorted && sortedDesc; + const isSortedAsc = isSorted && !isSortedDesc; + + if (isSorted) { + headerClasses.push('node-details-table-header-sorted'); + } + + const style = colStyles[index]; + const label = + (style.width === NODE_DETAILS_TABLE_CW.XS && NODE_DETAILS_TABLE_XS_LABEL[header.id]) ? + NODE_DETAILS_TABLE_XS_LABEL[header.id] : header.label; + + return ( + + ); + })} + + ); + } +} diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js index a7c820fc1..2cd8f9eba 100644 --- a/client/app/scripts/components/node-details/node-details-table.js +++ b/client/app/scripts/components/node-details/node-details-table.js @@ -2,59 +2,15 @@ import React from 'react'; import classNames from 'classnames'; import { find, get, union, sortBy, groupBy, concat } from 'lodash'; +import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits'; + import ShowMore from '../show-more'; import NodeDetailsTableRow from './node-details-table-row'; +import NodeDetailsTableHeaders from './node-details-table-headers'; import { ipToPaddedString } from '../../utils/string-utils'; - - -function isNumber(data) { - return data.dataType && data.dataType === 'number'; -} - - -function isIP(data) { - return data.dataType && data.dataType === 'ip'; -} - - -const CW = { - XS: '32px', - S: '50px', - M: '70px', - L: '120px', - XL: '140px', - XXL: '170px', -}; - - -const XS_LABEL = { - count: '#', - // TODO: consider changing the name of this field on the BE - container: '#', -}; - - -const COLUMN_WIDTHS = { - count: CW.XS, - container: CW.XS, - docker_container_created: CW.XXL, - docker_container_restart_count: CW.M, - docker_container_state_human: CW.XXL, - docker_container_uptime: '85px', - docker_cpu_total_usage: CW.M, - docker_memory_usage: CW.M, - open_files_count: CW.M, - pid: CW.S, - port: CW.S, - ppid: CW.S, - process_cpu_usage_percent: CW.M, - process_memory_usage_bytes: CW.M, - threads: CW.M, - - // e.g. details panel > pods - kubernetes_ip: CW.L, - kubernetes_state: CW.M, -}; +import { + isIP, isNumber, defaultSortDesc, getTableColumnsStyles +} from '../../utils/node-details-utils'; function getDefaultSortedBy(columns, nodes) { @@ -158,54 +114,27 @@ function getSortedNodes(nodes, sortedByHeader, sortedDesc) { } -function getColumnWidth(headers, h) { - // - // More beauty hacking, ports and counts can only get so big, free up WS for other longer - // fields like IPs! - // - return COLUMN_WIDTHS[h.id]; -} - - -function getColumnsStyles(headers) { - return headers.map((h, i) => ({ - width: getColumnWidth(headers, h, i), - textAlign: h.dataType === 'number' ? 'right' : 'left', - })); -} - - -function defaultSortDesc(header) { - return header && header.dataType === 'number'; -} - - export default class NodeDetailsTable extends React.Component { constructor(props, context) { super(props, context); - this.DEFAULT_LIMIT = 5; this.state = { - limit: props.limit || this.DEFAULT_LIMIT, + limit: props.limit || NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT, sortedDesc: this.props.sortedDesc, sortedBy: this.props.sortedBy }; this.handleLimitClick = this.handleLimitClick.bind(this); + this.updateSorted = this.updateSorted.bind(this); } - handleHeaderClick(ev, headerId, currentSortedBy, currentSortedDesc) { - ev.preventDefault(); - const header = this.getColumnHeaders().find(h => h.id === headerId); - const sortedBy = header.id; - const sortedDesc = header.id === currentSortedBy - ? !currentSortedDesc : defaultSortDesc(header); - this.setState({sortedBy, sortedDesc}); + updateSorted(sortedBy, sortedDesc) { + this.setState({ sortedBy, sortedDesc }); this.props.onSortChange(sortedBy, sortedDesc); } handleLimitClick() { - const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT; - this.setState({limit}); + const limit = this.state.limit ? 0 : NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT; + this.setState({ limit }); } getColumnHeaders() { @@ -213,60 +142,13 @@ export default class NodeDetailsTable extends React.Component { return [{id: 'label', label: this.props.label}].concat(columns); } - renderHeaders(sortedBy, sortedDesc) { - if (!this.props.nodes || this.props.nodes.length === 0) { - return null; - } - - const headers = this.getColumnHeaders(); - const colStyles = getColumnsStyles(headers); - - return ( - - {headers.map((header, i) => { - const headerClasses = ['node-details-table-header', 'truncate']; - const onHeaderClick = (ev) => { - this.handleHeaderClick(ev, header.id, sortedBy, sortedDesc); - }; - // sort by first metric by default - const isSorted = header.id === sortedBy; - const isSortedDesc = isSorted && sortedDesc; - const isSortedAsc = isSorted && !isSortedDesc; - - if (isSorted) { - headerClasses.push('node-details-table-header-sorted'); - } - - const style = colStyles[i]; - const label = (style.width === CW.XS && XS_LABEL[header.id]) ? - XS_LABEL[header.id] : - header.label; - - return ( - - ); - })} - - ); - } - render() { const { nodeIdKey, columns, topologyId, onClickRow, onMouseEnter, onMouseLeave, onMouseEnterRow, onMouseLeaveRow } = this.props; const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes); const sortedByHeader = this.getColumnHeaders().find(h => h.id === sortedBy); - const sortedDesc = this.state.sortedDesc !== null ? - this.state.sortedDesc : - defaultSortDesc(sortedByHeader); + const sortedDesc = this.state.sortedDesc || defaultSortDesc(sortedByHeader); let nodes = getSortedNodes(this.props.nodes, sortedByHeader, sortedDesc); const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit; @@ -277,13 +159,20 @@ export default class NodeDetailsTable extends React.Component { } const className = classNames('node-details-table-wrapper-wrapper', this.props.className); + const headers = this.getColumnHeaders(); + const styles = getTableColumnsStyles(headers); return (
- {isSortedAsc - && } - {isSortedDesc - && } - {column.label} -
+ className="node-details-generic-table-value truncate" + title={value} key={column.id} style={styles[index]}>
+ {isSortedAsc + && } + {isSortedDesc + && } + {label} +
- {isSortedAsc - && } - {isSortedDesc - && } - {label} -
- {this.renderHeaders(sortedBy, sortedDesc)} + {this.props.nodes && this.props.nodes.length > 0 && } pods + kubernetes_ip: NODE_DETAILS_TABLE_CW.XL, + kubernetes_state: NODE_DETAILS_TABLE_CW.M, + + // weave connections + weave_connection_connection: NODE_DETAILS_TABLE_CW.XXL, + weave_connection_state: NODE_DETAILS_TABLE_CW.L, + weave_connection_info: NODE_DETAILS_TABLE_CW.XL, +}; + +export const NODE_DETAILS_TABLE_XS_LABEL = { + count: '#', + // TODO: consider changing the name of this field on the BE + container: '#', +}; diff --git a/client/app/scripts/utils/node-details-utils.js b/client/app/scripts/utils/node-details-utils.js new file mode 100644 index 000000000..7d9c3d2aa --- /dev/null +++ b/client/app/scripts/utils/node-details-utils.js @@ -0,0 +1,36 @@ +import { NODE_DETAILS_TABLE_COLUMN_WIDTHS } from '../constants/styles'; + +export function isGenericTable(table) { + return (table.type || (table.get && table.get('type'))) === 'multicolumn-table'; +} + +export function isPropertyList(table) { + return (table.type || (table.get && table.get('type'))) === 'property-list'; +} + +export function isNumber(data) { + return data.dataType && data.dataType === 'number'; +} + +export function isIP(data) { + return data.dataType && data.dataType === 'ip'; +} + +export function genericTableEntryKey(row, column) { + const columnId = column.id || column.get('id'); + const rowId = row.id || row.get('id'); + return `${rowId}_${columnId}`; +} + +export function defaultSortDesc(header) { + return header && isNumber(header); +} + +export function getTableColumnsStyles(headers) { + return headers.map(header => ({ + // More beauty hacking, ports and counts can only get + // so big, free up WS for other longer fields like IPs! + width: NODE_DETAILS_TABLE_COLUMN_WIDTHS[header.id], + textAlign: isNumber(header) ? 'right' : 'left' + })); +} diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js index c297ae96a..2ea844c23 100644 --- a/client/app/scripts/utils/search-utils.js +++ b/client/app/scripts/utils/search-utils.js @@ -1,6 +1,7 @@ import { Map as makeMap, Set as makeSet, List as makeList } from 'immutable'; import { escapeRegExp } from 'lodash'; +import { isGenericTable, isPropertyList, genericTableEntryKey } from './node-details-utils'; import { slugify } from './string-utils'; // topolevel search fields @@ -148,20 +149,26 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) { }); } - // tables (envvars and labels) - const tables = node.get('tables'); - if (tables) { - 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, entries.get('value'), - query, prefix, entries.get('label')); - }); - } + // property lists + (node.get('tables') || []).filter(isPropertyList).forEach((propertyList) => { + (propertyList.get('rows') || []).forEach((row) => { + const entries = row.get('entries'); + const keyPath = [nodeId, 'labels', row.get('id')]; + nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'), + query, prefix, entries.get('label')); }); - } + }); + + // generic tables + (node.get('tables') || []).filter(isGenericTable).forEach((table) => { + (table.get('rows') || []).forEach((row) => { + table.get('columns').forEach((column) => { + const val = row.get('entries').get(column.get('id')); + const keyPath = [nodeId, 'tables', genericTableEntryKey(row, column)]; + nodeMatches = findNodeMatch(nodeMatches, keyPath, val, query); + }); + }); + }); } else if (metric) { const metrics = node.get('metrics'); if (metrics) { diff --git a/client/app/styles/main.less b/client/app/styles/main.less index e3e8d75cb..801ad53db 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -897,6 +897,17 @@ h2 { } } + &-generic-table { + width: 100%; + + tr { + display: flex; + th, td { + padding: 0 5px; + } + } + } + &-table { width: 100%; border-spacing: 0; diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go index 2fb3aea18..fde8ccf86 100644 --- a/probe/overlay/weave.go +++ b/probe/overlay/weave.go @@ -46,6 +46,9 @@ const ( WeavePluginTableID = "weave_plugin_table" WeavePluginStatus = "weave_plugin_status" WeavePluginDriver = "weave_plugin_driver" + WeaveConnectionsConnection = "weave_connection_connection" + WeaveConnectionsState = "weave_connection_state" + WeaveConnectionsInfo = "weave_connection_info" WeaveConnectionsTablePrefix = "weave_connections_table_" WeaveConnectionsMulticolumnTablePrefix = "weave_connections_multicolumn_table_" ) @@ -115,26 +118,25 @@ var ( }, WeaveConnectionsTablePrefix: { ID: WeaveConnectionsTablePrefix, - Label: "Connections", + Label: "Connections (old)", Type: report.PropertyListType, Prefix: WeaveConnectionsTablePrefix, }, WeaveConnectionsMulticolumnTablePrefix: { ID: WeaveConnectionsMulticolumnTablePrefix, - Label: "Connections (new)", Type: report.MulticolumnTableType, Prefix: WeaveConnectionsMulticolumnTablePrefix, Columns: []report.Column{ report.Column{ - ID: "ip", - Label: "IP", + ID: WeaveConnectionsConnection, + Label: "Connections", }, report.Column{ - ID: "state", + ID: WeaveConnectionsState, Label: "State", }, report.Column{ - ID: "info", + ID: WeaveConnectionsInfo, Label: "Info", }, }, @@ -488,9 +490,9 @@ func getConnectionsTable(router weave.Router) []report.Row { 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, + WeaveConnectionsConnection: fmt.Sprintf("%s %s", arrow, conn.Address), + WeaveConnectionsState: conn.State, + WeaveConnectionsInfo: conn.Info, }, }) }