diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 1227d2c1f4..5056865387 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -36,7 +36,13 @@ export function toggleHelp() { export function toggleGridMode(enabled) { - return {type: ActionTypes.SET_GRID_MODE, enabled}; + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.SET_GRID_MODE, + enabled + }); + updateRoute(getState); + }; } diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index 91b20458f5..356fd21350 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -35,11 +35,16 @@ export default class NodesGrid extends React.Component { const { margins, nodes, height } = this.props; const cmpStyle = { height, - paddingTop: margins.top, - paddingBottom: margins.bottom, + marginTop: margins.top, paddingLeft: margins.left, paddingRight: margins.right, }; + const tbodyHeight = height - 24 - 18; + const className = 'scroll-body'; + const tbodyStyle = { + height: `${tbodyHeight}px`, + paddingBottom: 160 + }; const detailsData = { label: 'procs', @@ -52,6 +57,9 @@ export default class NodesGrid extends React.Component {
') { dispatch(pinNextMetric(1)); + } else if (char === 't') { + dispatch(toggleGridMode(false)); + } else if (char === 'g') { + dispatch(toggleGridMode(true)); } else if (char === 'q') { dispatch(unpinMetric()); dispatch(selectMetric(null)); @@ -99,8 +103,8 @@ class App extends React.Component { } render() { - const { showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector, - showingTerminal } = this.props; + const { gridMode, showingDetails, showingHelp, showingMetricsSelector, + showingNetworkSelector, showingTerminal } = this.props; return (
@@ -124,11 +128,11 @@ class App extends React.Component { - + + {showingMetricsSelector && !gridMode && } + {showingNetworkSelector && !gridMode && } - {showingMetricsSelector && } - {showingNetworkSelector && } @@ -141,6 +145,7 @@ class App extends React.Component { function mapStateToProps(state) { return { activeTopologyOptions: getActiveTopologyOptions(state), + gridMode: state.get('gridMode'), routeSet: state.get('routeSet'), searchFocused: state.get('searchFocused'), searchQuery: state.get('searchQuery'), diff --git a/client/app/scripts/components/grid-mode-selector.js b/client/app/scripts/components/grid-mode-selector.js new file mode 100644 index 0000000000..18deba3f95 --- /dev/null +++ b/client/app/scripts/components/grid-mode-selector.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import classNames from 'classnames'; + +import { toggleGridMode } from '../actions/app-actions'; + +class GridModeSelector extends React.Component { + + constructor(props, context) { + super(props, context); + + this.enableGridMode = this.enableGridMode.bind(this); + this.disableGridMode = this.disableGridMode.bind(this); + } + + enableGridMode() { + return this.props.toggleGridMode(true); + } + + disableGridMode() { + return this.props.toggleGridMode(false); + } + + renderItem(icons, isSelected, onClick) { + const className = classNames('network-selector-action', { + 'network-selector-action-selected': isSelected + }); + return ( +
+ +
+ ); + } + + render() { + const { gridMode } = this.props; + + return ( +
+
+ {this.renderItem('fa fa-th', !gridMode, this.disableGridMode)} + {this.renderItem('fa fa-bars', gridMode, this.enableGridMode)} +
+
+ ); + } +} + +function mapStateToProps(state) { + return { + gridMode: state.get('gridMode'), + }; +} + +export default connect( + mapStateToProps, + { toggleGridMode } +)(GridModeSelector); diff --git a/client/app/scripts/components/node-details/node-details-table-node-metric.js b/client/app/scripts/components/node-details/node-details-table-node-metric.js index 4b1ec52610..b0dc2b79d2 100644 --- a/client/app/scripts/components/node-details/node-details-table-node-metric.js +++ b/client/app/scripts/components/node-details/node-details-table-node-metric.js @@ -4,7 +4,7 @@ import { formatMetric } from '../../utils/string-utils'; function NodeDetailsTableNodeMetric(props) { return ( - + {formatMetric(props.value, props)} ); diff --git a/client/app/scripts/components/node-details/node-details-table-row.js b/client/app/scripts/components/node-details/node-details-table-row.js index c7a26c138e..acd9a59415 100644 --- a/client/app/scripts/components/node-details/node-details-table-row.js +++ b/client/app/scripts/components/node-details/node-details-table-row.js @@ -20,23 +20,25 @@ function getValuesForNode(node) { } -function renderValues(node, columns = []) { +function renderValues(node, columns = [], columnWidths = []) { const fields = getValuesForNode(node); - return columns.map(({id}) => { + return columns.map(({id}, i) => { const field = fields[id]; + const style = { width: columnWidths[i] }; if (field) { if (field.valueType === 'metadata') { return ( {field.value} ); } - return ; + return ; } // empty cell to complete the row for proper hover - return ; + return ; }); } @@ -53,13 +55,15 @@ export default class NodeDetailsTableRow extends React.Component { } render() { - const { node, nodeIdKey, topologyId, columns, onMouseOverRow, selected } = this.props; - const values = renderValues(node, columns); + const { node, nodeIdKey, topologyId, columns, onMouseOverRow, selected, widths } = this.props; + const [firstColumnWidth, ...columnWidths] = widths; + console.log(widths); + const values = renderValues(node, columns, columnWidths); const nodeId = node[nodeIdKey]; const className = classNames('node-details-table-node', { selected }); return ( - + { + // + // Beauty hack: adjust first column width if there are only few columns; + // this assumes the other columns are narrow metric columns of 20% table width + // + if (i === 0) { + if (headers.length === 2) { + return '66%'; + } else if (headers.length === 3) { + return '50%'; + } else if (headers.length >= 3 && headers.length < 5) { + return '33%'; + } + } + + // + // 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]; + }); +} + + export default class NodeDetailsTable extends React.Component { constructor(props, context) { @@ -101,33 +134,20 @@ export default class NodeDetailsTable extends React.Component { this.setState({limit}); } + getColumnHeaders() { + const columns = this.props.columns || []; + return [{id: 'label', label: this.props.label}].concat(columns); + } + renderHeaders() { if (this.props.nodes && this.props.nodes.length > 0) { - const columns = this.props.columns || []; - const headers = [{id: 'label', label: this.props.label}].concat(columns); + const headers = this.getColumnHeaders(); + const widths = getColumnsWidths(headers); const defaultSortBy = getDefaultSortBy(this.props); - // Beauty hack: adjust first column width if there are only few columns; - // this assumes the other columns are narrow metric columns of 20% table width - if (headers.length === 2) { - headers[0].width = '66%'; - } else if (headers.length === 3) { - headers[0].width = '50%'; - } else if (headers.length >= 3 && headers.length < 5) { - headers[0].width = '33%'; - } - - // - // More beauty hacking, ports and counts can only get so big, free up WS for other longer - // fields like IPs! - // - headers.forEach(h => { - h.width = COLUMN_WIDTHS[h.id]; - }); - return ( - {headers.map(header => { + {headers.map((header, i) => { const headerClasses = ['node-details-table-header', 'truncate']; const onHeaderClick = ev => { this.handleHeaderClick(ev, header.id); @@ -144,8 +164,8 @@ export default class NodeDetailsTable extends React.Component { // set header width in percent const style = {}; - if (header.width) { - style.width = header.width; + if (widths[i]) { + style.width = widths[i]; } return ( @@ -183,14 +203,16 @@ export default class NodeDetailsTable extends React.Component { React.cloneElement(child, { nodeOrder }) )); + const className = classNames('node-details-table-wrapper-wrapper', this.props.className); + return ( -
+
{headers} - + {nodes && nodes.map(node => ( diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index 75e9459ab4..7080d831d9 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -7,7 +7,7 @@ import NodesError from '../charts/nodes-error'; import { isTopologyEmpty } from '../utils/topology-utils'; import { CANVAS_MARGINS } from '../constants/styles'; -const navbarHeight = 160; +const navbarHeight = 168; const marginTop = 0; /** @@ -75,6 +75,7 @@ class Nodes extends React.Component { +
{children}
); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 89e3de049a..2cf157da76 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -273,7 +273,6 @@ h2 { margin-bottom: 3px; border: 1px solid transparent; - background-color: #f7f7fa; &-active, &:hover { color: @text-color; background-color: @background-darker-secondary-color; @@ -1216,9 +1215,18 @@ h2 { .sidebar { position: fixed; - bottom: 16px; - left: 16px; + bottom: 12px; + left: 12px; + padding: 4px; font-size: .7rem; + border-radius: 8px; + border: 1px solid transparent; +} + +.sidebar-gridmode { + background-color: #e9e9f1; + border-color: @background-darker-color; + opacity: 0.9; } .search { @@ -1505,13 +1513,9 @@ h2 { .node-details-table-wrapper-wrapper { flex: 1; - overflow: scroll; display: flex; flex-direction: row; - margin: 8px 16px; width: 100%; - // border: 1px solid @background-darker-color; - padding-bottom: 36px; .node-details-table-wrapper { margin: 0; @@ -1532,6 +1536,28 @@ h2 { background-color: @background-darker-color; } } + } + + .scroll-body { + + table { + border-bottom: 1px solid #ccc; + } + + thead, tbody tr { + display: table; + width: 100%; + table-layout: fixed; + } + thead { + box-shadow: 0 4px 2px -2px rgba(0, 0, 0, 0.16); + border-bottom: 1px solid #aaa; + } + + tbody { + display: block; + overflow-y: scroll; + } } }